Support atomic organization role sets and owner-managed invitations
verify / verify (push) Successful in 4m17s
verify / verify (push) Successful in 4m17s
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
+37
-15
@@ -167,9 +167,28 @@ func (store *Store) OrganizationUserBindings(ctx context.Context, organizationID
|
||||
}
|
||||
|
||||
func (store *Store) ReplaceOrganizationUserRole(ctx context.Context, expected []string, replacement access.Binding, ownerRole string, audit access.AuditEvent) error {
|
||||
if !validOrganizationRoleReplacement(expected, replacement, ownerRole, audit) {
|
||||
return errors.New("authsqlite: invalid organization role replacement")
|
||||
return store.replaceOrganizationUserRoles(ctx, expected, []access.Binding{replacement}, ownerRole, audit, false)
|
||||
}
|
||||
|
||||
func (store *Store) ReplaceOrganizationUserRoles(ctx context.Context, expected []string, replacements []access.Binding, ownerRole string, audit access.AuditEvent) error {
|
||||
return store.replaceOrganizationUserRoles(ctx, expected, replacements, ownerRole, audit, true)
|
||||
}
|
||||
|
||||
func (store *Store) replaceOrganizationUserRoles(ctx context.Context, expected []string, replacements []access.Binding, ownerRole string, audit access.AuditEvent, requireOwner bool) error {
|
||||
if len(replacements) < 1 || len(replacements) > 16 {
|
||||
return errors.New("authsqlite: invalid organization role set")
|
||||
}
|
||||
replacement := replacements[0]
|
||||
roles := make([]string, 0, len(replacements))
|
||||
ids := make(map[string]bool, len(replacements))
|
||||
for _, value := range replacements {
|
||||
if !validOrganizationRoleReplacement(expected, value, ownerRole, audit) || value.SubjectID != replacement.SubjectID || value.Scope != replacement.Scope || value.GrantedBy != replacement.GrantedBy || !value.GrantedAt.Equal(replacement.GrantedAt) || ids[value.ID] || slices.Contains(roles, value.Role) {
|
||||
return errors.New("authsqlite: invalid organization role set")
|
||||
}
|
||||
roles = append(roles, value.Role)
|
||||
ids[value.ID] = true
|
||||
}
|
||||
slices.Sort(roles)
|
||||
tx, err := store.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -183,7 +202,7 @@ func (store *Store) ReplaceOrganizationUserRole(ctx context.Context, expected []
|
||||
result, err := tx.ExecContext(ctx, `UPDATE gwf_organization_memberships SET status=status
|
||||
WHERE organization_id=? AND user_id=? AND status='active'
|
||||
AND EXISTS (SELECT 1 FROM gwf_organizations o WHERE o.id=? AND o.status='active')
|
||||
AND EXISTS (SELECT 1 FROM gwf_users u WHERE u.id=? AND u.status='active')`, replacement.Scope.OrganizationID, replacement.GrantedBy, replacement.Scope.OrganizationID, replacement.GrantedBy)
|
||||
AND EXISTS (SELECT 1 FROM gwf_users u WHERE u.id=? AND u.status='active' AND u.registration_pending=0)`, replacement.Scope.OrganizationID, replacement.GrantedBy, replacement.Scope.OrganizationID, replacement.GrantedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -195,7 +214,7 @@ func (store *Store) ReplaceOrganizationUserRole(ctx context.Context, expected []
|
||||
if err = tx.QueryRowContext(ctx, `SELECT COUNT(*)
|
||||
FROM gwf_organization_memberships m
|
||||
JOIN gwf_organizations o ON o.id=m.organization_id AND o.status='active'
|
||||
JOIN gwf_users u ON u.id=m.user_id AND u.status='active'
|
||||
JOIN gwf_users u ON u.id=m.user_id AND u.status='active' AND u.registration_pending=0
|
||||
WHERE m.organization_id=? AND m.user_id=? AND m.status='active'`, replacement.Scope.OrganizationID, replacement.SubjectID).Scan(&active); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -231,10 +250,11 @@ func (store *Store) ReplaceOrganizationUserRole(ctx context.Context, expected []
|
||||
if !slices.Equal(currentIDs, expected) {
|
||||
return access.ErrRoleChangeConflict
|
||||
}
|
||||
if len(currentRoles) == 1 && currentRoles[0] == replacement.Role {
|
||||
slices.Sort(currentRoles)
|
||||
if slices.Equal(currentRoles, roles) {
|
||||
return access.ErrRoleUnchanged
|
||||
}
|
||||
if replacement.Role == ownerRole || slices.Contains(currentRoles, ownerRole) {
|
||||
if requireOwner || slices.Contains(roles, ownerRole) || slices.Contains(currentRoles, ownerRole) {
|
||||
actorIsOwner, ownerErr := hasDirectOwnerRole(ctx, tx, replacement.Scope.OrganizationID, replacement.GrantedBy, ownerRole)
|
||||
if ownerErr != nil {
|
||||
return ownerErr
|
||||
@@ -243,12 +263,12 @@ func (store *Store) ReplaceOrganizationUserRole(ctx context.Context, expected []
|
||||
return access.ErrOwnerAuthority
|
||||
}
|
||||
}
|
||||
if replacement.Role != ownerRole && slices.Contains(currentRoles, ownerRole) {
|
||||
if !slices.Contains(roles, ownerRole) && slices.Contains(currentRoles, ownerRole) {
|
||||
var otherOwners int
|
||||
if err = tx.QueryRowContext(ctx, `SELECT COUNT(DISTINCT b.subject_id)
|
||||
FROM gwf_access_bindings b
|
||||
JOIN gwf_organization_memberships m ON m.organization_id=b.organization_id AND m.user_id=b.subject_id AND m.status='active'
|
||||
JOIN gwf_users u ON u.id=m.user_id AND u.status='active'
|
||||
JOIN gwf_users u ON u.id=m.user_id AND u.status='active' AND u.registration_pending=0
|
||||
WHERE b.organization_id=? AND b.subject_kind='user' AND b.subject_id<>? AND b.role_name=?
|
||||
AND b.project_id IS NULL AND b.environment_id IS NULL AND b.service_id IS NULL
|
||||
AND b.revoked_at IS NULL`, replacement.Scope.OrganizationID, replacement.SubjectID, ownerRole).Scan(&otherOwners); err != nil {
|
||||
@@ -265,13 +285,15 @@ func (store *Store) ReplaceOrganizationUserRole(ctx context.Context, expected []
|
||||
AND revoked_at IS NULL`, replacement.GrantedBy, replacement.GrantedAt.Unix(), replacement.Scope.OrganizationID, replacement.SubjectID); err != nil {
|
||||
return err
|
||||
}
|
||||
result, err = tx.ExecContext(ctx, `INSERT INTO gwf_access_bindings(id,organization_id,subject_kind,subject_id,role_name,project_id,environment_id,service_id,granted_by_user_id,granted_at)
|
||||
SELECT ?,?,'user',?,?,NULL,NULL,NULL,?,? FROM gwf_access_roles WHERE name=?`, replacement.ID, replacement.Scope.OrganizationID, replacement.SubjectID, replacement.Role, replacement.GrantedBy, replacement.GrantedAt.Unix(), replacement.Role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if changed, _ := result.RowsAffected(); changed != 1 {
|
||||
return errors.New("authsqlite: replacement role has not been seeded")
|
||||
for _, value := range replacements {
|
||||
result, err = tx.ExecContext(ctx, `INSERT INTO gwf_access_bindings(id,organization_id,subject_kind,subject_id,role_name,project_id,environment_id,service_id,granted_by_user_id,granted_at)
|
||||
SELECT ?,?,'user',?,?,NULL,NULL,NULL,?,? FROM gwf_access_roles WHERE name=?`, value.ID, value.Scope.OrganizationID, value.SubjectID, value.Role, value.GrantedBy, value.GrantedAt.Unix(), value.Role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if changed, _ := result.RowsAffected(); changed != 1 {
|
||||
return errors.New("authsqlite: replacement role has not been seeded")
|
||||
}
|
||||
}
|
||||
if err = appendAccessAudit(ctx, tx, audit); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user