FolderAPI: Use different authorizer for multi-tenant api (#101372)
Use different authorizers depening on if we are running multi-tenant or single-tenant
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// 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 sutiable 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 := types.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{
|
||||
Verb: a.GetVerb(),
|
||||
Group: a.GetAPIGroup(),
|
||||
Resource: a.GetResource(),
|
||||
Namespace: a.GetNamespace(),
|
||||
Name: a.GetNamespace(),
|
||||
Subresource: a.GetSubresource(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "faild to perform authorization", err
|
||||
}
|
||||
|
||||
if !res.Allowed {
|
||||
return authorizer.DecisionDeny, "permission denied", nil
|
||||
}
|
||||
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package folders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
"github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/authlib/authz"
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
folderv0aplha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"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("nestedFolders")))
|
||||
|
||||
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 types.AuthInfo
|
||||
client types.AccessClient
|
||||
}
|
||||
|
||||
type expected struct {
|
||||
authorized authorizer.Decision
|
||||
err bool
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input input
|
||||
expeted expected
|
||||
}{
|
||||
{
|
||||
name: "non access policy idenity should not be able to authorize",
|
||||
input: input{
|
||||
verb: utils.VerbGet,
|
||||
info: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
UserID: 1,
|
||||
UserUID: "1",
|
||||
},
|
||||
},
|
||||
expeted: expected{
|
||||
authorized: authorizer.DecisionDeny,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "access policy identity with correct permissions should be able to authorize",
|
||||
input: input{
|
||||
verb: utils.VerbGet,
|
||||
info: authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "access-policy:123",
|
||||
},
|
||||
Rest: authn.AccessTokenClaims{
|
||||
Namespace: "stacks-1",
|
||||
Permissions: []string{
|
||||
"folder.grafana.app/folders:get",
|
||||
},
|
||||
},
|
||||
}),
|
||||
client: authz.NewClient(nil),
|
||||
},
|
||||
expeted: expected{
|
||||
authorized: authorizer.DecisionAllow,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "access policy identity without correct permissions should not be able to authorize",
|
||||
input: input{
|
||||
verb: utils.VerbGet,
|
||||
info: authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "access-policy:123",
|
||||
},
|
||||
Rest: authn.AccessTokenClaims{
|
||||
Namespace: "stacks-1",
|
||||
Permissions: []string{
|
||||
"folder.grafana.app/folders:create",
|
||||
},
|
||||
},
|
||||
}),
|
||||
client: authz.NewClient(nil),
|
||||
},
|
||||
expeted: expected{
|
||||
authorized: authorizer.DecisionDeny,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authz := newMultiTenantAuthorizer(tt.input.client)
|
||||
authorized, _, err := authz.Authorize(
|
||||
types.WithAuthInfo(context.Background(), tt.input.info),
|
||||
authorizer.AttributesRecord{User: tt.input.info, Verb: tt.input.verb, APIGroup: folderv0aplha1.GROUP, Resource: "folders", ResourceRequest: true, Name: "123", Namespace: "stacks-1"},
|
||||
)
|
||||
|
||||
if tt.expeted.err {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, authorizer.DecisionDeny, authorized)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expeted.authorized, authorized)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
authtypes "github.com/grafana/authlib/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@@ -50,10 +50,12 @@ type FolderAPIBuilder struct {
|
||||
folderSvc folder.Service
|
||||
folderPermissionsSvc accesscontrol.FolderPermissionsService
|
||||
storage grafanarest.Storage
|
||||
accessControl accesscontrol.AccessControl
|
||||
searcher resource.ResourceIndexClient
|
||||
cfg *setting.Cfg
|
||||
ignoreLegacy bool // skip legacy storage and only use unified storage
|
||||
|
||||
authorizer authorizer.Authorizer
|
||||
|
||||
searcher resource.ResourceIndexClient
|
||||
cfg *setting.Cfg
|
||||
ignoreLegacy bool // skip legacy storage and only use unified storage
|
||||
}
|
||||
|
||||
func RegisterAPIService(cfg *setting.Cfg,
|
||||
@@ -79,17 +81,18 @@ func RegisterAPIService(cfg *setting.Cfg,
|
||||
folderSvc: folderSvc,
|
||||
folderPermissionsSvc: folderPermissionsSvc,
|
||||
cfg: cfg,
|
||||
accessControl: accessControl,
|
||||
authorizer: newLegacyAuthorizer(accessControl),
|
||||
searcher: unified,
|
||||
}
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
|
||||
func NewAPIService() *FolderAPIBuilder {
|
||||
func NewAPIService(ac authtypes.AccessClient) *FolderAPIBuilder {
|
||||
return &FolderAPIBuilder{
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
namespacer: request.GetNamespaceMapper(nil),
|
||||
authorizer: newMultiTenantAuthorizer(ac),
|
||||
ignoreLegacy: true,
|
||||
}
|
||||
}
|
||||
@@ -201,58 +204,7 @@ type authorizerParams struct {
|
||||
}
|
||||
|
||||
func (b *FolderAPIBuilder) GetAuthorizer() 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 := b.accessControl.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
|
||||
return b.authorizer
|
||||
}
|
||||
|
||||
var folderValidationRules = struct {
|
||||
|
||||
@@ -8,211 +8,17 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"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/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
func TestFolderAPIBuilder_getAuthorizerFunc(t *testing.T) {
|
||||
type input struct {
|
||||
user identity.Requester
|
||||
verb string
|
||||
}
|
||||
type expect struct {
|
||||
eval string
|
||||
allow bool
|
||||
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{
|
||||
eval: "folders:create",
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not possible to create a folder without a user",
|
||||
input: input{
|
||||
user: nil,
|
||||
verb: string(utils.VerbCreate),
|
||||
},
|
||||
expect: expect{
|
||||
eval: "folders:create",
|
||||
err: errNoUser,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user without permissions should not be able to create a folder",
|
||||
input: input{
|
||||
user: &user.SignedInUser{},
|
||||
verb: string(utils.VerbCreate),
|
||||
},
|
||||
expect: expect{
|
||||
eval: "folders:create",
|
||||
},
|
||||
},
|
||||
{
|
||||
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{
|
||||
eval: "folders:create",
|
||||
},
|
||||
},
|
||||
{
|
||||
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{
|
||||
eval: "folders:read",
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
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{
|
||||
eval: "folders:delete",
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
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{
|
||||
eval: "folders:delete",
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
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{
|
||||
eval: "folders:write",
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
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{
|
||||
eval: "folders:write",
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b := &FolderAPIBuilder{
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures("nestedFolders")),
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
out, err := authorizerFunc(identity.WithRequester(ctx, 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)
|
||||
return
|
||||
}
|
||||
allow, _ := b.accessControl.Evaluate(ctx, out.user, out.evaluator)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expect.eval, out.evaluator.String())
|
||||
require.Equal(t, tt.expect.allow, allow)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderAPIBuilder_Validate_Create(t *testing.T) {
|
||||
type input struct {
|
||||
obj *v0alpha1.Folder
|
||||
@@ -294,12 +100,11 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) {
|
||||
us := storageMock{m, s}
|
||||
|
||||
b := &FolderAPIBuilder{
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures("nestedFolders")),
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -379,13 +184,12 @@ func TestFolderAPIBuilder_Validate_Delete(t *testing.T) {
|
||||
setupFn(m, tt.statsResponse)
|
||||
|
||||
b := &FolderAPIBuilder{
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures("nestedFolders")),
|
||||
searcher: sm,
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
searcher: sm,
|
||||
}
|
||||
|
||||
err := b.Validate(context.Background(), admission.NewAttributesRecord(
|
||||
@@ -545,13 +349,12 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) {
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := &FolderAPIBuilder{
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures("nestedFolders")),
|
||||
searcher: sm,
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
searcher: sm,
|
||||
}
|
||||
|
||||
err := b.Validate(context.Background(), admission.NewAttributesRecord(
|
||||
@@ -641,13 +444,12 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) {
|
||||
us := storageMock{m, s}
|
||||
sm := searcherMock{Mock: m}
|
||||
b := &FolderAPIBuilder{
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures("nestedFolders")),
|
||||
searcher: sm,
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
searcher: sm,
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -748,13 +550,12 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) {
|
||||
us := storageMock{m, s}
|
||||
sm := searcherMock{Mock: m}
|
||||
b := &FolderAPIBuilder{
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures("nestedFolders")),
|
||||
searcher: sm,
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
features: nil,
|
||||
namespacer: func(_ int64) string { return "123" },
|
||||
folderSvc: foldertest.NewFakeService(),
|
||||
storage: us,
|
||||
searcher: sm,
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user