RBAC: Add required component to perform access control checks for user api when running single tenant (#93104)
* Unexport store and create new constructor function * Add ResourceAuthorizer and LegacyAccessClient * Configure checks for user store * List with checks if AccessClient is configured * Allow system user service account to read all users --------- Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
co-authored by
Gabriel MABILLE
parent
bca8bd3c8b
commit
2e38329026
@@ -28,6 +28,10 @@ type AccessControl interface {
|
||||
// RegisterScopeAttributeResolver allows the caller to register a scope resolver for a
|
||||
// specific scope prefix (ex: datasources:name:)
|
||||
RegisterScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver)
|
||||
// WithoutResolvers copies AccessControl without any configured resolvers.
|
||||
// This is useful when we don't want to reuse any pre-configured resolvers
|
||||
// for a authorization call.
|
||||
WithoutResolvers() AccessControl
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
|
||||
@@ -205,6 +205,16 @@ func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver a
|
||||
a.resolvers.AddScopeAttributeResolver(prefix, resolver)
|
||||
}
|
||||
|
||||
func (a *AccessControl) WithoutResolvers() accesscontrol.AccessControl {
|
||||
return &AccessControl{
|
||||
features: a.features,
|
||||
log: a.log,
|
||||
zclient: a.zclient,
|
||||
metrics: a.metrics,
|
||||
resolvers: accesscontrol.NewResolvers(a.log),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg string, eval accesscontrol.Evaluator) {
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.debug")
|
||||
defer span.End()
|
||||
|
||||
@@ -75,6 +75,10 @@ func (f FakeAccessControl) Evaluate(ctx context.Context, user identity.Requester
|
||||
func (f FakeAccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
||||
}
|
||||
|
||||
func (f FakeAccessControl) WithoutResolvers() accesscontrol.AccessControl {
|
||||
return f
|
||||
}
|
||||
|
||||
type FakeStore struct {
|
||||
ExpectedUserPermissions []accesscontrol.Permission
|
||||
ExpectedBasicRolesPermissions []accesscontrol.Permission
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
)
|
||||
|
||||
// ResourceResolver is called before authorization is performed.
|
||||
// It can be used to translate resoruce name into one or more valid scopes that
|
||||
// will be used for authorization. If more than one scope is returned from a resolver
|
||||
// only one needs to match to allow call to be authorized.
|
||||
type ResourceResolver interface {
|
||||
Resolve(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error)
|
||||
}
|
||||
|
||||
// ResourceResolverFunc is an adapter so that functions can implement ResourceResolver.
|
||||
type ResourceResolverFunc func(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error)
|
||||
|
||||
func (r ResourceResolverFunc) Resolve(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error) {
|
||||
return r(ctx, ns, name)
|
||||
}
|
||||
|
||||
type ResourceAuthorizerOptions struct {
|
||||
// Resource is the resource name in plural.
|
||||
Resource string
|
||||
// Attr is attribute used for resource scope. It's usually 'id' or 'uid'
|
||||
// depending on what is stored for the resource.
|
||||
Attr string
|
||||
// Mapping is used to translate k8s verb to rbac action.
|
||||
// Key is the desired verb and value the rbac action it should be translated into.
|
||||
Mapping map[string]string
|
||||
// Resolver if passed can translate into one or more scopes used to authorize resource.
|
||||
// This is useful when stored scopes are based on something else than k8s name or
|
||||
// for resources that inherit permission from folder.
|
||||
Resolver ResourceResolver
|
||||
}
|
||||
|
||||
var _ claims.AccessClient = (*LegacyAccessClient)(nil)
|
||||
|
||||
func NewLegacyAccessClient(ac AccessControl, opts ...ResourceAuthorizerOptions) *LegacyAccessClient {
|
||||
stored := map[string]ResourceAuthorizerOptions{}
|
||||
|
||||
for _, o := range opts {
|
||||
if o.Mapping == nil {
|
||||
o.Mapping = map[string]string{}
|
||||
}
|
||||
stored[o.Resource] = o
|
||||
}
|
||||
|
||||
return &LegacyAccessClient{ac.WithoutResolvers(), stored}
|
||||
}
|
||||
|
||||
type LegacyAccessClient struct {
|
||||
ac AccessControl
|
||||
opts map[string]ResourceAuthorizerOptions
|
||||
}
|
||||
|
||||
// HasAccess implements claims.AccessClient.
|
||||
func (c *LegacyAccessClient) HasAccess(ctx context.Context, id claims.AuthInfo, req claims.AccessRequest) (bool, error) {
|
||||
ident, ok := id.(identity.Requester)
|
||||
if !ok {
|
||||
return false, errors.New("expected identity.Requester for legacy access control")
|
||||
}
|
||||
|
||||
opts, ok := c.opts[req.Resource]
|
||||
if !ok {
|
||||
// For now we fallback to grafana admin if no options are found for resource.
|
||||
if ident.GetIsGrafanaAdmin() {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
action, ok := opts.Mapping[req.Verb]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("missing action for %s %s", req.Verb, req.Resource)
|
||||
}
|
||||
|
||||
ns, err := claims.ParseNamespace(req.Namespace)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var eval Evaluator
|
||||
if req.Name != "" {
|
||||
if opts.Resolver != nil {
|
||||
scopes, err := opts.Resolver.Resolve(ctx, ns, req.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
eval = EvalPermission(action, scopes...)
|
||||
} else {
|
||||
eval = EvalPermission(action, fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, req.Name))
|
||||
}
|
||||
} else if req.Verb == "list" {
|
||||
// For list request we need to filter out in storage layer.
|
||||
eval = EvalPermission(action)
|
||||
} else {
|
||||
// Assuming that all non list request should have a valid name
|
||||
return false, fmt.Errorf("unhandled authorization: %s %s", req.Group, req.Verb)
|
||||
}
|
||||
|
||||
return c.ac.Evaluate(ctx, ident, eval)
|
||||
}
|
||||
|
||||
// Compile implements claims.AccessClient.
|
||||
func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, req claims.AccessRequest) (claims.AccessChecker, error) {
|
||||
ident, ok := id.(identity.Requester)
|
||||
if !ok {
|
||||
return nil, errors.New("expected identity.Requester for legacy access control")
|
||||
}
|
||||
|
||||
opts, ok := c.opts[req.Resource]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported resource: %s", req.Resource)
|
||||
}
|
||||
|
||||
action, ok := opts.Mapping[req.Verb]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing action for %s %s", req.Verb, req.Resource)
|
||||
}
|
||||
|
||||
check := Checker(ident, action)
|
||||
return func(_, name string) bool {
|
||||
return check(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, name))
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package accesscontrol_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
func TestResourceAuthorizer_HasAccess(t *testing.T) {
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
|
||||
t.Run("should have no opinion for non resource requests", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
})
|
||||
|
||||
ok, err := a.HasAccess(context.Background(), &identity.StaticRequester{}, claims.AccessRequest{
|
||||
Verb: "get",
|
||||
Resource: "dashboards",
|
||||
Namespace: "default",
|
||||
Name: "1",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, false, ok)
|
||||
})
|
||||
|
||||
t.Run("should reject when user don't have correct scope", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{
|
||||
"get": "dashboards:read",
|
||||
},
|
||||
})
|
||||
|
||||
ident := newIdent(
|
||||
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:2"},
|
||||
)
|
||||
|
||||
ok, err := a.HasAccess(context.Background(), ident, claims.AccessRequest{
|
||||
Verb: "get",
|
||||
Namespace: "default",
|
||||
Resource: "dashboards",
|
||||
Name: "1",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, ok)
|
||||
})
|
||||
|
||||
t.Run("should just check action for list requests", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{
|
||||
"list": "dashboards:read",
|
||||
},
|
||||
})
|
||||
|
||||
ident := newIdent(
|
||||
accesscontrol.Permission{Action: "dashboards:read"},
|
||||
)
|
||||
|
||||
ok, err := a.HasAccess(context.Background(), ident, claims.AccessRequest{
|
||||
Verb: "list",
|
||||
Namespace: "default",
|
||||
Resource: "dashboards",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
})
|
||||
|
||||
t.Run("should allow when user have correct scope", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{
|
||||
"get": "dashboards:read",
|
||||
},
|
||||
})
|
||||
|
||||
ident := newIdent(
|
||||
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:1"},
|
||||
)
|
||||
|
||||
ok, err := a.HasAccess(context.Background(), ident, claims.AccessRequest{
|
||||
Verb: "get",
|
||||
Namespace: "default",
|
||||
Resource: "dashboards",
|
||||
Name: "1",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func newIdent(permissions ...accesscontrol.Permission) *identity.StaticRequester {
|
||||
pmap := map[string][]string{}
|
||||
for _, p := range permissions {
|
||||
pmap[p.Action] = append(pmap[p.Action], p.Scope)
|
||||
}
|
||||
|
||||
return &identity.StaticRequester{
|
||||
OrgID: 1,
|
||||
Permissions: map[int64]map[string][]string{1: pmap},
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
)
|
||||
|
||||
func Checker(user *user.SignedInUser, action string) func(scopes ...string) bool {
|
||||
if user.Permissions == nil || user.Permissions[user.OrgID] == nil {
|
||||
return func(scopes ...string) bool { return false }
|
||||
}
|
||||
|
||||
userScopes, ok := user.Permissions[user.OrgID][action]
|
||||
func Checker(user identity.Requester, action string) func(scopes ...string) bool {
|
||||
permissions := user.GetPermissions()
|
||||
userScopes, ok := permissions[action]
|
||||
if !ok {
|
||||
return func(scopes ...string) bool { return false }
|
||||
}
|
||||
|
||||
@@ -265,3 +265,8 @@ func (m *Mock) SyncUserRoles(ctx context.Context, orgID int64, cmd accesscontrol
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithoutResolvers implements fullAccessControl.
|
||||
func (m *Mock) WithoutResolvers() accesscontrol.AccessControl {
|
||||
return m
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user