diff --git a/pkg/services/sqlstore/migrations/accesscontrol/errors.go b/pkg/services/sqlstore/migrations/accesscontrol/errors.go new file mode 100644 index 00000000000..fd1116f2e62 --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/errors.go @@ -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 +} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go new file mode 100644 index 00000000000..8e1cd545ce1 --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go @@ -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 +} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go new file mode 100644 index 00000000000..b79ba4c3d61 --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go @@ -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") +} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/util_test.go b/pkg/services/sqlstore/migrations/accesscontrol/util_test.go new file mode 100644 index 00000000000..9f555ab1745 --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/util_test.go @@ -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) + }) + } +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index efedc707e31..0b766b0a470 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -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) {