From 63a2ce72145064f88543f286790804a6f16a0b87 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 26 Mar 2025 16:07:01 +0100 Subject: [PATCH] Zanzana: Support subresources for users and service accounts (#102874) * Zanzana: Support subresources for users and service accounts * rename relationsFolder * fix linter error --- pkg/services/authz/zanzana/common/info.go | 14 ++++++-- pkg/services/authz/zanzana/common/tuple.go | 12 +++---- .../authz/zanzana/schema/schema_core.fga | 12 +++++++ .../zanzana/schema/schema_subresource.fga | 33 +++++++++++++++++++ .../authz/zanzana/server/server_check_test.go | 12 +++++++ .../authz/zanzana/server/server_list_test.go | 18 ++++++++++ .../authz/zanzana/server/server_test.go | 10 +++++- pkg/services/authz/zanzana/zanzana.go | 2 +- 8 files changed, 101 insertions(+), 12 deletions(-) diff --git a/pkg/services/authz/zanzana/common/info.go b/pkg/services/authz/zanzana/common/info.go index 65531c0129a..9506a8ac8d9 100644 --- a/pkg/services/authz/zanzana/common/info.go +++ b/pkg/services/authz/zanzana/common/info.go @@ -19,12 +19,22 @@ var typedResources = map[string]typeInfo{ folderalpha1.FolderResourceInfo.GroupResource().Group, folderalpha1.FolderResourceInfo.GroupResource().Resource, "", - ): {Type: "folder", Relations: RelationsFolder}, + ): {Type: "folder", Relations: RelationsTyped}, FormatGroupResource( iamalpha1.TeamResourceInfo.GroupResource().Group, iamalpha1.TeamResourceInfo.GroupResource().Resource, "", - ): {Type: "team", Relations: RelationsFolder}, + ): {Type: "team", Relations: RelationsTyped}, + FormatGroupResource( + iamalpha1.UserResourceInfo.GroupResource().Group, + iamalpha1.UserResourceInfo.GroupResource().Resource, + "", + ): {Type: "user", Relations: RelationsTyped}, + FormatGroupResource( + iamalpha1.ServiceAccountResourceInfo.GroupResource().Group, + iamalpha1.ServiceAccountResourceInfo.GroupResource().Resource, + "", + ): {Type: "service-account", Relations: RelationsTyped}, } 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 1369487e77a..d0a550107fc 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -92,8 +92,8 @@ var RelationsSubresource = []string{ RelationSubresourceSetPermissions, } -// RelationsFolder are relations that can be added on type "folder". -var RelationsFolder = append( +// RelationsTyped are relations that can be added to typed resources (folders, teams, users, etc). +var RelationsTyped = append( RelationsSubresource, RelationGet, RelationUpdate, @@ -160,10 +160,6 @@ 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) } @@ -228,7 +224,7 @@ func NewFolderResourceTuple(subject, relation, group, resource, subresource, fol } } -func NewTeamResourceTuple(subject, relation, group, resource, subresource, name string) *openfgav1.TupleKey { +func NewTypedResourceTuple(subject, relation, typ, group, resource, subresource, name string) *openfgav1.TupleKey { relation = SubresourceRelation(relation) var condition *openfgav1.RelationshipCondition if !isSubresourceRelationSet(relation) { @@ -247,7 +243,7 @@ func NewTeamResourceTuple(subject, relation, group, resource, subresource, name return &openfgav1.TupleKey{ User: subject, Relation: relation, - Object: NewTeamIdent(name), + Object: NewTypedIdent(typ, name), Condition: condition, } } diff --git a/pkg/services/authz/zanzana/schema/schema_core.fga b/pkg/services/authz/zanzana/schema/schema_core.fga index 81a5bf49257..d6d97b497c7 100644 --- a/pkg/services/authz/zanzana/schema/schema_core.fga +++ b/pkg/services/authz/zanzana/schema/schema_core.fga @@ -1,8 +1,16 @@ module core type user + relations + define get: [user, service-account, team#member, role#assignee] + define update: [user, service-account, team#member, role#assignee] + define delete: [user, service-account, team#member, role#assignee] type service-account + relations + define get: [user, service-account, team#member, role#assignee] + define update: [user, service-account, team#member, role#assignee] + define delete: [user, service-account, team#member, role#assignee] type render @@ -12,6 +20,10 @@ type role relations define assignee: [user, service-account, anonymous, team#member, role#assignee] + define get: [user, service-account, team#member, role#assignee] + define update: [user, service-account, team#member, role#assignee] + define delete: [user, service-account, team#member, role#assignee] + type team relations define admin: [user, service-account] diff --git a/pkg/services/authz/zanzana/schema/schema_subresource.fga b/pkg/services/authz/zanzana/schema/schema_subresource.fga index ca53a8cc541..b145075c6a6 100644 --- a/pkg/services/authz/zanzana/schema/schema_subresource.fga +++ b/pkg/services/authz/zanzana/schema/schema_subresource.fga @@ -25,6 +25,39 @@ extend type team 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 +extend type user + 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 + +extend type service-account + 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 + +extend type role + 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_test.go b/pkg/services/authz/zanzana/server/server_check_test.go index 0f78a8712ea..528eb483187 100644 --- a/pkg/services/authz/zanzana/server/server_check_test.go +++ b/pkg/services/authz/zanzana/server/server_check_test.go @@ -172,4 +172,16 @@ func testCheck(t *testing.T, server *Server) { require.NoError(t, err) assert.True(t, res.GetAllowed()) }) + + t.Run("user:15 should be able to read user subresources for user 1", func(t *testing.T) { + res, err := server.Check(newContextWithNamespace(), newReq("user:15", utils.VerbGet, "iam.grafana.app", "users", statusSubresource, "", "1")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + }) + + t.Run("user:16 should be able to read serviceaccount subresources for serviceaccount 1", func(t *testing.T) { + res, err := server.Check(newContextWithNamespace(), newReq("user:16", utils.VerbGet, "iam.grafana.app", "serviceaccounts", statusSubresource, "", "1")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + }) } diff --git a/pkg/services/authz/zanzana/server/server_list_test.go b/pkg/services/authz/zanzana/server/server_list_test.go index 02ca3ce9f37..2979e4118d4 100644 --- a/pkg/services/authz/zanzana/server/server_list_test.go +++ b/pkg/services/authz/zanzana/server/server_list_test.go @@ -137,4 +137,22 @@ func testList(t *testing.T, server *Server) { assert.Contains(t, res.GetItems(), "1") }) + + t.Run("user:15 should be able to list all subresources for user 1", func(t *testing.T) { + res, err := server.List(newContextWithNamespace(), newList("user:15", userGroup, userResource, statusSubresource)) + require.NoError(t, err) + assert.Len(t, res.GetItems(), 1) + assert.Len(t, res.GetFolders(), 0) + + assert.Contains(t, res.GetItems(), "1") + }) + + t.Run("user:16 should be able to list all subresources for service-account 1", func(t *testing.T) { + res, err := server.List(newContextWithNamespace(), newList("user:16", serviceAccountGroup, serviceAccountResource, 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 42770152e5e..ae035c474f4 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -32,6 +32,12 @@ const ( teamGroup = "iam.grafana.app" teamResource = "teams" + userGroup = "iam.grafana.app" + userResource = "users" + + serviceAccountGroup = "iam.grafana.app" + serviceAccountResource = "serviceaccounts" + statusSubresource = "status" ) @@ -107,7 +113,9 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { 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"), + common.NewTypedResourceTuple("user:14", common.RelationGet, common.TypeTeam, teamGroup, teamResource, statusSubresource, "1"), + common.NewTypedResourceTuple("user:15", common.RelationGet, common.TypeUser, userGroup, userResource, statusSubresource, "1"), + common.NewTypedResourceTuple("user:16", common.RelationGet, common.TypeServiceAccount, serviceAccountGroup, serviceAccountResource, statusSubresource, "1"), }, } for _, w := range writes.TupleKeys { diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index 45a39ac09f2..adf2dbab531 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -49,7 +49,7 @@ const ( ) var ( - RelationsFolder = common.RelationsFolder + RelationsFolder = common.RelationsTyped RelationsResouce = common.RelationsResource RelationsSubresource = common.RelationsSubresource )