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:
@@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user