diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 08cf99809cf..05f00cdc54c 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" - claims "github.com/grafana/authlib/types" + authlib "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 claims.AccessClient + accessClient authlib.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 claims.AccessClient, + accessClient authlib.AccessClient, provisioning provisioning.ProvisioningService, dashStore dashboards.Store, reg prometheus.Registerer, @@ -167,7 +167,7 @@ func RegisterAPIService( return builder } -func NewAPIService(ac claims.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceInfoProvider, pluginStore *pluginstore.Service) *DashboardsAPIBuilder { +func NewAPIService(ac authlib.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 := claims.ParseNamespace(a.GetNamespace()) + nsInfo, err := authlib.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 := claims.ParseNamespace(oldAccessor.GetNamespace()) + nsInfo, err := authlib.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 0b163cc395b..68ec8221242 100644 --- a/pkg/registry/apis/folders/authorizer.go +++ b/pkg/registry/apis/folders/authorizer.go @@ -2,89 +2,20 @@ package folders import ( "context" - "errors" - "slices" "k8s.io/apiserver/pkg/authorization/authorizer" - "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" + authlib "github.com/grafana/authlib/types" ) -// 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 { +func newAuthorizer(ac authlib.AccessChecker) authorizer.Authorizer { return authorizer.AuthorizerFunc(func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { - info, ok := types.AuthInfoFrom(ctx) + info, ok := authlib.AuthInfoFrom(ctx) if !ok { return authorizer.DecisionDeny, "missing auth info", nil } - // 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{ + res, err := ac.Check(ctx, info, authlib.CheckRequest{ Verb: a.GetVerb(), Group: a.GetAPIGroup(), Resource: a.GetResource(), @@ -94,7 +25,7 @@ func newMultiTenantAuthorizer(ac types.AccessClient) authorizer.Authorizer { }) if err != nil { - return authorizer.DecisionDeny, "faild to perform authorization", err + return authorizer.DecisionDeny, "failed 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 290f6e44252..586b88799d7 100644 --- a/pkg/registry/apis/folders/authorizer_test.go +++ b/pkg/registry/apis/folders/authorizer_test.go @@ -2,6 +2,7 @@ package folders import ( "context" + "fmt" "testing" "github.com/go-jose/go-jose/v3/jwt" @@ -10,183 +11,16 @@ import ( "github.com/grafana/authlib/authn" "github.com/grafana/authlib/authz" - "github.com/grafana/authlib/types" - + authlib "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 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) { +func TestFolderAuthorizer(t *testing.T) { type input struct { verb string - info types.AuthInfo - client types.AccessClient + info authlib.AuthInfo + client authlib.AccessChecker } type expected struct { @@ -195,21 +29,17 @@ func TestMultiTenantAuthorizer(t *testing.T) { } tests := []struct { - name string - input input - expeted expected + name string + input input + expected expected }{ { - name: "non access policy idenity should not be able to authorize", + name: "missing auth info", input: input{ - verb: utils.VerbGet, - info: &identity.StaticRequester{ - Type: types.TypeUser, - UserID: 1, - UserUID: "1", - }, + verb: utils.VerbGet, + client: authz.NewClient(nil), }, - expeted: expected{ + expected: expected{ authorized: authorizer.DecisionDeny, }, }, @@ -230,7 +60,7 @@ func TestMultiTenantAuthorizer(t *testing.T) { }), client: authz.NewClient(nil), }, - expeted: expected{ + expected: expected{ authorized: authorizer.DecisionAllow, }, }, @@ -251,28 +81,52 @@ func TestMultiTenantAuthorizer(t *testing.T) { }), client: authz.NewClient(nil), }, - expeted: expected{ + expected: 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 := newMultiTenantAuthorizer(tt.input.client) + authz := newAuthorizer(tt.input.client) authorized, _, err := authz.Authorize( - types.WithAuthInfo(context.Background(), tt.input.info), + authlib.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.expeted.err { + if tt.expected.err { require.Error(t, err) require.Equal(t, authorizer.DecisionDeny, authorized) return } require.NoError(t, err) - require.Equal(t, tt.expeted.authorized, authorized) + require.Equal(t, tt.expected.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 4467eac5065..3762b0df86f 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -2,7 +2,6 @@ package folders import ( "context" - "errors" "fmt" "strings" @@ -20,7 +19,6 @@ 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" @@ -40,9 +38,6 @@ 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 @@ -82,7 +77,7 @@ func RegisterAPIService(cfg *setting.Cfg, acService: acService, ac: accessControl, permissionsOnCreate: cfg.RBAC.PermissionsOnCreation("folder"), - authorizer: newLegacyAuthorizer(accessControl), + authorizer: newAuthorizer(accessClient), searcher: unified, permissionStore: reconcilers.NewZanzanaPermissionStore(zanzanaClient), } @@ -92,7 +87,7 @@ func RegisterAPIService(cfg *setting.Cfg, func NewAPIService(ac authlib.AccessClient) *FolderAPIBuilder { return &FolderAPIBuilder{ - authorizer: newMultiTenantAuthorizer(ac), + authorizer: newAuthorizer(ac), ignoreLegacy: true, } } @@ -216,11 +211,6 @@ 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 0bc402af857..c0bb442321d 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 + Action string // Verb has been mapped into an action 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 9933775c79b..345587300b1 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,18 +331,19 @@ 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("unsupport resource", "group", group, "resource", resource) + ctxLogger.Error("unsupported resource", "group", group, "resource", resource) return "", status.Error(codes.NotFound, "unsupported resource") } action, ok := t.Action(verb) if !ok { - ctxLogger.Error("unsupport verb", "group", group, "resource", resource, "verb", verb) + ctxLogger.Error("unsupported verb", "group", group, "resource", resource, "verb", verb) return "", status.Error(codes.NotFound, "unsupported verb") } @@ -570,7 +571,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() @@ -619,7 +620,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 } @@ -696,7 +697,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 } @@ -707,7 +708,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("unsupport resource", "group", req.Group, "resource", req.Resource) + ctxLogger.Error("unsupported 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 cee36a63d59..510df322d93 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,6 +297,75 @@ 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() @@ -317,7 +386,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", @@ -559,7 +628,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 @@ -575,7 +644,7 @@ func TestService_listPermission(t *testing.T) { Kind: "*", }, }, - list: ListRequest{ + list: listRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", @@ -611,7 +680,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", @@ -638,7 +707,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", @@ -667,7 +736,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", @@ -699,7 +768,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", @@ -713,7 +782,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", @@ -734,7 +803,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",