From 4c06b392da7b878c8bcc5e903bfcba5ba14e60e7 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:59:55 +0200 Subject: [PATCH] Don't let SCIM's active flag demote an admin (defense-in-depth) (#25948) The SCIM update_user (PUT) and patch_user (PATCH) handlers mapped the SCIM active field unconditionally onto the role column (role = 'user' if active else 'pending'), so a routine IdP sync or a misconfigured IdP that marked a locally-provisioned admin inactive would silently strip that admin's role and could lock an instance out of its own administration. Gate both active->role assignments on user.role != 'admin' so SCIM provisioning can activate/deactivate ordinary users but never demotes an existing admin; admin role changes continue to go through the dedicated admin endpoints. SCIM already cannot promote to admin (active only maps to user/pending), so this is symmetric. Credit to @HOHK0923 for surfacing the admin-demotion footgun. Co-authored-by: HOHK0923 <118590749+HOHK0923@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) --- backend/open_webui/routers/scim.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py index 9292523adc..e36afef173 100644 --- a/backend/open_webui/routers/scim.py +++ b/backend/open_webui/routers/scim.py @@ -672,7 +672,10 @@ async def update_user( if user_data.emails and len(user_data.emails) > 0: update_data['email'] = user_data.emails[0].value - if user_data.active is not None: + # Do not let SCIM's active flag demote an existing admin: a routine IdP sync or misconfiguration + # must not silently strip a locally-provisioned admin's role and lock the instance out. Admin + # role changes go through the dedicated admin endpoints, not SCIM provisioning. + if user_data.active is not None and user.role != 'admin': update_data['role'] = 'user' if user_data.active else 'pending' if user_data.photos and len(user_data.photos) > 0: @@ -719,7 +722,9 @@ async def patch_user( if op == 'replace': if path == 'active': - update_data['role'] = 'user' if value else 'pending' + # Same guard as update_user: never demote an existing admin via SCIM. + if user.role != 'admin': + update_data['role'] = 'user' if value else 'pending' elif path == 'userName': update_data['email'] = value elif path == 'displayName':