mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AccessControl: Team membership migration (#44065)
Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com>
This commit is contained in:
parent
dca3dddafd
commit
bc24fdcf8d
17
pkg/services/sqlstore/migrations/accesscontrol/errors.go
Normal file
17
pkg/services/sqlstore/migrations/accesscontrol/errors.go
Normal file
@ -0,0 +1,17 @@
|
||||
package accesscontrol
|
||||
|
||||
import "fmt"
|
||||
|
||||
var ErrAddTeamMembershipMigrations = fmt.Errorf("Error migrating team memberships")
|
||||
|
||||
type ErrUnknownRole struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (e *ErrUnknownRole) Error() string {
|
||||
return fmt.Sprintf("%v: Unable to find role in map: %s", ErrAddTeamMembershipMigrations, e.key)
|
||||
}
|
||||
|
||||
func (e *ErrUnknownRole) Unwrap() error {
|
||||
return ErrAddTeamMembershipMigrations
|
||||
}
|
@ -0,0 +1,416 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
TeamsMigrationID = "teams permissions migration"
|
||||
batchSize = 500
|
||||
)
|
||||
|
||||
func AddTeamMembershipMigrations(mg *migrator.Migrator) {
|
||||
mg.AddMigration(TeamsMigrationID, &teamPermissionMigrator{editorsCanAdmin: mg.Cfg.EditorsCanAdmin})
|
||||
}
|
||||
|
||||
var _ migrator.CodeMigration = new(teamPermissionMigrator)
|
||||
|
||||
type teamPermissionMigrator struct {
|
||||
migrator.MigrationBase
|
||||
editorsCanAdmin bool
|
||||
sess *xorm.Session
|
||||
dialect migrator.Dialect
|
||||
}
|
||||
|
||||
func (p *teamPermissionMigrator) getAssignmentKey(orgID int64, name string) string {
|
||||
return fmt.Sprint(orgID, "-", name)
|
||||
}
|
||||
|
||||
func (p *teamPermissionMigrator) SQL(dialect migrator.Dialect) string {
|
||||
return "code migration"
|
||||
}
|
||||
|
||||
func (p *teamPermissionMigrator) Exec(sess *xorm.Session, migrator *migrator.Migrator) error {
|
||||
p.sess = sess
|
||||
p.dialect = migrator.Dialect
|
||||
return p.migrateMemberships()
|
||||
}
|
||||
|
||||
func generateNewRoleUID(sess *xorm.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 (p *teamPermissionMigrator) findRole(orgID int64, name string) (accesscontrol.Role, error) {
|
||||
// check if role exists
|
||||
var role accesscontrol.Role
|
||||
_, err := p.sess.Table("role").Where("org_id = ? AND name = ?", orgID, name).Get(&role)
|
||||
return role, err
|
||||
}
|
||||
|
||||
func batch(count, batchSize int, eachFn func(start, end int) error) error {
|
||||
for i := 0; i < count; {
|
||||
end := i + batchSize
|
||||
if end > count {
|
||||
end = count
|
||||
}
|
||||
|
||||
if err := eachFn(i, end); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i = end
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *teamPermissionMigrator) bulkCreateRoles(allRoles []*accesscontrol.Role) ([]*accesscontrol.Role, error) {
|
||||
if len(allRoles) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
allCreatedRoles := make([]*accesscontrol.Role, 0, len(allRoles))
|
||||
|
||||
createRoles := p.createRoles
|
||||
if p.dialect.DriverName() == migrator.MySQL {
|
||||
createRoles = p.createRolesMySQL
|
||||
}
|
||||
|
||||
// bulk role creations
|
||||
err := batch(len(allRoles), batchSize, func(start, end int) error {
|
||||
roles := allRoles[start:end]
|
||||
createdRoles, err := createRoles(roles, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allCreatedRoles = append(allCreatedRoles, createdRoles...)
|
||||
return nil
|
||||
})
|
||||
|
||||
return allCreatedRoles, err
|
||||
}
|
||||
|
||||
// createRoles creates a list of roles and returns their id, orgID, name in a single query
|
||||
func (p *teamPermissionMigrator) createRoles(roles []*accesscontrol.Role, start int, end int) ([]*accesscontrol.Role, error) {
|
||||
ts := time.Now()
|
||||
createdRoles := make([]*accesscontrol.Role, 0, len(roles))
|
||||
valueStrings := make([]string, len(roles))
|
||||
args := make([]interface{}, 0, len(roles)*5)
|
||||
|
||||
for i, r := range roles {
|
||||
uid, err := generateNewRoleUID(p.sess, r.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valueStrings[i] = "(?, ?, ?, 1, ?, ?)"
|
||||
args = append(args, r.OrgID, uid, r.Name, ts, ts)
|
||||
}
|
||||
|
||||
// Insert and fetch at once
|
||||
valueString := strings.Join(valueStrings, ",")
|
||||
sql := fmt.Sprintf("INSERT INTO role (org_id, uid, name, version, created, updated) VALUES %s RETURNING id, org_id, name", valueString)
|
||||
if errCreate := p.sess.SQL(sql, args...).Find(&createdRoles); errCreate != nil {
|
||||
return nil, errCreate
|
||||
}
|
||||
|
||||
return createdRoles, nil
|
||||
}
|
||||
|
||||
// createRolesMySQL creates a list of roles then fetches them
|
||||
func (p *teamPermissionMigrator) createRolesMySQL(roles []*accesscontrol.Role, start int, end int) ([]*accesscontrol.Role, error) {
|
||||
ts := time.Now()
|
||||
createdRoles := make([]*accesscontrol.Role, 0, len(roles))
|
||||
|
||||
where := make([]string, len(roles))
|
||||
args := make([]interface{}, 0, len(roles)*2)
|
||||
|
||||
for i := range roles {
|
||||
uid, err := generateNewRoleUID(p.sess, roles[i].OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles[i].UID = uid
|
||||
roles[i].Created = ts
|
||||
roles[i].Updated = ts
|
||||
|
||||
where[i] = ("(org_id = ? AND uid = ?)")
|
||||
args = append(args, roles[i].OrgID, uid)
|
||||
}
|
||||
|
||||
// Insert roles
|
||||
if _, errCreate := p.sess.Table("role").Insert(&roles); errCreate != nil {
|
||||
return nil, errCreate
|
||||
}
|
||||
|
||||
// Fetch newly created roles
|
||||
if errFindInsertions := p.sess.Table("role").
|
||||
Where(strings.Join(where, " OR "), args...).
|
||||
Find(&createdRoles); errFindInsertions != nil {
|
||||
return nil, errFindInsertions
|
||||
}
|
||||
|
||||
return createdRoles, nil
|
||||
}
|
||||
|
||||
func (p *teamPermissionMigrator) bulkAssignRoles(rolesMap map[string]*accesscontrol.Role, assignments map[int64]map[string]struct{}) error {
|
||||
if len(assignments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ts := time.Now()
|
||||
|
||||
roleAssignments := make([]accesscontrol.UserRole, 0, len(assignments))
|
||||
for userID, rolesByRoleKey := range assignments {
|
||||
for key := range rolesByRoleKey {
|
||||
role, ok := rolesMap[key]
|
||||
if !ok {
|
||||
return &ErrUnknownRole{key}
|
||||
}
|
||||
|
||||
roleAssignments = append(roleAssignments, accesscontrol.UserRole{
|
||||
OrgID: role.OrgID,
|
||||
RoleID: role.ID,
|
||||
UserID: userID,
|
||||
Created: ts,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return batch(len(roleAssignments), batchSize, func(start, end int) error {
|
||||
roleAssignmentsChunk := roleAssignments[start:end]
|
||||
_, err := p.sess.Table("user_role").InsertMulti(roleAssignmentsChunk)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// setRolePermissions sets the role permissions deleting any team related ones before inserting any.
|
||||
func (p *teamPermissionMigrator) setRolePermissions(roleID int64, permissions []accesscontrol.Permission) error {
|
||||
// First drop existing permissions
|
||||
if _, errDeletingPerms := p.sess.Exec("DELETE FROM permission WHERE role_id = ? AND (action LIKE ? OR action LIKE ?)", roleID, "teams:%", "teams.permissions:%"); errDeletingPerms != nil {
|
||||
return errDeletingPerms
|
||||
}
|
||||
|
||||
// Then insert new permissions
|
||||
var newPermissions []accesscontrol.Permission
|
||||
now := time.Now()
|
||||
for _, permission := range permissions {
|
||||
permission.RoleID = roleID
|
||||
permission.Created = now
|
||||
permission.Updated = now
|
||||
newPermissions = append(newPermissions, permission)
|
||||
}
|
||||
|
||||
if _, errInsertPerms := p.sess.InsertMulti(&newPermissions); errInsertPerms != nil {
|
||||
return errInsertPerms
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapPermissionToFGAC translates the legacy membership (Member or Admin) into FGAC permissions
|
||||
func (p *teamPermissionMigrator) mapPermissionToFGAC(permission models.PermissionType, teamID int64) []accesscontrol.Permission {
|
||||
teamIDScope := accesscontrol.Scope("teams", "id", strconv.FormatInt(teamID, 10))
|
||||
switch permission {
|
||||
case 0:
|
||||
return []accesscontrol.Permission{{Action: "teams:read", Scope: teamIDScope}}
|
||||
case models.PERMISSION_ADMIN:
|
||||
return []accesscontrol.Permission{
|
||||
{Action: "teams:delete", Scope: teamIDScope},
|
||||
{Action: "teams:read", Scope: teamIDScope},
|
||||
{Action: "teams:write", Scope: teamIDScope},
|
||||
{Action: "teams.permissions:read", Scope: teamIDScope},
|
||||
{Action: "teams.permissions:write", Scope: teamIDScope},
|
||||
}
|
||||
default:
|
||||
return []accesscontrol.Permission{}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *teamPermissionMigrator) getUserRoleByOrgMapping() (map[int64]map[int64]string, error) {
|
||||
var orgUsers []*models.OrgUserDTO
|
||||
if err := p.sess.SQL(`SELECT * FROM org_user`).Cols("org_user.org_id", "org_user.user_id", "org_user.role").Find(&orgUsers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userRolesByOrg := map[int64]map[int64]string{}
|
||||
|
||||
// Loop through users and organise them by organization ID
|
||||
for _, orgUser := range orgUsers {
|
||||
orgRoles, initialized := userRolesByOrg[orgUser.OrgId]
|
||||
if !initialized {
|
||||
orgRoles = map[int64]string{}
|
||||
}
|
||||
|
||||
orgRoles[orgUser.UserId] = orgUser.Role
|
||||
userRolesByOrg[orgUser.OrgId] = orgRoles
|
||||
}
|
||||
|
||||
return userRolesByOrg, nil
|
||||
}
|
||||
|
||||
// migrateMemberships generate managed permissions for users based on their memberships to teams
|
||||
func (p *teamPermissionMigrator) migrateMemberships() error {
|
||||
// Fetch user roles in each org
|
||||
userRolesByOrg, err := p.getUserRoleByOrgMapping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch team memberships
|
||||
teamMemberships := []*models.TeamMember{}
|
||||
if err := p.sess.SQL("SELECT * FROM team_member").Find(&teamMemberships); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No need to create any roles if there is no team members
|
||||
if len(teamMemberships) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Loop through memberships and generate associated permissions
|
||||
// Downgrade team permissions if needed - only admins or editors (when editorsCanAdmin option is enabled)
|
||||
// can access team administration endpoints
|
||||
userPermissionsByOrg, errGen := p.generateAssociatedPermissions(teamMemberships, userRolesByOrg)
|
||||
if errGen != nil {
|
||||
return errGen
|
||||
}
|
||||
|
||||
// Sort roles that:
|
||||
// * need to be created and assigned (rolesToCreate, assignments)
|
||||
// * are already created and assigned (rolesByOrg)
|
||||
rolesToCreate, assignments, rolesByOrg, errOrganizeRoles := p.sortRolesToAssign(userPermissionsByOrg)
|
||||
if errOrganizeRoles != nil {
|
||||
return errOrganizeRoles
|
||||
}
|
||||
|
||||
// Create missing roles
|
||||
createdRoles, errCreate := p.bulkCreateRoles(rolesToCreate)
|
||||
if errCreate != nil {
|
||||
return errCreate
|
||||
}
|
||||
|
||||
// Populate rolesMap with the newly created roles
|
||||
for i := range createdRoles {
|
||||
roleKey := p.getAssignmentKey(createdRoles[i].OrgID, createdRoles[i].Name)
|
||||
rolesByOrg[roleKey] = createdRoles[i]
|
||||
}
|
||||
|
||||
// Assign newly created roles
|
||||
if errAssign := p.bulkAssignRoles(rolesByOrg, assignments); errAssign != nil {
|
||||
return errAssign
|
||||
}
|
||||
|
||||
// Set all roles teams related permissions
|
||||
return p.setRolePermissionsForOrgs(userPermissionsByOrg, rolesByOrg)
|
||||
}
|
||||
|
||||
func (p *teamPermissionMigrator) setRolePermissionsForOrgs(userPermissionsByOrg map[int64]map[int64][]accesscontrol.Permission, rolesByOrg map[string]*accesscontrol.Role) error {
|
||||
for orgID, userPermissions := range userPermissionsByOrg {
|
||||
for userID, permissions := range userPermissions {
|
||||
key := p.getAssignmentKey(orgID, fmt.Sprintf("managed:users:%d:permissions", userID))
|
||||
|
||||
role, ok := rolesByOrg[key]
|
||||
if !ok {
|
||||
return &ErrUnknownRole{key}
|
||||
}
|
||||
|
||||
if errSettingPerms := p.setRolePermissions(role.ID, permissions); errSettingPerms != nil {
|
||||
return errSettingPerms
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *teamPermissionMigrator) sortRolesToAssign(userPermissionsByOrg map[int64]map[int64][]accesscontrol.Permission) ([]*accesscontrol.Role, map[int64]map[string]struct{}, map[string]*accesscontrol.Role, error) {
|
||||
var rolesToCreate []*accesscontrol.Role
|
||||
|
||||
assignments := map[int64]map[string]struct{}{}
|
||||
|
||||
rolesByOrg := map[string]*accesscontrol.Role{}
|
||||
for orgID, userPermissions := range userPermissionsByOrg {
|
||||
for userID := range userPermissions {
|
||||
roleName := fmt.Sprintf("managed:users:%d:permissions", userID)
|
||||
role, errFindingRoles := p.findRole(orgID, roleName)
|
||||
if errFindingRoles != nil {
|
||||
return nil, nil, nil, errFindingRoles
|
||||
}
|
||||
|
||||
roleKey := p.getAssignmentKey(orgID, roleName)
|
||||
|
||||
if role.ID != 0 {
|
||||
rolesByOrg[roleKey] = &role
|
||||
} else {
|
||||
roleToCreate := &accesscontrol.Role{
|
||||
Name: roleName,
|
||||
OrgID: orgID,
|
||||
}
|
||||
rolesToCreate = append(rolesToCreate, roleToCreate)
|
||||
|
||||
userAssignments, initialized := assignments[userID]
|
||||
if !initialized {
|
||||
userAssignments = map[string]struct{}{}
|
||||
}
|
||||
|
||||
userAssignments[roleKey] = struct{}{}
|
||||
assignments[userID] = userAssignments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rolesToCreate, assignments, rolesByOrg, nil
|
||||
}
|
||||
|
||||
func (p *teamPermissionMigrator) generateAssociatedPermissions(teamMemberships []*models.TeamMember,
|
||||
userRolesByOrg map[int64]map[int64]string) (map[int64]map[int64][]accesscontrol.Permission, error) {
|
||||
userPermissionsByOrg := map[int64]map[int64][]accesscontrol.Permission{}
|
||||
|
||||
for _, m := range teamMemberships {
|
||||
// Downgrade team permissions if needed:
|
||||
// only admins or editors (when editorsCanAdmin option is enabled)
|
||||
// can access team administration endpoints
|
||||
if m.Permission == models.PERMISSION_ADMIN {
|
||||
if userRolesByOrg[m.OrgId][m.UserId] == string(models.ROLE_VIEWER) || (userRolesByOrg[m.OrgId][m.UserId] == string(models.ROLE_EDITOR) && !p.editorsCanAdmin) {
|
||||
m.Permission = 0
|
||||
|
||||
if _, err := p.sess.Cols("permission").Where("org_id=? and team_id=? and user_id=?", m.OrgId, m.TeamId, m.UserId).Update(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userPermissions, initialized := userPermissionsByOrg[m.OrgId]
|
||||
if !initialized {
|
||||
userPermissions = map[int64][]accesscontrol.Permission{}
|
||||
}
|
||||
userPermissions[m.UserId] = append(userPermissions[m.UserId], p.mapPermissionToFGAC(m.Permission, m.TeamId)...)
|
||||
userPermissionsByOrg[m.OrgId] = userPermissions
|
||||
}
|
||||
|
||||
return userPermissionsByOrg, nil
|
||||
}
|
413
pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go
Normal file
413
pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go
Normal file
@ -0,0 +1,413 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type rawPermission struct {
|
||||
Action, Scope string
|
||||
}
|
||||
|
||||
// Setup users
|
||||
var (
|
||||
now = time.Now()
|
||||
|
||||
users = []models.User{
|
||||
{
|
||||
Id: 1,
|
||||
Email: "viewer1@example.org",
|
||||
Name: "viewer1",
|
||||
Login: "viewer1",
|
||||
OrgId: 1,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Email: "viewer2@example.org",
|
||||
Name: "viewer2",
|
||||
Login: "viewer2",
|
||||
OrgId: 1,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Email: "editor1@example.org",
|
||||
Name: "editor1",
|
||||
Login: "editor1",
|
||||
OrgId: 1,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
Email: "admin1@example.org",
|
||||
Name: "admin1",
|
||||
Login: "admin1",
|
||||
OrgId: 1,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
Id: 5,
|
||||
Email: "editor2@example.org",
|
||||
Name: "editor2",
|
||||
Login: "editor2",
|
||||
OrgId: 2,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func convertToRawPermissions(permissions []accesscontrol.Permission) []rawPermission {
|
||||
raw := make([]rawPermission, len(permissions))
|
||||
for i, p := range permissions {
|
||||
raw[i] = rawPermission{Action: p.Action, Scope: p.Scope}
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func TestMigrations(t *testing.T) {
|
||||
// Run initial migration to have a working DB
|
||||
x := setupTestDB(t)
|
||||
|
||||
// Populate users and teams
|
||||
setupTeams(t, x)
|
||||
|
||||
// Create managed user roles with teams permissions (ex: teams:read and teams.permissions:read)
|
||||
setupUnecessaryFGACPermissions(t, x)
|
||||
|
||||
team1Scope := accesscontrol.Scope("teams", "id", "1")
|
||||
team2Scope := accesscontrol.Scope("teams", "id", "2")
|
||||
|
||||
type teamMigrationTestCase struct {
|
||||
desc string
|
||||
config *setting.Cfg
|
||||
expectedRolePerms map[string][]rawPermission
|
||||
}
|
||||
testCases := []teamMigrationTestCase{
|
||||
{
|
||||
desc: "with editors can admin",
|
||||
config: &setting.Cfg{
|
||||
EditorsCanAdmin: true,
|
||||
IsFeatureToggleEnabled: func(key string) bool { return key == "accesscontrol" },
|
||||
},
|
||||
expectedRolePerms: map[string][]rawPermission{
|
||||
"managed:users:1:permissions": {{Action: "teams:read", Scope: team1Scope}},
|
||||
"managed:users:2:permissions": {{Action: "teams:read", Scope: team1Scope}},
|
||||
"managed:users:3:permissions": {
|
||||
{Action: "teams:read", Scope: team1Scope},
|
||||
{Action: "teams:delete", Scope: team1Scope},
|
||||
{Action: "teams:write", Scope: team1Scope},
|
||||
{Action: "teams.permissions:read", Scope: team1Scope},
|
||||
{Action: "teams.permissions:write", Scope: team1Scope},
|
||||
},
|
||||
"managed:users:4:permissions": {
|
||||
{Action: "teams:read", Scope: team1Scope},
|
||||
{Action: "teams:delete", Scope: team1Scope},
|
||||
{Action: "teams:write", Scope: team1Scope},
|
||||
{Action: "teams.permissions:read", Scope: team1Scope},
|
||||
{Action: "teams.permissions:write", Scope: team1Scope},
|
||||
},
|
||||
"managed:users:5:permissions": {
|
||||
{Action: "teams:read", Scope: team2Scope},
|
||||
{Action: "users:read", Scope: "users:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "without editors can admin",
|
||||
config: &setting.Cfg{
|
||||
IsFeatureToggleEnabled: func(key string) bool { return key == "accesscontrol" },
|
||||
},
|
||||
expectedRolePerms: map[string][]rawPermission{
|
||||
"managed:users:1:permissions": {{Action: "teams:read", Scope: team1Scope}},
|
||||
"managed:users:2:permissions": {{Action: "teams:read", Scope: team1Scope}},
|
||||
"managed:users:3:permissions": {{Action: "teams:read", Scope: team1Scope}},
|
||||
"managed:users:4:permissions": {
|
||||
{Action: "teams:read", Scope: team1Scope},
|
||||
{Action: "teams:delete", Scope: team1Scope},
|
||||
{Action: "teams:write", Scope: team1Scope},
|
||||
{Action: "teams.permissions:read", Scope: team1Scope},
|
||||
{Action: "teams.permissions:write", Scope: team1Scope},
|
||||
},
|
||||
"managed:users:5:permissions": {
|
||||
{Action: "teams:read", Scope: team2Scope},
|
||||
{Action: "users:read", Scope: "users:*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// Remove migration
|
||||
_, errDeleteMig := x.Exec("DELETE FROM migration_log WHERE migration_id = ?", acmig.TeamsMigrationID)
|
||||
require.NoError(t, errDeleteMig)
|
||||
|
||||
// Run accesscontrol migration (permissions insertion should not have conflicted)
|
||||
acmigrator := migrator.NewMigrator(x, tc.config)
|
||||
acmig.AddTeamMembershipMigrations(acmigrator)
|
||||
|
||||
errRunningMig := acmigrator.Start()
|
||||
require.NoError(t, errRunningMig)
|
||||
|
||||
for _, user := range users {
|
||||
// Check managed roles exist
|
||||
roleName := fmt.Sprintf("managed:users:%d:permissions", user.Id)
|
||||
role := accesscontrol.Role{}
|
||||
hasRole, errManagedRoleSearch := x.Table("role").Where("org_id = ? AND name = ?", user.OrgId, roleName).Get(&role)
|
||||
|
||||
require.NoError(t, errManagedRoleSearch)
|
||||
assert.True(t, hasRole, "expected role to be granted to user", user, roleName)
|
||||
|
||||
// Check permissions associated with each role
|
||||
perms := []accesscontrol.Permission{}
|
||||
countUserPermissions, errManagedPermsSearch := x.Table("permission").Where("role_id = ?", role.ID).FindAndCount(&perms)
|
||||
|
||||
require.NoError(t, errManagedPermsSearch)
|
||||
assert.Equal(t, int64(len(tc.expectedRolePerms[roleName])), countUserPermissions, "expected role to be tied to permissions", user, role)
|
||||
|
||||
rawPerms := convertToRawPermissions(perms)
|
||||
for _, perm := range rawPerms {
|
||||
assert.Contains(t, tc.expectedRolePerms[roleName], perm)
|
||||
}
|
||||
|
||||
// Check assignment of the roles
|
||||
assign := accesscontrol.UserRole{}
|
||||
has, errAssignmentSearch := x.Table("user_role").Where("role_id = ? AND user_id = ?", role.ID, user.Id).Get(&assign)
|
||||
require.NoError(t, errAssignmentSearch)
|
||||
assert.True(t, has, "expected assignment of role to user", role, user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestDB(t *testing.T) *xorm.Engine {
|
||||
t.Helper()
|
||||
testDB := sqlutil.SQLite3TestDB()
|
||||
|
||||
const query = `select count(*) as count from migration_log`
|
||||
result := struct{ Count int }{}
|
||||
|
||||
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = migrator.NewDialect(x).CleanDB()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = x.SQL(query).Get(&result)
|
||||
require.Error(t, err)
|
||||
|
||||
mg := migrator.NewMigrator(x, &setting.Cfg{
|
||||
IsFeatureToggleEnabled: func(key string) bool { return key == "accesscontrol" },
|
||||
})
|
||||
migrations := &migrations.OSSMigrations{}
|
||||
migrations.AddMigration(mg)
|
||||
|
||||
err = mg.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
func setupTeams(t *testing.T, x *xorm.Engine) {
|
||||
t.Helper()
|
||||
|
||||
usersCount, errInsertUsers := x.Insert(users)
|
||||
require.NoError(t, errInsertUsers)
|
||||
require.Equal(t, int64(5), usersCount, "needed 5 users for this test to run")
|
||||
|
||||
orgUsers := []models.OrgUser{
|
||||
{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
Role: models.ROLE_VIEWER,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
OrgId: 1,
|
||||
UserId: 2,
|
||||
Role: models.ROLE_VIEWER,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
OrgId: 1,
|
||||
UserId: 3,
|
||||
Role: models.ROLE_EDITOR,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
OrgId: 1,
|
||||
UserId: 4,
|
||||
Role: models.ROLE_ADMIN,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
OrgId: 2,
|
||||
UserId: 5,
|
||||
Role: models.ROLE_EDITOR,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
}
|
||||
orgUsersCount, errInsertOrgUsers := x.Insert(orgUsers)
|
||||
require.NoError(t, errInsertOrgUsers)
|
||||
require.Equal(t, int64(5), orgUsersCount, "needed 5 users for this test to run")
|
||||
|
||||
// Setup teams (and members)
|
||||
teams := []models.Team{
|
||||
{
|
||||
OrgId: 1,
|
||||
Name: "teamOrg1",
|
||||
Email: "teamorg1@example.org",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
OrgId: 2,
|
||||
Name: "teamOrg2",
|
||||
Email: "teamorg2@example.org",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
}
|
||||
teamCount, errInsertTeams := x.Insert(teams)
|
||||
require.NoError(t, errInsertTeams)
|
||||
require.Equal(t, int64(2), teamCount, "needed 2 teams for this test to run")
|
||||
|
||||
members := []models.TeamMember{
|
||||
{
|
||||
// Can have viewer permissions
|
||||
OrgId: 1,
|
||||
TeamId: 1,
|
||||
UserId: 1,
|
||||
External: false,
|
||||
Permission: 0,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
// Cannot have admin permissions
|
||||
OrgId: 1,
|
||||
TeamId: 1,
|
||||
UserId: 2,
|
||||
External: false,
|
||||
Permission: models.PERMISSION_ADMIN,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
// Can have admin permissions
|
||||
OrgId: 1,
|
||||
TeamId: 1,
|
||||
UserId: 3,
|
||||
External: false,
|
||||
Permission: models.PERMISSION_ADMIN,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
// Can have admin permissions
|
||||
OrgId: 1,
|
||||
TeamId: 1,
|
||||
UserId: 4,
|
||||
External: false,
|
||||
Permission: models.PERMISSION_ADMIN,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
// Can have viewer permissions
|
||||
OrgId: 2,
|
||||
TeamId: 2,
|
||||
UserId: 5,
|
||||
External: false,
|
||||
Permission: 0,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
}
|
||||
membersCount, err := x.Insert(members)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(5), membersCount, "needed 5 members for this test to run")
|
||||
}
|
||||
|
||||
func setupUnecessaryFGACPermissions(t *testing.T, x *xorm.Engine) {
|
||||
t.Helper()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
role := accesscontrol.Role{
|
||||
// ID: 1, Not specifying this for pgsql to correctly increment sequence
|
||||
OrgID: 2,
|
||||
Version: 1,
|
||||
UID: "user5managedpermissions",
|
||||
Name: "managed:users:5:permissions",
|
||||
Updated: now,
|
||||
Created: now,
|
||||
}
|
||||
rolesCount, err := x.Insert(role)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), rolesCount, "needed 1 role for this test to run")
|
||||
|
||||
userRole := accesscontrol.UserRole{
|
||||
OrgID: 2,
|
||||
RoleID: 1,
|
||||
UserID: 5,
|
||||
Created: now,
|
||||
}
|
||||
userRoleCount, err := x.Insert(userRole)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), userRoleCount, "needed 1 assignment for this test to run")
|
||||
|
||||
permissions := []accesscontrol.Permission{
|
||||
{
|
||||
// Permission that shouldn't be removed
|
||||
RoleID: 1,
|
||||
Action: "users:read",
|
||||
Scope: "users:*",
|
||||
Updated: now,
|
||||
Created: now,
|
||||
},
|
||||
{
|
||||
// Permission that should be recreated
|
||||
RoleID: 1,
|
||||
Action: "teams:read",
|
||||
Scope: "teams:*",
|
||||
Updated: now,
|
||||
Created: now,
|
||||
},
|
||||
{
|
||||
// Permission that should be removed
|
||||
RoleID: 1,
|
||||
Action: "teams.permissions:read",
|
||||
Scope: "teams:*",
|
||||
Updated: now,
|
||||
Created: now,
|
||||
},
|
||||
}
|
||||
permissionsCount, err := x.Insert(permissions)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(3), permissionsCount, "needed 3 permissions for this test to run")
|
||||
}
|
42
pkg/services/sqlstore/migrations/accesscontrol/util_test.go
Normal file
42
pkg/services/sqlstore/migrations/accesscontrol/util_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBatch(t *testing.T) {
|
||||
type testCase struct {
|
||||
desc string
|
||||
batchSize int
|
||||
count int
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{desc: "empty", batchSize: 1, count: 0},
|
||||
{desc: "1 run of 5", batchSize: 5, count: 5},
|
||||
{desc: "10 runs of 5", batchSize: 5, count: 50},
|
||||
{desc: "unmatching end", batchSize: 10, count: 25},
|
||||
{desc: "batch bigger than count", batchSize: 500, count: 25},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
items := make([]int, tc.count)
|
||||
sum := 0
|
||||
|
||||
got := batch(len(items), tc.batchSize, func(start int, end int) error {
|
||||
chunk := items[start:end]
|
||||
for range chunk {
|
||||
sum += 1
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, got)
|
||||
require.Equal(t, tc.count, sum)
|
||||
})
|
||||
}
|
||||
}
|
@ -69,6 +69,12 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
|
||||
ualert.AddDashboardUIDPanelIDMigration(mg)
|
||||
accesscontrol.AddMigration(mg)
|
||||
addQueryHistoryMigrations(mg)
|
||||
|
||||
if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
|
||||
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
accesscontrol.AddTeamMembershipMigrations(mg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addMigrationLogMigrations(mg *Migrator) {
|
||||
|
Loading…
Reference in New Issue
Block a user