AuthZ: Allow create without scope for specific resources (#110867)

* AuthZ: Create without scope for resources outside of folders

* Make it explicit that create requires a scope check

* Update pkg/services/authz/rbac/service.go

* Use skipScope instead of ReqScope

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Explain why there is no need to skip scope for roles

---------

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Gabriel MABILLE
2025-09-11 11:54:41 +02:00
committed by GitHub
co-authored by Ieva
parent 995aea281d
commit 5ce13061d5
3 changed files with 81 additions and 29 deletions
+28 -17
View File
@@ -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,
},
},
})
+16 -8
View File
@@ -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
}
+37 -4
View File
@@ -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,