diff --git a/pkg/services/authz/zanzana/common/info.go b/pkg/services/authz/zanzana/common/info.go index ad5be54940a..0517a0be84a 100644 --- a/pkg/services/authz/zanzana/common/info.go +++ b/pkg/services/authz/zanzana/common/info.go @@ -7,14 +7,15 @@ import ( ) type TypeInfo struct { - Type string + Type string + Relations []string } var typedResources = map[string]TypeInfo{ FormatGroupResource( folderalpha1.FolderResourceInfo.GroupResource().Group, folderalpha1.FolderResourceInfo.GroupResource().Resource, - ): {Type: "folder"}, + ): {Type: "folder", Relations: append(ResourceRelations, RelationCreate)}, } func GetTypeInfo(group, resource string) (TypeInfo, bool) { diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index c69b0c33d5b..2e413545232 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -50,7 +50,6 @@ const ( var ResourceRelations = []string{ RelationRead, RelationWrite, - RelationCreate, RelationDelete, RelationPermissionsRead, RelationPermissionsWrite, @@ -58,6 +57,7 @@ var ResourceRelations = []string{ var FolderRelations = append( ResourceRelations, + RelationCreate, RelationFolderResourceRead, RelationFolderResourceWrite, RelationFolderResourceCreate, diff --git a/pkg/services/authz/zanzana/schema/schema_resource.fga b/pkg/services/authz/zanzana/schema/schema_resource.fga index 932e92181a1..951e1c151d2 100644 --- a/pkg/services/authz/zanzana/schema/schema_resource.fga +++ b/pkg/services/authz/zanzana/schema/schema_resource.fga @@ -20,7 +20,6 @@ type resource define admin: [user with group_filter, team#member with group_filter, role#assignee with group_filter] define read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or view - define create: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit define write: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit define delete: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit define permissions_read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin diff --git a/pkg/services/authz/zanzana/server/server_batch_check.go b/pkg/services/authz/zanzana/server/server_batch_check.go index 7c96df5ae79..86b78634a56 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check.go +++ b/pkg/services/authz/zanzana/server/server_batch_check.go @@ -60,6 +60,8 @@ func (s *Server) batchCheckItem( if err != nil { return nil, err } + + allowed = res.GetAllowed() groupResourceAccess[groupResource] = res.GetAllowed() } diff --git a/pkg/services/authz/zanzana/server/server_batch_check_test.go b/pkg/services/authz/zanzana/server/server_batch_check_test.go index 08a5644848d..ae8390ba951 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check_test.go +++ b/pkg/services/authz/zanzana/server/server_batch_check_test.go @@ -13,10 +13,10 @@ import ( ) func testBatchCheck(t *testing.T, server *Server) { - newReq := func(subject, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest { + newReq := func(subject, verb, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest { for i, item := range items { items[i] = &authzextv1.BatchCheckItem{ - Verb: utils.VerbGet, + Verb: verb, Group: group, Resource: resource, Name: item.GetName(), @@ -32,89 +32,116 @@ func testBatchCheck(t *testing.T, server *Server) { } t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) { - groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) - res, err := server.BatchCheck(context.Background(), newReq("user:1", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) + res, err := server.BatchCheck(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "2"}, })) require.NoError(t, err) - require.Len(t, res.Groups[groupPrefix].Items, 2) + require.Len(t, res.Groups[groupResource].Items, 2) - assert.True(t, res.Groups[groupPrefix].Items["1"]) - assert.False(t, res.Groups[groupPrefix].Items["2"]) + assert.True(t, res.Groups[groupResource].Items["1"]) + assert.False(t, res.Groups[groupResource].Items["2"]) }) t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through namespace", func(t *testing.T) { - groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) - res, err := server.BatchCheck(context.Background(), newReq("user:2", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) + res, err := server.BatchCheck(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "2"}, })) require.NoError(t, err) - assert.Len(t, res.Groups[groupPrefix].Items, 2) + assert.Len(t, res.Groups[groupResource].Items, 2) }) t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) { - groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) - res, err := server.BatchCheck(context.Background(), newReq("user:3", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) + res, err := server.BatchCheck(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "2"}, })) require.NoError(t, err) - require.Len(t, res.Groups[groupPrefix].Items, 2) + require.Len(t, res.Groups[groupResource].Items, 2) - assert.True(t, res.Groups[groupPrefix].Items["1"]) - assert.False(t, res.Groups[groupPrefix].Items["2"]) + assert.True(t, res.Groups[groupResource].Items["1"]) + assert.False(t, res.Groups[groupResource].Items["2"]) }) t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { - groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) - res, err := server.BatchCheck(context.Background(), newReq("user:4", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) + res, err := server.BatchCheck(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "3"}, {Name: "3", Folder: "2"}, })) require.NoError(t, err) - require.Len(t, res.Groups[groupPrefix].Items, 3) + require.Len(t, res.Groups[groupResource].Items, 3) - assert.True(t, res.Groups[groupPrefix].Items["1"]) - assert.True(t, res.Groups[groupPrefix].Items["2"]) - assert.False(t, res.Groups[groupPrefix].Items["3"]) + assert.True(t, res.Groups[groupResource].Items["1"]) + assert.True(t, res.Groups[groupResource].Items["2"]) + assert.False(t, res.Groups[groupResource].Items["3"]) }) t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) { - groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) - res, err := server.BatchCheck(context.Background(), newReq("user:5", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) + res, err := server.BatchCheck(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, {Name: "2", Folder: "2"}, })) require.NoError(t, err) - require.Len(t, res.Groups[groupPrefix].Items, 2) + require.Len(t, res.Groups[groupResource].Items, 2) - assert.True(t, res.Groups[groupPrefix].Items["1"]) - assert.False(t, res.Groups[groupPrefix].Items["2"]) + assert.True(t, res.Groups[groupResource].Items["1"]) + assert.False(t, res.Groups[groupResource].Items["2"]) }) t.Run("user:6 should be able to read folder 1", func(t *testing.T) { - groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource) - res, err := server.BatchCheck(context.Background(), newReq("user:6", folderGroup, folderResource, []*authzextv1.BatchCheckItem{ + groupResource := zanzana.FormatGroupResource(folderGroup, folderResource) + res, err := server.BatchCheck(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{ {Name: "1"}, {Name: "2"}, })) require.NoError(t, err) - require.Len(t, res.Groups[groupPrefix].Items, 2) + require.Len(t, res.Groups[groupResource].Items, 2) - assert.True(t, res.Groups[groupPrefix].Items["1"]) - assert.False(t, res.Groups[groupPrefix].Items["2"]) + assert.True(t, res.Groups[groupResource].Items["1"]) + assert.False(t, res.Groups[groupResource].Items["2"]) }) t.Run("user:7 should be able to read folder {1,2} through namespace access", func(t *testing.T) { - groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource) - res, err := server.BatchCheck(context.Background(), newReq("user:7", folderGroup, folderResource, []*authzextv1.BatchCheckItem{ + groupResource := zanzana.FormatGroupResource(folderGroup, folderResource) + res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{ {Name: "1"}, {Name: "2"}, })) require.NoError(t, err) - require.Len(t, res.Groups[groupPrefix].Items, 2) + require.Len(t, res.Groups[groupResource].Items, 2) + require.True(t, res.Groups[groupResource].Items["1"]) + require.True(t, res.Groups[groupResource].Items["2"]) + }) + + t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) { + groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) + res, err := server.BatchCheck(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + {Name: "10", Folder: "6"}, + {Name: "20", Folder: "6"}, + })) + require.NoError(t, err) + require.Len(t, res.Groups[groupResource].Items, 2) + require.True(t, res.Groups[groupResource].Items["10"]) + require.True(t, res.Groups[groupResource].Items["20"]) + }) + + t.Run("user:9 should be able to create dashboards in folder 6 through folder 5", func(t *testing.T) { + groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) + res, err := server.BatchCheck(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + {Name: "10", Folder: "6"}, + {Name: "20", Folder: "6"}, + })) + require.NoError(t, err) + t.Log(res.Groups) + require.Len(t, res.Groups[groupResource].Items, 2) + require.True(t, res.Groups[groupResource].Items["10"]) + require.True(t, res.Groups[groupResource].Items["20"]) }) } diff --git a/pkg/services/authz/zanzana/server/server_capabilities.go b/pkg/services/authz/zanzana/server/server_capabilities.go index fa62860e760..4d63841137d 100644 --- a/pkg/services/authz/zanzana/server/server_capabilities.go +++ b/pkg/services/authz/zanzana/server/server_capabilities.go @@ -21,7 +21,7 @@ func (s *Server) Capabilities(ctx context.Context, r *authzextv1.CapabilitiesReq func (s *Server) capabilitiesTyped(ctx context.Context, r *authzextv1.CapabilitiesRequest, info common.TypeInfo, store *storeInfo) (*authzextv1.CapabilitiesResponse, error) { out := make([]string, 0, len(common.ResourceRelations)) - for _, relation := range common.ResourceRelations { + for _, relation := range info.Relations { res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) if err != nil { return nil, err diff --git a/pkg/services/authz/zanzana/server/server_capabilities_test.go b/pkg/services/authz/zanzana/server/server_capabilities_test.go index 4cf7f979ac7..c1d43d8a61a 100644 --- a/pkg/services/authz/zanzana/server/server_capabilities_test.go +++ b/pkg/services/authz/zanzana/server/server_capabilities_test.go @@ -50,7 +50,7 @@ func testCapabilities(t *testing.T, server *Server) { t.Run("user:5 should be able to read, write, create and delete resource:dashboards.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) { res, err := server.Capabilities(context.Background(), newReq("user:5", dashboardGroup, dashboardResource, "1", "1")) require.NoError(t, err) - assert.Equal(t, []string{common.RelationRead, common.RelationWrite, common.RelationCreate, common.RelationDelete}, res.GetCapabilities()) + assert.Equal(t, []string{common.RelationRead, common.RelationWrite, common.RelationDelete}, res.GetCapabilities()) }) t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) { diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index cad15cb17ef..9c4de93eb68 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -83,28 +83,32 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation, name string, func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, resource, name, folder string, store *storeInfo) (*authzv1.CheckResponse, error) { groupResource := structpb.NewStringValue(common.FormatGroupResource(group, resource)) - // Check if subject has direct access to resource - res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - TupleKey: &openfgav1.CheckRequestTupleKey{ - User: subject, - Relation: relation, - Object: common.NewResourceIdent(group, resource, name), - }, - Context: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "requested_group": groupResource, + // Create relation can only exist on namespace or folder level. + // So we skip direct resource access check. + if relation != common.RelationCreate { + // Check if subject has direct access to resource + res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + TupleKey: &openfgav1.CheckRequestTupleKey{ + User: subject, + Relation: relation, + Object: common.NewResourceIdent(group, resource, name), }, - }, - }) + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "requested_group": groupResource, + }, + }, + }) - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - if res.GetAllowed() { - return &authzv1.CheckResponse{Allowed: true}, nil + if res.GetAllowed() { + return &authzv1.CheckResponse{Allowed: true}, nil + } } if folder == "" { @@ -112,7 +116,7 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res } // Check if subject has access as a sub resource for the folder - res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{ + res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ StoreId: store.ID, AuthorizationModelId: store.ModelID, TupleKey: &openfgav1.CheckRequestTupleKey{ diff --git a/pkg/services/authz/zanzana/server/server_check_test.go b/pkg/services/authz/zanzana/server/server_check_test.go index 0dab21d47fb..6d4c6e49a15 100644 --- a/pkg/services/authz/zanzana/server/server_check_test.go +++ b/pkg/services/authz/zanzana/server/server_check_test.go @@ -12,11 +12,11 @@ import ( ) func testCheck(t *testing.T, server *Server) { - newRead := func(subject, group, resource, folder, name string) *authzv1.CheckRequest { + newReq := func(subject, verb, group, resource, folder, name string) *authzv1.CheckRequest { return &authzv1.CheckRequest{ Namespace: namespace, Subject: subject, - Verb: utils.VerbGet, + Verb: verb, Group: group, Resource: resource, Name: name, @@ -25,85 +25,91 @@ func testCheck(t *testing.T, server *Server) { } t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) { - res, err := server.Check(context.Background(), newRead("user:1", dashboardGroup, dashboardResource, "1", "1")) + res, err := server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) // sanity check - res, err = server.Check(context.Background(), newRead("user:1", dashboardGroup, dashboardResource, "1", "2")) + res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "1", "2")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/1 through namespace", func(t *testing.T) { - res, err := server.Check(context.Background(), newRead("user:2", dashboardGroup, dashboardResource, "1", "1")) + res, err := server.Check(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) { - res, err := server.Check(context.Background(), newRead("user:3", dashboardGroup, dashboardResource, "1", "1")) + res, err := server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) // sanity check - res, err = server.Check(context.Background(), newRead("user:3", dashboardGroup, dashboardResource, "1", "2")) + res, err = server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "1", "2")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { - res, err := server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "1", "1")) + res, err := server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "3", "2")) + res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "3", "2")) require.NoError(t, err) assert.True(t, res.GetAllowed()) // sanity check - res, err = server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "1", "2")) + res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "1", "2")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "2", "2")) + res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "2", "2")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) { - res, err := server.Check(context.Background(), newRead("user:5", dashboardGroup, dashboardResource, "1", "1")) + res, err := server.Check(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) { - res, err := server.Check(context.Background(), newRead("user:6", folderGroup, folderResource, "", "1")) + res, err := server.Check(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:7 should be able to read folder one through namespace access", func(t *testing.T) { - res, err := server.Check(context.Background(), newRead("user:7", folderGroup, folderResource, "", "1")) + res, err := server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newRead("user:7", folderGroup, folderResource, "", "10")) + res, err = server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "10")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboar in folder 6 through folder 5", func(t *testing.T) { - res, err := server.Check(context.Background(), newRead("user:8", dashboardGroup, dashboardResource, "6", "10")) + res, err := server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "6", "10")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newRead("user:8", dashboardGroup, dashboardResource, "5", "11")) + res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "5", "11")) require.NoError(t, err) assert.True(t, res.GetAllowed()) - res, err = server.Check(context.Background(), newRead("user:8", folderGroup, folderResource, "4", "12")) + res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, folderGroup, folderResource, "4", "12")) require.NoError(t, err) assert.False(t, res.GetAllowed()) }) + + t.Run("user:9 should be able to create dashboards in folder 5", func(t *testing.T) { + res, err := server.Check(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "5", "")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + }) } diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index cbbe6c955d5..7b5e1709a66 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -92,7 +92,8 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { common.NewNamespaceResourceTuple("user:7", "read", folderGroup, folderResource), common.NewFolderParentTuple("5", "4"), common.NewFolderParentTuple("6", "5"), - common.NewFolderResourceTuple("user:8", "view", dashboardGroup, dashboardResource, "5"), + common.NewFolderResourceTuple("user:8", "edit", dashboardGroup, dashboardResource, "5"), + common.NewFolderResourceTuple("user:9", "create", dashboardGroup, dashboardResource, "5"), }, }, })