diff --git a/pkg/services/accesscontrol/acimpl/accesscontrol.go b/pkg/services/accesscontrol/acimpl/accesscontrol.go index ed3ee6eba45..f036c9d65f3 100644 --- a/pkg/services/accesscontrol/acimpl/accesscontrol.go +++ b/pkg/services/accesscontrol/acimpl/accesscontrol.go @@ -236,7 +236,7 @@ func (a *AccessControl) Check(ctx context.Context, req accesscontrol.CheckReques } // Check direct access to resource first - res, err := a.zclient.Check(ctx, in) + res, err := a.zclient.CheckObject(ctx, in) if err != nil { return false, err } @@ -262,7 +262,7 @@ func (a *AccessControl) Check(ctx context.Context, req accesscontrol.CheckReques TupleKey: folderKey, } - folderRes, err := a.zclient.Check(ctx, folderReq) + folderRes, err := a.zclient.CheckObject(ctx, folderReq) if err != nil { return false, err } diff --git a/pkg/services/accesscontrol/dualwrite/collectors.go b/pkg/services/accesscontrol/dualwrite/collectors.go index 2f9c2f058d4..26ba0bd9c41 100644 --- a/pkg/services/accesscontrol/dualwrite/collectors.go +++ b/pkg/services/accesscontrol/dualwrite/collectors.go @@ -58,6 +58,119 @@ func teamMembershipCollector(store db.DB) legacyTupleCollector { } } +// folderTreeCollector collects folder tree structure and writes it as relation tuples +func folderTreeCollector2(store db.DB) legacyTupleCollector { + return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) { + ctx, span := tracer.Start(ctx, "accesscontrol.migrator.folderTreeCollector") + defer span.End() + + const query = ` + SELECT uid, parent_uid, org_id FROM folder + ` + type folder struct { + OrgID int64 `xorm:"org_id"` + FolderUID string `xorm:"uid"` + ParentUID string `xorm:"parent_uid"` + } + + var folders []folder + err := store.WithDbSession(ctx, func(sess *db.Session) error { + return sess.SQL(query).Find(&folders) + }) + + if err != nil { + return nil, err + } + + tuples := make(map[string]map[string]*openfgav1.TupleKey) + + for _, f := range folders { + var tuple *openfgav1.TupleKey + if f.ParentUID == "" { + continue + } + + tuple = &openfgav1.TupleKey{ + Object: zanzana.NewTupleEntry("folder2", f.FolderUID, ""), + Relation: zanzana.RelationParent, + User: zanzana.NewTupleEntry("folder2", f.ParentUID, ""), + } + + if tuples[tuple.Object] == nil { + tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) + } + + tuples[tuple.Object][tuple.String()] = tuple + } + + return tuples, nil + } +} + +// managedPermissionsCollector collects managed permissions into provided tuple map. +// It will only store actions that are supported by our schema. Managed permissions can +// be directly mapped to user/team/role without having to write an intermediate role. +func managedPermissionsCollector2(store db.DB) legacyTupleCollector { + return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) { + query := ` + SELECT u.uid as user_uid, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id + FROM permission p + INNER JOIN role r ON p.role_id = r.id + LEFT JOIN user_role ur ON r.id = ur.role_id + LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ur.user_id + LEFT JOIN team_role tr ON r.id = tr.role_id + LEFT JOIN team t ON tr.team_id = t.id + LEFT JOIN builtin_role br ON r.id = br.role_id + WHERE r.name LIKE 'managed:%' + ` + type Permission struct { + RoleName string `xorm:"role_name"` + OrgID int64 `xorm:"org_id"` + Action string `xorm:"action"` + Kind string + Identifier string + UserUID string `xorm:"user_uid"` + TeamUID string `xorm:"team_uid"` + } + + var permissions []Permission + err := store.WithDbSession(ctx, func(sess *db.Session) error { + return sess.SQL(query).Find(&permissions) + }) + + if err != nil { + return nil, err + } + + tuples := make(map[string]map[string]*openfgav1.TupleKey) + + for _, p := range permissions { + var subject string + if len(p.UserUID) > 0 { + subject = zanzana.NewTupleEntry(zanzana.TypeUser, p.UserUID, "") + } else if len(p.TeamUID) > 0 { + subject = zanzana.NewTupleEntry(zanzana.TypeTeam, p.TeamUID, "member") + } else { + // FIXME(kalleep): Unsuported role binding (org role). We need to have basic roles in place + continue + } + + tuple, ok := zanzana.TranslateToResourceTuple(subject, p.Action, p.Kind, p.Identifier) + if !ok { + continue + } + + if tuples[tuple.Object] == nil { + tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) + } + + tuples[tuple.Object][tuple.String()] = tuple + } + + return tuples, nil + } +} + func zanzanaCollector(client zanzana.Client, relations []string) zanzanaTupleCollector { return func(ctx context.Context, client zanzana.Client, object string) (map[string]*openfgav1.TupleKey, error) { // list will use continuation token to collect all tuples for object and relation diff --git a/pkg/services/accesscontrol/dualwrite/reconciler.go b/pkg/services/accesscontrol/dualwrite/reconciler.go index 0850a5a33d8..4a1a3cc16cd 100644 --- a/pkg/services/accesscontrol/dualwrite/reconciler.go +++ b/pkg/services/accesscontrol/dualwrite/reconciler.go @@ -65,6 +65,18 @@ func NewZanzanaReconciler(client zanzana.Client, store db.DB, lock *serverlock.S zanzanaCollector(client, []string{zanzana.RelationTeamMember, zanzana.RelationTeamAdmin}), client, ), + newResourceReconciler( + "folder tree", + folderTreeCollector2(store), + zanzanaCollector(client, []string{zanzana.RelationParent}), + client, + ), + newResourceReconciler( + "managed permissison", + managedPermissionsCollector2(store), + zanzanaCollector(client, zanzana.ResourceRelations), + client, + ), }, } } @@ -136,7 +148,6 @@ func (r *ZanzanaReconciler) reconcile(ctx context.Context) { r.log.Debug("Finished reconciliation", "elapsed", time.Since(now)) } - // in tests we can skip creating a lock if r.lock == nil { run(ctx) return diff --git a/pkg/services/authz/zanzana/client.go b/pkg/services/authz/zanzana/client.go index 5a769323de8..ac83217924c 100644 --- a/pkg/services/authz/zanzana/client.go +++ b/pkg/services/authz/zanzana/client.go @@ -4,18 +4,23 @@ import ( "context" "fmt" - "google.golang.org/grpc" - + "github.com/grafana/authlib/authz" + "github.com/grafana/authlib/claims" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "google.golang.org/grpc" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authz/zanzana/client" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" "github.com/grafana/grafana/pkg/setting" ) // Client is a wrapper around [openfgav1.OpenFGAServiceClient] type Client interface { - Check(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) + authz.AccessClient + List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error) + + CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) Write(ctx context.Context, in *openfgav1.WriteRequest) error diff --git a/pkg/services/authz/zanzana/client/client.go b/pkg/services/authz/zanzana/client/client.go index cc29199270f..95855bd9386 100644 --- a/pkg/services/authz/zanzana/client/client.go +++ b/pkg/services/authz/zanzana/client/client.go @@ -11,9 +11,17 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/wrapperspb" + "github.com/grafana/authlib/authz" + authzv1 "github.com/grafana/authlib/authz/proto/v1" + "github.com/grafana/authlib/claims" + + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" ) +var _ authz.AccessClient = (*Client)(nil) + var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/authz/zanzana/client") type ClientOption func(c *Client) @@ -32,7 +40,10 @@ func WithLogger(logger log.Logger) ClientOption { type Client struct { logger log.Logger - client openfgav1.OpenFGAServiceClient + openfga openfgav1.OpenFGAServiceClient + authz authzv1.AuthzServiceClient + authzext authzextv1.AuthzExtentionServiceClient + tenantID string storeID string modelID string @@ -40,7 +51,9 @@ type Client struct { func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption) (*Client, error) { c := &Client{ - client: openfgav1.NewOpenFGAServiceClient(cc), + openfga: openfgav1.NewOpenFGAServiceClient(cc), + authz: authzv1.NewAuthzServiceClient(cc), + authzext: authzextv1.NewAuthzExtentionServiceClient(cc), } for _, o := range opts { @@ -72,13 +85,70 @@ func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption) return c, nil } -func (c *Client) Check(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) { +// Check implements authz.AccessClient. +func (c *Client) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) { ctx, span := tracer.Start(ctx, "authz.zanzana.client.Check") defer span.End() + res, err := c.authz.Check(ctx, &authzv1.CheckRequest{ + Subject: id.GetUID(), + Verb: req.Verb, + Group: req.Group, + Resource: req.Resource, + Namespace: req.Namespace, + Name: req.Name, + Subresource: req.Subresource, + Path: req.Path, + Folder: req.Folder, + }) + + if err != nil { + return authz.CheckResponse{}, err + } + + return authz.CheckResponse{Allowed: res.GetAllowed()}, nil +} + +func (c *Client) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) { + ctx, span := tracer.Start(ctx, "authz.zanzana.client.Compile") + defer span.End() + + _, err := c.authzext.List(ctx, &authzextv1.ListRequest{ + Subject: id.GetUID(), + Group: req.Group, + Verb: utils.VerbList, + Resource: req.Resource, + Namespace: req.Namespace, + }) + + if err != nil { + return nil, err + } + + // FIXME: implement checker + return func(namespace, name, folder string) bool { return false }, nil +} + +func (c *Client) List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error) { + ctx, span := tracer.Start(ctx, "authz.zanzana.client.List") + defer span.End() + + return c.authzext.List(ctx, &authzextv1.ListRequest{ + Subject: id.GetUID(), + Group: req.Group, + Verb: utils.VerbList, + Resource: req.Resource, + Namespace: req.Namespace, + }) +} + +func (c *Client) CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) { + ctx, span := tracer.Start(ctx, "authz.zanzana.client.CheckObject") + defer span.End() + in.StoreId = c.storeID in.AuthorizationModelId = c.modelID - return c.client.Check(ctx, in) + return c.openfga.Check(ctx, in) } func (c *Client) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error) { @@ -86,7 +156,7 @@ func (c *Client) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav defer span.End() in.StoreId = c.storeID - return c.client.Read(ctx, in) + return c.openfga.Read(ctx, in) } func (c *Client) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) { @@ -96,13 +166,13 @@ func (c *Client) ListObjects(ctx context.Context, in *openfgav1.ListObjectsReque in.StoreId = c.storeID in.AuthorizationModelId = c.modelID - return c.client.ListObjects(ctx, in) + return c.openfga.ListObjects(ctx, in) } func (c *Client) Write(ctx context.Context, in *openfgav1.WriteRequest) error { in.StoreId = c.storeID in.AuthorizationModelId = c.modelID - _, err := c.client.Write(ctx, in) + _, err := c.openfga.Write(ctx, in) return err } @@ -115,7 +185,7 @@ func (c *Client) getStore(ctx context.Context, name string) (*openfgav1.Store, e // We should create an issue to support some way to get stores by name. // For now we need to go thourh all stores until we find a match or we hit the end. for { - res, err := c.client.ListStores(ctx, &openfgav1.ListStoresRequest{ + res, err := c.openfga.ListStores(ctx, &openfgav1.ListStoresRequest{ PageSize: &wrapperspb.Int32Value{Value: 20}, ContinuationToken: continuationToken, }) @@ -142,7 +212,7 @@ func (c *Client) getStore(ctx context.Context, name string) (*openfgav1.Store, e func (c *Client) loadModel(ctx context.Context, storeID string) (string, error) { // ReadAuthorizationModels returns authorization models for a store sorted in descending order of creation. // So with a pageSize of 1 we will get the latest model. - res, err := c.client.ReadAuthorizationModels(ctx, &openfgav1.ReadAuthorizationModelsRequest{ + res, err := c.openfga.ReadAuthorizationModels(ctx, &openfgav1.ReadAuthorizationModelsRequest{ StoreId: storeID, PageSize: &wrapperspb.Int32Value{Value: 1}, }) diff --git a/pkg/services/authz/zanzana/client/noop.go b/pkg/services/authz/zanzana/client/noop.go index 1811bbffc35..2f2b1a498ae 100644 --- a/pkg/services/authz/zanzana/client/noop.go +++ b/pkg/services/authz/zanzana/client/noop.go @@ -3,16 +3,34 @@ package client import ( "context" + "github.com/grafana/authlib/authz" + "github.com/grafana/authlib/claims" openfgav1 "github.com/openfga/api/proto/openfga/v1" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" ) +var _ authz.AccessClient = (*NoopClient)(nil) + func NewNoop() *NoopClient { return &NoopClient{} } type NoopClient struct{} -func (nc NoopClient) Check(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) { +func (nc *NoopClient) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) { + return authz.CheckResponse{}, nil +} + +func (nc *NoopClient) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) { + return nil, nil +} + +func (nc *NoopClient) List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error) { + return nil, nil +} + +func (nc NoopClient) CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) { return nil, nil } diff --git a/pkg/services/authz/zanzana/common/info.go b/pkg/services/authz/zanzana/common/info.go new file mode 100644 index 00000000000..28c0774188a --- /dev/null +++ b/pkg/services/authz/zanzana/common/info.go @@ -0,0 +1,34 @@ +package common + +import ( + "github.com/grafana/grafana/pkg/apimachinery/utils" + + folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" +) + +type TypeInfo struct { + Type string +} + +var typedResources = map[string]TypeInfo{ + NewNamespaceResourceIdent( + folderalpha1.FolderResourceInfo.GroupResource().Group, + folderalpha1.FolderResourceInfo.GroupResource().Resource, + ): TypeInfo{Type: "folder2"}, +} + +func GetTypeInfo(group, resource string) (TypeInfo, bool) { + info, ok := typedResources[NewNamespaceResourceIdent(group, resource)] + return info, ok +} + +var VerbMapping = map[string]string{ + utils.VerbGet: "read", + utils.VerbList: "read", + utils.VerbWatch: "read", + utils.VerbCreate: "create", + utils.VerbUpdate: "write", + utils.VerbPatch: "write", + utils.VerbDelete: "delete", + utils.VerbDeleteCollection: "delete", +} diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go new file mode 100644 index 00000000000..11e3be3b732 --- /dev/null +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -0,0 +1,86 @@ +package common + +import ( + "fmt" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "google.golang.org/protobuf/types/known/structpb" +) + +const ( + resourceType = "resource" + namespaceType = "namespace" + folderResourceType = "folder_resource" +) + +func NewTypedIdent(typ string, name string) string { + return fmt.Sprintf("%s:%s", typ, name) +} + +func NewResourceIdent(group, resource, name string) string { + return fmt.Sprintf("%s:%s/%s", resourceType, FormatGroupResource(group, resource), name) +} + +func NewFolderResourceIdent(group, resource, folder string) string { + return fmt.Sprintf("%s:%s/%s", folderResourceType, FormatGroupResource(group, resource), folder) +} + +func NewNamespaceResourceIdent(group, resource string) string { + return fmt.Sprintf("%s:%s", namespaceType, FormatGroupResource(group, resource)) +} + +func FormatGroupResource(group, resource string) string { + return fmt.Sprintf("%s/%s", group, resource) +} + +func NewResourceTuple(subject, relation, group, resource, name string) *openfgav1.TupleKey { + return &openfgav1.TupleKey{ + User: subject, + Relation: relation, + Object: NewResourceIdent(group, resource, name), + Condition: &openfgav1.RelationshipCondition{ + Name: "group_filter", + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "resource_group": structpb.NewStringValue(FormatGroupResource(group, resource)), + }, + }, + }, + } +} + +func NewFolderResourceTuple(subject, relation, group, resource, folder string) *openfgav1.TupleKey { + return &openfgav1.TupleKey{ + User: subject, + Relation: relation, + Object: NewFolderResourceIdent(group, resource, folder), + Condition: &openfgav1.RelationshipCondition{ + Name: "group_filter", + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "resource_group": structpb.NewStringValue(FormatGroupResource(group, resource)), + }, + }, + }, + } +} + +func NewNamespaceResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey { + return &openfgav1.TupleKey{ + User: subject, + Relation: relation, + Object: NewNamespaceResourceIdent(group, resource), + } +} + +func NewFolderTuple(subject, relation, name string) *openfgav1.TupleKey { + return NewTypedTuple("folder2", subject, relation, name) +} + +func NewTypedTuple(typ, subject, relation, name string) *openfgav1.TupleKey { + return &openfgav1.TupleKey{ + User: subject, + Relation: relation, + Object: NewTypedIdent(typ, name), + } +} diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index 97c3f34d6a0..4aa25b11130 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -11,8 +11,6 @@ import ( "go.opentelemetry.io/otel" "google.golang.org/protobuf/types/known/wrapperspb" - "github.com/grafana/grafana/pkg/apimachinery/utils" - folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/schema" @@ -199,47 +197,3 @@ func (s *Server) loadModel(ctx context.Context, storeID string, modules []transf return writeRes.GetAuthorizationModelId(), nil } - -func newTypedIdent(typ string, name string) string { - return fmt.Sprintf("%s:%s", typ, name) -} - -func newResourceIdent(group, resource, name string) string { - return fmt.Sprintf("%s:%s/%s", resourceType, formatGroupResource(group, resource), name) -} - -func newFolderResourceIdent(group, resource, folder string) string { - return fmt.Sprintf("%s:%s/%s", folderResourceType, formatGroupResource(group, resource), folder) -} - -func newNamespaceResourceIdent(group, resource string) string { - return fmt.Sprintf("%s:%s", namespaceType, formatGroupResource(group, resource)) -} - -func formatGroupResource(group, resource string) string { - return fmt.Sprintf("%s/%s", group, resource) -} - -type TypeInfo struct { - typ string -} - -var typedResources = map[string]TypeInfo{ - newNamespaceResourceIdent(folderalpha1.GROUP, folderalpha1.RESOURCE): TypeInfo{typ: "folder2"}, -} - -func typeInfo(group, resource string) (TypeInfo, bool) { - info, ok := typedResources[newNamespaceResourceIdent(group, resource)] - return info, ok -} - -var mapping = map[string]string{ - utils.VerbGet: "read", - utils.VerbList: "read", - utils.VerbWatch: "read", - utils.VerbCreate: "create", - utils.VerbUpdate: "write", - utils.VerbPatch: "write", - utils.VerbDelete: "delete", - utils.VerbDeleteCollection: "delete", -} diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index 5f948c1d1d6..436c5a6c4b5 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -4,6 +4,7 @@ import ( "context" authzv1 "github.com/grafana/authlib/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" openfgav1 "github.com/openfga/api/proto/openfga/v1" "google.golang.org/protobuf/types/known/structpb" ) @@ -12,14 +13,14 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C ctx, span := tracer.Start(ctx, "authzServer.Check") defer span.End() - if info, ok := typeInfo(r.GetGroup(), r.GetResource()); ok { + if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok { return s.checkTyped(ctx, r, info) } return s.checkGeneric(ctx, r) } -func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info TypeInfo) (*authzv1.CheckResponse, error) { - relation := mapping[r.GetVerb()] +func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info common.TypeInfo) (*authzv1.CheckResponse, error) { + relation := common.VerbMapping[r.GetVerb()] // 1. check if subject has direct access to resource res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ @@ -28,7 +29,7 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info T TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, - Object: newTypedIdent(info.typ, r.GetName()), + Object: common.NewTypedIdent(info.Type, r.GetName()), }, }) if err != nil { @@ -46,7 +47,7 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info T TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, - Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()), + Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()), }, }) if err != nil { @@ -57,7 +58,7 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info T } func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) { - relation := mapping[r.GetVerb()] + relation := common.VerbMapping[r.GetVerb()] // 1. check if subject has direct access to resource res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ StoreId: s.storeID, @@ -65,11 +66,11 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, - Object: newResourceIdent(r.GetGroup(), r.GetResource(), r.GetName()), + Object: common.NewResourceIdent(r.GetGroup(), r.GetResource(), r.GetName()), }, Context: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())), + "requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())), }, }, }) @@ -90,7 +91,7 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, - Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()), + Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()), }, }) @@ -113,11 +114,11 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, - Object: newFolderResourceIdent(r.GetGroup(), r.GetResource(), r.GetFolder()), + Object: common.NewFolderResourceIdent(r.GetGroup(), r.GetResource(), r.GetFolder()), }, Context: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())), + "requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())), }, }, }) diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index 386d660b06b..f02249d6764 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -8,7 +8,7 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "google.golang.org/protobuf/types/known/structpb" - "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" ) @@ -16,14 +16,14 @@ func (s *Server) List(ctx context.Context, r *authzextv1.ListRequest) (*authzext ctx, span := tracer.Start(ctx, "authzServer.List") defer span.End() - if info, ok := typeInfo(r.GetGroup(), r.GetResource()); ok { + if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok { return s.listTyped(ctx, r, info) } return s.listGeneric(ctx, r) } -func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info TypeInfo) (*authzextv1.ListResponse, error) { - relation := mapping[r.GetVerb()] +func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info common.TypeInfo) (*authzextv1.ListResponse, error) { + relation := common.VerbMapping[r.GetVerb()] // 1. check if subject has access through namespace because then they can read all of them res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ @@ -32,7 +32,7 @@ func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, - Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()), + Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()), }, }) if err != nil { @@ -47,8 +47,8 @@ func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info listRes, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{ StoreId: s.storeID, AuthorizationModelId: s.modelID, - Type: info.typ, - Relation: mapping[utils.VerbGet], + Type: info.Type, + Relation: relation, User: r.GetSubject(), }) if err != nil { @@ -56,12 +56,12 @@ func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info } return &authzextv1.ListResponse{ - Items: typedObjects(info.typ, listRes.GetObjects()), + Items: typedObjects(info.Type, listRes.GetObjects()), }, nil } func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*authzextv1.ListResponse, error) { - relation := mapping[r.GetVerb()] + relation := common.VerbMapping[r.GetVerb()] // 1. check if subject has access through namespace because then they can read all of them res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ @@ -70,7 +70,7 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a TupleKey: &openfgav1.CheckRequestTupleKey{ User: r.GetSubject(), Relation: relation, - Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()), + Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()), }, }) if err != nil { @@ -90,7 +90,7 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a User: r.GetSubject(), Context: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())), + "requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())), }, }, }) @@ -107,7 +107,7 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a User: r.GetSubject(), Context: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())), + "requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())), }, }, }) diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 1ea694d2115..2383d2bf5ff 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -6,10 +6,10 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/structpb" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" "github.com/grafana/grafana/pkg/services/authz/zanzana/store" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" @@ -67,65 +67,17 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { AuthorizationModelId: srv.modelID, Writes: &openfgav1.WriteRequestWrites{ TupleKeys: []*openfgav1.TupleKey{ - newResourceTuple("user:1", "read", dashboardGroup, dashboardResource, "1"), - newNamespaceResourceTuple("user:2", "read", dashboardGroup, dashboardResource), - newResourceTuple("user:3", "view", dashboardGroup, dashboardResource, "1"), - newFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "1"), - newFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "3"), - newFolderResourceTuple("user:5", "view", dashboardGroup, dashboardResource, "1"), - newFolderTuple("user:6", "read", "1"), - newNamespaceResourceTuple("user:7", "read", folderGroup, folderResource), + common.NewResourceTuple("user:1", "read", dashboardGroup, dashboardResource, "1"), + common.NewNamespaceResourceTuple("user:2", "read", dashboardGroup, dashboardResource), + common.NewResourceTuple("user:3", "view", dashboardGroup, dashboardResource, "1"), + common.NewFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "1"), + common.NewFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "3"), + common.NewFolderResourceTuple("user:5", "view", dashboardGroup, dashboardResource, "1"), + common.NewFolderTuple("user:6", "read", "1"), + common.NewNamespaceResourceTuple("user:7", "read", folderGroup, folderResource), }, }, }) require.NoError(t, err) return srv } - -func newResourceTuple(subject, relation, group, resource, name string) *openfgav1.TupleKey { - return &openfgav1.TupleKey{ - User: subject, - Relation: relation, - Object: newResourceIdent(group, resource, name), - Condition: &openfgav1.RelationshipCondition{ - Name: "group_filter", - Context: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "resource_group": structpb.NewStringValue(formatGroupResource(group, resource)), - }, - }, - }, - } -} - -func newFolderResourceTuple(subject, relation, group, resource, folder string) *openfgav1.TupleKey { - return &openfgav1.TupleKey{ - User: subject, - Relation: relation, - Object: newFolderResourceIdent(group, resource, folder), - Condition: &openfgav1.RelationshipCondition{ - Name: "group_filter", - Context: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "resource_group": structpb.NewStringValue(formatGroupResource(group, resource)), - }, - }, - }, - } -} - -func newNamespaceResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey { - return &openfgav1.TupleKey{ - User: subject, - Relation: relation, - Object: newNamespaceResourceIdent(group, resource), - } -} - -func newFolderTuple(subject, relation, name string) *openfgav1.TupleKey { - return &openfgav1.TupleKey{ - User: subject, - Relation: relation, - Object: newTypedIdent("folder2", name), - } -} diff --git a/pkg/services/authz/zanzana/translations.go b/pkg/services/authz/zanzana/translations.go index c851547e973..5de7c7fe87e 100644 --- a/pkg/services/authz/zanzana/translations.go +++ b/pkg/services/authz/zanzana/translations.go @@ -1,5 +1,10 @@ package zanzana +import ( + dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" +) + type actionKindTranslation struct { objectType string orgScoped bool @@ -102,3 +107,67 @@ var basicRolesTranslations = map[string]string{ RoleViewer: "basic_viewer", RoleNone: "basic_none", } + +type resourceTranslation struct { + typ string + group string + resource string + mapping map[string]actionMappig +} + +type actionMappig struct { + relation string + group string + resource string +} + +func newMapping(relation string) actionMappig { + return newScopedMapping(relation, "", "") +} + +func newScopedMapping(relation, group, resource string) actionMappig { + return actionMappig{relation, group, resource} +} + +var ( + folderGroup = folderalpha1.FolderResourceInfo.GroupResource().Group + folderResource = folderalpha1.FolderResourceInfo.GroupResource().Resource + + dashboardGroup = dashboardalpha1.DashboardResourceInfo.GroupResource().Group + dashboardResource = dashboardalpha1.DashboardResourceInfo.GroupResource().Resource +) + +var resourceTranslations = map[string]resourceTranslation{ + KindFolders: { + typ: TypeFolder2, + group: folderGroup, + resource: folderResource, + mapping: map[string]actionMappig{ + "folders:read": newMapping(RelationRead), + "folders:write": newMapping(RelationWrite), + "folders:create": newMapping(RelationCreate), + "folders:delete": newMapping(RelationDelete), + "folders.permissions:read": newMapping(RelationPermissionsRead), + "folders.permissions:write": newMapping(RelationPermissionsWrite), + "dashboards:read": newScopedMapping(RelationRead, dashboardGroup, dashboardResource), + "dashboards:write": newScopedMapping(RelationWrite, dashboardGroup, dashboardResource), + "dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource), + "dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource), + "dashboards.permissions:read": newScopedMapping(RelationPermissionsRead, dashboardGroup, dashboardResource), + "dashboards.permissions:write": newScopedMapping(RelationPermissionsWrite, dashboardGroup, dashboardResource), + }, + }, + KindDashboards: { + typ: TypeResource, + group: dashboardGroup, + resource: dashboardResource, + mapping: map[string]actionMappig{ + "dashboards:read": newMapping(RelationRead), + "dashboards:write": newMapping(RelationWrite), + "dashboards:create": newMapping(RelationCreate), + "dashboards:delete": newMapping(RelationDelete), + "dashboards.permissions:read": newMapping(RelationPermissionsRead), + "dashboards.permissions:write": newMapping(RelationPermissionsWrite), + }, + }, +} diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index ece56e98179..8349f691e19 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -5,6 +5,7 @@ import ( "strconv" "strings" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" openfgav1 "github.com/openfga/api/proto/openfga/v1" ) @@ -13,8 +14,10 @@ const ( TypeTeam string = "team" TypeRole string = "role" TypeFolder string = "folder" + TypeFolder2 string = "folder2" TypeDashboard string = "dashboard" TypeOrg string = "org" + TypeResource string = "resource" ) const ( @@ -23,8 +26,19 @@ const ( RelationParent string = "parent" RelationAssignee string = "assignee" RelationOrg string = "org" + + // FIXME action sets + RelationAdmin string = "admin" + RelationRead string = "read" + RelationWrite string = "write" + RelationCreate string = "create" + RelationDelete string = "delete" + RelationPermissionsRead string = "permissions_read" + RelationPermissionsWrite string = "permissions_write" ) +var ResourceRelations = []string{RelationRead, RelationWrite, RelationCreate, RelationDelete, RelationPermissionsRead, RelationPermissionsWrite} + const ( KindOrg string = "org" KindDashboards string = "dashboards" @@ -89,6 +103,33 @@ func TranslateToTuple(user string, action, kind, identifier string, orgID int64) return tuple, true } +func TranslateToResourceTuple(subject string, action, kind, name string) (*openfgav1.TupleKey, bool) { + translation, ok := resourceTranslations[kind] + + if !ok { + return nil, false + } + + m, ok := translation.mapping[action] + if !ok { + return nil, false + } + + if translation.typ == TypeResource { + return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, name), true + } + + if translation.typ == TypeFolder2 { + if m.group != "" && m.resource != "" { + return common.NewFolderResourceTuple(subject, m.relation, m.group, m.resource, name), true + } + + return common.NewFolderTuple(subject, m.relation, name), true + } + + return common.NewTypedTuple(translation.typ, subject, m.relation, name), true +} + func TranslateToOrgTuple(user string, action string, orgID int64) (*openfgav1.TupleKey, bool) { typeTranslation, ok := actionKindTranslations[KindOrg] if !ok {