mirror of https://github.com/grafana/grafana
AccessControl: Add migration for seeding managed inherited permissions (#49337)
* AccessControl: Add migration for seeding managed inherited permissions Co-authored-by: Karl Persson <kalle.persson@grafana.com> * AccessControl: move to single file * AccessControl: Add tests for managed permission migration Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * AccessControl: Ensure no duplicate insertion Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * Remove commented code Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * Add code migration constant Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * Ensure DB is clean between tests Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * Update pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go Co-authored-by: Karl Persson <kalle.persson@grafana.com> Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>pull/49428/head
parent
18727f0bf5
commit
3250bf6b2b
@ -0,0 +1,163 @@ |
|||||||
|
// This migration ensures that permissions attributed to a managed role are also granted
|
||||||
|
// to parent roles.
|
||||||
|
// Example setup:
|
||||||
|
// editor read, query datasources:uid:2
|
||||||
|
// editor read, query datasources:uid:1
|
||||||
|
// admin read, query, write datasources:uid:1
|
||||||
|
// we'd need to create admin read, query, write datasources:uid:2
|
||||||
|
|
||||||
|
package accesscontrol |
||||||
|
|
||||||
|
import ( |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log" |
||||||
|
"github.com/grafana/grafana/pkg/models" |
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" |
||||||
|
"golang.org/x/text/cases" |
||||||
|
"golang.org/x/text/language" |
||||||
|
"xorm.io/xorm" |
||||||
|
) |
||||||
|
|
||||||
|
const ManagedPermissionsMigrationID = "managed permissions migration" |
||||||
|
|
||||||
|
func AddManagedPermissionsMigration(mg *migrator.Migrator) { |
||||||
|
mg.AddMigration(ManagedPermissionsMigrationID, &managedPermissionMigrator{}) |
||||||
|
} |
||||||
|
|
||||||
|
type managedPermissionMigrator struct { |
||||||
|
migrator.MigrationBase |
||||||
|
} |
||||||
|
|
||||||
|
func (sp *managedPermissionMigrator) SQL(dialect migrator.Dialect) string { |
||||||
|
return CodeMigrationSQL |
||||||
|
} |
||||||
|
|
||||||
|
func (sp *managedPermissionMigrator) Exec(sess *xorm.Session, mg *migrator.Migrator) error { |
||||||
|
logger := log.New("managed permissions migrator") |
||||||
|
|
||||||
|
type Permission struct { |
||||||
|
RoleName string `xorm:"role_name"` |
||||||
|
RoleID int64 `xorm:"role_id"` |
||||||
|
OrgID int64 `xorm:"org_id"` |
||||||
|
Action string |
||||||
|
Scope string |
||||||
|
} |
||||||
|
|
||||||
|
// get all permissions associated with a managed builtin role
|
||||||
|
managedPermissions := []Permission{} |
||||||
|
if errFindPermissions := sess.SQL(`SELECT r.name as role_name, r.id as role_id, r.org_id as org_id,p.action, p.scope |
||||||
|
FROM permission AS p |
||||||
|
INNER JOIN role AS r ON p.role_id = r.id |
||||||
|
WHERE r.name LIKE ?`, "managed:builtins%"). |
||||||
|
Find(&managedPermissions); errFindPermissions != nil { |
||||||
|
logger.Error("could not get the managed permissions", "error", errFindPermissions) |
||||||
|
return errFindPermissions |
||||||
|
} |
||||||
|
|
||||||
|
roleMap := make(map[int64]map[string]int64) // map[org_id][role_name] = role_id
|
||||||
|
permissionMap := make(map[int64]map[string]map[Permission]bool) // map[org_id][role_name][Permission] = toInsert
|
||||||
|
|
||||||
|
// for each managed permission make a map of which permissions need to be added to inheritors
|
||||||
|
for _, p := range managedPermissions { |
||||||
|
if _, ok := roleMap[p.OrgID]; !ok { |
||||||
|
roleMap[p.OrgID] = map[string]int64{p.RoleName: p.RoleID} |
||||||
|
} else { |
||||||
|
roleMap[p.OrgID][p.RoleName] = p.RoleID |
||||||
|
} |
||||||
|
|
||||||
|
// this ensures we can use p as a key in the map between different permissions
|
||||||
|
// ensuring we're only comparing on the action and scope
|
||||||
|
roleName := p.RoleName |
||||||
|
p.RoleName = "" |
||||||
|
p.RoleID = 0 |
||||||
|
|
||||||
|
// Add the permission to the map of permissions as "false" - already exists
|
||||||
|
if _, ok := permissionMap[p.OrgID]; !ok { |
||||||
|
permissionMap[p.OrgID] = map[string]map[Permission]bool{roleName: {p: false}} |
||||||
|
} else { |
||||||
|
if _, ok := permissionMap[p.OrgID][roleName]; !ok { |
||||||
|
permissionMap[p.OrgID][roleName] = map[Permission]bool{p: false} |
||||||
|
} else { |
||||||
|
permissionMap[p.OrgID][roleName][p] = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Add parent roles + permissions to the map as "true" -- need to be inserted
|
||||||
|
basicRoleName := ParseRoleFromName(roleName) |
||||||
|
for _, parent := range models.RoleType(basicRoleName).Parents() { |
||||||
|
parentManagedRoleName := "managed:builtins:" + strings.ToLower(string(parent)) + ":permissions" |
||||||
|
|
||||||
|
if _, ok := permissionMap[p.OrgID][parentManagedRoleName]; !ok { |
||||||
|
permissionMap[p.OrgID][parentManagedRoleName] = map[Permission]bool{p: true} |
||||||
|
} else { |
||||||
|
if _, ok := permissionMap[p.OrgID][parentManagedRoleName][p]; !ok { |
||||||
|
permissionMap[p.OrgID][parentManagedRoleName][p] = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
now := time.Now() |
||||||
|
|
||||||
|
// Create missing permissions
|
||||||
|
for orgID, orgMap := range permissionMap { |
||||||
|
for managedRole, permissions := range orgMap { |
||||||
|
// ensure managed role exists, create and add to map if it doesn't
|
||||||
|
ok, err := sess.Get(&accesscontrol.Role{Name: managedRole, OrgID: orgID}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if !ok { |
||||||
|
uid, err := generateNewRoleUID(sess, orgID) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
createdRole := accesscontrol.Role{Name: managedRole, OrgID: orgID, UID: uid, Created: now, Updated: now} |
||||||
|
if _, err := sess.Insert(&createdRole); err != nil { |
||||||
|
logger.Error("Unable to create managed role", "error", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
connection := accesscontrol.BuiltinRole{ |
||||||
|
RoleID: createdRole.ID, |
||||||
|
OrgID: orgID, |
||||||
|
Role: ParseRoleFromName(createdRole.Name), |
||||||
|
Created: now, |
||||||
|
Updated: now, |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := sess.Insert(&connection); err != nil { |
||||||
|
logger.Error("Unable to create managed role connection", "error", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
roleMap[orgID][managedRole] = createdRole.ID |
||||||
|
} |
||||||
|
|
||||||
|
// assign permissions if they don't exist to the role
|
||||||
|
roleID := roleMap[orgID][managedRole] |
||||||
|
for p, toInsert := range permissions { |
||||||
|
if toInsert { |
||||||
|
perm := accesscontrol.Permission{RoleID: roleID, Action: p.Action, Scope: p.Scope, Created: now, Updated: now} |
||||||
|
if _, err := sess.Insert(&perm); err != nil { |
||||||
|
logger.Error("Unable to create managed permission", "error", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Converts from managed:builtins:<role>:permissions to <Role>
|
||||||
|
// Example: managed:builtins:editor:permissions -> Editor
|
||||||
|
func ParseRoleFromName(roleName string) string { |
||||||
|
return cases.Title(language.AmericanEnglish). |
||||||
|
String(strings.TrimSuffix(strings.TrimPrefix(roleName, "managed:builtins:"), ":permissions")) |
||||||
|
} |
||||||
@ -0,0 +1,202 @@ |
|||||||
|
package test |
||||||
|
|
||||||
|
import ( |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log" |
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||||
|
acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" |
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" |
||||||
|
"github.com/grafana/grafana/pkg/setting" |
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
"xorm.io/xorm" |
||||||
|
) |
||||||
|
|
||||||
|
func TestManagedPermissionsMigration(t *testing.T) { |
||||||
|
// Run initial migration to have a working DB
|
||||||
|
x := setupTestDB(t) |
||||||
|
|
||||||
|
team1Scope := accesscontrol.Scope("teams", "id", "1") |
||||||
|
team2Scope := accesscontrol.Scope("teams", "id", "2") |
||||||
|
|
||||||
|
type teamMigrationTestCase struct { |
||||||
|
desc string |
||||||
|
putRolePerms map[int64]map[string][]rawPermission |
||||||
|
wantRolePerms map[int64]map[string][]rawPermission |
||||||
|
} |
||||||
|
testCases := []teamMigrationTestCase{ |
||||||
|
{ |
||||||
|
desc: "empty perms", |
||||||
|
putRolePerms: map[int64]map[string][]rawPermission{}, |
||||||
|
wantRolePerms: map[int64]map[string][]rawPermission{}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
desc: "only unrelated perms", |
||||||
|
putRolePerms: map[int64]map[string][]rawPermission{ |
||||||
|
1: { |
||||||
|
"managed:users:1:permissions": {{Action: "teams:read", Scope: team1Scope}}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
wantRolePerms: map[int64]map[string][]rawPermission{ |
||||||
|
1: { |
||||||
|
"managed:users:1:permissions": {{Action: "teams:read", Scope: team1Scope}}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
desc: "inherit permissions from managed role", |
||||||
|
putRolePerms: map[int64]map[string][]rawPermission{ |
||||||
|
1: { |
||||||
|
"managed:builtins:viewer:permissions": { |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
}, |
||||||
|
"managed:builtins:editor:permissions": { |
||||||
|
{Action: "teams:delete", Scope: team1Scope}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
2: { |
||||||
|
"managed:users:1:permissions": {{Action: "teams:read", Scope: team1Scope}}, |
||||||
|
"managed:builtins:viewer:permissions": { |
||||||
|
{Action: "teams:delete", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
}, |
||||||
|
"managed:builtins:editor:permissions": { |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
}, |
||||||
|
"managed:builtins:admin:permissions": { |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
{Action: "teams:write", Scope: team1Scope}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
wantRolePerms: map[int64]map[string][]rawPermission{ |
||||||
|
1: { |
||||||
|
"managed:builtins:viewer:permissions": { |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
}, |
||||||
|
"managed:builtins:editor:permissions": { |
||||||
|
{Action: "teams:delete", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
}, |
||||||
|
"managed:builtins:admin:permissions": { |
||||||
|
{Action: "teams:delete", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
2: { |
||||||
|
"managed:users:1:permissions": {{Action: "teams:read", Scope: team1Scope}}, |
||||||
|
"managed:builtins:viewer:permissions": { |
||||||
|
{Action: "teams:delete", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
}, |
||||||
|
"managed:builtins:editor:permissions": { |
||||||
|
{Action: "teams:delete", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
}, |
||||||
|
"managed:builtins:admin:permissions": { |
||||||
|
{Action: "teams:delete", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:read", Scope: team1Scope}, |
||||||
|
{Action: "teams.permissions:write", Scope: team2Scope}, |
||||||
|
{Action: "teams:write", Scope: team1Scope}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, tc := range testCases { |
||||||
|
t.Run(tc.desc, func(t *testing.T) { |
||||||
|
// Remove migration
|
||||||
|
_, errDeleteMig := x.Exec(`DELETE FROM migration_log WHERE migration_id = ?; |
||||||
|
DELETE FROM permission; DELETE FROM role`, acmig.ManagedPermissionsMigrationID) |
||||||
|
require.NoError(t, errDeleteMig) |
||||||
|
|
||||||
|
// put permissions
|
||||||
|
putTestPermissions(t, x, tc.putRolePerms) |
||||||
|
|
||||||
|
// Run accesscontrol migration (permissions insertion should not have conflicted)
|
||||||
|
acmigrator := migrator.NewMigrator(x, &setting.Cfg{Logger: log.New("acmigration.test")}) |
||||||
|
acmig.AddManagedPermissionsMigration(acmigrator) |
||||||
|
|
||||||
|
errRunningMig := acmigrator.Start(false, 0) |
||||||
|
require.NoError(t, errRunningMig) |
||||||
|
|
||||||
|
// verify got == want
|
||||||
|
for orgID, roles := range tc.wantRolePerms { |
||||||
|
for roleName := range roles { |
||||||
|
// Check managed roles exist
|
||||||
|
role := accesscontrol.Role{} |
||||||
|
hasRole, errManagedRoleSearch := x.Table("role").Where("org_id = ? AND name = ?", orgID, roleName).Get(&role) |
||||||
|
|
||||||
|
require.NoError(t, errManagedRoleSearch) |
||||||
|
require.True(t, hasRole, "expected role to exist", "orgID", orgID, "role", roleName) |
||||||
|
|
||||||
|
// Check permissions associated with each role
|
||||||
|
perms := []accesscontrol.Permission{} |
||||||
|
count, errManagedPermsSearch := x.Table("permission").Where("role_id = ?", role.ID).FindAndCount(&perms) |
||||||
|
|
||||||
|
require.NoError(t, errManagedPermsSearch) |
||||||
|
require.Equal(t, int64(len(tc.wantRolePerms[orgID][roleName])), count, "expected role to be tied to permissions", "orgID", orgID, "role", roleName) |
||||||
|
|
||||||
|
gotRawPerms := convertToRawPermissions(perms) |
||||||
|
require.ElementsMatch(t, gotRawPerms, tc.wantRolePerms[orgID][roleName], "expected role to have permissions", "orgID", orgID, "role", roleName) |
||||||
|
|
||||||
|
// Check assignment of the roles
|
||||||
|
br := accesscontrol.BuiltinRole{} |
||||||
|
has, errAssignmentSearch := x.Table("builtin_role").Where("role_id = ? AND role = ? AND org_id = ?", role.ID, acmig.ParseRoleFromName(roleName), orgID).Get(&br) |
||||||
|
require.NoError(t, errAssignmentSearch) |
||||||
|
require.True(t, has, "expected assignment of role to builtin role", "orgID", orgID, "role", roleName) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func putTestPermissions(t *testing.T, x *xorm.Engine, rolePerms map[int64]map[string][]rawPermission) { |
||||||
|
for orgID, roles := range rolePerms { |
||||||
|
for roleName, perms := range roles { |
||||||
|
uid := strconv.FormatInt(orgID, 10) + strings.ReplaceAll(roleName, ":", "_") |
||||||
|
role := accesscontrol.Role{ |
||||||
|
OrgID: orgID, |
||||||
|
Version: 1, |
||||||
|
UID: uid, |
||||||
|
Name: roleName, |
||||||
|
Updated: now, |
||||||
|
Created: now, |
||||||
|
} |
||||||
|
roleCount, errInsertRole := x.Insert(&role) |
||||||
|
require.NoError(t, errInsertRole) |
||||||
|
require.Equal(t, int64(1), roleCount) |
||||||
|
|
||||||
|
br := accesscontrol.BuiltinRole{ |
||||||
|
RoleID: role.ID, |
||||||
|
OrgID: role.OrgID, |
||||||
|
Role: acmig.ParseRoleFromName(roleName), |
||||||
|
Updated: now, |
||||||
|
Created: now, |
||||||
|
} |
||||||
|
brCount, err := x.Insert(br) |
||||||
|
require.NoError(t, err) |
||||||
|
require.Equal(t, int64(1), brCount) |
||||||
|
|
||||||
|
permissions := []accesscontrol.Permission{} |
||||||
|
for _, p := range perms { |
||||||
|
permissions = append(permissions, p.toPermission(role.ID, now)) |
||||||
|
} |
||||||
|
permissionsCount, err := x.Insert(permissions) |
||||||
|
require.NoError(t, err) |
||||||
|
require.Equal(t, int64(len(perms)), permissionsCount) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue