Files
grafana/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go
T
Gabriel MABILLE bc24fdcf8d AccessControl: Team membership migration (#44065)
Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
Co-authored-by: Jguer <joao.guerreiro@grafana.com>
2022-02-01 14:57:26 +01:00

417 lines
12 KiB
Go

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
}