The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/accesscontrol/resourcepermissions/service.go

608 lines
22 KiB

package resourcepermissions
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"golang.org/x/exp/slices"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
var _ pluginaccesscontrol.ActionSetRegistry = (ActionSetService)(nil)
type Store interface {
// SetUserResourcePermission sets permission for managed user role on a resource
SetUserResourcePermission(
ctx context.Context, orgID int64,
user accesscontrol.User,
cmd SetResourcePermissionCommand,
hook UserResourceHookFunc,
) (*accesscontrol.ResourcePermission, error)
// SetTeamResourcePermission sets permission for managed team role on a resource
SetTeamResourcePermission(
ctx context.Context, orgID, teamID int64,
cmd SetResourcePermissionCommand,
hook TeamResourceHookFunc,
) (*accesscontrol.ResourcePermission, error)
// SetBuiltInResourcePermission sets permissions for managed builtin role on a resource
SetBuiltInResourcePermission(
ctx context.Context, orgID int64, builtinRole string,
cmd SetResourcePermissionCommand,
hook BuiltinResourceHookFunc,
) (*accesscontrol.ResourcePermission, error)
SetResourcePermissions(
ctx context.Context, orgID int64,
commands []SetResourcePermissionsCommand,
hooks ResourceHooks,
) ([]accesscontrol.ResourcePermission, error)
// GetResourcePermissions will return all permission for supplied resource id
GetResourcePermissions(ctx context.Context, orgID int64, query GetResourcePermissionsQuery) ([]accesscontrol.ResourcePermission, error)
// DeleteResourcePermissions will delete all permissions for supplied resource id
DeleteResourcePermissions(ctx context.Context, orgID int64, cmd *DeleteResourcePermissionsCmd) error
}
func New(cfg *setting.Cfg,
options Options, features featuremgmt.FeatureToggles, router routing.RouteRegister, license licensing.Licensing,
ac accesscontrol.AccessControl, service accesscontrol.Service, sqlStore db.DB,
teamService team.Service, userService user.Service, actionSetService ActionSetService,
) (*Service, error) {
permissions := make([]string, 0, len(options.PermissionsToActions))
actionSet := make(map[string]struct{})
for permission, actions := range options.PermissionsToActions {
permissions = append(permissions, permission)
for _, a := range actions {
actionSet[a] = struct{}{}
}
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
actionSetService.StoreActionSet(GetActionSetName(options.Resource, permission), actions)
}
}
// Sort all permissions based on action length. Will be used when mapping between actions to permissions
sort.Slice(permissions, func(i, j int) bool {
return len(options.PermissionsToActions[permissions[i]]) > len(options.PermissionsToActions[permissions[j]])
})
actions := make([]string, 0, len(actionSet))
for action := range actionSet {
actions = append(actions, action)
}
s := &Service{
ac: ac,
features: features,
store: NewStore(cfg, sqlStore, features),
options: options,
license: license,
log: log.New("resourcepermissions"),
permissions: permissions,
actions: actions,
sqlStore: sqlStore,
service: service,
teamService: teamService,
userService: userService,
actionSetSvc: actionSetService,
}
s.api = newApi(cfg, ac, router, s)
if err := s.declareFixedRoles(); err != nil {
return nil, err
}
s.api.registerEndpoints()
return s, nil
}
// Service is used to create access control sub system including api / and service for managed resource permission
type Service struct {
ac accesscontrol.AccessControl
features featuremgmt.FeatureToggles
service accesscontrol.Service
store Store
api *api
license licensing.Licensing
log log.Logger
options Options
permissions []string
actions []string
sqlStore db.DB
teamService team.Service
userService user.Service
actionSetSvc ActionSetService
}
func (s *Service) GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.GetPermissions")
defer span.End()
var inheritedScopes []string
if s.options.InheritedScopesSolver != nil {
var err error
inheritedScopes, err = s.options.InheritedScopesSolver(ctx, user.GetOrgID(), resourceID)
if err != nil {
return nil, err
}
}
actions := s.actions
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
for _, action := range s.actions {
actionSets := s.actionSetSvc.ResolveAction(action)
for _, actionSet := range actionSets {
if !slices.Contains(actions, actionSet) {
actions = append(actions, actionSet)
}
}
}
}
resourcePermissions, err := s.store.GetResourcePermissions(ctx, user.GetOrgID(), GetResourcePermissionsQuery{
User: user,
Actions: actions,
Resource: s.options.Resource,
ResourceID: resourceID,
ResourceAttribute: s.options.ResourceAttribute,
InheritedScopes: inheritedScopes,
OnlyManaged: s.options.OnlyManaged,
EnforceAccessControl: s.license.FeatureEnabled("accesscontrol.enforcement"),
})
if err != nil {
return nil, err
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
for i := range resourcePermissions {
actions := resourcePermissions[i].Actions
var expandedActions []string
for _, action := range actions {
if isFolderOrDashboardAction(action) {
actionSetActions := s.actionSetSvc.ResolveActionSet(action)
if len(actionSetActions) > 0 {
// Add all actions for folder
if s.options.Resource == dashboards.ScopeFoldersRoot {
expandedActions = append(expandedActions, actionSetActions...)
continue
}
// This check is needed for resolving inherited permissions - we don't want to include
// actions that are not related to dashboards when expanding dashboard action sets
for _, actionSetAction := range actionSetActions {
if slices.Contains(s.actions, actionSetAction) {
expandedActions = append(expandedActions, actionSetAction)
}
}
continue
}
}
expandedActions = append(expandedActions, action)
}
resourcePermissions[i].Actions = expandedActions
}
}
return resourcePermissions, nil
}
func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetUserPermission")
defer span.End()
actions, err := s.mapPermission(permission)
if err != nil {
return nil, err
}
if err := s.validateResource(ctx, orgID, resourceID); err != nil {
return nil, err
}
if err := s.validateUser(ctx, orgID, user.ID); err != nil {
return nil, err
}
return s.store.SetUserResourcePermission(ctx, orgID, user, SetResourcePermissionCommand{
Actions: actions,
Permission: permission,
Resource: s.options.Resource,
ResourceID: resourceID,
ResourceAttribute: s.options.ResourceAttribute,
}, s.options.OnSetUser)
}
func (s *Service) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetTeamPermission")
defer span.End()
actions, err := s.mapPermission(permission)
if err != nil {
return nil, err
}
if err := s.validateTeam(ctx, orgID, teamID); err != nil {
return nil, err
}
if err := s.validateResource(ctx, orgID, resourceID); err != nil {
return nil, err
}
return s.store.SetTeamResourcePermission(ctx, orgID, teamID, SetResourcePermissionCommand{
Actions: actions,
Permission: permission,
Resource: s.options.Resource,
ResourceID: resourceID,
ResourceAttribute: s.options.ResourceAttribute,
}, s.options.OnSetTeam)
}
func (s *Service) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetBuiltInRolePermission")
defer span.End()
actions, err := s.mapPermission(permission)
if err != nil {
return nil, err
}
if err := s.validateBuiltinRole(ctx, builtInRole); err != nil {
return nil, err
}
if err := s.validateResource(ctx, orgID, resourceID); err != nil {
return nil, err
}
return s.store.SetBuiltInResourcePermission(ctx, orgID, builtInRole, SetResourcePermissionCommand{
Actions: actions,
Permission: permission,
Resource: s.options.Resource,
ResourceID: resourceID,
ResourceAttribute: s.options.ResourceAttribute,
}, s.options.OnSetBuiltInRole)
}
func (s *Service) SetPermissions(
ctx context.Context, orgID int64, resourceID string,
commands ...accesscontrol.SetResourcePermissionCommand,
) ([]accesscontrol.ResourcePermission, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetPermissions")
defer span.End()
if err := s.validateResource(ctx, orgID, resourceID); err != nil {
return nil, err
}
dbCommands := make([]SetResourcePermissionsCommand, 0, len(commands))
for _, cmd := range commands {
if cmd.UserID != 0 {
if err := s.validateUser(ctx, orgID, cmd.UserID); err != nil {
return nil, err
}
} else if cmd.TeamID != 0 {
if err := s.validateTeam(ctx, orgID, cmd.TeamID); err != nil {
return nil, err
}
} else {
if err := s.validateBuiltinRole(ctx, cmd.BuiltinRole); err != nil {
return nil, err
}
}
actions, err := s.mapPermission(cmd.Permission)
if err != nil {
return nil, err
}
dbCommands = append(dbCommands, SetResourcePermissionsCommand{
User: accesscontrol.User{ID: cmd.UserID},
TeamID: cmd.TeamID,
BuiltinRole: cmd.BuiltinRole,
SetResourcePermissionCommand: SetResourcePermissionCommand{
Actions: actions,
Resource: s.options.Resource,
ResourceID: resourceID,
ResourceAttribute: s.options.ResourceAttribute,
Permission: cmd.Permission,
},
})
}
return s.store.SetResourcePermissions(ctx, orgID, dbCommands, ResourceHooks{
User: s.options.OnSetUser,
Team: s.options.OnSetTeam,
BuiltInRole: s.options.OnSetBuiltInRole,
})
}
func (s *Service) MapActions(permission accesscontrol.ResourcePermission) string {
for _, p := range s.permissions {
if permission.Contains(s.options.PermissionsToActions[p]) {
return p
}
}
return ""
}
func (s *Service) DeleteResourcePermissions(ctx context.Context, orgID int64, resourceID string) error {
return s.store.DeleteResourcePermissions(ctx, orgID, &DeleteResourcePermissionsCmd{
Resource: s.options.Resource,
ResourceAttribute: s.options.ResourceAttribute,
ResourceID: resourceID,
})
}
func (s *Service) mapPermission(permission string) ([]string, error) {
if permission == "" {
return []string{}, nil
}
for k, v := range s.options.PermissionsToActions {
if permission == k {
return v, nil
}
}
return nil, ErrInvalidPermission.Build(ErrInvalidPermissionData(permission))
}
func (s *Service) validateResource(ctx context.Context, orgID int64, resourceID string) error {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateResource")
defer span.End()
if s.options.ResourceValidator != nil {
return s.options.ResourceValidator(ctx, orgID, resourceID)
}
return nil
}
func (s *Service) validateUser(ctx context.Context, orgID, userID int64) error {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateUser")
defer span.End()
if !s.options.Assignments.Users {
return ErrInvalidAssignment.Build(ErrInvalidAssignmentData("users"))
}
_, err := s.userService.GetSignedInUser(ctx, &user.GetSignedInUserQuery{OrgID: orgID, UserID: userID})
switch {
case errors.Is(err, user.ErrUserNotFound):
return accesscontrol.ErrAssignmentEntityNotFound.Build(accesscontrol.ErrAssignmentEntityNotFoundData("user"))
default:
return err
}
}
func (s *Service) validateTeam(ctx context.Context, orgID, teamID int64) error {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateTeam")
defer span.End()
if !s.options.Assignments.Teams {
return ErrInvalidAssignment.Build(ErrInvalidAssignmentData("teams"))
}
if _, err := s.teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{OrgID: orgID, ID: teamID}); err != nil {
switch {
case errors.Is(err, team.ErrTeamNotFound):
return accesscontrol.ErrAssignmentEntityNotFound.Build(accesscontrol.ErrAssignmentEntityNotFoundData("team"))
default:
return err
}
}
return nil
}
func (s *Service) validateBuiltinRole(ctx context.Context, builtinRole string) error {
_, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateBuiltinRole")
defer span.End()
if !s.options.Assignments.BuiltInRoles {
return ErrInvalidAssignment.Build(ErrInvalidAssignmentData("builtInRoles"))
}
if err := accesscontrol.ValidateBuiltInRoles([]string{builtinRole}); err != nil {
return err
}
return nil
}
func (s *Service) declareFixedRoles() error {
scopeAll := accesscontrol.Scope(s.options.Resource, "*")
readerRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: fmt.Sprintf("fixed:%s.permissions:reader", s.options.Resource),
DisplayName: s.options.ReaderRoleName,
Group: s.options.RoleGroup,
Permissions: []accesscontrol.Permission{
{Action: fmt.Sprintf("%s.permissions:read", s.options.Resource), Scope: scopeAll},
},
},
Grants: []string{string(org.RoleAdmin)},
}
writerRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: fmt.Sprintf("fixed:%s.permissions:writer", s.options.Resource),
DisplayName: s.options.WriterRoleName,
Group: s.options.RoleGroup,
Permissions: accesscontrol.ConcatPermissions(readerRole.Role.Permissions, []accesscontrol.Permission{
{Action: fmt.Sprintf("%s.permissions:write", s.options.Resource), Scope: scopeAll},
}),
},
Grants: []string{string(org.RoleAdmin)},
}
return s.service.DeclareFixedRoles(readerRole, writerRole)
}
type ActionSetService interface {
// ActionResolver defines method for expanding permissions from permissions with action sets to fine-grained permissions.
// We use an ActionResolver interface to avoid circular dependencies
accesscontrol.ActionResolver
// ResolveAction returns all the action sets that the action belongs to.
ResolveAction(action string) []string
// ResolveActionSet resolves an action set to a list of corresponding actions.
ResolveActionSet(actionSet string) []string
// StoreActionSet stores action set. If a set with the given name has already been stored, the new actions will be appended to the existing actions.
StoreActionSet(name string, actions []string)
pluginaccesscontrol.ActionSetRegistry
}
// ActionSet is a struct that represents a set of actions that can be performed on a resource.
// An example of an action set is "folders:edit" which represents the set of RBAC actions that are granted by edit access to a folder.
type ActionSet struct {
Action string `json:"action"`
Actions []string `json:"actions"`
}
type ActionSetStore interface {
// StoreActionSet stores action set. If a set with the given name has already been stored, the new actions will be appended to the existing actions.
StoreActionSet(name string, actions []string)
// ResolveActionSet resolves an action set to a list of corresponding actions.
ResolveActionSet(actionSet string) []string
// ResolveAction returns all the action sets that the action belongs to.
ResolveAction(action string) []string
// ResolveActionPrefix returns all action sets that include at least one action with the specified prefix
ResolveActionPrefix(prefix string) []string
// ExpandActionSetsWithFilter takes a set of permissions that might include some action set permissions, and returns a set of permissions with action sets expanded into underlying permissions.
// When action sets are expanded into the underlying permissions only those permissions whose action is matched by actionMatcher are included.
ExpandActionSetsWithFilter(permissions []accesscontrol.Permission, actionMatcher func(action string) bool) []accesscontrol.Permission
}
type ActionSetSvc struct {
features featuremgmt.FeatureToggles
store ActionSetStore
}
// NewActionSetService returns a new instance of InMemoryActionSetService.
func NewActionSetService(features featuremgmt.FeatureToggles) ActionSetService {
return &ActionSetSvc{
features: features,
store: NewInMemoryActionSetStore(features),
}
}
// ResolveAction returns all the action sets that the action belongs to.
func (a *ActionSetSvc) ResolveAction(action string) []string {
sets := a.store.ResolveAction(action)
filteredSets := make([]string, 0, len(sets))
for _, set := range sets {
// Only use action sets for folders and dashboards for now
// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
if !isFolderOrDashboardAction(set) {
continue
}
filteredSets = append(filteredSets, set)
}
return filteredSets
}
// ResolveActionPrefix returns all action sets that include at least one action with the specified prefix
func (a *ActionSetSvc) ResolveActionPrefix(actionPrefix string) []string {
sets := a.store.ResolveActionPrefix(actionPrefix)
filteredSets := make([]string, 0, len(sets))
for _, set := range sets {
// Only use action sets for folders and dashboards for now
// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
if !isFolderOrDashboardAction(set) {
continue
}
filteredSets = append(filteredSets, set)
}
return filteredSets
}
// ResolveActionSet resolves an action set to a list of corresponding actions.
func (a *ActionSetSvc) ResolveActionSet(actionSet string) []string {
// Only use action sets for folders and dashboards for now
// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
if !isFolderOrDashboardAction(actionSet) {
return nil
}
return a.store.ResolveActionSet(actionSet)
}
// StoreActionSet stores action set. If a set with the given name has already been stored, the new actions will be appended to the existing actions.
func (a *ActionSetSvc) StoreActionSet(name string, actions []string) {
// To avoid backwards incompatible changes, we don't want to store these actions in the DB
// Once action sets are fully enabled, we can include dashboards.ActionFoldersCreate in the list of other folder edit/admin actions
// Tracked in https://github.com/grafana/identity-access-team/issues/794
if name == "folders:edit" || name == "folders:admin" {
if !slices.Contains(a.ResolveActionSet(name), dashboards.ActionFoldersCreate) {
actions = append(actions, dashboards.ActionFoldersCreate)
}
}
a.store.StoreActionSet(name, actions)
}
// ExpandActionSets takes a set of permissions that might include some action set permissions, and returns a set of permissions with action sets expanded into underlying permissions
func (a *ActionSetSvc) ExpandActionSets(permissions []accesscontrol.Permission) []accesscontrol.Permission {
actionMatcher := func(_ string) bool {
return true
}
return a.ExpandActionSetsWithFilter(permissions, actionMatcher)
}
// ExpandActionSetsWithFilter works like ExpandActionSets, but it also takes a function for action filtering. When action sets are expanded into the underlying permissions,
// only those permissions whose action is matched by actionMatcher are included.
func (a *ActionSetSvc) ExpandActionSetsWithFilter(permissions []accesscontrol.Permission, actionMatcher func(action string) bool) []accesscontrol.Permission {
return a.store.ExpandActionSetsWithFilter(permissions, actionMatcher)
}
// RegisterActionSets allow the caller to expand the existing action sets with additional permissions
// This is intended to be used by plugins, and currently supports extending folder and dashboard action sets
func (a *ActionSetSvc) RegisterActionSets(ctx context.Context, pluginID string, registrations []plugins.ActionSet) error {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.RegisterActionSets")
defer span.End()
if !a.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) || !a.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
return nil
}
for _, reg := range registrations {
if err := pluginutils.ValidatePluginActionSet(pluginID, reg); err != nil {
return err
}
a.StoreActionSet(reg.Action, reg.Actions)
}
return nil
}
func isFolderOrDashboardAction(action string) bool {
return strings.HasPrefix(action, dashboards.ScopeDashboardsRoot) || strings.HasPrefix(action, dashboards.ScopeFoldersRoot)
}
// GetActionSetName function creates an action set from a list of actions and stores it inmemory.
func GetActionSetName(resource, permission string) string {
// lower cased
resource = strings.ToLower(resource)
permission = strings.ToLower(permission)
return fmt.Sprintf("%s:%s", resource, permission)
}