Folders: Use authlib.AccessClient in authorizer (#110602)

This commit is contained in:
Ryan McKinley
2025-09-09 13:43:48 +03:00
committed by GitHub
parent 53cd0882ed
commit 0cb52b8be0
7 changed files with 162 additions and 317 deletions
+6 -6
View File
@@ -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)
}
+5 -74
View File
@@ -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 {
+42 -188
View File
@@ -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)
}
+2 -12
View File
@@ -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
}
+3 -3
View File
@@ -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
+11 -10
View File
@@ -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")
}
+93 -24
View File
@@ -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",