diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 05f00cdc54c..08cf99809cf 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -19,7 +19,7 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" - authlib "github.com/grafana/authlib/types" + claims "github.com/grafana/authlib/types" internal "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" @@ -79,7 +79,7 @@ type DashboardsAPIBuilder struct { authorizer authorizer.Authorizer accessControl accesscontrol.AccessControl - accessClient authlib.AccessClient + accessClient claims.AccessClient legacy *DashboardStorage unified resource.ResourceClient dashboardProvisioningService dashboards.DashboardProvisioningService @@ -113,7 +113,7 @@ func RegisterAPIService( dashboardPermissions dashboards.PermissionsRegistrationService, dashboardPermissionsSvc accesscontrol.DashboardPermissionsService, accessControl accesscontrol.AccessControl, - accessClient authlib.AccessClient, + accessClient claims.AccessClient, provisioning provisioning.ProvisioningService, dashStore dashboards.Store, reg prometheus.Registerer, @@ -167,7 +167,7 @@ func RegisterAPIService( return builder } -func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceInfoProvider, pluginStore *pluginstore.Service) *DashboardsAPIBuilder { +func NewAPIService(ac claims.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceInfoProvider, pluginStore *pluginstore.Service) *DashboardsAPIBuilder { // TODO: Plugin store will soon be removed, // as the cases for plugin fetching is not needed. Keeping it now to not break implementation if pluginStore == nil { @@ -274,7 +274,7 @@ func (b *DashboardsAPIBuilder) validateDelete(ctx context.Context, a admission.A return nil } - nsInfo, err := authlib.ParseNamespace(a.GetNamespace()) + nsInfo, err := claims.ParseNamespace(a.GetNamespace()) if err != nil { return fmt.Errorf("%v: %w", "failed to parse namespace", err) } @@ -380,7 +380,7 @@ func (b *DashboardsAPIBuilder) validateUpdate(ctx context.Context, a admission.A } // Parse namespace for old dashboard - nsInfo, err := authlib.ParseNamespace(oldAccessor.GetNamespace()) + nsInfo, err := claims.ParseNamespace(oldAccessor.GetNamespace()) if err != nil { return fmt.Errorf("failed to parse namespace: %w", err) } diff --git a/pkg/registry/apis/folders/authorizer.go b/pkg/registry/apis/folders/authorizer.go index 68ec8221242..0b163cc395b 100644 --- a/pkg/registry/apis/folders/authorizer.go +++ b/pkg/registry/apis/folders/authorizer.go @@ -2,20 +2,89 @@ package folders import ( "context" + "errors" + "slices" "k8s.io/apiserver/pkg/authorization/authorizer" - authlib "github.com/grafana/authlib/types" + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" ) -func newAuthorizer(ac authlib.AccessChecker) authorizer.Authorizer { +// newLegacyAuthorizer creates an authorizer using legacy access control, this is only usable for single tenant api. +func newLegacyAuthorizer(ac accesscontrol.AccessControl) authorizer.Authorizer { + return authorizer.AuthorizerFunc(func(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { + in, err := authorizerFunc(ctx, attr) + if err != nil { + if errors.Is(err, errNoUser) { + return authorizer.DecisionDeny, "", nil + } + return authorizer.DecisionNoOpinion, "", nil + } + + ok, err := ac.Evaluate(ctx, in.user, in.evaluator) + if ok { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "folder", err + }) +} + +func authorizerFunc(ctx context.Context, attr authorizer.Attributes) (*authorizerParams, error) { + allowedVerbs := []string{utils.VerbCreate, utils.VerbDelete, utils.VerbList} + verb := attr.GetVerb() + name := attr.GetName() + if (!attr.IsResourceRequest()) || (name == "" && verb != utils.VerbCreate && slices.Contains(allowedVerbs, verb)) { + return nil, errNoResource + } + + // require a user + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, errNoUser + } + + scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(name) + var eval accesscontrol.Evaluator + + // "get" is used for sub-resources with GET http (parents, access, count) + switch verb { + case utils.VerbCreate: + eval = accesscontrol.EvalPermission(dashboards.ActionFoldersCreate) + case utils.VerbPatch: + fallthrough + case utils.VerbUpdate: + eval = accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, scope) + case utils.VerbDeleteCollection: + fallthrough + case utils.VerbDelete: + eval = accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, scope) + case utils.VerbList: + eval = accesscontrol.EvalPermission(dashboards.ActionFoldersRead) + default: + eval = accesscontrol.EvalPermission(dashboards.ActionFoldersRead, scope) + } + return &authorizerParams{evaluator: eval, user: user}, nil +} + +// newMultiTenantAuthorizer creates an authorizer suitable to multi-tenant setup. +// For now it only allow authorization of access tokens. +func newMultiTenantAuthorizer(ac types.AccessClient) authorizer.Authorizer { return authorizer.AuthorizerFunc(func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { - info, ok := authlib.AuthInfoFrom(ctx) + info, ok := types.AuthInfoFrom(ctx) if !ok { return authorizer.DecisionDeny, "missing auth info", nil } - res, err := ac.Check(ctx, info, authlib.CheckRequest{ + // For now we only allow access policy to authorize with multi-tenant setup + if !types.IsIdentityType(info.GetIdentityType(), types.TypeAccessPolicy) { + return authorizer.DecisionDeny, "permission denied", nil + } + + res, err := ac.Check(ctx, info, types.CheckRequest{ Verb: a.GetVerb(), Group: a.GetAPIGroup(), Resource: a.GetResource(), @@ -25,7 +94,7 @@ func newAuthorizer(ac authlib.AccessChecker) authorizer.Authorizer { }) if err != nil { - return authorizer.DecisionDeny, "failed to perform authorization", err + return authorizer.DecisionDeny, "faild to perform authorization", err } if !res.Allowed { diff --git a/pkg/registry/apis/folders/authorizer_test.go b/pkg/registry/apis/folders/authorizer_test.go index 586b88799d7..290f6e44252 100644 --- a/pkg/registry/apis/folders/authorizer_test.go +++ b/pkg/registry/apis/folders/authorizer_test.go @@ -2,7 +2,6 @@ package folders import ( "context" - "fmt" "testing" "github.com/go-jose/go-jose/v3/jwt" @@ -11,16 +10,183 @@ import ( "github.com/grafana/authlib/authn" "github.com/grafana/authlib/authz" - authlib "github.com/grafana/authlib/types" + "github.com/grafana/authlib/types" + folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/user" ) -func TestFolderAuthorizer(t *testing.T) { +func TestLegacyAuthorizer(t *testing.T) { + type input struct { + user identity.Requester + verb string + } + type expect struct { + authorized authorizer.Decision + err error + } + var orgID int64 = 1 + + tests := []struct { + name string + input input + expect expect + }{ + { + name: "user with create permissions should be able to create a folder", + input: input{ + user: &user.SignedInUser{ + UserID: 1, + OrgID: orgID, + Name: "123", + Permissions: map[int64]map[string][]string{ + orgID: {dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}}, + }, + }, + verb: string(utils.VerbCreate), + }, + expect: expect{ + authorized: authorizer.DecisionAllow, + }, + }, + { + name: "not possible to create a folder without a user", + input: input{ + user: nil, + verb: string(utils.VerbCreate), + }, + expect: expect{authorized: authorizer.DecisionDeny}, + }, + { + name: "user without permissions should not be able to create a folder", + input: input{ + user: &user.SignedInUser{}, + verb: string(utils.VerbCreate), + }, + expect: expect{authorized: authorizer.DecisionDeny}, + }, + { + name: "user in another orgId should not be able to create a folder ", + input: input{ + user: &user.SignedInUser{ + UserID: 1, + OrgID: 2, + Name: "123", + Permissions: map[int64]map[string][]string{ + orgID: {dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}}, + }, + }, + verb: string(utils.VerbCreate), + }, + expect: expect{authorized: authorizer.DecisionDeny}, + }, + { + name: "user with read permissions should be able to list folders", + input: input{ + user: &user.SignedInUser{ + UserID: 1, + OrgID: orgID, + Name: "123", + Permissions: map[int64]map[string][]string{ + orgID: {}, + }, + }, + verb: string(utils.VerbList), + }, + expect: expect{authorized: authorizer.DecisionDeny}, + }, + { + name: "user with delete permissions should be able to delete a folder", + input: input{ + user: &user.SignedInUser{ + UserID: 1, + OrgID: orgID, + Name: "123", + Permissions: map[int64]map[string][]string{ + orgID: {dashboards.ActionFoldersDelete: {dashboards.ScopeFoldersAll}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}}, + }, + }, + verb: string(utils.VerbDelete), + }, + expect: expect{authorized: authorizer.DecisionAllow}, + }, + { + name: "user without delete permissions should NOT be able to delete a folder", + input: input{ + user: &user.SignedInUser{ + UserID: 1, + OrgID: orgID, + Name: "123", + Permissions: map[int64]map[string][]string{ + orgID: {}, + }, + }, + verb: string(utils.VerbDelete), + }, + expect: expect{authorized: authorizer.DecisionDeny}, + }, + { + name: "user with write permissions should be able to update a folder", + input: input{ + user: &user.SignedInUser{ + UserID: 1, + OrgID: orgID, + Name: "123", + Permissions: map[int64]map[string][]string{ + orgID: {dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}}, + }, + }, + verb: string(utils.VerbUpdate), + }, + expect: expect{authorized: authorizer.DecisionAllow}, + }, + { + name: "user without write permissions should NOT be able to update a folder", + input: input{ + user: &user.SignedInUser{ + UserID: 1, + OrgID: orgID, + Name: "123", + Permissions: map[int64]map[string][]string{ + orgID: {}, + }, + }, + verb: string(utils.VerbUpdate), + }, + expect: expect{authorized: authorizer.DecisionDeny}, + }, + } + + authz := newLegacyAuthorizer(acimpl.ProvideAccessControl(featuremgmt.WithFeatures())) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + authorized, _, err := authz.Authorize( + identity.WithRequester(context.Background(), tt.input.user), + authorizer.AttributesRecord{User: tt.input.user, Verb: tt.input.verb, Resource: "folders", ResourceRequest: true, Name: "123"}, + ) + if tt.expect.err != nil { + require.Error(t, err) + require.Equal(t, authorizer.DecisionDeny, authorized) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expect.authorized, authorized) + }) + } +} + +func TestMultiTenantAuthorizer(t *testing.T) { type input struct { verb string - info authlib.AuthInfo - client authlib.AccessChecker + info types.AuthInfo + client types.AccessClient } type expected struct { @@ -29,17 +195,21 @@ func TestFolderAuthorizer(t *testing.T) { } tests := []struct { - name string - input input - expected expected + name string + input input + expeted expected }{ { - name: "missing auth info", + name: "non access policy idenity should not be able to authorize", input: input{ - verb: utils.VerbGet, - client: authz.NewClient(nil), + verb: utils.VerbGet, + info: &identity.StaticRequester{ + Type: types.TypeUser, + UserID: 1, + UserUID: "1", + }, }, - expected: expected{ + expeted: expected{ authorized: authorizer.DecisionDeny, }, }, @@ -60,7 +230,7 @@ func TestFolderAuthorizer(t *testing.T) { }), client: authz.NewClient(nil), }, - expected: expected{ + expeted: expected{ authorized: authorizer.DecisionAllow, }, }, @@ -81,52 +251,28 @@ func TestFolderAuthorizer(t *testing.T) { }), client: authz.NewClient(nil), }, - expected: expected{ + expeted: expected{ authorized: authorizer.DecisionDeny, }, }, - { - name: "with client error", - input: input{ - verb: utils.VerbGet, - info: authn.NewIDTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{}, &authn.Claims[authn.IDTokenClaims]{}), - client: &dummyClient{ - check: func(ctx context.Context, info authlib.AuthInfo, req authlib.CheckRequest) (authlib.CheckResponse, error) { - return authlib.CheckResponse{}, fmt.Errorf("nope") - }, - }, - }, - expected: expected{ - authorized: authorizer.DecisionDeny, - err: true, - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - authz := newAuthorizer(tt.input.client) + authz := newMultiTenantAuthorizer(tt.input.client) authorized, _, err := authz.Authorize( - authlib.WithAuthInfo(context.Background(), tt.input.info), + types.WithAuthInfo(context.Background(), tt.input.info), authorizer.AttributesRecord{User: tt.input.info, Verb: tt.input.verb, APIGroup: folders.GROUP, Resource: "folders", ResourceRequest: true, Name: "123", Namespace: "stacks-1"}, ) - if tt.expected.err { + if tt.expeted.err { require.Error(t, err) require.Equal(t, authorizer.DecisionDeny, authorized) return } require.NoError(t, err) - require.Equal(t, tt.expected.authorized, authorized) + require.Equal(t, tt.expeted.authorized, authorized) }) } } - -type dummyClient struct { - check func(ctx context.Context, info authlib.AuthInfo, req authlib.CheckRequest) (authlib.CheckResponse, error) -} - -func (c *dummyClient) Check(ctx context.Context, info authlib.AuthInfo, req authlib.CheckRequest) (authlib.CheckResponse, error) { - return c.check(ctx, info, req) -} diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 3762b0df86f..4467eac5065 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -2,6 +2,7 @@ package folders import ( "context" + "errors" "fmt" "strings" @@ -19,6 +20,7 @@ import ( authlib "github.com/grafana/authlib/types" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/apps/iam/pkg/reconcilers" + "github.com/grafana/grafana/pkg/apimachinery/identity" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -38,6 +40,9 @@ var _ builder.APIGroupValidation = (*FolderAPIBuilder)(nil) var resourceInfo = folders.FolderResourceInfo +var errNoUser = errors.New("valid user is required") +var errNoResource = errors.New("resource name is required") + // This is used just so wire has something unique to return type FolderAPIBuilder struct { features featuremgmt.FeatureToggles @@ -77,7 +82,7 @@ func RegisterAPIService(cfg *setting.Cfg, acService: acService, ac: accessControl, permissionsOnCreate: cfg.RBAC.PermissionsOnCreation("folder"), - authorizer: newAuthorizer(accessClient), + authorizer: newLegacyAuthorizer(accessControl), searcher: unified, permissionStore: reconcilers.NewZanzanaPermissionStore(zanzanaClient), } @@ -87,7 +92,7 @@ func RegisterAPIService(cfg *setting.Cfg, func NewAPIService(ac authlib.AccessClient) *FolderAPIBuilder { return &FolderAPIBuilder{ - authorizer: newAuthorizer(ac), + authorizer: newMultiTenantAuthorizer(ac), ignoreLegacy: true, } } @@ -211,6 +216,11 @@ func (b *FolderAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAP return oas, nil } +type authorizerParams struct { + user identity.Requester + evaluator accesscontrol.Evaluator +} + func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer { return b.authorizer } diff --git a/pkg/services/authz/rbac/models.go b/pkg/services/authz/rbac/models.go index c0bb442321d..0bc402af857 100644 --- a/pkg/services/authz/rbac/models.go +++ b/pkg/services/authz/rbac/models.go @@ -2,11 +2,11 @@ package rbac import claims "github.com/grafana/authlib/types" -type checkRequest struct { +type CheckRequest struct { Namespace claims.NamespaceInfo IdentityType claims.IdentityType UserUID string - Action string // Verb has been mapped into an action + Action string Group string Resource string Verb string @@ -14,7 +14,7 @@ type checkRequest struct { ParentFolder string } -type listRequest struct { +type ListRequest struct { Namespace claims.NamespaceInfo IdentityType claims.IdentityType UserUID string diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 345587300b1..9933775c79b 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -228,7 +228,7 @@ func (s *Service) List(ctx context.Context, req *authzv1.ListRequest) (*authzv1. return resp, err } -func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRequest) (*checkRequest, error) { +func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRequest) (*CheckRequest, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.validateCheckRequest") defer span.End() @@ -247,7 +247,7 @@ func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRe return nil, err } - checkReq := &checkRequest{ + checkReq := &CheckRequest{ Namespace: ns, UserUID: userUID, IdentityType: idType, @@ -261,7 +261,7 @@ func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRe return checkReq, nil } -func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequest) (*listRequest, error) { +func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequest) (*ListRequest, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.validateListRequest") defer span.End() @@ -280,7 +280,7 @@ func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequ return nil, err } - listReq := &listRequest{ + listReq := &ListRequest{ Namespace: ns, UserUID: userUID, IdentityType: idType, @@ -331,19 +331,18 @@ func (s *Service) validateSubject(ctx context.Context, subject string) (string, return userUID, identityType, nil } -// Find the action for a selected verb func (s *Service) validateAction(ctx context.Context, group, resource, verb string) (string, error) { ctxLogger := s.logger.FromContext(ctx) t, ok := s.mapper.Get(group, resource) if !ok { - ctxLogger.Error("unsupported resource", "group", group, "resource", resource) + ctxLogger.Error("unsupport resource", "group", group, "resource", resource) return "", status.Error(codes.NotFound, "unsupported resource") } action, ok := t.Action(verb) if !ok { - ctxLogger.Error("unsupported verb", "group", group, "resource", resource, "verb", verb) + ctxLogger.Error("unsupport verb", "group", group, "resource", resource, "verb", verb) return "", status.Error(codes.NotFound, "unsupported verb") } @@ -571,7 +570,7 @@ func (s *Service) getUserBasicRole(ctx context.Context, ns types.NamespaceInfo, return *basicRole, nil } -func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool, req *checkRequest) (bool, error) { +func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool, req *CheckRequest) (bool, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.checkPermission", trace.WithAttributes( attribute.Int("scope_count", len(scopeMap)))) defer span.End() @@ -620,7 +619,7 @@ func getScopeMap(permissions []accesscontrol.Permission) map[string]bool { return permMap } -func (s *Service) checkInheritedPermissions(ctx context.Context, scopeMap map[string]bool, req *checkRequest) (bool, error) { +func (s *Service) checkInheritedPermissions(ctx context.Context, scopeMap map[string]bool, req *CheckRequest) (bool, error) { if req.ParentFolder == "" { return false, nil } @@ -697,7 +696,7 @@ func (s *Service) buildFolderTree(ctx context.Context, ns types.NamespaceInfo) ( return res.(folderTree), nil } -func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, req *listRequest) (*authzv1.ListResponse, error) { +func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, req *ListRequest) (*authzv1.ListResponse, error) { if scopeMap["*"] { return &authzv1.ListResponse{All: true}, nil } @@ -708,7 +707,7 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, t, ok := s.mapper.Get(req.Group, req.Resource) if !ok { - ctxLogger.Error("unsupported resource", "group", req.Group, "resource", req.Resource) + ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource) return nil, status.Error(codes.NotFound, "unsupported resource") } diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index 510df322d93..cee36a63d59 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -16,7 +16,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" "github.com/grafana/authlib/cache" "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -30,7 +30,7 @@ func TestService_checkPermission(t *testing.T) { type testCase struct { name string permissions []accesscontrol.Permission - check checkRequest + check CheckRequest folders []store.Folder expected bool } @@ -47,7 +47,7 @@ func TestService_checkPermission(t *testing.T) { Identifier: "some_dashboard", }, }, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -66,7 +66,7 @@ func TestService_checkPermission(t *testing.T) { Identifier: "another_dashboard", }, }, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -85,7 +85,7 @@ func TestService_checkPermission(t *testing.T) { Identifier: "*", }, }, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -103,7 +103,7 @@ func TestService_checkPermission(t *testing.T) { Attribute: "*", }, }, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -120,7 +120,7 @@ func TestService_checkPermission(t *testing.T) { Kind: "*", }, }, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -139,7 +139,7 @@ func TestService_checkPermission(t *testing.T) { Identifier: "general", }, }, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:create", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -154,7 +154,7 @@ func TestService_checkPermission(t *testing.T) { Action: "dashboards:create", }, }, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:create", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -165,7 +165,7 @@ func TestService_checkPermission(t *testing.T) { { name: "should return false if user has no permissions on resource", permissions: []accesscontrol.Permission{}, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -187,7 +187,7 @@ func TestService_checkPermission(t *testing.T) { {UID: "parent"}, {UID: "child", ParentUID: strPtr("parent")}, }, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -208,7 +208,7 @@ func TestService_checkPermission(t *testing.T) { }, }, folders: []store.Folder{{UID: "parent"}}, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:create", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -230,7 +230,7 @@ func TestService_checkPermission(t *testing.T) { }, }, folders: []store.Folder{{UID: "parent"}, {UID: "other_parent"}}, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:create", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -252,7 +252,7 @@ func TestService_checkPermission(t *testing.T) { }, }, folders: []store.Folder{{UID: "parent"}}, - check: checkRequest{ + check: CheckRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -273,7 +273,7 @@ func TestService_checkPermission(t *testing.T) { Identifier: "some_datasource", }, }, - check: checkRequest{ + check: CheckRequest{ Action: "datasources:query", Group: "query.grafana.app", Resource: "query", @@ -297,75 +297,6 @@ func TestService_checkPermission(t *testing.T) { } } -func TestService_mapping(t *testing.T) { - type testCase struct { - name string - input *authzv1.CheckRequest - output *checkRequest - err string - } - - ns := "default" - testUserA := &identity.StaticRequester{ - Type: types.TypeUser, - Login: "test", - UserID: 123, - UserUID: "u123", - OrgRole: identity.RoleAdmin, - IsGrafanaAdmin: true, // can do anything - Namespace: ns, - OrgID: 1, - } - ctx := types.WithAuthInfo(request.WithNamespace(context.Background(), ns), testUserA) - - testCases := []testCase{ - { - name: "should return true if user has permission", - input: &authzv1.CheckRequest{ - Group: "folder.grafana.app", - Resource: "folders", - Name: "aaa", - Verb: utils.VerbCreate, - Folder: "folder", - }, - output: &checkRequest{ - Action: "folders:create", - Group: "folder.grafana.app", - Resource: "folders", - Name: "aaa", - Verb: "create", - ParentFolder: "folder", - Namespace: types.NamespaceInfo{ - Value: ns, - OrgID: 1, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - s := setupService() - tc.input.Namespace = ns - tc.input.Subject = testUserA.GetUID() // the subject string - - got, err := s.validateCheckRequest(ctx, tc.input) - if tc.err != "" { - require.Error(t, err) - require.ErrorContains(t, err, tc.err) - return - } - require.NoError(t, err) - require.NotNil(t, got) - - tc.output.IdentityType = types.TypeUser - tc.output.UserUID = testUserA.GetIdentifier() - - require.Equal(t, tc.output, got) - }) - } -} - func TestService_checkPermission_folderCacheMissRecovery(t *testing.T) { s := setupService() ctx := context.Background() @@ -386,7 +317,7 @@ func TestService_checkPermission_folderCacheMissRecovery(t *testing.T) { s.folderCache.Set(ctx, folderCacheKey("default"), newFolderTree([]store.Folder{{UID: "root"}})) // Perform check on sub folder - check := checkRequest{ + check := CheckRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -628,7 +559,7 @@ func TestService_listPermission(t *testing.T) { name string permissions []accesscontrol.Permission folders []store.Folder - list listRequest + list ListRequest expectedItems []string expectedFolders []string expectedAll bool @@ -644,7 +575,7 @@ func TestService_listPermission(t *testing.T) { Kind: "*", }, }, - list: listRequest{ + list: ListRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -680,7 +611,7 @@ func TestService_listPermission(t *testing.T) { {UID: "some_folder_1"}, {UID: "some_folder_2"}, }, - list: listRequest{ + list: ListRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -707,7 +638,7 @@ func TestService_listPermission(t *testing.T) { {UID: "some_folder_subsubchild", ParentUID: strPtr("some_folder_subchild2")}, {UID: "some_folder_1", ParentUID: strPtr("some_other_folder")}, }, - list: listRequest{ + list: ListRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -736,7 +667,7 @@ func TestService_listPermission(t *testing.T) { {UID: "some_folder_parent"}, {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, }, - list: listRequest{ + list: ListRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -768,7 +699,7 @@ func TestService_listPermission(t *testing.T) { {UID: "some_folder_subchild", ParentUID: strPtr("some_folder_child")}, {UID: "some_folder_child2", ParentUID: strPtr("some_folder_parent")}, }, - list: listRequest{ + list: ListRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -782,7 +713,7 @@ func TestService_listPermission(t *testing.T) { folders: []store.Folder{ {UID: "some_folder_1"}, }, - list: listRequest{ + list: ListRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -803,7 +734,7 @@ func TestService_listPermission(t *testing.T) { {UID: "some_folder_parent"}, {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, }, - list: listRequest{ + list: ListRequest{ Action: "folders:read", Group: "folder.grafana.app", Resource: "folders",