diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index bf7d7a1fe33..cc94052438c 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -16,9 +16,12 @@ const ( TypeRenderService string = "render" TypeTeam string = "team" TypeRole string = "role" - TypeFolder string = "folder" - TypeResource string = "resource" - TypeNamespace string = "namespace" +) + +const ( + TypeFolder string = "folder" + TypeResource string = "resource" + TypeGroupResouce string = "group_resource" ) const ( @@ -46,8 +49,8 @@ const ( RelationFolderResourceDelete string = "resource_" + RelationDelete ) -// RelationsNamespace are relations that can be added on type "namespace". -var RelationsNamespace = []string{ +// RelationsGroupResource are relations that can be added on type "group_resource". +var RelationsGroupResource = []string{ RelationGet, RelationUpdate, RelationCreate, @@ -78,8 +81,8 @@ var RelationsFolder = append( RelationDelete, ) -func IsNamespaceRelation(relation string) bool { - return isValidRelation(relation, RelationsNamespace) +func IsGroupResourceRelation(relation string) bool { + return isValidRelation(relation, RelationsGroupResource) } func IsFolderResourceRelation(relation string) bool { @@ -115,8 +118,8 @@ func NewFolderIdent(name string) string { return fmt.Sprintf("%s:%s", TypeFolder, name) } -func NewNamespaceResourceIdent(group, resource string) string { - return fmt.Sprintf("%s:%s", TypeNamespace, FormatGroupResource(group, resource)) +func NewGroupResourceIdent(group, resource string) string { + return fmt.Sprintf("%s:%s", TypeGroupResouce, FormatGroupResource(group, resource)) } func FormatGroupResource(group, resource string) string { @@ -169,11 +172,11 @@ func NewFolderResourceTuple(subject, relation, group, resource, folder string) * } } -func NewNamespaceResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey { +func NewGroupResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey { return &openfgav1.TupleKey{ User: subject, Relation: relation, - Object: NewNamespaceResourceIdent(group, resource), + Object: NewGroupResourceIdent(group, resource), } } @@ -289,7 +292,7 @@ func AddRenderContext(req *openfgav1.CheckRequest) { req.ContextualTuples.TupleKeys = append(req.ContextualTuples.TupleKeys, &openfgav1.TupleKey{ User: req.TupleKey.User, Relation: RelationSetView, - Object: NewNamespaceResourceIdent( + Object: NewGroupResourceIdent( dashboardalpha1.DashboardResourceInfo.GroupResource().Group, dashboardalpha1.DashboardResourceInfo.GroupResource().Resource, ), diff --git a/pkg/services/authz/zanzana/schema/README.md b/pkg/services/authz/zanzana/schema/README.md index e2e82966b36..4462e0f18e0 100644 --- a/pkg/services/authz/zanzana/schema/README.md +++ b/pkg/services/authz/zanzana/schema/README.md @@ -2,10 +2,10 @@ Here's some notes about [OpenFGA authorization model](https://openfga.dev/docs/modeling/getting-started) (schema) using to model access control in Grafana. -## Namespace level permissions +## GroupResource level permissions -A relation to a namespace object grant access to all objects of the GroupResource in the entire namespace. -They take the form of `{ “user”: “user:1”, relation: “read”, object:”namespace:dashboard.grafana.app/dashboard” }`. This +A relation to a group_resource object grants access to all objects of the GroupResource. +They take the form of `{ “user”: “user:1”, relation: “read”, object:”group_resource:dashboard.grafana.app/dashboard” }`. This example would grant `user:1` access to all `dashboard.grafana.app/dashboard` in the namespace. ## Folder level permissions diff --git a/pkg/services/authz/zanzana/schema/schema_core.fga b/pkg/services/authz/zanzana/schema/schema_core.fga index d0a2ff71cc9..8dd5f382279 100644 --- a/pkg/services/authz/zanzana/schema/schema_core.fga +++ b/pkg/services/authz/zanzana/schema/schema_core.fga @@ -6,24 +6,12 @@ type service-account type render -type namespace - relations - define view: [user, service-account, render, team#member, role#assignee] or edit - define edit: [user, service-account, team#member, role#assignee] or admin - define admin: [user, service-account, team#member, role#assignee] - - define get: [user, service-account, render, team#member, role#assignee] or view - define create: [user, service-account, team#member, role#assignee] or edit - define update: [user, service-account, team#member, role#assignee] or edit - define delete: [user, service-account, team#member, role#assignee] or edit - type role relations define assignee: [user, service-account, team#member, role#assignee] type team relations - # Action sets define admin: [user, service-account] define member: [user, service-account] or admin diff --git a/pkg/services/authz/zanzana/schema/schema_resource.fga b/pkg/services/authz/zanzana/schema/schema_resource.fga index 9ef4e3ca79e..b97831d2163 100644 --- a/pkg/services/authz/zanzana/schema/schema_resource.fga +++ b/pkg/services/authz/zanzana/schema/schema_resource.fga @@ -11,6 +11,17 @@ extend type folder 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 +type group_resource + relations + define view: [user, service-account, render, team#member, role#assignee] or edit + define edit: [user, service-account, team#member, role#assignee] or admin + define admin: [user, service-account, team#member, role#assignee] + + define get: [user, service-account, render, team#member, role#assignee] or view + define create: [user, service-account, team#member, role#assignee] or edit + define update: [user, service-account, team#member, role#assignee] or edit + define delete: [user, service-account, team#member, role#assignee] or edit + type resource relations define view: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or edit diff --git a/pkg/services/authz/zanzana/server/server_batch_check.go b/pkg/services/authz/zanzana/server/server_batch_check.go index 86b78634a56..46ae37189a7 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check.go +++ b/pkg/services/authz/zanzana/server/server_batch_check.go @@ -56,7 +56,7 @@ func (s *Server) batchCheckItem( allowed, ok := groupResourceAccess[groupResource] if !ok { - res, err := s.checkNamespace(ctx, r.GetSubject(), relation, item.GetGroup(), item.GetResource(), store) + res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, item.GetGroup(), item.GetResource(), store) if err != nil { return nil, err } 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 ae8390ba951..5da2d2f76ee 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check_test.go +++ b/pkg/services/authz/zanzana/server/server_batch_check_test.go @@ -44,7 +44,7 @@ func testBatchCheck(t *testing.T, server *Server) { 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) { + t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through group_resource", func(t *testing.T) { groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) res, err := server.BatchCheck(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ {Name: "1", Folder: "1"}, @@ -108,7 +108,7 @@ func testBatchCheck(t *testing.T, server *Server) { 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) { + t.Run("user:7 should be able to read folder {1,2} through group_resource access", func(t *testing.T) { groupResource := zanzana.FormatGroupResource(folderGroup, folderResource) res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{ {Name: "1"}, diff --git a/pkg/services/authz/zanzana/server/server_capabilities.go b/pkg/services/authz/zanzana/server/server_capabilities.go index 3d3974cd9a8..f80ea3c5fb2 100644 --- a/pkg/services/authz/zanzana/server/server_capabilities.go +++ b/pkg/services/authz/zanzana/server/server_capabilities.go @@ -22,7 +22,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.RelationsResource)) for _, relation := range info.Relations { - res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) + res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (s *Server) capabilitiesTyped(ctx context.Context, r *authzextv1.Capabiliti func (s *Server) capabilitiesGeneric(ctx context.Context, r *authzextv1.CapabilitiesRequest, store *storeInfo) (*authzextv1.CapabilitiesResponse, error) { out := make([]string, 0, len(common.RelationsResource)) for _, relation := range common.RelationsResource { - res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) + res, err := s.checkGroupResource(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 051ff2c6ed1..f12bd2ea6d1 100644 --- a/pkg/services/authz/zanzana/server/server_capabilities_test.go +++ b/pkg/services/authz/zanzana/server/server_capabilities_test.go @@ -29,7 +29,7 @@ func testCapabilities(t *testing.T, server *Server) { assert.Equal(t, []string{common.RelationGet, common.RelationUpdate}, res.GetCapabilities()) }) - t.Run("user:2 should be able to read and write resource:dashboards.grafana.app/dashboards/1 through namespace", func(t *testing.T) { + t.Run("user:2 should be able to read and write resource:dashboards.grafana.app/dashboards/1 through group_resource", func(t *testing.T) { res, err := server.Capabilities(context.Background(), newReq("user:2", dashboardGroup, dashboardResource, "1", "1")) require.NoError(t, err) assert.Equal(t, []string{common.RelationGet, common.RelationUpdate}, res.GetCapabilities()) @@ -59,7 +59,7 @@ func testCapabilities(t *testing.T, server *Server) { assert.Equal(t, []string{common.RelationGet}, res.GetCapabilities()) }) - t.Run("user:7 should be able to read folder one through namespace access", func(t *testing.T) { + t.Run("user:7 should be able to read folder one through group_resource access", func(t *testing.T) { res, err := server.Capabilities(context.Background(), newReq("user:7", folderGroup, folderResource, "", "1")) require.NoError(t, err) assert.Equal(t, []string{common.RelationGet}, res.GetCapabilities()) diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index c97e7e808bc..f7e2f489a1b 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -21,7 +21,7 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C } relation := common.VerbMapping[r.GetVerb()] - res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) + res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) if err != nil { return nil, err } @@ -36,10 +36,10 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C return s.checkGeneric(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), r.GetName(), r.GetFolder(), store) } -// checkTyped checks on the root "namespace". If subject has access through the namespace they have access to -// every resource for that "GroupResource". -func (s *Server) checkNamespace(ctx context.Context, subject, relation, group, resource string, store *storeInfo) (*authzv1.CheckResponse, error) { - if !common.IsNamespaceRelation(relation) { +// checkGroupResource check if subject has access to the full "GroupResource", if they do they can access every object +// within it. +func (s *Server) checkGroupResource(ctx context.Context, subject, relation, group, resource string, store *storeInfo) (*authzv1.CheckResponse, error) { + if !common.IsGroupResourceRelation(relation) { return &authzv1.CheckResponse{Allowed: false}, nil } @@ -49,7 +49,7 @@ func (s *Server) checkNamespace(ctx context.Context, subject, relation, group, r TupleKey: &openfgav1.CheckRequestTupleKey{ User: subject, Relation: relation, - Object: common.NewNamespaceResourceIdent(group, resource), + Object: common.NewGroupResourceIdent(group, resource), }, } diff --git a/pkg/services/authz/zanzana/server/server_check_test.go b/pkg/services/authz/zanzana/server/server_check_test.go index 6d4c6e49a15..0177b8e22a3 100644 --- a/pkg/services/authz/zanzana/server/server_check_test.go +++ b/pkg/services/authz/zanzana/server/server_check_test.go @@ -35,7 +35,7 @@ func testCheck(t *testing.T, server *Server) { 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) { + t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/1 through group_resource", func(t *testing.T) { res, err := server.Check(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1")) require.NoError(t, err) assert.True(t, res.GetAllowed()) @@ -83,7 +83,7 @@ func testCheck(t *testing.T, server *Server) { assert.True(t, res.GetAllowed()) }) - t.Run("user:7 should be able to read folder one through namespace access", func(t *testing.T) { + t.Run("user:7 should be able to read folder one through group_resource access", func(t *testing.T) { res, err := server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "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 c1ee3aaa0bc..0baa35ba020 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -22,7 +22,7 @@ func (s *Server) List(ctx context.Context, r *authzextv1.ListRequest) (*authzext relation := common.VerbMapping[r.GetVerb()] - res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) + res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) if err != nil { return nil, err } diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 4327c55d602..a3b39913091 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -82,14 +82,14 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { TupleKeys: []*openfgav1.TupleKey{ common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "1"), common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "1"), - common.NewNamespaceResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource), - common.NewNamespaceResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource), + 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.NewNamespaceResourceTuple("user:7", common.RelationGet, folderGroup, folderResource), + 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"), diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index bb81103b86a..5160a3f83d9 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -18,7 +18,7 @@ const ( TypeRole = common.TypeRole TypeFolder = common.TypeFolder TypeResource = common.TypeResource - TypeNamespace = common.TypeNamespace + TypeNamespace = common.TypeGroupResouce ) const ( @@ -95,7 +95,7 @@ func TranslateToResourceTuple(subject string, action, kind, name string) (*openf } if name == "*" { - return common.NewNamespaceResourceTuple(subject, m.relation, translation.group, translation.resource), true + return common.NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource), true } if translation.typ == TypeResource {