mirror of https://github.com/grafana/grafana
AccessControl: Team membership migration (#44065)
Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com>pull/44684/head
parent
dca3dddafd
commit
bc24fdcf8d
@ -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 |
||||
} |
@ -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") |
||||
} |
@ -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) |
||||
}) |
||||
} |
||||
} |
Loading…
Reference in new issue