diff --git a/pkg/services/accesscontrol/dualwrite/collectors.go b/pkg/services/accesscontrol/dualwrite/collectors.go index 871df2edf3f..1580b151b09 100644 --- a/pkg/services/accesscontrol/dualwrite/collectors.go +++ b/pkg/services/accesscontrol/dualwrite/collectors.go @@ -167,8 +167,7 @@ func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } - // For resource actions on folders we need to merge the tuples into one with combined - // group_resources. + // For resource actions on folders we need to merge the tuples into one with combined subresources. if zanzana.IsFolderResourceTuple(tuple) { key := tupleStringWithoutCondition(tuple) if t, ok := tuples[tuple.Object][key]; ok { @@ -386,8 +385,7 @@ func rolePermissionsCollector(store db.DB) legacyTupleCollector { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } - // For resource actions on folders we need to merge the tuples into one with combined - // group_resources. + // For resource actions on folders we need to merge the tuples into one with combined subresources. if zanzana.IsFolderResourceTuple(tuple) { key := tupleStringWithoutCondition(tuple) if t, ok := tuples[tuple.Object][key]; ok { @@ -449,8 +447,7 @@ func fixedRolePermissionsCollector(store db.DB) legacyTupleCollector { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } - // For resource actions on folders we need to merge the tuples into one with combined - // group_resources. + // For resource actions on folders we need to merge the tuples into one with combined subresources. if zanzana.IsFolderResourceTuple(tuple) { key := tupleStringWithoutCondition(tuple) if t, ok := tuples[tuple.Object][key]; ok { diff --git a/pkg/services/accesscontrol/dualwrite/resource_reconciler.go b/pkg/services/accesscontrol/dualwrite/resource_reconciler.go index af505837569..63a4f8b25eb 100644 --- a/pkg/services/accesscontrol/dualwrite/resource_reconciler.go +++ b/pkg/services/accesscontrol/dualwrite/resource_reconciler.go @@ -62,7 +62,7 @@ func (r resourceReconciler) reconcile(ctx context.Context, namespace string) err continue } - // 4. For folder resource tuples we also need to compare the stored group_resources + // 4. For folder resource tuples we also need to compare the stored subresources if zanzana.IsFolderResourceTuple(t) && t.String() != stored.String() { deletes = append(deletes, &openfgav1.TupleKeyWithoutCondition{ User: t.User, diff --git a/pkg/services/authz/zanzana/common/info.go b/pkg/services/authz/zanzana/common/info.go index 54fb8f2a35a..65531c0129a 100644 --- a/pkg/services/authz/zanzana/common/info.go +++ b/pkg/services/authz/zanzana/common/info.go @@ -1,10 +1,11 @@ package common import ( + authzv1 "github.com/grafana/authlib/authz/proto/v1" "google.golang.org/protobuf/types/known/structpb" - authzv1 "github.com/grafana/authlib/authz/proto/v1" folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + iamalpha1 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) @@ -19,6 +20,11 @@ var typedResources = map[string]typeInfo{ folderalpha1.FolderResourceInfo.GroupResource().Resource, "", ): {Type: "folder", Relations: RelationsFolder}, + FormatGroupResource( + iamalpha1.TeamResourceInfo.GroupResource().Group, + iamalpha1.TeamResourceInfo.GroupResource().Resource, + "", + ): {Type: "team", Relations: RelationsFolder}, } func getTypeInfo(group, resource string) (typeInfo, bool) { @@ -133,13 +139,10 @@ func (r ResourceInfo) Type() string { } func (r ResourceInfo) Context() *structpb.Struct { - if !r.IsGeneric() { - return nil - } - return &structpb.Struct{ Fields: map[string]*structpb.Value{ "requested_group": structpb.NewStringValue(r.GroupResource()), + "subresource": structpb.NewStringValue(r.GroupResource()), }, } } @@ -147,3 +150,7 @@ func (r ResourceInfo) Context() *structpb.Struct { func (r ResourceInfo) IsValidRelation(relation string) bool { return isValidRelation(relation, r.relations) } + +func (r ResourceInfo) HasSubresource() bool { + return r.subresource != "" +} diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index c238ac0f0f9..c1fa549286d 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -30,6 +30,7 @@ const ( TypeFolderPrefix string = TypeFolder + ":" TypeResourcePrefix string = TypeResource + ":" TypeGroupResoucePrefix string = TypeGroupResouce + ":" + TypeTeamPrefix string = TypeTeam + ":" ) const ( @@ -50,16 +51,16 @@ const ( RelationGetPermissions string = "get_permissions" RelationSetPermissions string = "set_permissions" - RelationFolderResourceSetView string = "resource_" + RelationSetView - RelationFolderResourceSetEdit string = "resource_" + RelationSetEdit - RelationFolderResourceSetAdmin string = "resource_" + RelationSetAdmin + RelationSubresourceSetView string = "resource_" + RelationSetView + RelationSubresourceSetEdit string = "resource_" + RelationSetEdit + RelationSubresourceSetAdmin string = "resource_" + RelationSetAdmin - RelationFolderResourceGet string = "resource_" + RelationGet - RelationFolderResourceUpdate string = "resource_" + RelationUpdate - RelationFolderResourceCreate string = "resource_" + RelationCreate - RelationFolderResourceDelete string = "resource_" + RelationDelete - RelationFolderResourceGetPermissions string = "resource_" + RelationGetPermissions - RelationFolderResourceSetPermissions string = "resource_" + RelationSetPermissions + RelationSubresourceGet string = "resource_" + RelationGet + RelationSubresourceUpdate string = "resource_" + RelationUpdate + RelationSubresourceCreate string = "resource_" + RelationCreate + RelationSubresourceDelete string = "resource_" + RelationDelete + RelationSubresourceGetPermissions string = "resource_" + RelationGetPermissions + RelationSubresourceSetPermissions string = "resource_" + RelationSetPermissions ) // RelationsGroupResource are relations that can be added on type "group_resource". @@ -81,19 +82,19 @@ var RelationsResource = []string{ RelationSetPermissions, } -// RelationsFolderResource are relations that can be added on type "folder" for child resources. -var RelationsFolderResource = []string{ - RelationFolderResourceGet, - RelationFolderResourceUpdate, - RelationFolderResourceCreate, - RelationFolderResourceDelete, - RelationFolderResourceGetPermissions, - RelationFolderResourceSetPermissions, +// RelationsSubresource are relations that can be added on typed resources for subresources. +var RelationsSubresource = []string{ + RelationSubresourceGet, + RelationSubresourceUpdate, + RelationSubresourceCreate, + RelationSubresourceDelete, + RelationSubresourceGetPermissions, + RelationSubresourceSetPermissions, } // RelationsFolder are relations that can be added on type "folder". var RelationsFolder = append( - RelationsFolderResource, + RelationsSubresource, RelationGet, RelationUpdate, RelationCreate, @@ -130,8 +131,8 @@ func IsGroupResourceRelation(relation string) bool { return isValidRelation(relation, RelationsGroupResource) } -func IsFolderResourceRelation(relation string) bool { - return isValidRelation(relation, RelationsFolderResource) +func IsSubresourceRelation(relation string) bool { + return isValidRelation(relation, RelationsSubresource) } func isValidRelation(relation string, valid []string) bool { @@ -143,7 +144,7 @@ func isValidRelation(relation string, valid []string) bool { return false } -func FolderResourceRelation(relation string) string { +func SubresourceRelation(relation string) string { return TypeResource + "_" + relation } @@ -159,6 +160,10 @@ func NewFolderIdent(name string) string { return TypeFolderPrefix + name } +func NewTeamIdent(name string) string { + return TypeTeamPrefix + name +} + func NewGroupResourceIdent(group, resource, subresource string) string { return TypeGroupResoucePrefix + FormatGroupResource(group, resource, subresource) } @@ -193,21 +198,21 @@ func NewResourceTuple(subject, relation, group, resource, subresource, name stri } } -func isFolderResourceRelationSet(relation string) bool { - return relation == RelationFolderResourceSetView || - relation == RelationFolderResourceSetEdit || - relation == RelationFolderResourceSetAdmin +func isSubresourceRelationSet(relation string) bool { + return relation == RelationSubresourceSetView || + relation == RelationSubresourceSetEdit || + relation == RelationSubresourceSetAdmin } func NewFolderResourceTuple(subject, relation, group, resource, subresource, folder string) *openfgav1.TupleKey { - relation = FolderResourceRelation(relation) + relation = SubresourceRelation(relation) var condition *openfgav1.RelationshipCondition - if !isFolderResourceRelationSet(relation) { + if !isSubresourceRelationSet(relation) { condition = &openfgav1.RelationshipCondition{ - Name: "folder_group_filter", + Name: "subresource_filter", Context: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "group_resources": structpb.NewListValue(&structpb.ListValue{ + "subresources": structpb.NewListValue(&structpb.ListValue{ Values: []*structpb.Value{structpb.NewStringValue(FormatGroupResource(group, resource, subresource))}, }), }, @@ -223,6 +228,30 @@ func NewFolderResourceTuple(subject, relation, group, resource, subresource, fol } } +func NewTeamResourceTuple(subject, relation, group, resource, subresource, name string) *openfgav1.TupleKey { + relation = SubresourceRelation(relation) + var condition *openfgav1.RelationshipCondition + if !isSubresourceRelationSet(relation) { + condition = &openfgav1.RelationshipCondition{ + Name: "subresource_filter", + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "subresources": structpb.NewListValue(&structpb.ListValue{ + Values: []*structpb.Value{structpb.NewStringValue(FormatGroupResource(group, resource, subresource))}, + }), + }, + }, + } + } + + return &openfgav1.TupleKey{ + User: subject, + Relation: relation, + Object: NewTeamIdent(name), + Condition: condition, + } +} + func NewGroupResourceTuple(subject, relation, group, resource, subresource string) *openfgav1.TupleKey { return &openfgav1.TupleKey{ User: subject, diff --git a/pkg/services/authz/zanzana/schema/README.md b/pkg/services/authz/zanzana/schema/README.md index 3a41e96a11a..55425c151e7 100644 --- a/pkg/services/authz/zanzana/schema/README.md +++ b/pkg/services/authz/zanzana/schema/README.md @@ -16,7 +16,7 @@ them. This is because we want to store the folder tree relations. To grant a user access to a specific folder we store `{ “user”: “user:1”, relation: “read”, object:”folder:” }` To grant a user access to sub resources of a folder we store ``{ “user”: “user:1”, relation: “resource_read”, object:”folder:”}` with additional context. -This context holds all GroupResources in a list e.g. `{ "group_resources": ["dashboard.grafana.app/dashboards", "alerting.grafana.app/rules" ] }`. +This context holds all GroupResources in a list e.g. `{ "subresources": ["dashboard.grafana.app/dashboards", "alerting.grafana.app/rules" ] }`. ## Resource level permissions @@ -33,6 +33,15 @@ To grant a user access to the subresource of the specific resource we store foll It's also possible to grant user access to all subresources for specific resource type. It can be done with following tuple: `{ “user”: “user:1”, relation: “read”, object:”resource:dashboard.grafana.app/dashboards/” }`. +For the typed resources (like folders, users, teams, etc) subresources work in a little bit different way. Since typed resources only have ID in the name, subresources are added to the `subresource_filter`. For example, to grant user access to folder subresource, following tuple will be created: + +``` +{ “user”: “user:1”, relation: “resource_read”, object:”folder:” } +context: { "subresource_filter": ["folder.grafana.app/folders/"] } +``` + +Note that relation is translated from `read` to `resource_read`. This is required to distinguish access between resource and its subresources. When check request is performed, we check if request contains subresource. If so, context filter and translated relation are used. + ## Managed permissions In the RBAC model managed permissions stored as a special "managed" role permissions. OpenFGA model allows to assign permissions directly to users, so it produces following tuples: diff --git a/pkg/services/authz/zanzana/schema/schema.go b/pkg/services/authz/zanzana/schema/schema.go index b4c3b7fcd53..3c50bf0836d 100644 --- a/pkg/services/authz/zanzana/schema/schema.go +++ b/pkg/services/authz/zanzana/schema/schema.go @@ -13,6 +13,8 @@ var ( folderDSL string //go:embed schema_resource.fga resourceDSL string + //go:embed schema_subresource.fga + subresourceDSL string ) var SchemaModules = []transformer.ModuleFile{ @@ -28,4 +30,8 @@ var SchemaModules = []transformer.ModuleFile{ Name: "schema_resource.fga", Contents: resourceDSL, }, + { + Name: "schema_subresource.fga", + Contents: subresourceDSL, + }, } diff --git a/pkg/services/authz/zanzana/schema/schema_resource.fga b/pkg/services/authz/zanzana/schema/schema_resource.fga index b9893b2a997..3372c0bff21 100644 --- a/pkg/services/authz/zanzana/schema/schema_resource.fga +++ b/pkg/services/authz/zanzana/schema/schema_resource.fga @@ -1,19 +1,5 @@ module resource -extend type folder - relations - define resource_view: [user, service-account, team#member, role#assignee] or resource_edit or resource_view from parent - define resource_edit: [user, service-account, team#member, role#assignee] or resource_admin or resource_edit from parent - define resource_admin: [user, service-account, team#member, role#assignee] or resource_admin from parent - - define resource_get: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_view or resource_get from parent - define resource_create: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_create from parent - define resource_update: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_update from parent - define resource_delete: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_delete from parent - - define resource_get_permissions: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_get_permissions from parent - define resource_set_permissions: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_set_permissions from parent - type group_resource relations define view: [user, service-account, render, team#member, role#assignee] or edit @@ -44,7 +30,3 @@ type resource condition group_filter(requested_group: string, group_resource: string) { requested_group == group_resource } - -condition folder_group_filter(requested_group: string, group_resources: list) { - requested_group in group_resources -} diff --git a/pkg/services/authz/zanzana/schema/schema_subresource.fga b/pkg/services/authz/zanzana/schema/schema_subresource.fga new file mode 100644 index 00000000000..ca53a8cc541 --- /dev/null +++ b/pkg/services/authz/zanzana/schema/schema_subresource.fga @@ -0,0 +1,30 @@ +module resource + +extend type folder + relations + define resource_view: [user, service-account, team#member, role#assignee] or resource_edit or resource_view from parent + define resource_edit: [user, service-account, team#member, role#assignee] or resource_admin or resource_edit from parent + define resource_admin: [user, service-account, team#member, role#assignee] or resource_admin from parent + + define resource_get: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_view or resource_get from parent + define resource_create: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit or resource_create from parent + define resource_update: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit or resource_update from parent + define resource_delete: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit or resource_delete from parent + + define resource_get_permissions: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_admin or resource_get_permissions from parent + define resource_set_permissions: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_admin or resource_set_permissions from parent + +extend type team + relations + define resource_view: [user, service-account, team#member, role#assignee] or resource_edit + define resource_edit: [user, service-account, team#member, role#assignee] or resource_admin + define resource_admin: [user, service-account, team#member, role#assignee] + + define resource_get: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_view + define resource_create: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit + define resource_update: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit + define resource_delete: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit + +condition subresource_filter(subresource: string, subresources: list) { + subresource in subresources +} diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index 838a39aba83..1c5094158d2 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -76,6 +76,39 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation string, resou return &authzv1.CheckResponse{Allowed: false}, nil } + var ( + resourceIdent = resource.ResourceIdent() + resourceCtx = resource.Context() + subresourceRelation = common.SubresourceRelation(relation) + ) + + if resource.HasSubresource() { + // Check if subject has access as a subresource + res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + TupleKey: &openfgav1.CheckRequestTupleKey{ + User: subject, + Relation: subresourceRelation, + Object: resourceIdent, + }, + Context: resourceCtx, + ContextualTuples: contextuals, + }) + + if err != nil { + return nil, err + } + + if res.GetAllowed() { + return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil + } + } + + if resourceIdent == "" { + return &authzv1.CheckResponse{Allowed: false}, nil + } + // Check if subject has direct access to resource res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ StoreId: store.ID, @@ -83,7 +116,7 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation string, resou TupleKey: &openfgav1.CheckRequestTupleKey{ User: subject, Relation: relation, - Object: resource.ResourceIdent(), + Object: resourceIdent, }, ContextualTuples: contextuals, }) @@ -105,10 +138,10 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res var ( folderIdent = resource.FolderIdent() resourceCtx = resource.Context() - folderRelation = common.FolderResourceRelation(relation) + folderRelation = common.SubresourceRelation(relation) ) - if folderIdent != "" && common.IsFolderResourceRelation(folderRelation) { + if folderIdent != "" && common.IsSubresourceRelation(folderRelation) { // Check if subject has access as a sub resource for the folder res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ StoreId: store.ID, diff --git a/pkg/services/authz/zanzana/server/server_check_test.go b/pkg/services/authz/zanzana/server/server_check_test.go index 38cc1198e48..0f78a8712ea 100644 --- a/pkg/services/authz/zanzana/server/server_check_test.go +++ b/pkg/services/authz/zanzana/server/server_check_test.go @@ -152,4 +152,24 @@ func testCheck(t *testing.T, server *Server) { require.NoError(t, err) assert.False(t, res.GetAllowed()) }) + + t.Run("user:13 should be able to read folder status for all subfolders of folder 5", func(t *testing.T) { + res, err := server.Check(newContextWithNamespace(), newReq("user:13", utils.VerbGet, folderGroup, folderResource, statusSubresource, "", "5")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + + res, err = server.Check(newContextWithNamespace(), newReq("user:13", utils.VerbGet, folderGroup, folderResource, statusSubresource, "", "6")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + + res, err = server.Check(newContextWithNamespace(), newReq("user:13", utils.VerbGet, folderGroup, folderResource, statusSubresource, "", "4")) + require.NoError(t, err) + assert.False(t, res.GetAllowed()) + }) + + t.Run("user:14 should be able to read team subresources for team 1", func(t *testing.T) { + res, err := server.Check(newContextWithNamespace(), newReq("user:14", utils.VerbGet, "iam.grafana.app", "teams", statusSubresource, "", "1")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + }) } diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index c435cbc90cb..ba864645d82 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -56,6 +56,31 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour return &authzv1.ListResponse{}, nil } + var ( + subresourceRelation = common.SubresourceRelation(relation) + resourceCtx = resource.Context() + ) + + var items []string + if resource.HasSubresource() && common.IsSubresourceRelation(subresourceRelation) { + // List requested subresources + res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + Type: resource.Type(), + Relation: subresourceRelation, + User: subject, + Context: resourceCtx, + ContextualTuples: contextuals, + }) + + if err != nil { + return nil, err + } + + items = append(items, typedObjects(resource.Type(), res.GetObjects())...) + } + // List all resources user has access too res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{ StoreId: store.ID, @@ -68,21 +93,22 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour if err != nil { return nil, err } + items = append(items, typedObjects(resource.Type(), res.GetObjects())...) return &authzv1.ListResponse{ - Items: typedObjects(resource.Type(), res.GetObjects()), + Items: items, }, nil } func (s *Server) listGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, contextuals *openfgav1.ContextualTupleKeys, store *storeInfo) (*authzv1.ListResponse, error) { var ( - folderRelation = common.FolderResourceRelation(relation) + folderRelation = common.SubresourceRelation(relation) resourceCtx = resource.Context() ) // 1. List all folders subject has access to resource type in var folders []string - if common.IsFolderResourceRelation(folderRelation) { + if common.IsSubresourceRelation(folderRelation) { res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{ StoreId: store.ID, AuthorizationModelId: store.ModelID, diff --git a/pkg/services/authz/zanzana/server/server_list_test.go b/pkg/services/authz/zanzana/server/server_list_test.go index 92a115a8bed..02ca3ce9f37 100644 --- a/pkg/services/authz/zanzana/server/server_list_test.go +++ b/pkg/services/authz/zanzana/server/server_list_test.go @@ -118,4 +118,23 @@ func testList(t *testing.T, server *Server) { assert.Contains(t, res.GetFolders(), "5") assert.Contains(t, res.GetFolders(), "6") }) + + t.Run("user:13 should be able to list all subresources in folder 5 and 6", func(t *testing.T) { + res, err := server.List(newContextWithNamespace(), newList("user:13", folderGroup, folderResource, statusSubresource)) + require.NoError(t, err) + assert.Len(t, res.GetItems(), 2) + assert.Len(t, res.GetFolders(), 0) + + assert.Contains(t, res.GetItems(), "5") + assert.Contains(t, res.GetItems(), "6") + }) + + t.Run("user:14 should be able to list all subresources for team 1", func(t *testing.T) { + res, err := server.List(newContextWithNamespace(), newList("user:14", teamGroup, teamResource, statusSubresource)) + require.NoError(t, err) + assert.Len(t, res.GetItems(), 1) + assert.Len(t, res.GetFolders(), 0) + + assert.Contains(t, res.GetItems(), "1") + }) } diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 8a1fd1786ea..42770152e5e 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -29,6 +29,9 @@ const ( folderGroup = "folder.grafana.app" folderResource = "folders" + teamGroup = "iam.grafana.app" + teamResource = "teams" + statusSubresource = "status" ) @@ -83,31 +86,38 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { require.NoError(t, err) // seed tuples + writes := &openfgav1.WriteRequestWrites{ + TupleKeys: []*openfgav1.TupleKey{ + common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), + common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource, ""), + common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource, ""), + common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "", "1"), + common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "3"), + common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "1"), + common.NewFolderTuple("user:6", common.RelationGet, "1"), + common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource, ""), + common.NewFolderParentTuple("5", "4"), + common.NewFolderParentTuple("6", "5"), + common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "5"), + common.NewFolderResourceTuple("user:9", common.RelationCreate, dashboardGroup, dashboardResource, "", "5"), + common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "10"), + common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "11"), + common.NewGroupResourceTuple("user:11", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource), + common.NewFolderResourceTuple("user:12", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "5"), + common.NewFolderResourceTuple("user:13", common.RelationGet, folderGroup, folderResource, statusSubresource, "5"), + common.NewTeamResourceTuple("user:14", common.RelationGet, teamGroup, teamResource, statusSubresource, "1"), + }, + } + for _, w := range writes.TupleKeys { + t.Log(w.String()) + } + _, err = openfga.Write(context.Background(), &openfgav1.WriteRequest{ StoreId: storeInf.ID, AuthorizationModelId: storeInf.ModelID, - Writes: &openfgav1.WriteRequestWrites{ - TupleKeys: []*openfgav1.TupleKey{ - common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), - common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), - common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource, ""), - common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource, ""), - common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "", "1"), - common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), - common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "3"), - common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "1"), - common.NewFolderTuple("user:6", common.RelationGet, "1"), - common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource, ""), - common.NewFolderParentTuple("5", "4"), - common.NewFolderParentTuple("6", "5"), - common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "5"), - common.NewFolderResourceTuple("user:9", common.RelationCreate, dashboardGroup, dashboardResource, "", "5"), - common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "10"), - common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "11"), - common.NewGroupResourceTuple("user:11", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource), - common.NewFolderResourceTuple("user:12", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "5"), - }, - }, + Writes: writes, }) require.NoError(t, err) return srv diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index 327618384af..45a39ac09f2 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -38,20 +38,20 @@ const ( RelationCreate = common.RelationCreate RelationDelete = common.RelationDelete - RelationFolderResourceSetView = common.RelationFolderResourceSetView - RelationFolderResourceSetEdit = common.RelationFolderResourceSetEdit - RelationFolderResourceSetAdmin = common.RelationFolderResourceSetAdmin + RelationSubresourceSetView = common.RelationSubresourceSetView + RelationSubresourceSetEdit = common.RelationSubresourceSetEdit + RelationSubresourceSetAdmin = common.RelationSubresourceSetAdmin - RelationFolderResourceRead = common.RelationFolderResourceGet - RelationFolderResourceWrite = common.RelationFolderResourceUpdate - RelationFolderResourceCreate = common.RelationFolderResourceCreate - RelationFolderResourceDelete = common.RelationFolderResourceDelete + RelationSubresourceRead = common.RelationSubresourceGet + RelationSubresourceWrite = common.RelationSubresourceUpdate + RelationSubresourceCreate = common.RelationSubresourceCreate + RelationSubresourceDelete = common.RelationSubresourceDelete ) var ( - RelationsFolder = common.RelationsFolder - RelationsResouce = common.RelationsResource - RelationsFolderResource = common.RelationsFolderResource + RelationsFolder = common.RelationsFolder + RelationsResouce = common.RelationsResource + RelationsSubresource = common.RelationsSubresource ) const ( @@ -118,8 +118,8 @@ func IsFolderResourceTuple(t *openfgav1.TupleKey) bool { } func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) { - va := a.Condition.Context.Fields["group_resources"] - vb := b.Condition.Context.Fields["group_resources"] + va := a.Condition.Context.Fields["subresources"] + vb := b.Condition.Context.Fields["subresources"] va.GetListValue().Values = append(va.GetListValue().Values, vb.GetListValue().Values...) }