From 9d2f5ef62faa6c33ff05147ad5be536a3032b6e1 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 26 Aug 2022 17:10:35 +0200 Subject: [PATCH] RBAC: Add function to generate wildcards from prefix (#54275) * RBAC: Move metadata to own file * RBAC: Rename test files * RBAC: Add wildcard structure and helper function to generate wildcards from prefix * RBAC: Refactor filter to use WildcardsFromPrefix * RBAC: Refactor GetResourceMetadata to use WildcardsFromPrefix --- pkg/services/accesscontrol/accesscontrol.go | 74 ------------------- pkg/services/accesscontrol/filter.go | 20 +---- pkg/services/accesscontrol/metadata.go | 63 ++++++++++++++++ ...l_bench_test.go => metadata_bench_test.go} | 0 ...accesscontrol_test.go => metadata_test.go} | 0 pkg/services/accesscontrol/scope.go | 29 ++++++++ pkg/services/accesscontrol/scope_test.go | 33 +++++++++ 7 files changed, 128 insertions(+), 91 deletions(-) create mode 100644 pkg/services/accesscontrol/metadata.go rename pkg/services/accesscontrol/{accesscontrol_bench_test.go => metadata_bench_test.go} (100%) rename pkg/services/accesscontrol/{accesscontrol_test.go => metadata_test.go} (100%) diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index eb5ffb8e10f..91e1bfab902 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -92,10 +92,6 @@ type User struct { IsExternal bool } -// Metadata contains user accesses for a given resource -// Ex: map[string]bool{"create":true, "delete": true} -type Metadata map[string]bool - // HasGlobalAccess checks user access with globally assigned permissions only func HasGlobalAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { @@ -184,66 +180,6 @@ func ValidateScope(scope string) bool { return !strings.ContainsAny(prefix, "*?") } -func addActionToMetadata(allMetadata map[string]Metadata, action, id string) map[string]Metadata { - metadata, initialized := allMetadata[id] - if !initialized { - metadata = Metadata{action: true} - } else { - metadata[action] = true - } - allMetadata[id] = metadata - return allMetadata -} - -// GetResourcesMetadata returns a map of accesscontrol metadata, listing for each resource, users available actions -func GetResourcesMetadata(ctx context.Context, permissions map[string][]string, prefix string, resourceIDs map[string]bool) map[string]Metadata { - rootPrefix, attributePrefix, ok := extractPrefixes(prefix) - if !ok { - return map[string]Metadata{} - } - - allScope := GetResourceAllScope(strings.TrimSuffix(rootPrefix, ":")) - allAttributeScope := Scope(strings.TrimSuffix(attributePrefix, ":"), "*") - - // index of the attribute in the scope - attributeIndex := len(attributePrefix) - - // Loop through permissions once - result := map[string]Metadata{} - - for action, scopes := range permissions { - for _, scope := range scopes { - if scope == "*" || scope == allScope || scope == allAttributeScope { - // Add global action to all resources - for id := range resourceIDs { - result = addActionToMetadata(result, action, id) - } - } else { - if len(scope) > attributeIndex && strings.HasPrefix(scope, attributePrefix) && resourceIDs[scope[attributeIndex:]] { - // Add action to a specific resource - result = addActionToMetadata(result, action, scope[attributeIndex:]) - } - } - } - } - - return result -} - -// MergeMeta will merge actions matching prefix of second metadata into first -func MergeMeta(prefix string, first Metadata, second Metadata) Metadata { - if first == nil { - first = Metadata{} - } - - for key := range second { - if strings.HasPrefix(key, prefix) { - first[key] = true - } - } - return first -} - func ManagedUserRoleName(userID int64) string { return fmt.Sprintf("managed:users:%d:permissions", userID) } @@ -256,16 +192,6 @@ func ManagedBuiltInRoleName(builtInRole string) string { return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(builtInRole)) } -func extractPrefixes(prefix string) (string, string, bool) { - parts := strings.Split(strings.TrimSuffix(prefix, ":"), ":") - if len(parts) != 2 { - return "", "", false - } - rootPrefix := parts[0] + ":" - attributePrefix := rootPrefix + parts[1] + ":" - return rootPrefix, attributePrefix, true -} - func IsDisabled(cfg *setting.Cfg) bool { return !cfg.RBACEnabled } diff --git a/pkg/services/accesscontrol/filter.go b/pkg/services/accesscontrol/filter.go index 80f45b11b50..67ea04a03c2 100644 --- a/pkg/services/accesscontrol/filter.go +++ b/pkg/services/accesscontrol/filter.go @@ -88,33 +88,19 @@ func Filter(user *user.SignedInUser, sqlID, prefix string, actions ...string) (S func ParseScopes(prefix string, scopes []string) (ids map[interface{}]struct{}, hasWildcard bool) { ids = make(map[interface{}]struct{}) - rootPrefix, attributePrefix, ok := extractPrefixes(prefix) - if !ok { - return nil, false - } - parser := parseStringAttribute if strings.HasSuffix(prefix, ":id:") { parser = parseIntAttribute } - allScope := rootPrefix + "*" - allAttributeScope := attributePrefix + "*" + wildcards := WildcardsFromPrefix(prefix) for _, scope := range scopes { - if scope == "*" { + if wildcards.Contains(scope) { return nil, true } - if strings.HasPrefix(scope, rootPrefix) { - if scope == allScope || scope == allAttributeScope { - return nil, true - } - - if !strings.HasPrefix(scope, prefix) { - continue - } - + if strings.HasPrefix(scope, prefix) { if id, err := parser(scope); err == nil { ids[id] = struct{}{} } diff --git a/pkg/services/accesscontrol/metadata.go b/pkg/services/accesscontrol/metadata.go new file mode 100644 index 00000000000..15dbde04153 --- /dev/null +++ b/pkg/services/accesscontrol/metadata.go @@ -0,0 +1,63 @@ +package accesscontrol + +import ( + "context" + "strings" +) + +// Metadata contains user accesses for a given resource +// Ex: map[string]bool{"create":true, "delete": true} +type Metadata map[string]bool + +// GetResourcesMetadata returns a map of accesscontrol metadata, listing for each resource, users available actions +func GetResourcesMetadata(ctx context.Context, permissions map[string][]string, prefix string, resourceIDs map[string]bool) map[string]Metadata { + wildcards := WildcardsFromPrefix(prefix) + + // index of the prefix in the scope + prefixIndex := len(prefix) + + // Loop through permissions once + result := map[string]Metadata{} + + for action, scopes := range permissions { + for _, scope := range scopes { + if wildcards.Contains(scope) { + for id := range resourceIDs { + result = addActionToMetadata(result, action, id) + } + break + } + if len(scope) > prefixIndex && strings.HasPrefix(scope, prefix) && resourceIDs[scope[prefixIndex:]] { + // Add action to a specific resource + result = addActionToMetadata(result, action, scope[prefixIndex:]) + } + } + } + + return result +} + +func addActionToMetadata(allMetadata map[string]Metadata, action, id string) map[string]Metadata { + metadata, initialized := allMetadata[id] + if !initialized { + metadata = Metadata{action: true} + } else { + metadata[action] = true + } + allMetadata[id] = metadata + return allMetadata +} + +// MergeMeta will merge actions matching prefix of second metadata into first +func MergeMeta(prefix string, first Metadata, second Metadata) Metadata { + if first == nil { + first = Metadata{} + } + + for key := range second { + if strings.HasPrefix(key, prefix) { + first[key] = true + } + } + return first +} diff --git a/pkg/services/accesscontrol/accesscontrol_bench_test.go b/pkg/services/accesscontrol/metadata_bench_test.go similarity index 100% rename from pkg/services/accesscontrol/accesscontrol_bench_test.go rename to pkg/services/accesscontrol/metadata_bench_test.go diff --git a/pkg/services/accesscontrol/accesscontrol_test.go b/pkg/services/accesscontrol/metadata_test.go similarity index 100% rename from pkg/services/accesscontrol/accesscontrol_test.go rename to pkg/services/accesscontrol/metadata_test.go diff --git a/pkg/services/accesscontrol/scope.go b/pkg/services/accesscontrol/scope.go index 6de7abbced5..de03856dd5a 100644 --- a/pkg/services/accesscontrol/scope.go +++ b/pkg/services/accesscontrol/scope.go @@ -141,3 +141,32 @@ func (s scopeProviderImpl) GetResourceAllScope() string { func (s scopeProviderImpl) GetResourceAllIDScope() string { return GetResourceAllIDScope(s.root) } + +// WildcardsFromPrefix generates valid wildcards from prefix +// datasource:uid: => "*", "datasource:*", "datasource:uid:*" +func WildcardsFromPrefix(prefix string) Wildcards { + var b strings.Builder + wildcards := Wildcards{"*"} + parts := strings.Split(prefix, ":") + for _, p := range parts { + if p == "" { + continue + } + b.WriteString(p) + b.WriteRune(':') + wildcards = append(wildcards, b.String()+"*") + } + return wildcards +} + +type Wildcards []string + +// Contains check if wildcards contains scope +func (wildcards Wildcards) Contains(scope string) bool { + for _, w := range wildcards { + if scope == w { + return true + } + } + return false +} diff --git a/pkg/services/accesscontrol/scope_test.go b/pkg/services/accesscontrol/scope_test.go index f0ccae5f63b..ea4538dee68 100644 --- a/pkg/services/accesscontrol/scope_test.go +++ b/pkg/services/accesscontrol/scope_test.go @@ -51,3 +51,36 @@ func Test_ScopePrefix(t *testing.T) { }) } } + +func TestWildcardsFromPrefix(t *testing.T) { + type testCase struct { + desc string + prefix string + expected Wildcards + } + + tests := []testCase{ + { + desc: "should handle empty prefix", + prefix: "", + expected: Wildcards{"*"}, + }, + { + desc: "should generate wildcards for prefix", + prefix: "dashboards:uid", + expected: Wildcards{"*", "dashboards:*", "dashboards:uid:*"}, + }, + { + desc: "should handle trailing :", + prefix: "dashboards:uid:", + expected: Wildcards{"*", "dashboards:*", "dashboards:uid:*"}, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + wildcards := WildcardsFromPrefix(tt.prefix) + assert.Equal(t, tt.expected, wildcards) + }) + } +}