mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-23 07:11:46 +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;
|
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(`
|
db.prepare(`
|
||||||
UPDATE users SET
|
UPDATE users SET
|
||||||
username = COALESCE(?, username),
|
username = COALESCE(?, username),
|
||||||
|
|||||||
@@ -385,8 +385,20 @@ export function findOrCreateUser(
|
|||||||
if (process.env.OIDC_ADMIN_VALUE) {
|
if (process.env.OIDC_ADMIN_VALUE) {
|
||||||
const newRole = resolveOidcRole(userInfo, false);
|
const newRole = resolveOidcRole(userInfo, false);
|
||||||
if (user.role !== newRole) {
|
if (user.role !== newRole) {
|
||||||
db.prepare('UPDATE users SET role = ? WHERE id = ?').run(newRole, user.id);
|
// Never let the claim-based downgrade strip the last admin. The bootstrap
|
||||||
user = { ...user, role: newRole } as User;
|
// 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 };
|
return { user };
|
||||||
|
|||||||
Reference in New Issue
Block a user