grafana/pkg/services/accesscontrol/resourcepermissions/store.go
Ieva cfa1a2c55f
RBAC: Split non-empty scopes into kind, attribute and identifier fields for better search performance (#71933)
* add a feature toggle

* add the fields for attribute, kind and identifier to permission

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>

* set the new fields when new permissions are stored

* add migrations

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>

* remove comments

* Update pkg/services/accesscontrol/migrator/migrator.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* feedback: put column migrations behind the feature toggle, added an index, changed how wildcard scopes are split

* PR feedback: add a comment and revert an accidentally changed file

* PR feedback: handle the case with : in resource identifier

* switch from checking feature toggle through cfg to checking it through featuremgmt

* don't put the column migrations behind a feature toggle after all - this breaks permission queries from db

---------

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
2023-07-21 15:23:01 +01:00

691 lines
19 KiB
Go

package resourcepermissions
import (
"context"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
)
func NewStore(sql db.DB, features featuremgmt.FeatureToggles) *store {
return &store{sql, features}
}
type store struct {
sql db.DB
features featuremgmt.FeatureToggles
}
type flatResourcePermission struct {
ID int64 `xorm:"id"`
RoleName string
Action string
Scope string
UserId int64
UserLogin string
UserEmail string
TeamId int64
TeamEmail string
Team string
BuiltInRole string
Created time.Time
Updated time.Time
}
func (p *flatResourcePermission) IsManaged(scope string) bool {
return strings.HasPrefix(p.RoleName, accesscontrol.ManagedRolePrefix) && p.Scope == scope
}
// IsInherited returns true for scopes from managed permissions that don't directly match the required scope
// (ie, managed permissions on a parent resource)
func (p *flatResourcePermission) IsInherited(scope string) bool {
return strings.HasPrefix(p.RoleName, accesscontrol.ManagedRolePrefix) && p.Scope != scope
}
type DeleteResourcePermissionsCmd struct {
Resource string
ResourceAttribute string
ResourceID string
}
func (s *store) DeleteResourcePermissions(ctx context.Context, orgID int64, cmd *DeleteResourcePermissionsCmd) error {
scope := accesscontrol.Scope(cmd.Resource, cmd.ResourceAttribute, cmd.ResourceID)
err := s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
var permissionIDs []int64
err := sess.SQL(
"SELECT permission.id FROM permission INNER JOIN role ON permission.role_id = role.id WHERE permission.scope = ? AND role.org_id = ?",
scope, orgID).Find(&permissionIDs)
if err != nil {
return err
}
if err := deletePermissions(sess, permissionIDs); err != nil {
return err
}
return err
})
return err
}
func (s *store) SetUserResourcePermission(
ctx context.Context, orgID int64, usr accesscontrol.User,
cmd SetResourcePermissionCommand,
hook UserResourceHookFunc,
) (*accesscontrol.ResourcePermission, error) {
if usr.ID == 0 {
return nil, user.ErrUserNotFound
}
var err error
var permission *accesscontrol.ResourcePermission
err = s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
permission, err = s.setUserResourcePermission(sess, orgID, usr, cmd, hook)
return err
})
return permission, err
}
func (s *store) setUserResourcePermission(
sess *db.Session, orgID int64, user accesscontrol.User,
cmd SetResourcePermissionCommand,
hook UserResourceHookFunc,
) (*accesscontrol.ResourcePermission, error) {
permission, err := s.setResourcePermission(sess, orgID, accesscontrol.ManagedUserRoleName(user.ID), s.userAdder(sess, orgID, user.ID), cmd)
if err != nil {
return nil, err
}
if hook != nil {
if err := hook(sess, orgID, user, cmd.ResourceID, cmd.Permission); err != nil {
return nil, err
}
}
return permission, nil
}
func (s *store) SetTeamResourcePermission(
ctx context.Context, orgID, teamID int64,
cmd SetResourcePermissionCommand,
hook TeamResourceHookFunc,
) (*accesscontrol.ResourcePermission, error) {
if teamID == 0 {
return nil, team.ErrTeamNotFound
}
var err error
var permission *accesscontrol.ResourcePermission
err = s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
permission, err = s.setTeamResourcePermission(sess, orgID, teamID, cmd, hook)
return err
})
return permission, err
}
func (s *store) setTeamResourcePermission(
sess *db.Session, orgID, teamID int64,
cmd SetResourcePermissionCommand,
hook TeamResourceHookFunc,
) (*accesscontrol.ResourcePermission, error) {
permission, err := s.setResourcePermission(sess, orgID, accesscontrol.ManagedTeamRoleName(teamID), s.teamAdder(sess, orgID, teamID), cmd)
if err != nil {
return nil, err
}
if hook != nil {
if err := hook(sess, orgID, teamID, cmd.ResourceID, cmd.Permission); err != nil {
return nil, err
}
}
return permission, nil
}
func (s *store) SetBuiltInResourcePermission(
ctx context.Context, orgID int64, builtInRole string,
cmd SetResourcePermissionCommand,
hook BuiltinResourceHookFunc,
) (*accesscontrol.ResourcePermission, error) {
if !org.RoleType(builtInRole).IsValid() || builtInRole == accesscontrol.RoleGrafanaAdmin {
return nil, fmt.Errorf("invalid role: %s", builtInRole)
}
var err error
var permission *accesscontrol.ResourcePermission
err = s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
permission, err = s.setBuiltInResourcePermission(sess, orgID, builtInRole, cmd, hook)
return err
})
if err != nil {
return nil, err
}
return permission, nil
}
func (s *store) setBuiltInResourcePermission(
sess *db.Session, orgID int64, builtInRole string,
cmd SetResourcePermissionCommand,
hook BuiltinResourceHookFunc,
) (*accesscontrol.ResourcePermission, error) {
permission, err := s.setResourcePermission(sess, orgID, accesscontrol.ManagedBuiltInRoleName(builtInRole), s.builtInRoleAdder(sess, orgID, builtInRole), cmd)
if err != nil {
return nil, err
}
if hook != nil {
if err := hook(sess, orgID, builtInRole, cmd.ResourceID, cmd.Permission); err != nil {
return nil, err
}
}
return permission, nil
}
func (s *store) SetResourcePermissions(
ctx context.Context, orgID int64,
commands []SetResourcePermissionsCommand,
hooks ResourceHooks,
) ([]accesscontrol.ResourcePermission, error) {
var err error
var permissions []accesscontrol.ResourcePermission
err = s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
for _, cmd := range commands {
var p *accesscontrol.ResourcePermission
if cmd.User.ID != 0 {
p, err = s.setUserResourcePermission(sess, orgID, cmd.User, cmd.SetResourcePermissionCommand, hooks.User)
} else if cmd.TeamID != 0 {
p, err = s.setTeamResourcePermission(sess, orgID, cmd.TeamID, cmd.SetResourcePermissionCommand, hooks.Team)
} else if org.RoleType(cmd.BuiltinRole).IsValid() || cmd.BuiltinRole == accesscontrol.RoleGrafanaAdmin {
p, err = s.setBuiltInResourcePermission(sess, orgID, cmd.BuiltinRole, cmd.SetResourcePermissionCommand, hooks.BuiltInRole)
}
if err != nil {
return err
}
if p != nil {
permissions = append(permissions, *p)
}
}
return nil
})
return permissions, err
}
type roleAdder func(roleID int64) error
func (s *store) setResourcePermission(
sess *db.Session, orgID int64, roleName string, adder roleAdder, cmd SetResourcePermissionCommand,
) (*accesscontrol.ResourcePermission, error) {
role, err := s.getOrCreateManagedRole(sess, orgID, roleName, adder)
if err != nil {
return nil, err
}
rawSQL := `SELECT p.* FROM permission as p INNER JOIN role r on r.id = p.role_id WHERE r.id = ? AND p.scope = ?`
var current []accesscontrol.Permission
scope := accesscontrol.Scope(cmd.Resource, cmd.ResourceAttribute, cmd.ResourceID)
if err := sess.SQL(rawSQL, role.ID, scope).Find(&current); err != nil {
return nil, err
}
missing := make(map[string]struct{}, len(cmd.Actions))
for _, a := range cmd.Actions {
missing[a] = struct{}{}
}
var remove []int64
for _, p := range current {
if _, ok := missing[p.Action]; ok {
delete(missing, p.Action)
} else if !ok {
remove = append(remove, p.ID)
}
}
if err := deletePermissions(sess, remove); err != nil {
return nil, err
}
if err := s.createPermissions(sess, role.ID, cmd.Resource, cmd.ResourceID, cmd.ResourceAttribute, missing); err != nil {
return nil, err
}
permissions, err := s.getPermissions(sess, cmd.Resource, cmd.ResourceID, cmd.ResourceAttribute, role.ID)
if err != nil {
return nil, err
}
permission := flatPermissionsToResourcePermission(scope, permissions)
if permission == nil {
return &accesscontrol.ResourcePermission{}, nil
}
return permission, nil
}
func (s *store) GetResourcePermissions(ctx context.Context, orgID int64, query GetResourcePermissionsQuery) ([]accesscontrol.ResourcePermission, error) {
var result []accesscontrol.ResourcePermission
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
var err error
result, err = s.getResourcePermissions(sess, orgID, query)
return err
})
return result, err
}
func (s *store) getResourcePermissions(sess *db.Session, orgID int64, query GetResourcePermissionsQuery) ([]accesscontrol.ResourcePermission, error) {
if len(query.Actions) == 0 {
return nil, nil
}
rawSelect := `
SELECT
p.*,
r.name as role_name,
`
userSelect := rawSelect + `
ur.user_id AS user_id,
u.login AS user_login,
u.email AS user_email,
0 AS team_id,
'' AS team,
'' AS team_email,
'' AS built_in_role
`
teamSelect := rawSelect + `
0 AS user_id,
'' AS user_login,
'' AS user_email,
tr.team_id AS team_id,
t.name AS team,
t.email AS team_email,
'' AS built_in_role
`
builtinSelect := rawSelect + `
0 AS user_id,
'' AS user_login,
'' AS user_email,
0 as team_id,
'' AS team,
'' AS team_email,
br.role AS built_in_role
`
rawFrom := `
FROM permission p
INNER JOIN role r ON p.role_id = r.id
`
userFrom := rawFrom + `
INNER JOIN user_role ur ON r.id = ur.role_id AND (ur.org_id = 0 OR ur.org_id = ?)
INNER JOIN ` + s.sql.GetDialect().Quote("user") + ` u ON ur.user_id = u.id
`
teamFrom := rawFrom + `
INNER JOIN team_role tr ON r.id = tr.role_id AND (tr.org_id = 0 OR tr.org_id = ?)
INNER JOIN team t ON tr.team_id = t.id
`
builtinFrom := rawFrom + `
INNER JOIN builtin_role br ON r.id = br.role_id AND (br.org_id = 0 OR br.org_id = ?)
`
where := `WHERE (r.org_id = ? OR r.org_id = 0) AND (p.scope = '*' OR p.scope = ? OR p.scope = ? OR p.scope = ?`
scope := accesscontrol.Scope(query.Resource, query.ResourceAttribute, query.ResourceID)
args := []interface{}{
orgID,
orgID,
accesscontrol.Scope(query.Resource, "*"),
accesscontrol.Scope(query.Resource, query.ResourceAttribute, "*"),
scope,
}
if len(query.InheritedScopes) > 0 {
where += ` OR p.scope IN(?` + strings.Repeat(",?", len(query.InheritedScopes)-1) + `)`
for _, scope := range query.InheritedScopes {
args = append(args, scope)
}
}
where += `) AND p.action IN (?` + strings.Repeat(",?", len(query.Actions)-1) + `)`
if query.OnlyManaged {
where += `AND r.name LIKE 'managed:%'`
}
for _, a := range query.Actions {
args = append(args, a)
}
initialLength := len(args)
userQuery := userSelect + userFrom + where
if query.EnforceAccessControl {
userFilter, err := accesscontrol.Filter(query.User, "u.id", "users:id:", accesscontrol.ActionOrgUsersRead)
if err != nil {
return nil, err
}
userQuery += " AND " + userFilter.Where
args = append(args, userFilter.Args...)
}
teamFilter, err := accesscontrol.Filter(query.User, "t.id", "teams:id:", accesscontrol.ActionTeamsRead)
if err != nil {
return nil, err
}
team := teamSelect + teamFrom + where + " AND " + teamFilter.Where
args = append(args, args[:initialLength]...)
args = append(args, teamFilter.Args...)
builtin := builtinSelect + builtinFrom + where
args = append(args, args[:initialLength]...)
sql := userQuery + " UNION " + team + " UNION " + builtin
queryResults := make([]flatResourcePermission, 0)
if err := sess.SQL(sql, args...).Find(&queryResults); err != nil {
return nil, err
}
var result []accesscontrol.ResourcePermission
users, teams, builtins := groupPermissionsByAssignment(queryResults)
for _, p := range users {
result = append(result, flatPermissionsToResourcePermissions(scope, p)...)
}
for _, p := range teams {
result = append(result, flatPermissionsToResourcePermissions(scope, p)...)
}
for _, p := range builtins {
result = append(result, flatPermissionsToResourcePermissions(scope, p)...)
}
return result, nil
}
func groupPermissionsByAssignment(permissions []flatResourcePermission) (map[int64][]flatResourcePermission, map[int64][]flatResourcePermission, map[string][]flatResourcePermission) {
users := make(map[int64][]flatResourcePermission)
teams := make(map[int64][]flatResourcePermission)
builtins := make(map[string][]flatResourcePermission)
for _, p := range permissions {
if p.UserId != 0 {
users[p.UserId] = append(users[p.UserId], p)
} else if p.TeamId != 0 {
teams[p.TeamId] = append(teams[p.TeamId], p)
} else if p.BuiltInRole != "" {
builtins[p.BuiltInRole] = append(builtins[p.BuiltInRole], p)
}
}
return users, teams, builtins
}
func flatPermissionsToResourcePermissions(scope string, permissions []flatResourcePermission) []accesscontrol.ResourcePermission {
var managed, inherited, provisioned []flatResourcePermission
for _, p := range permissions {
if p.IsManaged(scope) {
managed = append(managed, p)
} else if p.IsInherited(scope) {
inherited = append(inherited, p)
} else {
provisioned = append(provisioned, p)
}
}
var result []accesscontrol.ResourcePermission
if g := flatPermissionsToResourcePermission(scope, managed); g != nil {
result = append(result, *g)
}
if g := flatPermissionsToResourcePermission(scope, inherited); g != nil {
result = append(result, *g)
}
if g := flatPermissionsToResourcePermission(scope, provisioned); g != nil {
result = append(result, *g)
}
return result
}
func flatPermissionsToResourcePermission(scope string, permissions []flatResourcePermission) *accesscontrol.ResourcePermission {
if len(permissions) == 0 {
return nil
}
actions := make([]string, 0, len(permissions))
for _, p := range permissions {
actions = append(actions, p.Action)
}
first := permissions[0]
return &accesscontrol.ResourcePermission{
ID: first.ID,
RoleName: first.RoleName,
Actions: actions,
Scope: first.Scope,
UserId: first.UserId,
UserLogin: first.UserLogin,
UserEmail: first.UserEmail,
TeamId: first.TeamId,
TeamEmail: first.TeamEmail,
Team: first.Team,
BuiltInRole: first.BuiltInRole,
Created: first.Created,
Updated: first.Updated,
IsManaged: first.IsManaged(scope),
IsInherited: first.IsInherited(scope),
}
}
func (s *store) userAdder(sess *db.Session, orgID, userID int64) roleAdder {
return func(roleID int64) error {
if res, err := sess.Query("SELECT 1 FROM user_role WHERE org_id=? AND user_id=? AND role_id=?", orgID, userID, roleID); err != nil {
return err
} else if len(res) == 1 {
return fmt.Errorf("role is already added to this user")
}
userRole := &accesscontrol.UserRole{
OrgID: orgID,
UserID: userID,
RoleID: roleID,
Created: time.Now(),
}
_, err := sess.Insert(userRole)
return err
}
}
func (s *store) teamAdder(sess *db.Session, orgID, teamID int64) roleAdder {
return func(roleID int64) error {
if res, err := sess.Query("SELECT 1 FROM team_role WHERE org_id=? AND team_id=? AND role_id=?", orgID, teamID, roleID); err != nil {
return err
} else if len(res) == 1 {
return fmt.Errorf("role is already added to this team")
}
teamRole := &accesscontrol.TeamRole{
OrgID: orgID,
TeamID: teamID,
RoleID: roleID,
Created: time.Now(),
}
_, err := sess.Insert(teamRole)
return err
}
}
func (s *store) builtInRoleAdder(sess *db.Session, orgID int64, builtinRole string) roleAdder {
return func(roleID int64) error {
if res, err := sess.Query("SELECT 1 FROM builtin_role WHERE role_id=? AND role=? AND org_id=?", roleID, builtinRole, orgID); err != nil {
return err
} else if len(res) == 1 {
return fmt.Errorf("built-in role already has the role granted")
}
_, err := sess.Table("builtin_role").Insert(accesscontrol.BuiltinRole{
RoleID: roleID,
OrgID: orgID,
Role: builtinRole,
Updated: time.Now(),
Created: time.Now(),
})
return err
}
}
func (s *store) getOrCreateManagedRole(sess *db.Session, orgID int64, name string, add roleAdder) (*accesscontrol.Role, error) {
role := accesscontrol.Role{OrgID: orgID, Name: name}
has, err := sess.Where("org_id = ? AND name = ?", orgID, name).Get(&role)
// If managed role does not exist, create it and add it to user/team/builtin
if !has {
uid, err := generateNewRoleUID(sess, orgID)
if err != nil {
return nil, err
}
role = accesscontrol.Role{
OrgID: orgID,
Name: name,
UID: uid,
Created: time.Now(),
Updated: time.Now(),
}
if _, err := sess.Insert(&role); err != nil {
return nil, err
}
if err := add(role.ID); err != nil {
return nil, err
}
}
if err != nil {
return nil, err
}
return &role, nil
}
func generateNewRoleUID(sess *db.Session, orgID int64) (string, error) {
for i := 0; i < 3; i++ {
uid := util.GenerateShortUID()
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&accesscontrol.Role{})
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", fmt.Errorf("failed to generate uid")
}
func (s *store) getPermissions(sess *db.Session, resource, resourceID, resourceAttribute string, roleID int64) ([]flatResourcePermission, error) {
var result []flatResourcePermission
rawSql := `
SELECT
p.*,
ur.user_id AS user_id,
u.login AS user_login,
u.email AS user_email,
tr.team_id AS team_id,
t.name AS team,
t.email AS team_email,
r.name as role_name,
br.role AS built_in_role
FROM permission p
INNER JOIN role r ON p.role_id = r.id
LEFT JOIN team_role tr ON r.id = tr.role_id
LEFT JOIN team t ON tr.team_id = t.id
LEFT JOIN user_role ur ON r.id = ur.role_id
LEFT JOIN ` + s.sql.GetDialect().Quote("user") + ` u ON ur.user_id = u.id
LEFT JOIN builtin_role br ON r.id = br.role_id
WHERE r.id = ? AND p.scope = ?
`
if err := sess.SQL(rawSql, roleID, accesscontrol.Scope(resource, resourceAttribute, resourceID)).Find(&result); err != nil {
return nil, err
}
return result, nil
}
func (s *store) createPermissions(sess *db.Session, roleID int64, resource, resourceID, resourceAttribute string, actions map[string]struct{}) error {
if len(actions) == 0 {
return nil
}
permissions := make([]accesscontrol.Permission, 0, len(actions))
for action := range actions {
p := managedPermission(action, resource, resourceID, resourceAttribute)
p.RoleID = roleID
p.Created = time.Now()
p.Updated = time.Now()
if s.features.IsEnabled(featuremgmt.FlagSplitScopes) {
p.Kind, p.Attribute, p.Identifier = p.SplitScope()
}
permissions = append(permissions, p)
}
if _, err := sess.InsertMulti(&permissions); err != nil {
return err
}
return nil
}
func deletePermissions(sess *db.Session, ids []int64) error {
if len(ids) == 0 {
return nil
}
rawSQL := "DELETE FROM permission WHERE id IN(?" + strings.Repeat(",?", len(ids)-1) + ")"
args := make([]interface{}, 0, len(ids)+1)
args = append(args, rawSQL)
for _, id := range ids {
args = append(args, id)
}
_, err := sess.Exec(args...)
if err != nil {
return err
}
return nil
}
func managedPermission(action, resource string, resourceID, resourceAttribute string) accesscontrol.Permission {
return accesscontrol.Permission{
Action: action,
Scope: accesscontrol.Scope(resource, resourceAttribute, resourceID),
}
}