diff --git a/pkg/services/authz/rbac/mapper.go b/pkg/services/authz/rbac/mapper.go index 28537ad638d..e15d336fbb7 100644 --- a/pkg/services/authz/rbac/mapper.go +++ b/pkg/services/authz/rbac/mapper.go @@ -19,13 +19,16 @@ type Mapping interface { AllActions() []string // HasFolderSupport returns true if the translation supports folders. HasFolderSupport() bool + // SkipScopeOnCreate returns true if the translation does not require a scope on create. + SkipScopeOnCreate() bool } type translation struct { - resource string - attribute string - verbMapping map[string]string - folderSupport bool + resource string + attribute string + verbMapping map[string]string + folderSupport bool + skipScopeOnCreate bool } func (t translation) Action(verb string) (string, bool) { @@ -58,6 +61,10 @@ func (t translation) HasFolderSupport() bool { return t.folderSupport } +func (t translation) SkipScopeOnCreate() bool { + return t.skipScopeOnCreate +} + // MapperRegistry is a registry of mappers that maps a group and resource to a translation. type MapperRegistry interface { // Get returns the permission mapper for the given group and resource. @@ -69,7 +76,7 @@ type MapperRegistry interface { type mapper map[string]map[string]translation -func newResourceTranslation(resource string, attribute string, folderSupport bool) translation { +func newResourceTranslation(resource string, attribute string, folderSupport, skipScopeOnCreate bool) translation { defaultMapping := func(r string) map[string]string { return map[string]string{ utils.VerbGet: fmt.Sprintf("%s:read", r), @@ -86,25 +93,27 @@ func newResourceTranslation(resource string, attribute string, folderSupport boo } return translation{ - resource: resource, - attribute: attribute, - verbMapping: defaultMapping(resource), - folderSupport: folderSupport, + resource: resource, + attribute: attribute, + verbMapping: defaultMapping(resource), + folderSupport: folderSupport, + skipScopeOnCreate: skipScopeOnCreate, } } func NewMapperRegistry() MapperRegistry { mapper := mapper(map[string]map[string]translation{ "dashboard.grafana.app": { - "dashboards": newResourceTranslation("dashboards", "uid", true), + "dashboards": newResourceTranslation("dashboards", "uid", true, false), }, "folder.grafana.app": { - "folders": newResourceTranslation("folders", "uid", true), + "folders": newResourceTranslation("folders", "uid", true, false), }, "iam.grafana.app": { // Teams is a special case. We translate user permissions from id to uid based. - "teams": newResourceTranslation("teams", "uid", false), - "coreroles": newResourceTranslation("roles", "uid", false), + "teams": newResourceTranslation("teams", "uid", false, true), + // No need to skip scope on create for roles because we translate `permissions:type:delegate` to `roles:*`` + "coreroles": newResourceTranslation("roles", "uid", false, false), "roles": translation{ resource: "roles", attribute: "uid", @@ -118,12 +127,13 @@ func NewMapperRegistry() MapperRegistry { utils.VerbList: "roles:read", utils.VerbWatch: "roles:read", }, - folderSupport: false, + folderSupport: false, + skipScopeOnCreate: false, }, }, "secret.grafana.app": { - "securevalues": newResourceTranslation("secret.securevalues", "uid", false), - "keepers": newResourceTranslation("secret.keepers", "uid", false), + "securevalues": newResourceTranslation("secret.securevalues", "uid", false, false), + "keepers": newResourceTranslation("secret.keepers", "uid", false, false), }, "query.grafana.app": { "query": translation{ @@ -132,7 +142,8 @@ func NewMapperRegistry() MapperRegistry { verbMapping: map[string]string{ utils.VerbCreate: "datasources:query", }, - folderSupport: false, + folderSupport: false, + skipScopeOnCreate: false, }, }, }) diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 9933775c79b..9fcd3eec3f0 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -581,8 +581,22 @@ func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool, return len(scopeMap) > 0, nil } - if req.Verb == utils.VerbCreate && req.ParentFolder == "" { - req.ParentFolder = accesscontrol.GeneralFolderUID + t, ok := s.mapper.Get(req.Group, req.Resource) + if !ok { + ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource) + return false, status.Error(codes.NotFound, "unsupported resource") + } + + if req.Verb == utils.VerbCreate { + // Resource doesn't require scope on create, so allow if the user has the action + if t.SkipScopeOnCreate() { + return scopeMap[""], nil + } + // If creating a resource that goes in a folder, but no folder is specified, + // assume parent folder is the general folder + if t.HasFolderSupport() && req.ParentFolder == "" { + req.ParentFolder = accesscontrol.GeneralFolderUID + } } // Wildcard grant, no further checks needed @@ -590,12 +604,6 @@ func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool, return true, nil } - t, ok := s.mapper.Get(req.Group, req.Resource) - if !ok { - ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource) - return false, status.Error(codes.NotFound, "unsupported resource") - } - if req.Name != "" && scopeMap[t.Scope(req.Name)] { return true, nil } diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index cee36a63d59..a4d732a193d 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -263,21 +263,54 @@ func TestService_checkPermission(t *testing.T) { expected: true, }, { - name: "should return true for datasources if service has permission", + name: "should allow querying a datasource", permissions: []accesscontrol.Permission{ { Action: "datasources:query", - Scope: "datasources:uid:some_datasource", + Scope: "datasources:uid:ds1", Kind: "datasources", Attribute: "uid", - Identifier: "some_datasource", + Identifier: "ds1", }, }, check: CheckRequest{ Action: "datasources:query", Group: "query.grafana.app", Resource: "query", - Name: "some_datasource", + Name: "ds1", + Verb: utils.VerbCreate, + }, + expected: true, + }, + { + name: "should deny querying a datasource without correct permission", + permissions: []accesscontrol.Permission{ + { + Action: "datasources:query", + Scope: "datasources:uid:ds2", + Kind: "datasources", + Attribute: "uid", + Identifier: "ds2", + }, + }, + check: CheckRequest{ + Action: "datasources:query", + Group: "query.grafana.app", + Resource: "query", + Name: "ds1", + Verb: utils.VerbCreate, + }, + expected: false, + }, + { + name: "should allow creating a team (no scope needed)", + permissions: []accesscontrol.Permission{ + {Action: "teams:create"}, + }, + check: CheckRequest{ + Action: "teams:create", + Group: "iam.grafana.app", + Resource: "teams", Verb: utils.VerbCreate, }, expected: true,