mirror of https://github.com/grafana/grafana
RBAC: Add permission registry (#91247)
* RBAC: Permission registry * Populate permission registry * Wire * conflic_user_cmd * Update pkg/services/accesscontrol/permreg/permreg_test.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * PR feedback Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Remove ToDo, tackle in subsequent PR --------- Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>pull/91466/head
parent
391284bb33
commit
8988e04044
@ -0,0 +1,183 @@ |
||||
package permreg |
||||
|
||||
import ( |
||||
"strings" |
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
) |
||||
|
||||
var ( |
||||
// ErrInvalidScope is returned when the scope is not valid for the action
|
||||
ErrInvalidScopeTplt = "invalid scope: {{.Public.Scope}}, for action: {{.Public.Action}}, expected prefixes are {{.Public.ValidScopesFormat}}" |
||||
ErrBaseInvalidScope = errutil.BadRequest("permreg.invalid-scope").MustTemplate(ErrInvalidScopeTplt, errutil.WithPublic(ErrInvalidScopeTplt)) |
||||
|
||||
ErrUnknownActionTplt = "unknown action: {{.Public.Action}}, was not found in the list of valid actions" |
||||
ErrBaseUnknownAction = errutil.BadRequest("permreg.unknown-action").MustTemplate(ErrUnknownActionTplt, errutil.WithPublic(ErrUnknownActionTplt)) |
||||
) |
||||
|
||||
func ErrInvalidScope(scope string, action string, validScopePrefixes PrefixSet) error { |
||||
if len(validScopePrefixes) == 0 { |
||||
return ErrBaseInvalidScope.Build(errutil.TemplateData{Public: map[string]any{"Scope": scope, "Action": action, "ValidScopesFormat": "[none]"}}) |
||||
} |
||||
formats := generateValidScopeFormats(validScopePrefixes) |
||||
return ErrBaseInvalidScope.Build(errutil.TemplateData{Public: map[string]any{"Scope": scope, "Action": action, "ValidScopesFormat": formats}}) |
||||
} |
||||
|
||||
func ErrUnknownAction(action string) error { |
||||
return ErrBaseUnknownAction.Build(errutil.TemplateData{Public: map[string]any{"Action": action}}) |
||||
} |
||||
|
||||
func generateValidScopeFormats(acceptedScopePrefixes PrefixSet) []string { |
||||
if len(acceptedScopePrefixes) == 0 { |
||||
return []string{} |
||||
} |
||||
acceptedPrefixesList := make([]string, 0, 10) |
||||
acceptedPrefixesList = append(acceptedPrefixesList, "*") |
||||
for prefix := range acceptedScopePrefixes { |
||||
parts := strings.Split(prefix, ":") |
||||
// If the prefix has an attribute part add the intermediate format kind:*
|
||||
if len(parts) > 2 { |
||||
acceptedPrefixesList = append(acceptedPrefixesList, parts[0]+":*") |
||||
} |
||||
// Add the most specific format kind:attribute:*
|
||||
acceptedPrefixesList = append(acceptedPrefixesList, prefix+"*") |
||||
} |
||||
return acceptedPrefixesList |
||||
} |
||||
|
||||
type PermissionRegistry interface { |
||||
RegisterPluginScope(scope string) |
||||
RegisterPermission(action, scope string) |
||||
IsPermissionValid(action, scope string) error |
||||
GetScopePrefixes(action string) (PrefixSet, bool) |
||||
} |
||||
|
||||
type PrefixSet map[string]bool |
||||
|
||||
var _ PermissionRegistry = &permissionRegistry{} |
||||
|
||||
type permissionRegistry struct { |
||||
actionScopePrefixes map[string]PrefixSet // TODO use thread safe map
|
||||
kindScopePrefix map[string]string |
||||
logger log.Logger |
||||
} |
||||
|
||||
func ProvidePermissionRegistry() PermissionRegistry { |
||||
return newPermissionRegistry() |
||||
} |
||||
|
||||
func newPermissionRegistry() *permissionRegistry { |
||||
// defaultKindScopes maps the most specific accepted scope prefix for a given kind (folders, dashboards, etc)
|
||||
defaultKindScopes := map[string]string{ |
||||
"teams": "teams:id:", |
||||
"users": "users:id:", |
||||
"datasources": "datasources:uid:", |
||||
"dashboards": "dashboards:uid:", |
||||
"folders": "folders:uid:", |
||||
"annotations": "annotations:type:", |
||||
"apikeys": "apikeys:id:", |
||||
"orgs": "orgs:id:", |
||||
"plugins": "plugins:id:", |
||||
"provisioners": "provisioners:", |
||||
"reports": "reports:id:", |
||||
"permissions": "permissions:type:", |
||||
"serviceaccounts": "serviceaccounts:id:", |
||||
"settings": "settings:", |
||||
"global.users": "global.users:id:", |
||||
"roles": "roles:uid:", |
||||
"services": "services:", |
||||
} |
||||
return &permissionRegistry{ |
||||
actionScopePrefixes: make(map[string]PrefixSet, 200), |
||||
kindScopePrefix: defaultKindScopes, |
||||
logger: log.New("accesscontrol.permreg"), |
||||
} |
||||
} |
||||
|
||||
func (pr *permissionRegistry) RegisterPluginScope(scope string) { |
||||
if scope == "" { |
||||
return |
||||
} |
||||
|
||||
scopeParts := strings.Split(scope, ":") |
||||
// If the scope is already registered, return
|
||||
if _, found := pr.kindScopePrefix[scopeParts[0]]; found { |
||||
return |
||||
} |
||||
|
||||
// If the scope contains an attribute part, register the kind and attribute
|
||||
if len(scopeParts) > 2 { |
||||
kind, attr := scopeParts[0], scopeParts[1] |
||||
pr.kindScopePrefix[kind] = kind + ":" + attr + ":" |
||||
pr.logger.Debug("registered scope prefix", "kind", kind, "scope_prefix", kind+":"+attr+":") |
||||
return |
||||
} |
||||
|
||||
pr.logger.Debug("registered scope prefix", "kind", scopeParts[0], "scope_prefix", scopeParts[0]+":") |
||||
pr.kindScopePrefix[scopeParts[0]] = scopeParts[0] + ":" |
||||
} |
||||
|
||||
func (pr *permissionRegistry) RegisterPermission(action, scope string) { |
||||
if _, ok := pr.actionScopePrefixes[action]; !ok { |
||||
pr.actionScopePrefixes[action] = PrefixSet{} |
||||
} |
||||
|
||||
if scope == "" { |
||||
// scopeless action
|
||||
return |
||||
} |
||||
|
||||
kind := strings.Split(scope, ":")[0] |
||||
scopePrefix, ok := pr.kindScopePrefix[kind] |
||||
if !ok { |
||||
pr.logger.Warn("unknown scope prefix", "scope", scope) |
||||
return |
||||
} |
||||
|
||||
// Add a new entry in case the scope is not empty
|
||||
pr.actionScopePrefixes[action][scopePrefix] = true |
||||
} |
||||
|
||||
func (pr *permissionRegistry) IsPermissionValid(action, scope string) error { |
||||
validScopePrefixes, ok := pr.actionScopePrefixes[action] |
||||
if !ok { |
||||
return ErrUnknownAction(action) |
||||
} |
||||
|
||||
if ok && len(validScopePrefixes) == 0 { |
||||
// Expecting action without any scope
|
||||
if scope != "" { |
||||
return ErrInvalidScope(scope, action, nil) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
if !isScopeValid(scope, validScopePrefixes) { |
||||
return ErrInvalidScope(scope, action, validScopePrefixes) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func isScopeValid(scope string, validScopePrefixes PrefixSet) bool { |
||||
// Super wildcard scope
|
||||
if scope == "*" { |
||||
return true |
||||
} |
||||
for scopePrefix := range validScopePrefixes { |
||||
// Correct scope prefix
|
||||
if strings.HasPrefix(scope, scopePrefix) { |
||||
return true |
||||
} |
||||
// Scope is wildcard of the correct prefix
|
||||
if strings.HasSuffix(scope, ":*") && strings.HasPrefix(scopePrefix, scope[:len(scope)-2]) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (pr *permissionRegistry) GetScopePrefixes(action string) (PrefixSet, bool) { |
||||
set, ok := pr.actionScopePrefixes[action] |
||||
return set, ok |
||||
} |
@ -0,0 +1,246 @@ |
||||
package permreg |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func Test_permissionRegistry_RegisterPluginScope(t *testing.T) { |
||||
tests := []struct { |
||||
scope string |
||||
wantKind string |
||||
wantScope string |
||||
}{ |
||||
{ |
||||
scope: "folders:uid:AABBCC", |
||||
wantKind: "folders", |
||||
wantScope: "folders:uid:", |
||||
}, |
||||
{ |
||||
scope: "plugins:id:test-app", |
||||
wantKind: "plugins", |
||||
wantScope: "plugins:id:", |
||||
}, |
||||
{ |
||||
scope: "resource:uid:res", |
||||
wantKind: "resource", |
||||
wantScope: "resource:uid:", |
||||
}, |
||||
{ |
||||
scope: "resource:*", |
||||
wantKind: "resource", |
||||
wantScope: "resource:", |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.scope, func(t *testing.T) { |
||||
pr := newPermissionRegistry() |
||||
pr.RegisterPluginScope(tt.scope) |
||||
got, ok := pr.kindScopePrefix[tt.wantKind] |
||||
require.True(t, ok) |
||||
require.Equal(t, tt.wantScope, got) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func Test_permissionRegistry_RegisterPermission(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
action string |
||||
scope string |
||||
wantKind string |
||||
wantPrefixSet PrefixSet |
||||
wantSkip bool |
||||
}{ |
||||
{ |
||||
name: "register folders read", |
||||
action: "folders:read", |
||||
scope: "folders:*", |
||||
wantKind: "folders", |
||||
wantPrefixSet: PrefixSet{"folders:uid:": true}, |
||||
}, |
||||
{ |
||||
name: "register app plugin settings read", |
||||
action: "test-app.settings:read", |
||||
wantKind: "settings", |
||||
wantPrefixSet: PrefixSet{}, |
||||
}, |
||||
{ |
||||
name: "register an action on an unknown kind", |
||||
action: "unknown:action", |
||||
scope: "unknown:uid:*", |
||||
wantPrefixSet: PrefixSet{}, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
pr := newPermissionRegistry() |
||||
pr.RegisterPermission(tt.action, tt.scope) |
||||
got, ok := pr.actionScopePrefixes[tt.action] |
||||
require.True(t, ok) |
||||
for k, v := range got { |
||||
require.Equal(t, v, tt.wantPrefixSet[k]) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func Test_permissionRegistry_IsPermissionValid(t *testing.T) { |
||||
pr := newPermissionRegistry() |
||||
pr.RegisterPermission("folders:read", "folders:uid:") |
||||
pr.RegisterPermission("test-app.settings:read", "") |
||||
|
||||
tests := []struct { |
||||
name string |
||||
action string |
||||
scope string |
||||
wantErr bool |
||||
}{ |
||||
{ |
||||
name: "valid folders read", |
||||
action: "folders:read", |
||||
scope: "folders:uid:AABBCC", |
||||
wantErr: false, |
||||
}, |
||||
{ |
||||
name: "valid folders read with wildcard", |
||||
action: "folders:read", |
||||
scope: "folders:uid:*", |
||||
wantErr: false, |
||||
}, |
||||
{ |
||||
name: "valid folders read with kind level wildcard", |
||||
action: "folders:read", |
||||
scope: "folders:*", |
||||
wantErr: false, |
||||
}, |
||||
{ |
||||
name: "valid folders read with super wildcard", |
||||
action: "folders:read", |
||||
scope: "*", |
||||
wantErr: false, |
||||
}, |
||||
{ |
||||
name: "invalid folders read with wrong kind", |
||||
action: "folders:read", |
||||
scope: "unknown:uid:AABBCC", |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
name: "invalid folders read with wrong attribute", |
||||
action: "folders:read", |
||||
scope: "folders:id:3", |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
name: "valid app plugin settings read", |
||||
action: "test-app.settings:read", |
||||
scope: "", |
||||
wantErr: false, |
||||
}, |
||||
{ |
||||
name: "app plugin settings read with a scope", |
||||
action: "test-app.settings:read", |
||||
scope: "folders:uid:*", |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
name: "unknown action", |
||||
action: "unknown:write", |
||||
scope: "", |
||||
wantErr: true, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
err := pr.IsPermissionValid(tt.action, tt.scope) |
||||
if tt.wantErr { |
||||
require.Error(t, err) |
||||
return |
||||
} |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func Test_permissionRegistry_GetScopePrefixes(t *testing.T) { |
||||
pr := newPermissionRegistry() |
||||
pr.RegisterPermission("folders:read", "folders:uid:") |
||||
pr.RegisterPermission("test-app.settings:read", "") |
||||
|
||||
tests := []struct { |
||||
name string |
||||
action string |
||||
want PrefixSet |
||||
shouldExist bool |
||||
}{ |
||||
{ |
||||
name: "get folders read scope prefixes", |
||||
action: "folders:read", |
||||
want: PrefixSet{"folders:uid:": true}, |
||||
shouldExist: true, |
||||
}, |
||||
{ |
||||
name: "get app plugin settings read scope prefixes", |
||||
action: "test-app.settings:read", |
||||
want: PrefixSet{}, |
||||
shouldExist: true, |
||||
}, |
||||
{ |
||||
name: "get unknown action scope prefixes", |
||||
action: "unknown:write", |
||||
want: PrefixSet{}, |
||||
shouldExist: false, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
got, got1 := pr.GetScopePrefixes(tt.action) |
||||
if !tt.shouldExist { |
||||
require.False(t, got1) |
||||
return |
||||
} |
||||
require.True(t, got1) |
||||
require.Len(t, tt.want, len(got)) |
||||
for k, v := range got { |
||||
require.Equal(t, v, tt.want[k]) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func Test_generateValidScopeFormats(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
prefixSet PrefixSet |
||||
want []string |
||||
}{ |
||||
{ |
||||
name: "empty prefix set", |
||||
prefixSet: PrefixSet{}, |
||||
want: []string{}, |
||||
}, |
||||
{ |
||||
name: "short prefix", |
||||
prefixSet: PrefixSet{"folders:": true}, |
||||
want: []string{"*", "folders:*"}, |
||||
}, |
||||
{ |
||||
name: "single prefix", |
||||
prefixSet: PrefixSet{"folders:uid:": true}, |
||||
want: []string{"*", "folders:*", "folders:uid:*"}, |
||||
}, |
||||
{ |
||||
name: "multiple prefixes", |
||||
prefixSet: PrefixSet{"folders:uid:": true, "dashboards:uid:": true}, |
||||
want: []string{"*", "folders:*", "folders:uid:*", "dashboards:*", "dashboards:uid:*"}, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
got := generateValidScopeFormats(tt.prefixSet) |
||||
require.ElementsMatch(t, tt.want, got) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
package test |
||||
|
||||
import "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" |
||||
|
||||
func ProvidePermissionRegistry() permreg.PermissionRegistry { |
||||
permReg := permreg.ProvidePermissionRegistry() |
||||
// Test core permissions
|
||||
permReg.RegisterPermission("datasources:read", "datasources:uid:") |
||||
permReg.RegisterPermission("dashboards:read", "dashboards:uid:") |
||||
permReg.RegisterPermission("dashboards:read", "folders:uid:") |
||||
permReg.RegisterPermission("folders:read", "folders:uid:") |
||||
// Test plugins permissions
|
||||
permReg.RegisterPermission("plugins.app:access", "plugins:id:") |
||||
// App
|
||||
permReg.RegisterPermission("test-app:read", "") |
||||
permReg.RegisterPermission("test-app.settings:read", "") |
||||
permReg.RegisterPermission("test-app.projects:read", "") |
||||
// App 1
|
||||
permReg.RegisterPermission("test-app1.catalog:read", "") |
||||
permReg.RegisterPermission("test-app1.announcements:read", "") |
||||
return permReg |
||||
} |
Loading…
Reference in new issue