mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 11:16:55 +00:00
fix(rbac): enforce admin seat cap on promotion and harden last-admin and audit paths (#1266)
Promoting a user to admin now respects the per-tier admin seat limit the same way user creation does, closing a path that let an operator exceed the cap by creating an account and then editing its role to admin. The last-admin guard for demote and delete now runs the admin-count re-check and the write in a single transaction, so two concurrent admin changes can no longer race the admin count to zero and lock everyone out. Admin two-factor resets are now recorded once with their own audit summary instead of being mislabeled as a user creation by the audit middleware.
This commit is contained in:
@@ -2622,6 +2622,38 @@ export class DatabaseService {
|
||||
this.db.prepare('DELETE FROM users WHERE id = ?').run(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically apply `updates` unless doing so would demote the last remaining
|
||||
* admin. Returns false (nothing written) when the change would leave zero
|
||||
* admins, true otherwise. The current-role read, the admin count, and the
|
||||
* write run in a single transaction so a concurrent demote or delete of the
|
||||
* other admin cannot race the count to zero.
|
||||
*/
|
||||
public updateUserIfNotLastAdmin(id: number, updates: Partial<{ username: string; password_hash: string; role: string; email: string }>): boolean {
|
||||
return this.transaction(() => {
|
||||
if (updates.role !== undefined && updates.role !== 'admin') {
|
||||
const current = this.db.prepare('SELECT role FROM users WHERE id = ?').get(id) as { role: string } | undefined;
|
||||
if (current?.role === 'admin' && this.getAdminCount() <= 1) return false;
|
||||
}
|
||||
this.updateUser(id, updates);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically delete the user unless it is the last remaining admin. Returns
|
||||
* false (nothing deleted) in that case, true otherwise. Same single-
|
||||
* transaction guard as {@link updateUserIfNotLastAdmin}.
|
||||
*/
|
||||
public deleteUserIfNotLastAdmin(id: number): boolean {
|
||||
return this.transaction(() => {
|
||||
const current = this.db.prepare('SELECT role FROM users WHERE id = ?').get(id) as { role: string } | undefined;
|
||||
if (current?.role === 'admin' && this.getAdminCount() <= 1) return false;
|
||||
this.deleteUser(id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public getUserCount(): number {
|
||||
return (this.db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number })?.count || 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user