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>
This commit is contained in:
Jguer 2022-05-23 14:50:10 +00:00 committed by GitHub
parent 18727f0bf5
commit 3250bf6b2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 384 additions and 4 deletions

View File

@ -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"))
}

View File

@ -1,6 +1,10 @@
package accesscontrol package accesscontrol
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" import (
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
const CodeMigrationSQL = "code migration"
func AddMigration(mg *migrator.Migrator) { func AddMigration(mg *migrator.Migrator) {
permissionV1 := migrator.Table{ permissionV1 := migrator.Table{

View File

@ -7,6 +7,7 @@ import (
"xorm.io/xorm" "xorm.io/xorm"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrations"
@ -22,6 +23,16 @@ type rawPermission struct {
Action, Scope string Action, Scope string
} }
func (rp *rawPermission) toPermission(roleID int64, ts time.Time) accesscontrol.Permission {
return accesscontrol.Permission{
RoleID: roleID,
Action: rp.Action,
Scope: rp.Scope,
Updated: ts,
Created: ts,
}
}
// Setup users // Setup users
var ( var (
now = time.Now() now = time.Now()
@ -209,9 +220,7 @@ func setupTestDB(t *testing.T) *xorm.Engine {
err = migrator.NewDialect(x).CleanDB() err = migrator.NewDialect(x).CleanDB()
require.NoError(t, err) require.NoError(t, err)
mg := migrator.NewMigrator(x, &setting.Cfg{ mg := migrator.NewMigrator(x, &setting.Cfg{Logger: log.New("acmigration.test")})
IsFeatureToggleEnabled: func(key string) bool { return key == "accesscontrol" },
})
migrations := &migrations.OSSMigrations{} migrations := &migrations.OSSMigrations{}
migrations.AddMigration(mg) migrations.AddMigration(mg)

View File

@ -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)
}
}
}

View File

@ -97,6 +97,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
addPublicDashboardMigration(mg) addPublicDashboardMigration(mg)
ualert.CreateDefaultFoldersForAlertingMigration(mg) ualert.CreateDefaultFoldersForAlertingMigration(mg)
addDbFileStorageMigration(mg) addDbFileStorageMigration(mg)
accesscontrol.AddManagedPermissionsMigration(mg)
} }
func addMigrationLogMigrations(mg *Migrator) { func addMigrationLogMigrations(mg *Migrator) {