mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
fix(auth): keep the last admin when OIDC claims would demote it (#1274)
On OIDC-only instances the bootstrap admin (first SSO user) rarely carries the configured admin claim, so a forced re-login — e.g. after a JWT-secret rotation — re-derived its role purely from claims and demoted it to user, locking the instance out with no recovery. The OIDC login role sync now skips a downgrade that would strip the last remaining admin, and the admin user-update endpoint guards the same case.
This commit is contained in:
@@ -141,6 +141,16 @@ export function updateUser(id: string, data: { username?: string; email?: string
|
||||
}
|
||||
const passwordHash = password ? bcrypt.hashSync(password, BCRYPT_COST) : null;
|
||||
|
||||
// Don't let the admin UI demote the last remaining admin — that would leave the
|
||||
// instance with no one able to manage it (and on OIDC-only setups, no recovery). #1274
|
||||
if (role && role !== 'admin') {
|
||||
const current = db.prepare('SELECT role FROM users WHERE id = ?').get(id) as { role?: string } | undefined;
|
||||
if (current?.role === 'admin') {
|
||||
const adminCount = (db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get() as { count: number }).count;
|
||||
if (adminCount <= 1) return { error: 'Cannot remove the last admin', status: 400 };
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE users SET
|
||||
username = COALESCE(?, username),
|
||||
|
||||
@@ -385,8 +385,20 @@ export function findOrCreateUser(
|
||||
if (process.env.OIDC_ADMIN_VALUE) {
|
||||
const newRole = resolveOidcRole(userInfo, false);
|
||||
if (user.role !== newRole) {
|
||||
db.prepare('UPDATE users SET role = ? WHERE id = ?').run(newRole, user.id);
|
||||
user = { ...user, role: newRole } as User;
|
||||
// Never let the claim-based downgrade strip the last admin. The bootstrap
|
||||
// admin (first SSO user) usually doesn't carry the admin claim, so a forced
|
||||
// re-login — e.g. after a JWT-secret rotation — would otherwise demote it and
|
||||
// lock an OIDC-only instance out for good. #1274
|
||||
const demotingLastAdmin =
|
||||
user.role === 'admin' &&
|
||||
newRole !== 'admin' &&
|
||||
(db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get() as { count: number }).count <= 1;
|
||||
if (demotingLastAdmin) {
|
||||
console.warn(`[OIDC] Kept admin role for user ${user.id}: their OIDC claims map to '${newRole}', but they are the only admin — demoting would lock the instance out.`);
|
||||
} else {
|
||||
db.prepare('UPDATE users SET role = ? WHERE id = ?').run(newRole, user.id);
|
||||
user = { ...user, role: newRole } as User;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { user };
|
||||
|
||||
Reference in New Issue
Block a user