mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access Control: Refactor scope resolvers with support to resolve into several scopes (#48202)
* Refactor Scope resolver to support resolving into several scopes * Change permission evaluator to match at least one of passed scopes
This commit is contained in:
parent
9622e7457e
commit
de50f39c12
@ -401,20 +401,21 @@ func (hs *HTTPServer) GetAnnotationTags(c *models.ReqContext) response.Response
|
|||||||
return response.JSON(http.StatusOK, annotations.GetAnnotationTagsResponse{Result: result})
|
return response.JSON(http.StatusOK, annotations.GetAnnotationTagsResponse{Result: result})
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnnotationTypeScopeResolver provides an AttributeScopeResolver able to
|
// AnnotationTypeScopeResolver provides an ScopeAttributeResolver able to
|
||||||
// resolve annotation types. Scope "annotations:id:<id>" will be translated to "annotations:type:<type>,
|
// resolve annotation types. Scope "annotations:id:<id>" will be translated to "annotations:type:<type>,
|
||||||
// where <type> is the type of annotation with id <id>.
|
// where <type> is the type of annotation with id <id>.
|
||||||
func AnnotationTypeScopeResolver() (string, accesscontrol.AttributeScopeResolveFunc) {
|
func AnnotationTypeScopeResolver() (string, accesscontrol.ScopeAttributeResolver) {
|
||||||
annotationTypeResolver := func(ctx context.Context, orgID int64, initialScope string) (string, error) {
|
prefix := accesscontrol.ScopeAnnotationsProvider.GetResourceScope("")
|
||||||
|
return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
|
||||||
scopeParts := strings.Split(initialScope, ":")
|
scopeParts := strings.Split(initialScope, ":")
|
||||||
if scopeParts[0] != accesscontrol.ScopeAnnotationsRoot || len(scopeParts) != 3 {
|
if scopeParts[0] != accesscontrol.ScopeAnnotationsRoot || len(scopeParts) != 3 {
|
||||||
return "", accesscontrol.ErrInvalidScope
|
return nil, accesscontrol.ErrInvalidScope
|
||||||
}
|
}
|
||||||
|
|
||||||
annotationIdStr := scopeParts[2]
|
annotationIdStr := scopeParts[2]
|
||||||
annotationId, err := strconv.Atoi(annotationIdStr)
|
annotationId, err := strconv.Atoi(annotationIdStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", accesscontrol.ErrInvalidScope
|
return nil, accesscontrol.ErrInvalidScope
|
||||||
}
|
}
|
||||||
|
|
||||||
// tempUser is used to resolve annotation type.
|
// tempUser is used to resolve annotation type.
|
||||||
@ -431,16 +432,15 @@ func AnnotationTypeScopeResolver() (string, accesscontrol.AttributeScopeResolveF
|
|||||||
|
|
||||||
annotation, resp := findAnnotationByID(ctx, annotations.GetRepository(), int64(annotationId), tempUser)
|
annotation, resp := findAnnotationByID(ctx, annotations.GetRepository(), int64(annotationId), tempUser)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
return "", errors.New("could not resolve annotation type")
|
return nil, errors.New("could not resolve annotation type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if annotation.GetType() == annotations.Organization {
|
if annotation.GetType() == annotations.Organization {
|
||||||
return accesscontrol.ScopeAnnotationsTypeOrganization, nil
|
return []string{accesscontrol.ScopeAnnotationsTypeOrganization}, nil
|
||||||
} else {
|
} else {
|
||||||
return accesscontrol.ScopeAnnotationsTypeDashboard, nil
|
return []string{accesscontrol.ScopeAnnotationsTypeDashboard}, nil
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
return accesscontrol.ScopeAnnotationsProvider.GetResourceScope(""), annotationTypeResolver
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int64) (bool, error) {
|
func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int64) (bool, error) {
|
||||||
|
@ -722,7 +722,7 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
setUpRBACGuardian(t)
|
setUpRBACGuardian(t)
|
||||||
sc.acmock.
|
sc.acmock.
|
||||||
RegisterAttributeScopeResolver(AnnotationTypeScopeResolver())
|
RegisterScopeAttributeResolver(AnnotationTypeScopeResolver())
|
||||||
setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgId)
|
setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgId)
|
||||||
|
|
||||||
r := callAPI(sc.server, tt.args.method, tt.args.url, tt.args.body, t)
|
r := callAPI(sc.server, tt.args.method, tt.args.url, tt.args.body, t)
|
||||||
@ -780,13 +780,14 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
resolved, err := resolver(context.Background(), 1, tc.given)
|
resolved, err := resolver.Resolve(context.Background(), 1, tc.given)
|
||||||
if tc.wantErr != nil {
|
if tc.wantErr != nil {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Equal(t, tc.wantErr, err)
|
require.Equal(t, tc.wantErr, err)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, tc.want, resolved)
|
require.Len(t, resolved, 1)
|
||||||
|
require.Equal(t, tc.want, resolved[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -265,7 +265,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
hs.registerRoutes()
|
hs.registerRoutes()
|
||||||
|
|
||||||
// Register access control scope resolver for annotations
|
// Register access control scope resolver for annotations
|
||||||
hs.AccessControl.RegisterAttributeScopeResolver(AnnotationTypeScopeResolver())
|
hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver())
|
||||||
|
|
||||||
if err := hs.declareFixedRoles(); err != nil {
|
if err := hs.declareFixedRoles(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -32,9 +32,9 @@ type AccessControl interface {
|
|||||||
// assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
// assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
||||||
DeclareFixedRoles(...RoleRegistration) error
|
DeclareFixedRoles(...RoleRegistration) error
|
||||||
|
|
||||||
// RegisterAttributeScopeResolver allows the caller to register a scope resolver for a
|
// RegisterScopeAttributeResolver allows the caller to register a scope resolver for a
|
||||||
// specific scope prefix (ex: datasources:name:)
|
// specific scope prefix (ex: datasources:name:)
|
||||||
RegisterAttributeScopeResolver(scopePrefix string, resolver AttributeScopeResolveFunc)
|
RegisterScopeAttributeResolver(scopePrefix string, resolver ScopeAttributeResolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PermissionsProvider interface {
|
type PermissionsProvider interface {
|
||||||
|
@ -14,7 +14,7 @@ type Evaluator interface {
|
|||||||
// Evaluate permissions that are grouped by action
|
// Evaluate permissions that are grouped by action
|
||||||
Evaluate(permissions map[string][]string) (bool, error)
|
Evaluate(permissions map[string][]string) (bool, error)
|
||||||
// MutateScopes executes a sequence of ScopeModifier functions on all embedded scopes of an evaluator and returns a new Evaluator
|
// MutateScopes executes a sequence of ScopeModifier functions on all embedded scopes of an evaluator and returns a new Evaluator
|
||||||
MutateScopes(context.Context, ...ScopeMutator) (Evaluator, error)
|
MutateScopes(ctx context.Context, mutate ScopeAttributeMutator) (Evaluator, error)
|
||||||
// String returns a string representation of permission required by the evaluator
|
// String returns a string representation of permission required by the evaluator
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
fmt.GoStringer
|
fmt.GoStringer
|
||||||
@ -22,7 +22,7 @@ type Evaluator interface {
|
|||||||
|
|
||||||
var _ Evaluator = new(permissionEvaluator)
|
var _ Evaluator = new(permissionEvaluator)
|
||||||
|
|
||||||
// EvalPermission returns an evaluator that will require all scopes in combination with action to match
|
// EvalPermission returns an evaluator that will require at least one of passed scopes to match
|
||||||
func EvalPermission(action string, scopes ...string) Evaluator {
|
func EvalPermission(action string, scopes ...string) Evaluator {
|
||||||
return permissionEvaluator{Action: action, Scopes: scopes}
|
return permissionEvaluator{Action: action, Scopes: scopes}
|
||||||
}
|
}
|
||||||
@ -43,29 +43,19 @@ func (p permissionEvaluator) Evaluate(permissions map[string][]string) (bool, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, target := range p.Scopes {
|
for _, target := range p.Scopes {
|
||||||
var err error
|
|
||||||
var matches bool
|
|
||||||
|
|
||||||
for _, scope := range userScopes {
|
for _, scope := range userScopes {
|
||||||
matches, err = match(scope, target)
|
if match(scope, target) {
|
||||||
if err != nil {
|
return true, nil
|
||||||
return false, err
|
|
||||||
}
|
}
|
||||||
if matches {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matches {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func match(scope, target string) (bool, error) {
|
func match(scope, target string) bool {
|
||||||
if scope == "" {
|
if scope == "" {
|
||||||
return false, nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ValidateScope(scope) {
|
if !ValidateScope(scope) {
|
||||||
@ -74,7 +64,7 @@ func match(scope, target string) (bool, error) {
|
|||||||
"scope", scope,
|
"scope", scope,
|
||||||
"reason", "scopes should not contain meta-characters like * or ?, except in the last position",
|
"reason", "scopes should not contain meta-characters like * or ?, except in the last position",
|
||||||
)
|
)
|
||||||
return false, nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix, last := scope[:len(scope)-1], scope[len(scope)-1]
|
prefix, last := scope[:len(scope)-1], scope[len(scope)-1]
|
||||||
@ -82,29 +72,25 @@ func match(scope, target string) (bool, error) {
|
|||||||
if last == '*' {
|
if last == '*' {
|
||||||
if strings.HasPrefix(target, prefix) {
|
if strings.HasPrefix(target, prefix) {
|
||||||
logger.Debug("matched scope", "user scope", scope, "target scope", target)
|
logger.Debug("matched scope", "user scope", scope, "target scope", target)
|
||||||
return true, nil
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return scope == target, nil
|
return scope == target
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p permissionEvaluator) MutateScopes(ctx context.Context, modifiers ...ScopeMutator) (Evaluator, error) {
|
func (p permissionEvaluator) MutateScopes(ctx context.Context, mutate ScopeAttributeMutator) (Evaluator, error) {
|
||||||
var err error
|
|
||||||
if p.Scopes == nil {
|
if p.Scopes == nil {
|
||||||
return EvalPermission(p.Action), nil
|
return EvalPermission(p.Action), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
scopes := make([]string, 0, len(p.Scopes))
|
scopes := make([]string, 0, len(p.Scopes))
|
||||||
for _, scope := range p.Scopes {
|
for _, scope := range p.Scopes {
|
||||||
modified := scope
|
mutated, err := mutate(ctx, scope)
|
||||||
for _, modifier := range modifiers {
|
|
||||||
modified, err = modifier(ctx, modified)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
scopes = append(scopes, mutated...)
|
||||||
scopes = append(scopes, modified)
|
|
||||||
}
|
}
|
||||||
return EvalPermission(p.Action, scopes...), nil
|
return EvalPermission(p.Action, scopes...), nil
|
||||||
}
|
}
|
||||||
@ -137,10 +123,10 @@ func (a allEvaluator) Evaluate(permissions map[string][]string) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a allEvaluator) MutateScopes(ctx context.Context, modifiers ...ScopeMutator) (Evaluator, error) {
|
func (a allEvaluator) MutateScopes(ctx context.Context, mutate ScopeAttributeMutator) (Evaluator, error) {
|
||||||
var modified []Evaluator
|
var modified []Evaluator
|
||||||
for _, e := range a.allOf {
|
for _, e := range a.allOf {
|
||||||
i, err := e.MutateScopes(ctx, modifiers...)
|
i, err := e.MutateScopes(ctx, mutate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -191,10 +177,10 @@ func (a anyEvaluator) Evaluate(permissions map[string][]string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a anyEvaluator) MutateScopes(ctx context.Context, modifiers ...ScopeMutator) (Evaluator, error) {
|
func (a anyEvaluator) MutateScopes(ctx context.Context, mutate ScopeAttributeMutator) (Evaluator, error) {
|
||||||
var modified []Evaluator
|
var modified []Evaluator
|
||||||
for _, e := range a.anyOf {
|
for _, e := range a.anyOf {
|
||||||
i, err := e.MutateScopes(ctx, modifiers...)
|
i, err := e.MutateScopes(ctx, mutate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -25,11 +25,11 @@ func TestPermission_Evaluate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should evaluate to true when allEvaluator required scopes matches",
|
desc: "should evaluate to true when at least one scope matches",
|
||||||
expected: true,
|
expected: true,
|
||||||
evaluator: EvalPermission("reports:read", "reports:1", "reports:2"),
|
evaluator: EvalPermission("reports:read", "reports:1", "reports:2"),
|
||||||
permissions: map[string][]string{
|
permissions: map[string][]string{
|
||||||
"reports:read": {"reports:1", "reports:2"},
|
"reports:read": {"reports:2"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -41,11 +41,11 @@ func TestPermission_Evaluate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should evaluate to false when only one of required scopes exists",
|
desc: "should evaluate to false when no scopes matches",
|
||||||
expected: false,
|
expected: false,
|
||||||
evaluator: EvalPermission("reports:read", "reports:1", "reports:2"),
|
evaluator: EvalPermission("reports:read", "reports:1", "reports:2"),
|
||||||
permissions: map[string][]string{
|
permissions: map[string][]string{
|
||||||
"reports:read": {"reports:1"},
|
"reports:read": {"reports:9", "reports:10"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,9 @@ type Mock struct {
|
|||||||
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
|
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
|
||||||
GetUserBuiltInRolesFunc func(user *models.SignedInUser) []string
|
GetUserBuiltInRolesFunc func(user *models.SignedInUser) []string
|
||||||
RegisterFixedRolesFunc func() error
|
RegisterFixedRolesFunc func() error
|
||||||
RegisterAttributeScopeResolverFunc func(string, accesscontrol.AttributeScopeResolveFunc)
|
RegisterScopeAttributeResolverFunc func(string, accesscontrol.ScopeAttributeResolver)
|
||||||
|
|
||||||
scopeResolver accesscontrol.ScopeResolver
|
scopeResolvers accesscontrol.ScopeResolvers
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the mock stays in line with the interface
|
// Ensure the mock stays in line with the interface
|
||||||
@ -59,7 +59,7 @@ func New() *Mock {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
permissions: []*accesscontrol.Permission{},
|
permissions: []*accesscontrol.Permission{},
|
||||||
builtInRoles: []string{},
|
builtInRoles: []string{},
|
||||||
scopeResolver: accesscontrol.NewScopeResolver(),
|
scopeResolvers: accesscontrol.NewScopeResolvers(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return mock
|
return mock
|
||||||
@ -98,7 +98,7 @@ func (m *Mock) Evaluate(ctx context.Context, user *models.SignedInUser, evaluato
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeMutator := m.scopeResolver.GetResolveAttributeScopeMutator(user.OrgId)
|
attributeMutator := m.scopeResolvers.GetScopeAttributeMutator(user.OrgId)
|
||||||
resolvedEvaluator, err := evaluator.MutateScopes(ctx, attributeMutator)
|
resolvedEvaluator, err := evaluator.MutateScopes(ctx, attributeMutator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -178,13 +178,11 @@ func (m *Mock) RegisterFixedRoles(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterAttributeScopeResolver allows the caller to register a scope resolver for a
|
func (m *Mock) RegisterScopeAttributeResolver(scopePrefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
||||||
// specific scope prefix (ex: datasources:name:)
|
m.scopeResolvers.AddScopeAttributeResolver(scopePrefix, resolver)
|
||||||
func (m *Mock) RegisterAttributeScopeResolver(scopePrefix string, resolver accesscontrol.AttributeScopeResolveFunc) {
|
|
||||||
m.scopeResolver.AddAttributeResolver(scopePrefix, resolver)
|
|
||||||
m.Calls.RegisterAttributeScopeResolver = append(m.Calls.RegisterAttributeScopeResolver, []struct{}{})
|
m.Calls.RegisterAttributeScopeResolver = append(m.Calls.RegisterAttributeScopeResolver, []struct{}{})
|
||||||
// Use override if provided
|
// Use override if provided
|
||||||
if m.RegisterAttributeScopeResolverFunc != nil {
|
if m.RegisterScopeAttributeResolverFunc != nil {
|
||||||
m.RegisterAttributeScopeResolverFunc(scopePrefix, resolver)
|
m.RegisterScopeAttributeResolverFunc(scopePrefix, resolver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ func ProvideOSSAccessControl(features featuremgmt.FeatureToggles, provider acces
|
|||||||
features: features,
|
features: features,
|
||||||
provider: provider,
|
provider: provider,
|
||||||
log: log.New("accesscontrol"),
|
log: log.New("accesscontrol"),
|
||||||
scopeResolver: accesscontrol.NewScopeResolver(),
|
scopeResolvers: accesscontrol.NewScopeResolvers(),
|
||||||
roles: accesscontrol.BuildMacroRoleDefinitions(),
|
roles: accesscontrol.BuildMacroRoleDefinitions(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ func ProvideOSSAccessControl(features featuremgmt.FeatureToggles, provider acces
|
|||||||
type OSSAccessControlService struct {
|
type OSSAccessControlService struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
features featuremgmt.FeatureToggles
|
features featuremgmt.FeatureToggles
|
||||||
scopeResolver accesscontrol.ScopeResolver
|
scopeResolvers accesscontrol.ScopeResolvers
|
||||||
provider accesscontrol.PermissionsProvider
|
provider accesscontrol.PermissionsProvider
|
||||||
registrations accesscontrol.RegistrationList
|
registrations accesscontrol.RegistrationList
|
||||||
roles map[string]*accesscontrol.RoleDTO
|
roles map[string]*accesscontrol.RoleDTO
|
||||||
@ -92,7 +92,7 @@ func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.Si
|
|||||||
user.Permissions[user.OrgId] = accesscontrol.GroupScopesByAction(permissions)
|
user.Permissions[user.OrgId] = accesscontrol.GroupScopesByAction(permissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeMutator := ac.scopeResolver.GetResolveAttributeScopeMutator(user.OrgId)
|
attributeMutator := ac.scopeResolvers.GetScopeAttributeMutator(user.OrgId)
|
||||||
resolvedEvaluator, err := evaluator.MutateScopes(ctx, attributeMutator)
|
resolvedEvaluator, err := evaluator.MutateScopes(ctx, attributeMutator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -124,7 +124,7 @@ func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user
|
|||||||
|
|
||||||
permissions = append(permissions, dbPermissions...)
|
permissions = append(permissions, dbPermissions...)
|
||||||
resolved := make([]*accesscontrol.Permission, 0, len(permissions))
|
resolved := make([]*accesscontrol.Permission, 0, len(permissions))
|
||||||
keywordMutator := ac.scopeResolver.GetResolveKeywordScopeMutator(user)
|
keywordMutator := ac.scopeResolvers.GetScopeKeywordMutator(user)
|
||||||
for _, p := range permissions {
|
for _, p := range permissions {
|
||||||
// if the permission has a keyword in its scope it will be resolved
|
// if the permission has a keyword in its scope it will be resolved
|
||||||
p.Scope, err = keywordMutator(ctx, p.Scope)
|
p.Scope, err = keywordMutator(ctx, p.Scope)
|
||||||
@ -217,8 +217,8 @@ func (ac *OSSAccessControlService) DeclareFixedRoles(registrations ...accesscont
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterAttributeScopeResolver allows the caller to register scope resolvers for a
|
// RegisterScopeAttributeResolver allows the caller to register scope resolvers for a
|
||||||
// specific scope prefix (ex: datasources:name:)
|
// specific scope prefix (ex: datasources:name:)
|
||||||
func (ac *OSSAccessControlService) RegisterAttributeScopeResolver(scopePrefix string, resolver accesscontrol.AttributeScopeResolveFunc) {
|
func (ac *OSSAccessControlService) RegisterScopeAttributeResolver(scopePrefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
||||||
ac.scopeResolver.AddAttributeResolver(scopePrefix, resolver)
|
ac.scopeResolvers.AddScopeAttributeResolver(scopePrefix, resolver)
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ func setupTestEnv(t testing.TB) *OSSAccessControlService {
|
|||||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol),
|
features: featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol),
|
||||||
log: log.New("accesscontrol"),
|
log: log.New("accesscontrol"),
|
||||||
registrations: accesscontrol.RegistrationList{},
|
registrations: accesscontrol.RegistrationList{},
|
||||||
scopeResolver: accesscontrol.NewScopeResolver(),
|
scopeResolvers: accesscontrol.NewScopeResolvers(),
|
||||||
provider: database.ProvideService(sqlstore.InitTestDB(t)),
|
provider: database.ProvideService(sqlstore.InitTestDB(t)),
|
||||||
roles: accesscontrol.BuildMacroRoleDefinitions(),
|
roles: accesscontrol.BuildMacroRoleDefinitions(),
|
||||||
}
|
}
|
||||||
@ -439,12 +439,12 @@ func TestOSSAccessControlService_Evaluate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Grants: []string{"Viewer"},
|
Grants: []string{"Viewer"},
|
||||||
}
|
}
|
||||||
userLoginScopeSolver := func(ctx context.Context, orgID int64, initialScope string) (string, error) {
|
userLoginScopeSolver := accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
|
||||||
if initialScope == "users:login:testUser" {
|
if initialScope == "users:login:testUser" {
|
||||||
return "users:id:2", nil
|
return []string{"users:id:2"}, nil
|
||||||
}
|
|
||||||
return initialScope, nil
|
|
||||||
}
|
}
|
||||||
|
return []string{initialScope}, nil
|
||||||
|
})
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -475,7 +475,7 @@ func TestOSSAccessControlService_Evaluate(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Setup
|
// Setup
|
||||||
ac := setupTestEnv(t)
|
ac := setupTestEnv(t)
|
||||||
ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver)
|
ac.RegisterScopeAttributeResolver("users:login:", userLoginScopeSolver)
|
||||||
|
|
||||||
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
||||||
err := ac.DeclareFixedRoles(registration)
|
err := ac.DeclareFixedRoles(registration)
|
||||||
|
140
pkg/services/accesscontrol/resolvers.go
Normal file
140
pkg/services/accesscontrol/resolvers.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package accesscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ttl = 30 * time.Second
|
||||||
|
cleanInterval = 2 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewScopeResolvers() ScopeResolvers {
|
||||||
|
return ScopeResolvers{
|
||||||
|
keywordResolvers: map[string]ScopeKeywordResolver{
|
||||||
|
"users:self": userSelfResolver,
|
||||||
|
},
|
||||||
|
attributeResolvers: map[string]ScopeAttributeResolver{},
|
||||||
|
cache: localcache.New(ttl, cleanInterval),
|
||||||
|
log: log.New("accesscontrol.resolver"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeResolvers struct {
|
||||||
|
log log.Logger
|
||||||
|
cache *localcache.CacheService
|
||||||
|
keywordResolvers map[string]ScopeKeywordResolver
|
||||||
|
attributeResolvers map[string]ScopeAttributeResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScopeResolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator {
|
||||||
|
return func(ctx context.Context, scope string) ([]string, error) {
|
||||||
|
key := getScopeCacheKey(orgID, scope)
|
||||||
|
// Check cache before computing the scope
|
||||||
|
if cachedScope, ok := s.cache.Get(key); ok {
|
||||||
|
scopes := cachedScope.([]string)
|
||||||
|
s.log.Debug("used cache to resolve '%v' to '%v'", scope, scopes)
|
||||||
|
return scopes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := ScopePrefix(scope)
|
||||||
|
if resolver, ok := s.attributeResolvers[prefix]; ok {
|
||||||
|
scopes, err := resolver.Resolve(ctx, orgID, scope)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not resolve %v: %w", scope, err)
|
||||||
|
}
|
||||||
|
// Cache result
|
||||||
|
s.cache.Set(key, scopes, ttl)
|
||||||
|
s.log.Debug("resolved '%v' to '%v'", scope, scopes)
|
||||||
|
return scopes, nil
|
||||||
|
}
|
||||||
|
return []string{scope}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScopeResolvers) GetScopeKeywordMutator(user *models.SignedInUser) ScopeKeywordMutator {
|
||||||
|
return func(ctx context.Context, scope string) (string, error) {
|
||||||
|
if resolver, ok := s.keywordResolvers[scope]; ok {
|
||||||
|
scopes, err := resolver.Resolve(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("could not resolve %v: %w", scope, err)
|
||||||
|
}
|
||||||
|
s.log.Debug("resolved '%v' to '%v'", scope, scopes)
|
||||||
|
return scopes, nil
|
||||||
|
}
|
||||||
|
// By default, the scope remains unchanged
|
||||||
|
return scope, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScopeResolvers) AddScopeKeywordResolver(keyword string, resolver ScopeKeywordResolver) {
|
||||||
|
s.log.Debug("adding scope keyword resolver for '%v'", keyword)
|
||||||
|
s.keywordResolvers[keyword] = resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScopeResolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) {
|
||||||
|
s.log.Debug("adding scope attribute resolver for '%v'", prefix)
|
||||||
|
s.attributeResolvers[prefix] = resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScopeAttributeResolver is used to resolve attributes in scopes to one or more scopes that are
|
||||||
|
// evaluated by logical or. E.g. "dashboards:id:1" -> "dashboards:uid:test-dashboard" or "folder:uid:test-folder"
|
||||||
|
type ScopeAttributeResolver interface {
|
||||||
|
Resolve(ctx context.Context, orgID int64, scope string) ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScopeAttributeResolverFunc is an adapter to allow functions to implement ScopeAttributeResolver interface
|
||||||
|
type ScopeAttributeResolverFunc func(ctx context.Context, orgID int64, scope string) ([]string, error)
|
||||||
|
|
||||||
|
func (f ScopeAttributeResolverFunc) Resolve(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||||
|
return f(ctx, orgID, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeAttributeMutator func(context.Context, string) ([]string, error)
|
||||||
|
|
||||||
|
// ScopeKeywordResolver is used to resolve keywords in scopes e.g. "users:self" -> "user:id:1".
|
||||||
|
// These type of resolvers is used when fetching stored permissions
|
||||||
|
type ScopeKeywordResolver interface {
|
||||||
|
Resolve(ctx context.Context, user *models.SignedInUser) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScopeKeywordResolverFunc is an adapter to allow functions to implement ScopeKeywordResolver interface
|
||||||
|
type ScopeKeywordResolverFunc func(ctx context.Context, user *models.SignedInUser) (string, error)
|
||||||
|
|
||||||
|
func (f ScopeKeywordResolverFunc) Resolve(ctx context.Context, user *models.SignedInUser) (string, error) {
|
||||||
|
return f(ctx, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeKeywordMutator func(context.Context, string) (string, error)
|
||||||
|
|
||||||
|
// getScopeCacheKey creates an identifier to fetch and store resolution of scopes in the cache
|
||||||
|
func getScopeCacheKey(orgID int64, scope string) string {
|
||||||
|
return fmt.Sprintf("%s-%v", scope, orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
//ScopeInjector inject request params into the templated scopes. e.g. "settings:" + eval.Parameters(":id")
|
||||||
|
func ScopeInjector(params ScopeParams) ScopeAttributeMutator {
|
||||||
|
return func(_ context.Context, scope string) ([]string, error) {
|
||||||
|
tmpl, err := template.New("scope").Parse(scope)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err = tmpl.Execute(&buf, params); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []string{buf.String()}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userSelfResolver = ScopeKeywordResolverFunc(func(ctx context.Context, user *models.SignedInUser) (string, error) {
|
||||||
|
return Scope("users", "id", fmt.Sprintf("%v", user.UserId)), nil
|
||||||
|
})
|
159
pkg/services/accesscontrol/resolvers_test.go
Normal file
159
pkg/services/accesscontrol/resolvers_test.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package accesscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveKeywordScope(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
user *models.SignedInUser
|
||||||
|
permission Permission
|
||||||
|
want Permission
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no scope",
|
||||||
|
user: testUser,
|
||||||
|
permission: Permission{Action: "users:read"},
|
||||||
|
want: Permission{Action: "users:read"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user if resolution",
|
||||||
|
user: testUser,
|
||||||
|
permission: Permission{Action: "users:read", Scope: "users:self"},
|
||||||
|
want: Permission{Action: "users:read", Scope: "users:id:2"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
var err error
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
resolvers := NewScopeResolvers()
|
||||||
|
scopeModifier := resolvers.GetScopeKeywordMutator(tt.user)
|
||||||
|
tt.permission.Scope, err = scopeModifier(context.TODO(), tt.permission.Scope)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err, "expected an error during the resolution of the scope")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, tt.want, tt.permission, "permission did not match expected resolution")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testUser = &models.SignedInUser{
|
||||||
|
UserId: 2,
|
||||||
|
OrgId: 3,
|
||||||
|
OrgName: "TestOrg",
|
||||||
|
OrgRole: models.ROLE_VIEWER,
|
||||||
|
Login: "testUser",
|
||||||
|
Name: "Test User",
|
||||||
|
Email: "testuser@example.org",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveAttributeScope(t *testing.T) {
|
||||||
|
// Calls allow us to see how many times the fakeDataSourceResolution has been called
|
||||||
|
calls := 0
|
||||||
|
fakeDataSourceResolver := ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
|
||||||
|
calls++
|
||||||
|
if initialScope == "datasources:name:testds" {
|
||||||
|
return []string{Scope("datasources", "id", "1")}, nil
|
||||||
|
} else if initialScope == "datasources:name:testds2" {
|
||||||
|
return []string{Scope("datasources", "id", "2")}, nil
|
||||||
|
} else if initialScope == "datasources:name:test:ds4" {
|
||||||
|
return []string{Scope("datasources", "id", "4")}, nil
|
||||||
|
} else if initialScope == "datasources:name:testds5*" {
|
||||||
|
return []string{Scope("datasources", "id", "5")}, nil
|
||||||
|
} else {
|
||||||
|
return nil, models.ErrDataSourceNotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
orgID int64
|
||||||
|
evaluator Evaluator
|
||||||
|
wantEvaluator Evaluator
|
||||||
|
wantCalls int
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should work with scope less permissions",
|
||||||
|
evaluator: EvalPermission("datasources:read"),
|
||||||
|
wantEvaluator: EvalPermission("datasources:read"),
|
||||||
|
wantCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should handle an error",
|
||||||
|
orgID: 1,
|
||||||
|
evaluator: EvalPermission("datasources:read", Scope("datasources", "name", "testds3")),
|
||||||
|
wantErr: models.ErrDataSourceNotFound,
|
||||||
|
wantCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should resolve a scope",
|
||||||
|
orgID: 1,
|
||||||
|
evaluator: EvalPermission("datasources:read", Scope("datasources", "name", "testds")),
|
||||||
|
wantEvaluator: EvalPermission("datasources:read", Scope("datasources", "id", "1")),
|
||||||
|
wantCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should resolve nested scopes with cache",
|
||||||
|
orgID: 1,
|
||||||
|
evaluator: EvalAll(
|
||||||
|
EvalPermission("datasources:read", Scope("datasources", "name", "testds")),
|
||||||
|
EvalAny(
|
||||||
|
EvalPermission("datasources:read", Scope("datasources", "name", "testds")),
|
||||||
|
EvalPermission("datasources:read", Scope("datasources", "name", "testds2")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
wantEvaluator: EvalAll(
|
||||||
|
EvalPermission("datasources:read", Scope("datasources", "id", "1")),
|
||||||
|
EvalAny(
|
||||||
|
EvalPermission("datasources:read", Scope("datasources", "id", "1")),
|
||||||
|
EvalPermission("datasources:read", Scope("datasources", "id", "2")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
wantCalls: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should resolve name with colon",
|
||||||
|
orgID: 1,
|
||||||
|
evaluator: EvalPermission("datasources:read", Scope("datasources", "name", "test:ds4")),
|
||||||
|
wantEvaluator: EvalPermission("datasources:read", Scope("datasources", "id", "4")),
|
||||||
|
wantCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should resolve names with '*'",
|
||||||
|
orgID: 1,
|
||||||
|
evaluator: EvalPermission("datasources:read", Scope("datasources", "name", "testds5*")),
|
||||||
|
wantEvaluator: EvalPermission("datasources:read", Scope("datasources", "id", "5")),
|
||||||
|
wantCalls: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
resolvers := NewScopeResolvers()
|
||||||
|
|
||||||
|
// Reset calls counter
|
||||||
|
calls = 0
|
||||||
|
// Register a resolution method
|
||||||
|
resolvers.AddScopeAttributeResolver("datasources:name:", fakeDataSourceResolver)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
mutate := resolvers.GetScopeAttributeMutator(tt.orgID)
|
||||||
|
resolvedEvaluator, err := tt.evaluator.MutateScopes(context.Background(), mutate)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
assert.ErrorAs(t, err, &tt.wantErr, "expected an error during the resolution of the scope")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, tt.wantEvaluator, resolvedEvaluator, "permission did not match expected resolution")
|
||||||
|
assert.Equal(t, tt.wantCalls, calls, "cache has not been used")
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,11 @@
|
|||||||
package accesscontrol
|
package accesscontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ttl = 30 * time.Second
|
|
||||||
cleanInterval = 2 * time.Minute
|
|
||||||
maxPrefixParts = 2
|
maxPrefixParts = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -68,95 +58,6 @@ func Field(key string) string {
|
|||||||
return fmt.Sprintf(`{{ .%s }}`, key)
|
return fmt.Sprintf(`{{ .%s }}`, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScopeMutator alters a Scope to return a new modified Scope
|
|
||||||
type ScopeMutator func(context.Context, string) (string, error)
|
|
||||||
|
|
||||||
type KeywordScopeResolveFunc func(*models.SignedInUser) (string, error)
|
|
||||||
|
|
||||||
// ScopeResolver is used to resolve scope keywords such as `self` or `current` into `id` based scopes and scope attributes such as `name` or `uid` into `id` based scopes.
|
|
||||||
type ScopeResolver struct {
|
|
||||||
keywordResolvers map[string]KeywordScopeResolveFunc
|
|
||||||
attributeResolvers map[string]AttributeScopeResolveFunc
|
|
||||||
cache *localcache.CacheService
|
|
||||||
log log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewScopeResolver() ScopeResolver {
|
|
||||||
return ScopeResolver{
|
|
||||||
keywordResolvers: map[string]KeywordScopeResolveFunc{
|
|
||||||
"users:self": resolveUserSelf,
|
|
||||||
},
|
|
||||||
attributeResolvers: map[string]AttributeScopeResolveFunc{},
|
|
||||||
cache: localcache.New(ttl, cleanInterval),
|
|
||||||
log: log.New("accesscontrol.scoperesolution"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ScopeResolver) AddKeywordResolver(keyword string, fn KeywordScopeResolveFunc) {
|
|
||||||
s.log.Debug("adding keyword resolution for '%v'", keyword)
|
|
||||||
s.keywordResolvers[keyword] = fn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ScopeResolver) AddAttributeResolver(prefix string, fn AttributeScopeResolveFunc) {
|
|
||||||
s.log.Debug("adding attribute resolution for '%v'", prefix)
|
|
||||||
s.attributeResolvers[prefix] = fn
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveUserSelf(u *models.SignedInUser) (string, error) {
|
|
||||||
return Scope("users", "id", fmt.Sprintf("%v", u.UserId)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetResolveKeywordScopeMutator returns a function to resolve scope with keywords such as `self` or `current` into `id` based scopes
|
|
||||||
func (s *ScopeResolver) GetResolveKeywordScopeMutator(user *models.SignedInUser) ScopeMutator {
|
|
||||||
return func(_ context.Context, scope string) (string, error) {
|
|
||||||
var err error
|
|
||||||
// By default the scope remains unchanged
|
|
||||||
resolvedScope := scope
|
|
||||||
if fn, ok := s.keywordResolvers[scope]; ok {
|
|
||||||
resolvedScope, err = fn(user)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not resolve %v: %w", scope, err)
|
|
||||||
}
|
|
||||||
s.log.Debug("resolved '%v' to '%v'", scope, resolvedScope)
|
|
||||||
}
|
|
||||||
return resolvedScope, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttributeScopeResolveFunc func(ctx context.Context, orgID int64, initialScope string) (string, error)
|
|
||||||
|
|
||||||
// getCacheKey creates an identifier to fetch and store resolution of scopes in the cache
|
|
||||||
func getCacheKey(orgID int64, scope string) string {
|
|
||||||
return fmt.Sprintf("%s-%v", scope, orgID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetResolveAttributeScopeMutator returns a function to resolve scopes with attributes such as `name` or `uid` into `id` based scopes
|
|
||||||
func (s *ScopeResolver) GetResolveAttributeScopeMutator(orgID int64) ScopeMutator {
|
|
||||||
return func(ctx context.Context, scope string) (string, error) {
|
|
||||||
// Check cache before computing the scope
|
|
||||||
if cachedScope, ok := s.cache.Get(getCacheKey(orgID, scope)); ok {
|
|
||||||
resolvedScope := cachedScope.(string)
|
|
||||||
s.log.Debug("used cache to resolve '%v' to '%v'", scope, resolvedScope)
|
|
||||||
return resolvedScope, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
// By default the scope remains unchanged
|
|
||||||
resolvedScope := scope
|
|
||||||
prefix := ScopePrefix(scope)
|
|
||||||
if fn, ok := s.attributeResolvers[prefix]; ok {
|
|
||||||
resolvedScope, err = fn(ctx, orgID, scope)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not resolve %v: %w", scope, err)
|
|
||||||
}
|
|
||||||
// Cache result
|
|
||||||
s.cache.Set(getCacheKey(orgID, scope), resolvedScope, ttl)
|
|
||||||
s.log.Debug("resolved '%v' to '%v'", scope, resolvedScope)
|
|
||||||
}
|
|
||||||
return resolvedScope, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScopePrefix returns the prefix associated to a given scope
|
// ScopePrefix returns the prefix associated to a given scope
|
||||||
// we assume prefixes are all in the form <resource>:<attribute>:<value>
|
// we assume prefixes are all in the form <resource>:<attribute>:<value>
|
||||||
// ex: "datasources:name:test" returns "datasources:name:"
|
// ex: "datasources:name:test" returns "datasources:name:"
|
||||||
@ -169,21 +70,6 @@ func ScopePrefix(scope string) string {
|
|||||||
return strings.Join(parts, ":")
|
return strings.Join(parts, ":")
|
||||||
}
|
}
|
||||||
|
|
||||||
//Inject params into the evaluator's templated scopes. e.g. "settings:" + eval.Parameters(":id")
|
|
||||||
func ScopeInjector(params ScopeParams) ScopeMutator {
|
|
||||||
return func(_ context.Context, scope string) (string, error) {
|
|
||||||
tmpl, err := template.New("scope").Parse(scope)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err = tmpl.Execute(&buf, params); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScopeProvider provides methods that construct scopes
|
// ScopeProvider provides methods that construct scopes
|
||||||
type ScopeProvider interface {
|
type ScopeProvider interface {
|
||||||
GetResourceScope(resourceID string) string
|
GetResourceScope(resourceID string) string
|
||||||
|
@ -1,165 +1,12 @@
|
|||||||
package accesscontrol
|
package accesscontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testUser = &models.SignedInUser{
|
func Test_ScopePrefix(t *testing.T) {
|
||||||
UserId: 2,
|
|
||||||
OrgId: 3,
|
|
||||||
OrgName: "TestOrg",
|
|
||||||
OrgRole: models.ROLE_VIEWER,
|
|
||||||
Login: "testUser",
|
|
||||||
Name: "Test User",
|
|
||||||
Email: "testuser@example.org",
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveKeywordedScope(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
user *models.SignedInUser
|
|
||||||
permission Permission
|
|
||||||
want Permission
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no scope",
|
|
||||||
user: testUser,
|
|
||||||
permission: Permission{Action: "users:read"},
|
|
||||||
want: Permission{Action: "users:read"},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "user if resolution",
|
|
||||||
user: testUser,
|
|
||||||
permission: Permission{Action: "users:read", Scope: "users:self"},
|
|
||||||
want: Permission{Action: "users:read", Scope: "users:id:2"},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
var err error
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
resolver := NewScopeResolver()
|
|
||||||
scopeModifier := resolver.GetResolveKeywordScopeMutator(tt.user)
|
|
||||||
tt.permission.Scope, err = scopeModifier(context.TODO(), tt.permission.Scope)
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err, "expected an error during the resolution of the scope")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.EqualValues(t, tt.want, tt.permission, "permission did not match expected resolution")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestScopeResolver_ResolveAttribute(t *testing.T) {
|
|
||||||
// Calls allow us to see how many times the fakeDataSourceResolution has been called
|
|
||||||
calls := 0
|
|
||||||
fakeDataSourceResolution := func(ctx context.Context, orgID int64, initialScope string) (string, error) {
|
|
||||||
calls++
|
|
||||||
if initialScope == "datasources:name:testds" {
|
|
||||||
return Scope("datasources", "id", "1"), nil
|
|
||||||
} else if initialScope == "datasources:name:testds2" {
|
|
||||||
return Scope("datasources", "id", "2"), nil
|
|
||||||
} else if initialScope == "datasources:name:test:ds4" {
|
|
||||||
return Scope("datasources", "id", "4"), nil
|
|
||||||
} else if initialScope == "datasources:name:testds5*" {
|
|
||||||
return Scope("datasources", "id", "5"), nil
|
|
||||||
} else {
|
|
||||||
return "", models.ErrDataSourceNotFound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
orgID int64
|
|
||||||
evaluator Evaluator
|
|
||||||
wantEvaluator Evaluator
|
|
||||||
wantCalls int
|
|
||||||
wantErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "should work with scope less permissions",
|
|
||||||
evaluator: EvalPermission("datasources:read"),
|
|
||||||
wantEvaluator: EvalPermission("datasources:read"),
|
|
||||||
wantCalls: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "should handle an error",
|
|
||||||
orgID: 1,
|
|
||||||
evaluator: EvalPermission("datasources:read", Scope("datasources", "name", "testds3")),
|
|
||||||
wantErr: models.ErrDataSourceNotFound,
|
|
||||||
wantCalls: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "should resolve a scope",
|
|
||||||
orgID: 1,
|
|
||||||
evaluator: EvalPermission("datasources:read", Scope("datasources", "name", "testds")),
|
|
||||||
wantEvaluator: EvalPermission("datasources:read", Scope("datasources", "id", "1")),
|
|
||||||
wantCalls: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "should resolve nested scopes with cache",
|
|
||||||
orgID: 1,
|
|
||||||
evaluator: EvalAll(
|
|
||||||
EvalPermission("datasources:read", Scope("datasources", "name", "testds")),
|
|
||||||
EvalAny(
|
|
||||||
EvalPermission("datasources:read", Scope("datasources", "name", "testds")),
|
|
||||||
EvalPermission("datasources:read", Scope("datasources", "name", "testds2")),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
wantEvaluator: EvalAll(
|
|
||||||
EvalPermission("datasources:read", Scope("datasources", "id", "1")),
|
|
||||||
EvalAny(
|
|
||||||
EvalPermission("datasources:read", Scope("datasources", "id", "1")),
|
|
||||||
EvalPermission("datasources:read", Scope("datasources", "id", "2")),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
wantCalls: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "should resolve name with colon",
|
|
||||||
orgID: 1,
|
|
||||||
evaluator: EvalPermission("datasources:read", Scope("datasources", "name", "test:ds4")),
|
|
||||||
wantEvaluator: EvalPermission("datasources:read", Scope("datasources", "id", "4")),
|
|
||||||
wantCalls: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "should resolve names with '*'",
|
|
||||||
orgID: 1,
|
|
||||||
evaluator: EvalPermission("datasources:read", Scope("datasources", "name", "testds5*")),
|
|
||||||
wantEvaluator: EvalPermission("datasources:read", Scope("datasources", "id", "5")),
|
|
||||||
wantCalls: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
resolver := NewScopeResolver()
|
|
||||||
|
|
||||||
// Reset calls counter
|
|
||||||
calls = 0
|
|
||||||
// Register a resolution method
|
|
||||||
resolver.AddAttributeResolver("datasources:name:", fakeDataSourceResolution)
|
|
||||||
|
|
||||||
// Test
|
|
||||||
scopeModifier := resolver.GetResolveAttributeScopeMutator(tt.orgID)
|
|
||||||
resolvedEvaluator, err := tt.evaluator.MutateScopes(context.TODO(), scopeModifier)
|
|
||||||
if tt.wantErr != nil {
|
|
||||||
assert.ErrorAs(t, err, &tt.wantErr, "expected an error during the resolution of the scope")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.EqualValues(t, tt.wantEvaluator, resolvedEvaluator, "permission did not match expected resolution")
|
|
||||||
|
|
||||||
assert.Equal(t, tt.wantCalls, calls, "cache has not been used")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_scopePrefix(t *testing.T) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
scope string
|
scope string
|
||||||
|
@ -28,49 +28,47 @@ var (
|
|||||||
ScopeFoldersProvider = ac.NewScopeProvider(ScopeFoldersRoot)
|
ScopeFoldersProvider = ac.NewScopeProvider(ScopeFoldersRoot)
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewNameScopeResolver provides an AttributeScopeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope.
|
// NewFolderNameScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope.
|
||||||
func NewNameScopeResolver(db Store) (string, ac.AttributeScopeResolveFunc) {
|
func NewFolderNameScopeResolver(db Store) (string, ac.ScopeAttributeResolver) {
|
||||||
prefix := ScopeFoldersProvider.GetResourceScopeName("")
|
prefix := ScopeFoldersProvider.GetResourceScopeName("")
|
||||||
resolver := func(ctx context.Context, orgID int64, scope string) (string, error) {
|
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||||
if !strings.HasPrefix(scope, prefix) {
|
if !strings.HasPrefix(scope, prefix) {
|
||||||
return "", ac.ErrInvalidScope
|
return nil, ac.ErrInvalidScope
|
||||||
}
|
}
|
||||||
nsName := scope[len(prefix):]
|
nsName := scope[len(prefix):]
|
||||||
if len(nsName) == 0 {
|
if len(nsName) == 0 {
|
||||||
return "", ac.ErrInvalidScope
|
return nil, ac.ErrInvalidScope
|
||||||
}
|
}
|
||||||
folder, err := db.GetFolderByTitle(ctx, orgID, nsName)
|
folder, err := db.GetFolderByTitle(ctx, orgID, nsName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ScopeFoldersProvider.GetResourceScopeUID(folder.Uid), nil
|
return []string{ScopeFoldersProvider.GetResourceScopeUID(folder.Uid)}, nil
|
||||||
}
|
})
|
||||||
return prefix, resolver
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIDScopeResolver provides an AttributeScopeResolver that is able to convert a scope prefixed with "folders:id:" into an uid based scope.
|
// NewFolderIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:id:" into an uid based scope.
|
||||||
func NewIDScopeResolver(db Store) (string, ac.AttributeScopeResolveFunc) {
|
func NewFolderIDScopeResolver(db Store) (string, ac.ScopeAttributeResolver) {
|
||||||
prefix := ScopeFoldersProvider.GetResourceScope("")
|
prefix := ScopeFoldersProvider.GetResourceScope("")
|
||||||
resolver := func(ctx context.Context, orgID int64, scope string) (string, error) {
|
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||||
if !strings.HasPrefix(scope, prefix) {
|
if !strings.HasPrefix(scope, prefix) {
|
||||||
return "", ac.ErrInvalidScope
|
return nil, ac.ErrInvalidScope
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := strconv.ParseInt(scope[len(prefix):], 10, 64)
|
id, err := strconv.ParseInt(scope[len(prefix):], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", ac.ErrInvalidScope
|
return nil, ac.ErrInvalidScope
|
||||||
}
|
}
|
||||||
|
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID), nil
|
return []string{ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
folder, err := db.GetFolderByID(ctx, orgID, id)
|
folder, err := db.GetFolderByID(ctx, orgID, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ScopeFoldersProvider.GetResourceScopeUID(folder.Uid), nil
|
return []string{ScopeFoldersProvider.GetResourceScopeUID(folder.Uid)}, nil
|
||||||
}
|
})
|
||||||
return prefix, resolver
|
|
||||||
}
|
}
|
||||||
|
@ -15,16 +15,16 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewNameScopeResolver(t *testing.T) {
|
func TestNewFolderNameScopeResolver(t *testing.T) {
|
||||||
t.Run("prefix should be expected", func(t *testing.T) {
|
t.Run("prefix should be expected", func(t *testing.T) {
|
||||||
prefix, _ := NewNameScopeResolver(&FakeDashboardStore{})
|
prefix, _ := NewFolderNameScopeResolver(&FakeDashboardStore{})
|
||||||
require.Equal(t, "folders:name:", prefix)
|
require.Equal(t, "folders:name:", prefix)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("resolver should convert to uid scope", func(t *testing.T) {
|
t.Run("resolver should convert to uid scope", func(t *testing.T) {
|
||||||
dashboardStore := &FakeDashboardStore{}
|
dashboardStore := &FakeDashboardStore{}
|
||||||
|
|
||||||
_, resolver := NewNameScopeResolver(dashboardStore)
|
_, resolver := NewFolderNameScopeResolver(dashboardStore)
|
||||||
|
|
||||||
orgId := rand.Int63()
|
orgId := rand.Int63()
|
||||||
title := "Very complex :title with: and /" + util.GenerateShortUID()
|
title := "Very complex :title with: and /" + util.GenerateShortUID()
|
||||||
@ -36,53 +36,54 @@ func TestNewNameScopeResolver(t *testing.T) {
|
|||||||
|
|
||||||
scope := "folders:name:" + title
|
scope := "folders:name:" + title
|
||||||
|
|
||||||
resolvedScope, err := resolver(context.Background(), orgId, scope)
|
resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Len(t, resolvedScopes, 1)
|
||||||
|
|
||||||
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScope)
|
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScopes[0])
|
||||||
|
|
||||||
dashboardStore.AssertCalled(t, "GetFolderByTitle", mock.Anything, orgId, title)
|
dashboardStore.AssertCalled(t, "GetFolderByTitle", mock.Anything, orgId, title)
|
||||||
})
|
})
|
||||||
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
|
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
|
||||||
dashboardStore := &FakeDashboardStore{}
|
dashboardStore := &FakeDashboardStore{}
|
||||||
_, resolver := NewNameScopeResolver(dashboardStore)
|
_, resolver := NewFolderNameScopeResolver(dashboardStore)
|
||||||
|
|
||||||
_, err := resolver(context.Background(), rand.Int63(), "folders:id:123")
|
_, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:123")
|
||||||
require.ErrorIs(t, err, ac.ErrInvalidScope)
|
require.ErrorIs(t, err, ac.ErrInvalidScope)
|
||||||
})
|
})
|
||||||
t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) {
|
t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) {
|
||||||
dashboardStore := &FakeDashboardStore{}
|
dashboardStore := &FakeDashboardStore{}
|
||||||
_, resolver := NewNameScopeResolver(dashboardStore)
|
_, resolver := NewFolderNameScopeResolver(dashboardStore)
|
||||||
|
|
||||||
_, err := resolver(context.Background(), rand.Int63(), "folders:name:")
|
_, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:name:")
|
||||||
require.ErrorIs(t, err, ac.ErrInvalidScope)
|
require.ErrorIs(t, err, ac.ErrInvalidScope)
|
||||||
})
|
})
|
||||||
t.Run("returns 'not found' if folder does not exist", func(t *testing.T) {
|
t.Run("returns 'not found' if folder does not exist", func(t *testing.T) {
|
||||||
dashboardStore := &FakeDashboardStore{}
|
dashboardStore := &FakeDashboardStore{}
|
||||||
|
|
||||||
_, resolver := NewNameScopeResolver(dashboardStore)
|
_, resolver := NewFolderNameScopeResolver(dashboardStore)
|
||||||
|
|
||||||
orgId := rand.Int63()
|
orgId := rand.Int63()
|
||||||
dashboardStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
|
dashboardStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
|
||||||
|
|
||||||
scope := "folders:name:" + util.GenerateShortUID()
|
scope := "folders:name:" + util.GenerateShortUID()
|
||||||
|
|
||||||
resolvedScope, err := resolver(context.Background(), orgId, scope)
|
resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope)
|
||||||
require.ErrorIs(t, err, models.ErrDashboardNotFound)
|
require.ErrorIs(t, err, models.ErrDashboardNotFound)
|
||||||
require.Empty(t, resolvedScope)
|
require.Nil(t, resolvedScopes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewIDScopeResolver(t *testing.T) {
|
func TestNewFolderIDScopeResolver(t *testing.T) {
|
||||||
t.Run("prefix should be expected", func(t *testing.T) {
|
t.Run("prefix should be expected", func(t *testing.T) {
|
||||||
prefix, _ := NewIDScopeResolver(&FakeDashboardStore{})
|
prefix, _ := NewFolderIDScopeResolver(&FakeDashboardStore{})
|
||||||
require.Equal(t, "folders:id:", prefix)
|
require.Equal(t, "folders:id:", prefix)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("resolver should convert to uid scope", func(t *testing.T) {
|
t.Run("resolver should convert to uid scope", func(t *testing.T) {
|
||||||
dashboardStore := &FakeDashboardStore{}
|
dashboardStore := &FakeDashboardStore{}
|
||||||
|
|
||||||
_, resolver := NewIDScopeResolver(dashboardStore)
|
_, resolver := NewFolderIDScopeResolver(dashboardStore)
|
||||||
|
|
||||||
orgId := rand.Int63()
|
orgId := rand.Int63()
|
||||||
uid := util.GenerateShortUID()
|
uid := util.GenerateShortUID()
|
||||||
@ -92,18 +93,18 @@ func TestNewIDScopeResolver(t *testing.T) {
|
|||||||
|
|
||||||
scope := "folders:id:" + strconv.FormatInt(db.Id, 10)
|
scope := "folders:id:" + strconv.FormatInt(db.Id, 10)
|
||||||
|
|
||||||
resolvedScope, err := resolver(context.Background(), orgId, scope)
|
resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Len(t, resolvedScopes, 1)
|
||||||
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScope)
|
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScopes[0])
|
||||||
|
|
||||||
dashboardStore.AssertCalled(t, "GetFolderByID", mock.Anything, orgId, db.Id)
|
dashboardStore.AssertCalled(t, "GetFolderByID", mock.Anything, orgId, db.Id)
|
||||||
})
|
})
|
||||||
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
|
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
|
||||||
dashboardStore := &FakeDashboardStore{}
|
dashboardStore := &FakeDashboardStore{}
|
||||||
_, resolver := NewIDScopeResolver(dashboardStore)
|
_, resolver := NewFolderIDScopeResolver(dashboardStore)
|
||||||
|
|
||||||
_, err := resolver(context.Background(), rand.Int63(), "folders:uid:123")
|
_, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:uid:123")
|
||||||
require.ErrorIs(t, err, ac.ErrInvalidScope)
|
require.ErrorIs(t, err, ac.ErrInvalidScope)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -112,33 +113,34 @@ func TestNewIDScopeResolver(t *testing.T) {
|
|||||||
dashboardStore = &FakeDashboardStore{}
|
dashboardStore = &FakeDashboardStore{}
|
||||||
orgId = rand.Int63()
|
orgId = rand.Int63()
|
||||||
scope = "folders:id:0"
|
scope = "folders:id:0"
|
||||||
_, resolver = NewIDScopeResolver(dashboardStore)
|
_, resolver = NewFolderIDScopeResolver(dashboardStore)
|
||||||
)
|
)
|
||||||
|
|
||||||
resolvedScope, err := resolver(context.Background(), orgId, scope)
|
resolved, err := resolver.Resolve(context.Background(), orgId, scope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, "folders:uid:general", resolvedScope)
|
require.Len(t, resolved, 1)
|
||||||
|
require.Equal(t, "folders:uid:general", resolved[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) {
|
t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) {
|
||||||
dashboardStore := &FakeDashboardStore{}
|
dashboardStore := &FakeDashboardStore{}
|
||||||
_, resolver := NewIDScopeResolver(dashboardStore)
|
_, resolver := NewFolderIDScopeResolver(dashboardStore)
|
||||||
|
|
||||||
_, err := resolver(context.Background(), rand.Int63(), "folders:id:")
|
_, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:")
|
||||||
require.ErrorIs(t, err, ac.ErrInvalidScope)
|
require.ErrorIs(t, err, ac.ErrInvalidScope)
|
||||||
})
|
})
|
||||||
t.Run("returns 'not found' if folder does not exist", func(t *testing.T) {
|
t.Run("returns 'not found' if folder does not exist", func(t *testing.T) {
|
||||||
dashboardStore := &FakeDashboardStore{}
|
dashboardStore := &FakeDashboardStore{}
|
||||||
|
|
||||||
_, resolver := NewIDScopeResolver(dashboardStore)
|
_, resolver := NewFolderIDScopeResolver(dashboardStore)
|
||||||
|
|
||||||
orgId := rand.Int63()
|
orgId := rand.Int63()
|
||||||
dashboardStore.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
|
dashboardStore.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
|
||||||
|
|
||||||
scope := "folders:id:10"
|
scope := "folders:id:10"
|
||||||
resolvedScope, err := resolver(context.Background(), orgId, scope)
|
resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope)
|
||||||
require.ErrorIs(t, err, models.ErrDashboardNotFound)
|
require.ErrorIs(t, err, models.ErrDashboardNotFound)
|
||||||
require.Empty(t, resolvedScope)
|
require.Nil(t, resolvedScopes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -32,8 +32,8 @@ func ProvideFolderService(
|
|||||||
searchService *search.SearchService, features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices,
|
searchService *search.SearchService, features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices,
|
||||||
ac accesscontrol.AccessControl, sqlStore sqlstore.Store,
|
ac accesscontrol.AccessControl, sqlStore sqlstore.Store,
|
||||||
) *FolderServiceImpl {
|
) *FolderServiceImpl {
|
||||||
ac.RegisterAttributeScopeResolver(dashboards.NewNameScopeResolver(dashboardStore))
|
ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(dashboardStore))
|
||||||
ac.RegisterAttributeScopeResolver(dashboards.NewIDScopeResolver(dashboardStore))
|
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(dashboardStore))
|
||||||
|
|
||||||
return &FolderServiceImpl{
|
return &FolderServiceImpl{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
@ -68,8 +68,8 @@ func ProvideService(
|
|||||||
ac: ac,
|
ac: ac,
|
||||||
}
|
}
|
||||||
|
|
||||||
ac.RegisterAttributeScopeResolver(NewNameScopeResolver(store))
|
ac.RegisterScopeAttributeResolver(NewNameScopeResolver(store))
|
||||||
ac.RegisterAttributeScopeResolver(NewIDScopeResolver(store))
|
ac.RegisterScopeAttributeResolver(NewIDScopeResolver(store))
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@ -82,55 +82,55 @@ type DataSourceRetriever interface {
|
|||||||
|
|
||||||
const secretType = "datasource"
|
const secretType = "datasource"
|
||||||
|
|
||||||
// NewNameScopeResolver provides an AttributeScopeResolver able to
|
// NewNameScopeResolver provides an ScopeAttributeResolver able to
|
||||||
// translate a scope prefixed with "datasources:name:" into an uid based scope.
|
// translate a scope prefixed with "datasources:name:" into an uid based scope.
|
||||||
func NewNameScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) {
|
func NewNameScopeResolver(db DataSourceRetriever) (string, accesscontrol.ScopeAttributeResolver) {
|
||||||
prefix := datasources.ScopeProvider.GetResourceScopeName("")
|
prefix := datasources.ScopeProvider.GetResourceScopeName("")
|
||||||
return prefix, func(ctx context.Context, orgID int64, initialScope string) (string, error) {
|
return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
|
||||||
if !strings.HasPrefix(initialScope, prefix) {
|
if !strings.HasPrefix(initialScope, prefix) {
|
||||||
return "", accesscontrol.ErrInvalidScope
|
return nil, accesscontrol.ErrInvalidScope
|
||||||
}
|
}
|
||||||
|
|
||||||
dsName := initialScope[len(prefix):]
|
dsName := initialScope[len(prefix):]
|
||||||
if dsName == "" {
|
if dsName == "" {
|
||||||
return "", accesscontrol.ErrInvalidScope
|
return nil, accesscontrol.ErrInvalidScope
|
||||||
}
|
}
|
||||||
|
|
||||||
query := models.GetDataSourceQuery{Name: dsName, OrgId: orgID}
|
query := models.GetDataSourceQuery{Name: dsName, OrgId: orgID}
|
||||||
if err := db.GetDataSource(ctx, &query); err != nil {
|
if err := db.GetDataSource(ctx, &query); err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return datasources.ScopeProvider.GetResourceScopeUID(query.Result.Uid), nil
|
return []string{datasources.ScopeProvider.GetResourceScopeUID(query.Result.Uid)}, nil
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIDScopeResolver provides an AttributeScopeResolver able to
|
// NewIDScopeResolver provides an ScopeAttributeResolver able to
|
||||||
// translate a scope prefixed with "datasources:id:" into an uid based scope.
|
// translate a scope prefixed with "datasources:id:" into an uid based scope.
|
||||||
func NewIDScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) {
|
func NewIDScopeResolver(db DataSourceRetriever) (string, accesscontrol.ScopeAttributeResolver) {
|
||||||
prefix := datasources.ScopeProvider.GetResourceScope("")
|
prefix := datasources.ScopeProvider.GetResourceScope("")
|
||||||
return prefix, func(ctx context.Context, orgID int64, initialScope string) (string, error) {
|
return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
|
||||||
if !strings.HasPrefix(initialScope, prefix) {
|
if !strings.HasPrefix(initialScope, prefix) {
|
||||||
return "", accesscontrol.ErrInvalidScope
|
return nil, accesscontrol.ErrInvalidScope
|
||||||
}
|
}
|
||||||
|
|
||||||
id := initialScope[len(prefix):]
|
id := initialScope[len(prefix):]
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return "", accesscontrol.ErrInvalidScope
|
return nil, accesscontrol.ErrInvalidScope
|
||||||
}
|
}
|
||||||
|
|
||||||
dsID, err := strconv.ParseInt(id, 10, 64)
|
dsID, err := strconv.ParseInt(id, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", accesscontrol.ErrInvalidScope
|
return nil, accesscontrol.ErrInvalidScope
|
||||||
}
|
}
|
||||||
|
|
||||||
query := models.GetDataSourceQuery{Id: dsID, OrgId: orgID}
|
query := models.GetDataSourceQuery{Id: dsID, OrgId: orgID}
|
||||||
if err := db.GetDataSource(ctx, &query); err != nil {
|
if err := db.GetDataSource(ctx, &query); err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return datasources.ScopeProvider.GetResourceScopeUID(query.Result.Uid), nil
|
return []string{datasources.ScopeProvider.GetResourceScopeUID(query.Result.Uid)}, nil
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error {
|
func (s *Service) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error {
|
||||||
|
@ -109,13 +109,14 @@ func TestService_NameScopeResolver(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
resolved, err := resolver(context.Background(), 1, tc.given)
|
resolved, err := resolver.Resolve(context.Background(), 1, tc.given)
|
||||||
if tc.wantErr != nil {
|
if tc.wantErr != nil {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Equal(t, tc.wantErr, err)
|
require.Equal(t, tc.wantErr, err)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, tc.want, resolved)
|
require.Len(t, resolved, 1)
|
||||||
|
require.Equal(t, tc.want, resolved[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -164,13 +165,14 @@ func TestService_IDScopeResolver(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
resolved, err := resolver(context.Background(), 1, tc.given)
|
resolved, err := resolver.Resolve(context.Background(), 1, tc.given)
|
||||||
if tc.wantErr != nil {
|
if tc.wantErr != nil {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Equal(t, tc.wantErr, err)
|
require.Equal(t, tc.wantErr, err)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, tc.want, resolved)
|
require.Len(t, resolved, 1)
|
||||||
|
require.Equal(t, tc.want, resolved[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user