The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/accesscontrol/authorizer.go

163 lines
5.2 KiB

package accesscontrol
import (
"context"
"errors"
"fmt"
"github.com/grafana/authlib/authz"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
// 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
// Unchecked is used to skip authorization checks for specified verbs.
// This takes precedence over configured Mapping
Unchecked map[string]bool
// 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.
// If no mapping is provided it will get a "default" mapping.
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 _ authz.AccessClient = (*LegacyAccessClient)(nil)
func NewLegacyAccessClient(ac AccessControl, opts ...ResourceAuthorizerOptions) *LegacyAccessClient {
stored := map[string]ResourceAuthorizerOptions{}
defaultMapping := func(r string) map[string]string {
return map[string]string{
utils.VerbGet: fmt.Sprintf("%s:read", r),
utils.VerbList: fmt.Sprintf("%s:read", r),
utils.VerbWatch: fmt.Sprintf("%s:read", r),
utils.VerbCreate: fmt.Sprintf("%s:create", r),
utils.VerbUpdate: fmt.Sprintf("%s:write", r),
utils.VerbPatch: fmt.Sprintf("%s:write", r),
utils.VerbDelete: fmt.Sprintf("%s:delete", r),
utils.VerbDeleteCollection: fmt.Sprintf("%s:delete", r),
}
}
for _, o := range opts {
if o.Unchecked == nil {
o.Unchecked = map[string]bool{}
}
if o.Mapping == nil {
o.Mapping = defaultMapping(o.Resource)
}
stored[o.Resource] = o
}
return &LegacyAccessClient{ac.WithoutResolvers(), stored}
}
type LegacyAccessClient struct {
ac AccessControl
opts map[string]ResourceAuthorizerOptions
}
func (c *LegacyAccessClient) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) {
ident, ok := id.(identity.Requester)
if !ok {
return authz.CheckResponse{}, 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 authz.CheckResponse{Allowed: true}, nil
}
return authz.CheckResponse{}, nil
}
skip := opts.Unchecked[req.Verb]
if skip {
return authz.CheckResponse{Allowed: true}, nil
}
action, ok := opts.Mapping[req.Verb]
if !ok {
return authz.CheckResponse{}, fmt.Errorf("missing action for %s %s", req.Verb, req.Resource)
}
ns, err := claims.ParseNamespace(req.Namespace)
if err != nil {
return authz.CheckResponse{}, err
}
var eval Evaluator
if req.Name != "" {
if opts.Resolver != nil {
scopes, err := opts.Resolver.Resolve(ctx, ns, req.Name)
if err != nil {
return authz.CheckResponse{}, err
}
eval = EvalPermission(action, scopes...)
} else {
eval = EvalPermission(action, fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, req.Name))
}
} else if req.Verb == utils.VerbList {
// 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 authz.CheckResponse{}, fmt.Errorf("unhandled authorization: %s %s", req.Group, req.Verb)
}
allowed, err := c.ac.Evaluate(ctx, ident, eval)
if err != nil {
return authz.CheckResponse{}, err
}
return authz.CheckResponse{Allowed: allowed}, nil
}
func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, 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[utils.VerbList]
if !ok {
return nil, fmt.Errorf("missing action for %s %s", utils.VerbList, req.Resource)
}
check := Checker(ident, action)
return func(_, name, _ string) bool {
return check(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, name))
}, nil
}