Give local users their own manager: custom passwords and role reassignment

Move user management out of Settings into its own /users page (nav-gated
to owners), let an owner type a specific password on reset instead of
always generating a random one, and add role reassignment via a new
PUT /auth/users/{id}/role endpoint. Role changes revoke the target's
existing sessions, same as a password reset, so a demoted user can't
keep acting under a stale, higher-privileged session.
This commit is contained in:
2026-08-21 14:23:52 -07:00
parent 4b5dae5879
commit 864e68253a
10 changed files with 887 additions and 225 deletions
+29
View File
@@ -229,6 +229,35 @@ func (s *Store) SetPasswordHash(ctx context.Context, userID, hash string) error
return tx.Commit(ctx)
}
// SetRole also revokes every existing session for userID, in the same
// transaction -- Session.Role is a snapshot taken at login (see
// SetPasswordHash's doc comment above for why), so without this a
// demoted user would keep acting under their old, higher-privileged
// role for the rest of an already-issued session's lifetime.
func (s *Store) SetRole(ctx context.Context, userID string, role authz.Role) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
tag, err := tx.Exec(ctx, `
UPDATE tenant_memberships SET role = $1
WHERE user_id = $2 AND tenant_id = $3
AND EXISTS (SELECT 1 FROM users WHERE id = $2 AND username IS NOT NULL)`,
string(role), userID, defaultTenantID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
if _, err := tx.Exec(ctx, `DELETE FROM local_sessions WHERE user_id = $1`, userID); err != nil {
return err
}
return tx.Commit(ctx)
}
// CountLocalUsers backs -seed-admin's idempotency check (see
// cmd/api/main.go's runSeedAdmin): a deployment that already has at
// least one local user never gets a second auto-created admin account.