mirror of https://github.com/grafana/grafana
FolderAPI: Use different authorizer for multi-tenant api (#101372)
Use different authorizers depening on if we are running multi-tenant or single-tenantpull/101450/head
parent
5652e0b835
commit
d6b6a9da7b
@ -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) |
||||
}) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue