Test Folder's GetAuthorizer (#95266)

* WIP: setup to test folders GetAuthorizer

* Setup test

* Extract authorizer fn for tests

* Setup internal test fn

* Better define test inputs

* Add FolderAPI builder to the test

* First test passing

* Test getAuthorize for the create method

* Change authorizerFunc's signature

* [REVIEW] code readability

* Name error

* [REVIEW] add one more test case. Lint

* Remove empty line
This commit is contained in:
Leonor Oliveira 2024-10-28 11:00:19 +01:00 committed by GitHub
parent 4a4ad66210
commit 5cf86c981f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 175 additions and 38 deletions

View File

@ -2,6 +2,7 @@ package folders
import ( import (
"context" "context"
"errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -30,6 +31,9 @@ var _ builder.APIGroupBuilder = (*FolderAPIBuilder)(nil)
var resourceInfo = v0alpha1.FolderResourceInfo var resourceInfo = v0alpha1.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 // This is used just so wire has something unique to return
type FolderAPIBuilder struct { type FolderAPIBuilder struct {
gv schema.GroupVersion gv schema.GroupVersion
@ -154,42 +158,59 @@ func (b *FolderAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAP
return oas, nil return oas, nil
} }
func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer { type authorizerParams struct {
return authorizer.AuthorizerFunc( user identity.Requester
func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { evaluator accesscontrol.Evaluator
verb := attr.GetVerb() }
name := attr.GetName()
if (!attr.IsResourceRequest()) || (name == "" && verb != utils.VerbCreate) { func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return authorizer.DecisionNoOpinion, "", nil return authorizer.AuthorizerFunc(func(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
} in, err := authorizerFunc(ctx, attr)
if err != nil {
// require a user if errors.Is(err, errNoUser) {
user, err := identity.GetRequester(ctx) return authorizer.DecisionDeny, "", nil
if err != nil { }
return authorizer.DecisionDeny, "valid user is required", err return authorizer.DecisionNoOpinion, "", nil
} }
scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(name) ok, err := b.accessControl.Evaluate(ctx, in.user, in.evaluator)
eval := accesscontrol.EvalPermission(dashboards.ActionFoldersRead, scope) if ok {
return authorizer.DecisionAllow, "", nil
// "get" is used for sub-resources with GET http (parents, access, count) }
switch verb { return authorizer.DecisionDeny, "folder", err
case utils.VerbCreate: })
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersCreate) }
case utils.VerbPatch:
fallthrough func authorizerFunc(ctx context.Context, attr authorizer.Attributes) (*authorizerParams, error) {
case utils.VerbUpdate: verb := attr.GetVerb()
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, scope) name := attr.GetName()
case utils.VerbDeleteCollection: if (!attr.IsResourceRequest()) || (name == "" && verb != utils.VerbCreate) {
fallthrough return nil, errNoResource
case utils.VerbDelete: }
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, scope)
} // require a user
user, err := identity.GetRequester(ctx)
ok, err := b.accessControl.Evaluate(ctx, user, eval) if err != nil {
if ok { return nil, errNoUser
return authorizer.DecisionAllow, "", nil }
}
return authorizer.DecisionDeny, "folder", err 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)
default:
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersRead, scope)
}
return &authorizerParams{evaluator: eval, user: user}, nil
} }

View File

@ -0,0 +1,116 @@
package folders
import (
"context"
"testing"
"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/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
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",
},
},
}
b := &FolderAPIBuilder{
gv: resourceInfo.GroupVersion(),
features: nil,
namespacer: func(_ int64) string { return "123" },
folderSvc: foldertest.NewFakeService(),
accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures("nestedFolders"), zanzana.NewNoopClient()),
}
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)
})
}
}