diff --git a/go.mod b/go.mod index c9c72bcfffa..2dc129f0a1c 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,7 @@ require ( github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad github.com/grafana/alerting v0.0.0-20241211182001-0f317eb6b2f7 // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8 // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f // @grafana/identity-access-team github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 // @grafana/identity-access-team github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d // @grafana/dataviz-squad github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code diff --git a/go.sum b/go.sum index aae520af318..9ec99010343 100644 --- a/go.sum +++ b/go.sum @@ -2278,8 +2278,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20241211182001-0f317eb6b2f7 h1:VGLUQ2mwzlF1NGwTxpSfv1RnuOsDlNh/NT5KRvhZ0sQ= github.com/grafana/alerting v0.0.0-20241211182001-0f317eb6b2f7/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8 h1:ctiV5lFI0zdccm4uRLSHIpdUukLvDNc9KGtYKal2eXE= -github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI= +github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f h1:BcgUTu26JtOudfpQ8LoLpZNV2CdMEyhLUZweCUgETZw= +github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI= github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk= github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= diff --git a/go.work.sum b/go.work.sum index 2e694d503fd..b894f6ed7c0 100644 --- a/go.work.sum +++ b/go.work.sum @@ -656,6 +656,7 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw= github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index bfb99716434..7c8b0d59039 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.23.1 require ( - github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8 // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f // @grafana/identity-access-team github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 // @grafana/identity-access-team github.com/stretchr/testify v1.10.0 k8s.io/apimachinery v0.32.0 diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 13db00d3704..646c090d037 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -28,8 +28,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8 h1:ctiV5lFI0zdccm4uRLSHIpdUukLvDNc9KGtYKal2eXE= -github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI= +github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f h1:BcgUTu26JtOudfpQ8LoLpZNV2CdMEyhLUZweCUgETZw= +github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI= github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk= github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/pkg/services/accesscontrol/dualwrite/collectors.go b/pkg/services/accesscontrol/dualwrite/collectors.go index 095ee20790f..929c0e2a062 100644 --- a/pkg/services/accesscontrol/dualwrite/collectors.go +++ b/pkg/services/accesscontrol/dualwrite/collectors.go @@ -394,6 +394,19 @@ 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. + if zanzana.IsFolderResourceTuple(tuple) { + key := tupleStringWithoutCondition(tuple) + if t, ok := tuples[tuple.Object][key]; ok { + zanzana.MergeFolderResourceTuples(t, tuple) + } else { + tuples[tuple.Object][key] = tuple + } + + continue + } + tuples[tuple.Object][tuple.String()] = tuple } diff --git a/pkg/services/authz/zanzana/common/info.go b/pkg/services/authz/zanzana/common/info.go index 5ec718d9933..5383c8b7af3 100644 --- a/pkg/services/authz/zanzana/common/info.go +++ b/pkg/services/authz/zanzana/common/info.go @@ -1,46 +1,122 @@ package common import ( - "github.com/grafana/grafana/pkg/apimachinery/utils" + "google.golang.org/protobuf/types/known/structpb" + authzv1 "github.com/grafana/authlib/authz/proto/v1" folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) -type TypeInfo struct { +type typeInfo struct { Type string Relations []string } -func (t TypeInfo) IsValidRelation(relation string) bool { - return isValidRelation(relation, t.Relations) -} - -var typedResources = map[string]TypeInfo{ +var typedResources = map[string]typeInfo{ FormatGroupResource( folderalpha1.FolderResourceInfo.GroupResource().Group, folderalpha1.FolderResourceInfo.GroupResource().Resource, + "", ): {Type: "folder", Relations: RelationsFolder}, } -func GetTypeInfo(group, resource string) (TypeInfo, bool) { - info, ok := typedResources[FormatGroupResource(group, resource)] +func getTypeInfo(group, resource string) (typeInfo, bool) { + info, ok := typedResources[FormatGroupResource(group, resource, "")] return info, ok } -var VerbMapping = map[string]string{ - utils.VerbGet: RelationGet, - utils.VerbList: RelationGet, - utils.VerbWatch: RelationGet, - utils.VerbCreate: RelationCreate, - utils.VerbUpdate: RelationUpdate, - utils.VerbPatch: RelationUpdate, - utils.VerbDelete: RelationDelete, - utils.VerbDeleteCollection: RelationDelete, +func NewResourceInfoFromCheck(r *authzv1.CheckRequest) ResourceInfo { + if info, ok := getTypeInfo(r.GetGroup(), r.GetResource()); ok { + return newResource(info.Type, r.GetGroup(), r.GetResource(), r.GetName(), r.GetFolder(), r.GetSubresource(), info.Relations) + } + return newResource(TypeResource, r.GetGroup(), r.GetResource(), r.GetName(), r.GetFolder(), r.GetSubresource(), RelationsResource) } -var RelationToVerbMapping = map[string]string{ - RelationGet: utils.VerbGet, - RelationCreate: utils.VerbCreate, - RelationUpdate: utils.VerbUpdate, - RelationDelete: utils.VerbDelete, +func NewResourceInfoFromBatchItem(i *authzextv1.BatchCheckItem) ResourceInfo { + if info, ok := getTypeInfo(i.GetGroup(), i.GetResource()); ok { + return newResource(info.Type, i.GetGroup(), i.GetResource(), i.GetName(), i.GetFolder(), i.GetSubresource(), info.Relations) + } + return newResource(TypeResource, i.GetGroup(), i.GetResource(), i.GetName(), i.GetFolder(), i.GetSubresource(), RelationsResource) +} + +func NewResourceInfoFromList(r *authzv1.ListRequest) ResourceInfo { + if info, ok := getTypeInfo(r.GetGroup(), r.GetResource()); ok { + return newResource(info.Type, r.GetGroup(), r.GetResource(), "", "", r.GetSubresource(), info.Relations) + } + return newResource(TypeResource, r.GetGroup(), r.GetResource(), "", "", r.GetSubresource(), RelationsResource) +} + +func newResource(typ string, group, resource, name, folder, subresource string, relations []string) ResourceInfo { + return ResourceInfo{ + typ: typ, + group: group, + resource: resource, + name: name, + folder: folder, + subresource: subresource, + relations: relations, + } +} + +type ResourceInfo struct { + typ string + group string + resource string + name string + folder string + subresource string + relations []string +} + +func (r ResourceInfo) GroupResource() string { + return FormatGroupResource(r.group, r.resource, r.subresource) +} + +func (r ResourceInfo) GroupResourceIdent() string { + return NewGroupResourceIdent(r.group, r.resource, r.subresource) +} + +func (r ResourceInfo) ResourceIdent() string { + if r.name == "" { + return "" + } + + if r.IsGeneric() { + return NewResourceIdent(r.group, r.resource, r.subresource, r.name) + } + + return NewTypedIdent(r.typ, r.name) +} + +func (r ResourceInfo) FolderIdent() string { + if r.folder == "" { + return "" + } + + return NewFolderIdent(r.folder) +} + +func (r ResourceInfo) IsGeneric() bool { + return r.typ == TypeResource +} + +func (r ResourceInfo) Type() string { + return r.typ +} + +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()), + }, + } +} + +func (r ResourceInfo) IsValidRelation(relation string) bool { + return isValidRelation(relation, r.relations) } diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index 59b20741fb9..3e15c9f97fa 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -6,10 +6,13 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "google.golang.org/protobuf/types/known/structpb" + "github.com/grafana/grafana/pkg/apimachinery/utils" dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) +const ClusterNamespace = "cluster" + const ( TypeUser string = "user" TypeServiceAccount string = "service-account" @@ -82,7 +85,25 @@ var RelationsFolder = append( RelationDelete, ) -const ClusterNamespace = "cluster" +// VerbMapping is mapping a k8s verb to a zanzana relation. +var VerbMapping = map[string]string{ + utils.VerbGet: RelationGet, + utils.VerbList: RelationGet, + utils.VerbWatch: RelationGet, + utils.VerbCreate: RelationCreate, + utils.VerbUpdate: RelationUpdate, + utils.VerbPatch: RelationUpdate, + utils.VerbDelete: RelationDelete, + utils.VerbDeleteCollection: RelationDelete, +} + +// RelationToVerbMapping is mapping a zanzana relation to k8s verb. +var RelationToVerbMapping = map[string]string{ + RelationGet: utils.VerbGet, + RelationCreate: utils.VerbCreate, + RelationUpdate: utils.VerbUpdate, + RelationDelete: utils.VerbDelete, +} func IsGroupResourceRelation(relation string) bool { return isValidRelation(relation, RelationsGroupResource) @@ -92,10 +113,6 @@ func IsFolderResourceRelation(relation string) bool { return isValidRelation(relation, RelationsFolderResource) } -func IsResourceRelation(relation string) bool { - return isValidRelation(relation, RelationsResource) -} - func isValidRelation(relation string, valid []string) bool { for _, r := range valid { if r == relation { @@ -113,32 +130,36 @@ 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", TypeResource, FormatGroupResource(group, resource), name) +func NewResourceIdent(group, resource, subresource, name string) string { + return fmt.Sprintf("%s:%s/%s", TypeResource, FormatGroupResource(group, resource, subresource), name) } func NewFolderIdent(name string) string { return fmt.Sprintf("%s:%s", TypeFolder, name) } -func NewGroupResourceIdent(group, resource string) string { - return fmt.Sprintf("%s:%s", TypeGroupResouce, FormatGroupResource(group, resource)) +func NewGroupResourceIdent(group, resource, subresource string) string { + return fmt.Sprintf("%s:%s", TypeGroupResouce, FormatGroupResource(group, resource, subresource)) } -func FormatGroupResource(group, resource string) string { +func FormatGroupResource(group, resource, subresource string) string { + if subresource != "" { + return fmt.Sprintf("%s/%s/%s", group, resource, subresource) + } + return fmt.Sprintf("%s/%s", group, resource) } -func NewResourceTuple(subject, relation, group, resource, name string) *openfgav1.TupleKey { +func NewResourceTuple(subject, relation, group, resource, subresource, name string) *openfgav1.TupleKey { return &openfgav1.TupleKey{ User: subject, Relation: relation, - Object: NewResourceIdent(group, resource, name), + Object: NewResourceIdent(group, resource, subresource, name), Condition: &openfgav1.RelationshipCondition{ Name: "group_filter", Context: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "group_resource": structpb.NewStringValue(FormatGroupResource(group, resource)), + "group_resource": structpb.NewStringValue(FormatGroupResource(group, resource, subresource)), }, }, }, @@ -151,7 +172,7 @@ func isFolderResourceRelationSet(relation string) bool { relation == RelationFolderResourceSetAdmin } -func NewFolderResourceTuple(subject, relation, group, resource, folder string) *openfgav1.TupleKey { +func NewFolderResourceTuple(subject, relation, group, resource, subresource, folder string) *openfgav1.TupleKey { relation = FolderResourceRelation(relation) var condition *openfgav1.RelationshipCondition if !isFolderResourceRelationSet(relation) { @@ -160,7 +181,7 @@ func NewFolderResourceTuple(subject, relation, group, resource, folder string) * Context: &structpb.Struct{ Fields: map[string]*structpb.Value{ "group_resources": structpb.NewListValue(&structpb.ListValue{ - Values: []*structpb.Value{structpb.NewStringValue(FormatGroupResource(group, resource))}, + Values: []*structpb.Value{structpb.NewStringValue(FormatGroupResource(group, resource, subresource))}, }), }, }, @@ -175,18 +196,18 @@ func NewFolderResourceTuple(subject, relation, group, resource, folder string) * } } -func NewGroupResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey { +func NewGroupResourceTuple(subject, relation, group, resource, subresource string) *openfgav1.TupleKey { return &openfgav1.TupleKey{ User: subject, Relation: relation, - Object: NewGroupResourceIdent(group, resource), + Object: NewGroupResourceIdent(group, resource, subresource), } } func NewFolderParentTuple(folder, parent string) *openfgav1.TupleKey { return &openfgav1.TupleKey{ Object: NewFolderIdent(folder), - Relation: "parent", + Relation: RelationParent, User: NewFolderIdent(parent), } } @@ -306,14 +327,7 @@ func AddRenderContext(req *openfgav1.CheckRequest) { Object: NewGroupResourceIdent( dashboardalpha1.DashboardResourceInfo.GroupResource().Group, dashboardalpha1.DashboardResourceInfo.GroupResource().Resource, + "", ), }) } - -func NewResourceContext(group, resource string) *structpb.Struct { - return &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "requested_group": structpb.NewStringValue(FormatGroupResource(group, resource)), - }, - } -} diff --git a/pkg/services/authz/zanzana/server/server_batch_check.go b/pkg/services/authz/zanzana/server/server_batch_check.go index 46ae37189a7..f9fd378c681 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check.go +++ b/pkg/services/authz/zanzana/server/server_batch_check.go @@ -30,7 +30,7 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest return nil, err } - groupResource := common.FormatGroupResource(item.GetGroup(), item.GetResource()) + groupResource := common.FormatGroupResource(item.GetGroup(), item.GetResource(), item.GetSubresource()) if _, ok := batchRes.Groups[groupResource]; !ok { batchRes.Groups[groupResource] = &authzextv1.BatchCheckGroupResource{ Items: make(map[string]bool), @@ -51,12 +51,13 @@ func (s *Server) batchCheckItem( ) (*authzv1.CheckResponse, error) { var ( relation = common.VerbMapping[item.GetVerb()] - groupResource = common.FormatGroupResource(item.GetGroup(), item.GetResource()) + resource = common.NewResourceInfoFromBatchItem(item) + groupResource = resource.GroupResource() ) allowed, ok := groupResourceAccess[groupResource] if !ok { - res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, item.GetGroup(), item.GetResource(), store) + res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, store) if err != nil { return nil, err } @@ -69,8 +70,9 @@ func (s *Server) batchCheckItem( return &authzv1.CheckResponse{Allowed: true}, nil } - if info, ok := common.GetTypeInfo(item.GetGroup(), item.GetResource()); ok { - return s.checkTyped(ctx, r.GetSubject(), relation, item.GetName(), info, store) + if resource.IsGeneric() { + return s.checkGeneric(ctx, r.GetSubject(), relation, resource, store) } - return s.checkGeneric(ctx, r.GetSubject(), relation, item.GetGroup(), item.GetResource(), item.GetName(), item.GetFolder(), store) + + return s.checkTyped(ctx, r.GetSubject(), relation, resource, store) } 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 5da2d2f76ee..23222f85157 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check_test.go +++ b/pkg/services/authz/zanzana/server/server_batch_check_test.go @@ -9,18 +9,19 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) func testBatchCheck(t *testing.T, server *Server) { - newReq := func(subject, verb, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest { + newReq := func(subject, verb, group, resource, subresource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest { for i, item := range items { items[i] = &authzextv1.BatchCheckItem{ - Verb: verb, - Group: group, - Resource: resource, - Name: item.GetName(), - Folder: item.GetFolder(), + Verb: verb, + Group: group, + Resource: resource, + Subresource: subresource, + Name: item.GetName(), + Folder: item.GetFolder(), } } @@ -32,8 +33,8 @@ 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) { - groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) - res, err := server.BatchCheck(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + groupResource := common.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"}, })) @@ -45,8 +46,8 @@ func testBatchCheck(t *testing.T, server *Server) { }) 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{ + groupResource := common.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"}, })) @@ -55,8 +56,8 @@ func testBatchCheck(t *testing.T, server *Server) { }) t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) { - groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) - res, err := server.BatchCheck(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + groupResource := common.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"}, })) @@ -68,8 +69,8 @@ func testBatchCheck(t *testing.T, server *Server) { }) t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { - groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) - res, err := server.BatchCheck(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + groupResource := common.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"}, @@ -83,8 +84,8 @@ func testBatchCheck(t *testing.T, server *Server) { }) t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) { - groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource) - res, err := server.BatchCheck(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ + groupResource := common.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"}, })) @@ -96,8 +97,8 @@ func testBatchCheck(t *testing.T, server *Server) { }) t.Run("user:6 should be able to read folder 1", func(t *testing.T) { - groupResource := zanzana.FormatGroupResource(folderGroup, folderResource) - res, err := server.BatchCheck(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{ + groupResource := common.FormatGroupResource(folderGroup, folderResource, "") + res, err := server.BatchCheck(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{ {Name: "1"}, {Name: "2"}, })) @@ -109,8 +110,8 @@ func testBatchCheck(t *testing.T, server *Server) { }) 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{ + groupResource := common.FormatGroupResource(folderGroup, folderResource, "") + res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{ {Name: "1"}, {Name: "2"}, })) @@ -121,8 +122,8 @@ func testBatchCheck(t *testing.T, server *Server) { }) 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{ + groupResource := common.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"}, })) @@ -133,8 +134,8 @@ func testBatchCheck(t *testing.T, server *Server) { }) 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{ + groupResource := common.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"}, })) @@ -144,4 +145,50 @@ func testBatchCheck(t *testing.T, server *Server) { require.True(t, res.Groups[groupResource].Items["10"]) require.True(t, res.Groups[groupResource].Items["20"]) }) + + t.Run("user:10 should be able to get dashboard status for 10 and 11", func(t *testing.T) { + groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource) + res, err := server.BatchCheck(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ + {Name: "10", Folder: "6"}, + {Name: "11", Folder: "6"}, + {Name: "12", Folder: "6"}, + })) + require.NoError(t, err) + t.Log(res.Groups) + require.Len(t, res.Groups[groupResource].Items, 3) + require.True(t, res.Groups[groupResource].Items["10"]) + require.True(t, res.Groups[groupResource].Items["11"]) + require.False(t, res.Groups[groupResource].Items["12"]) + }) + + t.Run("user:11 should be able to get dashboard status for 10, 11 and 12 through group_resource", func(t *testing.T) { + groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource) + res, err := server.BatchCheck(context.Background(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ + {Name: "10", Folder: "6"}, + {Name: "11", Folder: "6"}, + {Name: "12", Folder: "6"}, + })) + require.NoError(t, err) + t.Log(res.Groups) + require.Len(t, res.Groups[groupResource].Items, 3) + require.True(t, res.Groups[groupResource].Items["10"]) + require.True(t, res.Groups[groupResource].Items["11"]) + require.True(t, res.Groups[groupResource].Items["12"]) + }) + + t.Run("user:12 should be able to get dashboard status in folder 5 and 6", func(t *testing.T) { + groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource) + res, err := server.BatchCheck(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ + {Name: "10", Folder: "5"}, + {Name: "11", Folder: "6"}, + {Name: "12", Folder: "6"}, + {Name: "13", Folder: "1"}, + })) + require.NoError(t, err) + require.Len(t, res.Groups[groupResource].Items, 4) + require.True(t, res.Groups[groupResource].Items["10"]) + require.True(t, res.Groups[groupResource].Items["11"]) + require.True(t, res.Groups[groupResource].Items["12"]) + require.False(t, res.Groups[groupResource].Items["13"]) + }) } diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index a59d8f9ef29..90ea4fbd9a0 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -21,7 +21,9 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C } relation := common.VerbMapping[r.GetVerb()] - res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) + + resource := common.NewResourceInfoFromCheck(r) + res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, store) if err != nil { return nil, err } @@ -30,15 +32,16 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C return res, nil } - if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok { - return s.checkTyped(ctx, r.GetSubject(), relation, r.GetName(), info, store) + if resource.IsGeneric() { + return s.checkGeneric(ctx, r.GetSubject(), relation, resource, store) } - return s.checkGeneric(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), r.GetName(), r.GetFolder(), store) + + return s.checkTyped(ctx, r.GetSubject(), relation, resource, store) } // 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) { +func (s *Server) checkGroupResource(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.CheckResponse, error) { if !common.IsGroupResourceRelation(relation) { return &authzv1.CheckResponse{Allowed: false}, nil } @@ -49,7 +52,7 @@ func (s *Server) checkGroupResource(ctx context.Context, subject, relation, grou TupleKey: &openfgav1.CheckRequestTupleKey{ User: subject, Relation: relation, - Object: common.NewGroupResourceIdent(group, resource), + Object: resource.GroupResourceIdent(), }, } @@ -66,8 +69,8 @@ func (s *Server) checkGroupResource(ctx context.Context, subject, relation, grou } // checkTyped checks on our typed resources e.g. folder. -func (s *Server) checkTyped(ctx context.Context, subject, relation, name string, info common.TypeInfo, store *storeInfo) (*authzv1.CheckResponse, error) { - if !info.IsValidRelation(relation) { +func (s *Server) checkTyped(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.CheckResponse, error) { + if !resource.IsValidRelation(relation) { return &authzv1.CheckResponse{Allowed: false}, nil } @@ -78,7 +81,7 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation, name string, TupleKey: &openfgav1.CheckRequestTupleKey{ User: subject, Relation: relation, - Object: common.NewTypedIdent(info.Type, name), + Object: resource.ResourceIdent(), }, }) if err != nil { @@ -95,21 +98,22 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation, name string, // checkGeneric check our generic "resource" type. It checks: // 1. If subject has access as a sub resource for a folder. // 2. If subject has direct access to resource. -func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, resource, name, folder string, store *storeInfo) (*authzv1.CheckResponse, error) { +func (s *Server) checkGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.CheckResponse, error) { var ( - resourceCtx = common.NewResourceContext(group, resource) + folderIdent = resource.FolderIdent() + resourceCtx = resource.Context() folderRelation = common.FolderResourceRelation(relation) ) - if folder != "" && common.IsFolderResourceRelation(folderRelation) { + if folderIdent != "" && common.IsFolderResourceRelation(folderRelation) { // Check if subject has access as a sub resource for the folder res, err := s.check(ctx, &openfgav1.CheckRequest{ StoreId: store.ID, AuthorizationModelId: store.ModelID, TupleKey: &openfgav1.CheckRequestTupleKey{ User: subject, - Relation: common.FolderResourceRelation(relation), - Object: common.NewFolderIdent(folder), + Relation: folderRelation, + Object: folderIdent, }, Context: resourceCtx, }) @@ -123,7 +127,8 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res } } - if !common.IsResourceRelation(relation) { + resourceIdent := resource.ResourceIdent() + if !resource.IsValidRelation(relation) || resourceIdent == "" { return &authzv1.CheckResponse{Allowed: false}, nil } @@ -134,7 +139,7 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res TupleKey: &openfgav1.CheckRequestTupleKey{ User: subject, Relation: relation, - Object: common.NewResourceIdent(group, resource, name), + Object: resourceIdent, }, Context: resourceCtx, }) diff --git a/pkg/services/authz/zanzana/server/server_check_test.go b/pkg/services/authz/zanzana/server/server_check_test.go index 0177b8e22a3..f1b0f550c58 100644 --- a/pkg/services/authz/zanzana/server/server_check_test.go +++ b/pkg/services/authz/zanzana/server/server_check_test.go @@ -12,104 +12,145 @@ import ( ) func testCheck(t *testing.T, server *Server) { - newReq := func(subject, verb, group, resource, folder, name string) *authzv1.CheckRequest { + newReq := func(subject, verb, group, resource, subresource, folder, name string) *authzv1.CheckRequest { return &authzv1.CheckRequest{ - Namespace: namespace, - Subject: subject, - Verb: verb, - Group: group, - Resource: resource, - Name: name, - Folder: folder, + Namespace: namespace, + Subject: subject, + Verb: verb, + Group: group, + Resource: resource, + Subresource: subresource, + Name: name, + Folder: folder, } } 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(), newReq("user:1", utils.VerbGet, 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(), newReq("user:1", utils.VerbGet, 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()) + + // sanity check no access to subresource + res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "1")) 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 group_resource", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:2", utils.VerbGet, 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(), newReq("user:3", utils.VerbGet, 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(), newReq("user:3", utils.VerbGet, 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(), newReq("user:4", utils.VerbGet, 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(), newReq("user:4", utils.VerbGet, 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(), newReq("user:4", utils.VerbGet, 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(), newReq("user:4", utils.VerbGet, 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(), newReq("user:5", utils.VerbGet, 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(), newReq("user:6", utils.VerbGet, 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 group_resource access", func(t *testing.T) { - res, err := server.Check(context.Background(), newReq("user:7", utils.VerbGet, 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(), newReq("user:7", utils.VerbGet, 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(), newReq("user:8", utils.VerbGet, 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(), newReq("user:8", utils.VerbGet, 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(), newReq("user:8", utils.VerbGet, 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", "")) + res, err := server.Check(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", "5", "")) require.NoError(t, err) assert.True(t, res.GetAllowed()) }) + + t.Run("user:10 should be able to read dashboard status for dashboard 10", func(t *testing.T) { + res, err := server.Check(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + + res, err = server.Check(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "1")) + require.NoError(t, err) + assert.False(t, res.GetAllowed()) + }) + + t.Run("user:11 should be able to read dashboard status for dashboard 10 through group_resource", func(t *testing.T) { + res, err := server.Check(context.Background(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + }) + + t.Run("user:12 should be able to read dashboard status for all dashboards in folder 5", func(t *testing.T) { + res, err := server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + + res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "11")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + + // inherited from folder 5 + res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12")) + require.NoError(t, err) + assert.True(t, res.GetAllowed()) + + res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13")) + require.NoError(t, err) + assert.False(t, res.GetAllowed()) + }) } diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index 399cb2bef32..1100152de3d 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -21,8 +21,9 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis } relation := common.VerbMapping[r.GetVerb()] + resource := common.NewResourceInfoFromList(r) - res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) + res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, store) if err != nil { return nil, err } @@ -31,11 +32,11 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis return &authzv1.ListResponse{All: true}, nil } - if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok { - return s.listTyped(ctx, r.GetSubject(), relation, info, store) + if resource.IsGeneric() { + return s.listGeneric(ctx, r.GetSubject(), relation, resource, store) } - return s.listGeneric(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store) + return s.listTyped(ctx, r.GetSubject(), relation, resource, store) } func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) { @@ -50,8 +51,8 @@ func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequ return s.openfga.ListObjects(ctx, req) } -func (s *Server) listTyped(ctx context.Context, subject, relation string, info common.TypeInfo, store *storeInfo) (*authzv1.ListResponse, error) { - if !info.IsValidRelation(relation) { +func (s *Server) listTyped(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.ListResponse, error) { + if !resource.IsValidRelation(relation) { return &authzv1.ListResponse{}, nil } @@ -59,7 +60,7 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, info c res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{ StoreId: store.ID, AuthorizationModelId: store.ModelID, - Type: info.Type, + Type: resource.Type(), Relation: relation, User: subject, }) @@ -68,14 +69,14 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, info c } return &authzv1.ListResponse{ - Items: typedObjects(info.Type, res.GetObjects()), + Items: typedObjects(resource.Type(), res.GetObjects()), }, nil } -func (s *Server) listGeneric(ctx context.Context, subject, relation, group, resource string, store *storeInfo) (*authzv1.ListResponse, error) { +func (s *Server) listGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.ListResponse, error) { var ( - resourceCtx = common.NewResourceContext(group, resource) folderRelation = common.FolderResourceRelation(relation) + resourceCtx = resource.Context() ) // 1. List all folders subject has access to resource type in @@ -98,8 +99,8 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation, group, reso } // 2. List all resource directly assigned to subject - var resources []string - if common.IsResourceRelation(relation) { + var objects []string + if resource.IsValidRelation(relation) { res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{ StoreId: store.ID, AuthorizationModelId: store.ModelID, @@ -112,12 +113,12 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation, group, reso return nil, err } - resources = res.GetObjects() + objects = res.GetObjects() } return &authzv1.ListResponse{ Folders: folderObject(folders), - Items: directObjects(group, resource, resources), + Items: directObjects(resource.GroupResource(), objects), }, nil } @@ -129,8 +130,8 @@ func typedObjects(typ string, objects []string) []string { return objects } -func directObjects(group, resource string, objects []string) []string { - prefix := fmt.Sprintf("%s:%s/%s/", resourceType, group, resource) +func directObjects(gr string, objects []string) []string { + prefix := fmt.Sprintf("%s:%s/", resourceType, gr) for i := range objects { objects[i] = strings.TrimPrefix(objects[i], prefix) } diff --git a/pkg/services/authz/zanzana/server/server_list_test.go b/pkg/services/authz/zanzana/server/server_list_test.go index 7a222fbb3b6..f1c1af4e0fb 100644 --- a/pkg/services/authz/zanzana/server/server_list_test.go +++ b/pkg/services/authz/zanzana/server/server_list_test.go @@ -12,18 +12,19 @@ import ( ) func testList(t *testing.T, server *Server) { - newList := func(subject, group, resource string) *authzv1.ListRequest { + newList := func(subject, group, resource, subresource string) *authzv1.ListRequest { return &authzv1.ListRequest{ - Namespace: namespace, - Verb: utils.VerbList, - Subject: subject, - Group: group, - Resource: resource, + Namespace: namespace, + Verb: utils.VerbList, + Subject: subject, + Group: group, + Resource: resource, + Subresource: subresource, } } t.Run("user:1 should list resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:1", dashboardGroup, dashboardResource)) + res, err := server.List(context.Background(), newList("user:1", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 1) assert.Len(t, res.GetFolders(), 0) @@ -31,7 +32,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:2 should be able to list all through group", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:2", dashboardGroup, dashboardResource)) + res, err := server.List(context.Background(), newList("user:2", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.True(t, res.GetAll()) assert.Len(t, res.GetItems(), 0) @@ -39,7 +40,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:3 should be able to list resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:3", dashboardGroup, dashboardResource)) + res, err := server.List(context.Background(), newList("user:3", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 1) @@ -48,24 +49,17 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:4 should be able to list all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:4", dashboardGroup, dashboardResource)) + res, err := server.List(context.Background(), newList("user:4", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 0) assert.Len(t, res.GetFolders(), 2) - first := res.GetFolders()[0] - second := res.GetFolders()[1] - - if first == "3" { - first, second = second, first - } - - assert.Equal(t, first, "1") - assert.Equal(t, second, "3") + assert.Contains(t, res.GetFolders(), "1") + assert.Contains(t, res.GetFolders(), "3") }) - t.Run("user:5 should be get list all dashboard.grafana.app/dashboards in folder 1 with set relation", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:5", dashboardGroup, dashboardResource)) + t.Run("user:5 should be list all dashboard.grafana.app/dashboards in folder 1 with set relation", func(t *testing.T) { + res, err := server.List(context.Background(), newList("user:5", dashboardGroup, dashboardResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 0) assert.Len(t, res.GetFolders(), 1) @@ -73,7 +67,7 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:6 should be able to list folder 1", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:6", folderGroup, folderResource)) + res, err := server.List(context.Background(), newList("user:6", folderGroup, folderResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 1) assert.Len(t, res.GetFolders(), 0) @@ -81,10 +75,47 @@ func testList(t *testing.T, server *Server) { }) t.Run("user:7 should be able to list all folders", func(t *testing.T) { - res, err := server.List(context.Background(), newList("user:7", folderGroup, folderResource)) + res, err := server.List(context.Background(), newList("user:7", folderGroup, folderResource, "")) require.NoError(t, err) assert.Len(t, res.GetItems(), 0) assert.Len(t, res.GetFolders(), 0) assert.True(t, res.GetAll()) }) + + t.Run("user:8 should be able to list resoruce:dashboard.grafana.app/dashboard in folder 6 and folder 5", func(t *testing.T) { + res, err := server.List(context.Background(), newList("user:8", dashboardGroup, dashboardResource, "")) + require.NoError(t, err) + assert.Len(t, res.GetFolders(), 2) + + assert.Contains(t, res.GetFolders(), "5") + assert.Contains(t, res.GetFolders(), "6") + }) + + t.Run("user:10 should be able to get resoruce:dashboard.grafana.app/dashboard/status for 10 and 11", func(t *testing.T) { + res, err := server.List(context.Background(), newList("user:10", dashboardGroup, dashboardResource, statusSubresource)) + require.NoError(t, err) + assert.Len(t, res.GetFolders(), 0) + assert.Len(t, res.GetItems(), 2) + + assert.Contains(t, res.GetItems(), "10") + assert.Contains(t, res.GetItems(), "11") + }) + + t.Run("user:11 should be able to list all resoruce:dashboard.grafana.app/dashboard/status ", func(t *testing.T) { + res, err := server.List(context.Background(), newList("user:11", dashboardGroup, dashboardResource, statusSubresource)) + require.NoError(t, err) + assert.Len(t, res.GetItems(), 0) + assert.Len(t, res.GetFolders(), 0) + assert.True(t, res.GetAll()) + }) + + t.Run("user:12 should be able to list all resoruce:dashboard.grafana.app/dashboard/status in folder 5 and 6", func(t *testing.T) { + res, err := server.List(context.Background(), newList("user:12", dashboardGroup, dashboardResource, statusSubresource)) + require.NoError(t, err) + assert.Len(t, res.GetItems(), 0) + assert.Len(t, res.GetFolders(), 2) + + assert.Contains(t, res.GetFolders(), "5") + assert.Contains(t, res.GetFolders(), "6") + }) } diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 556356dc897..cf77dd52615 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -24,6 +24,8 @@ const ( folderGroup = "folder.grafana.app" folderResource = "folders" + + statusSubresource = "status" ) func TestMain(m *testing.M) { @@ -76,20 +78,24 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { 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.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.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", "create", dashboardGroup, dashboardResource, "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"), }, }, }) diff --git a/pkg/services/authz/zanzana/translations.go b/pkg/services/authz/zanzana/translations.go index 79e103cb5ee..70bf8337c6c 100644 --- a/pkg/services/authz/zanzana/translations.go +++ b/pkg/services/authz/zanzana/translations.go @@ -29,17 +29,18 @@ type resourceTranslation struct { } type actionMappig struct { - relation string - group string - resource string + relation string + group string + resource string + subresource string } -func newMapping(relation string) actionMappig { - return newScopedMapping(relation, "", "") +func newMapping(relation, subresource string) actionMappig { + return newScopedMapping(relation, "", "", subresource) } -func newScopedMapping(relation, group, resource string) actionMappig { - return actionMappig{relation, group, resource} +func newScopedMapping(relation, group, resource, subresource string) actionMappig { + return actionMappig{relation, group, resource, subresource} } var ( @@ -56,14 +57,14 @@ var resourceTranslations = map[string]resourceTranslation{ group: folderGroup, resource: folderResource, mapping: map[string]actionMappig{ - "folders:read": newMapping(RelationGet), - "folders:write": newMapping(RelationUpdate), - "folders:create": newMapping(RelationCreate), - "folders:delete": newMapping(RelationDelete), - "dashboards:read": newScopedMapping(RelationGet, dashboardGroup, dashboardResource), - "dashboards:write": newScopedMapping(RelationUpdate, dashboardGroup, dashboardResource), - "dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource), - "dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource), + "folders:read": newMapping(RelationGet, ""), + "folders:write": newMapping(RelationUpdate, ""), + "folders:create": newMapping(RelationCreate, ""), + "folders:delete": newMapping(RelationDelete, ""), + "dashboards:read": newScopedMapping(RelationGet, dashboardGroup, dashboardResource, ""), + "dashboards:write": newScopedMapping(RelationUpdate, dashboardGroup, dashboardResource, ""), + "dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource, ""), + "dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource, ""), }, }, KindDashboards: { @@ -71,10 +72,10 @@ var resourceTranslations = map[string]resourceTranslation{ group: dashboardGroup, resource: dashboardResource, mapping: map[string]actionMappig{ - "dashboards:read": newMapping(RelationGet), - "dashboards:write": newMapping(RelationUpdate), - "dashboards:create": newMapping(RelationCreate), - "dashboards:delete": newMapping(RelationDelete), + "dashboards:read": newMapping(RelationGet, ""), + "dashboards:write": newMapping(RelationUpdate, ""), + "dashboards:create": newMapping(RelationCreate, ""), + "dashboards:delete": newMapping(RelationDelete, ""), }, }, } diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index 110832d347e..696f75c1e82 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -70,8 +70,6 @@ var ( ToOpenFGATuples = common.ToOpenFGATuples ToOpenFGATupleKey = common.ToOpenFGATupleKey ToOpenFGATupleKeyWithoutCondition = common.ToOpenFGATupleKeyWithoutCondition - - FormatGroupResource = common.FormatGroupResource ) // NewTupleEntry constructs new openfga entry type:name[#relation]. @@ -98,16 +96,16 @@ func TranslateToResourceTuple(subject string, action, kind, name string) (*openf } if name == "*" { - return common.NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource), true + return common.NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource), true } if translation.typ == TypeResource { - return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, name), true + return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource, name), true } if translation.typ == TypeFolder { if m.group != "" && m.resource != "" { - return common.NewFolderResourceTuple(subject, m.relation, m.group, m.resource, name), true + return common.NewFolderResourceTuple(subject, m.relation, m.group, m.resource, m.subresource, name), true } return common.NewFolderTuple(subject, m.relation, name), true @@ -177,7 +175,7 @@ func TranslateToGroupResource(kind string) string { if !ok { return "" } - return common.FormatGroupResource(translation.group, translation.resource) + return common.FormatGroupResource(translation.group, translation.resource, "") } func TranslateBasicRole(name string) string {