From 558ea2a152af6d8231276cb110736b3a139c9c38 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:44:36 +0200 Subject: [PATCH] fix: don't block first-admin signup on stale ENABLE_SIGNUP (#24821) Symptom On a fresh install (zero users) the frontend shows the mandatory "Create Admin Account" onboarding screen, but POST /api/v1/auths/signup returns 403 ACCESS_PROHIBITED ("You do not have permission to access this resource."). Wiping the database does not help when the config layer is backed by Redis (the value survives in the Redis/valkey volume), or when only the user table is cleared (the config row survives in Postgres). The instance is then unrecoverable through the UI. Root cause signup_handler() auto-sets request.app.state.config.ENABLE_SIGNUP = False immediately after the first admin is created. That value is persisted by the config layer (the Postgres config table, and Redis when REDIS_URL is set). On a later zero-user database the persisted False is read back, so ENABLE_SIGNUP resolves False even though no users exist. The old gate was: if WEBUI_AUTH: if not ENABLE_SIGNUP or not ENABLE_LOGIN_FORM: if has_users or not ENABLE_INITIAL_ADMIN_SIGNUP: 403 ENABLE_INITIAL_ADMIN_SIGNUP defaults to False, so with zero users the inner test (has_users or not ENABLE_INITIAL_ADMIN_SIGNUP) is True, and a stale ENABLE_SIGNUP=False trips the outer test, producing a 403 on the only UI path that can create the first admin. The frontend decides to show onboarding purely from user_count == 0, so frontend and backend disagree and the instance bricks. Change Split the gate by has_users. Subsequent signups (has_users True) are unchanged: still gated by ENABLE_SIGNUP and ENABLE_LOGIN_FORM. The first user (has_users False, the bootstrap admin the onboarding screen invites) is gated only by the admin-chosen ENABLE_LOGIN_FORM (the documented SSO-only hard-disable) unless ENABLE_INITIAL_ADMIN_SIGNUP is set. It is no longer gated by ENABLE_SIGNUP, which in the zero-user state is never an admin decision but the post-first-admin auto-disable leaking across a database reset. Why this is safe (full case analysis) For WEBUI_AUTH the gate has 16 input combinations over (has_users, ENABLE_SIGNUP, ENABLE_LOGIN_FORM, ENABLE_INITIAL_ADMIN_SIGNUP). Old and new are identical in 15 of them: * All 8 has_users=True cases: both reduce to "403 iff not ENABLE_SIGNUP or not ENABLE_LOGIN_FORM". Unchanged. * 7 of the 8 has_users=False cases: identical. The only changed case is has_users=False, ENABLE_SIGNUP=False, ENABLE_LOGIN_FORM=True, ENABLE_INITIAL_ADMIN_SIGNUP=False: old behaviour 403, new behaviour allow. The new condition is a strict subset of the old (new-403 implies old-403), so the change never newly blocks any request that previously succeeded; it only stops blocking that one bootstrap state. That state has no legitimate deployment. With the login form enabled and zero users the onboarding form is already served, and the only operator-configurable way to keep the first signup closed (SSO-only: ENABLE_LOGIN_FORM=False, optionally with ENABLE_INITIAL_ADMIN_SIGNUP) is preserved byte for byte. ENABLE_SIGNUP=False with zero users is not an operator choice, it is the automatic post-first-admin disable, so the old behaviour there was purely a brick with no recovery path. No security control is weakened: ENABLE_LOGIN_FORM and ENABLE_INITIAL_ADMIN_SIGNUP keep their exact meaning, and the WEBUI_AUTH=False path is untouched. This is not Redis-specific: it reproduces with Redis disabled through the Postgres config table alone (clear the user table, keep the config row). Verification Drove the real signup endpoint across a 10-case matrix on freshly migrated databases, including the full end-to-end first-admin creation (returns role=admin, row persisted as admin) and the preserved SSO-only, subsequent-signup and no-auth behaviours. All pass. Co-authored-by: Claude Opus 4.7 (1M context) --- backend/open_webui/routers/auths.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 1dc280c858..434a6349de 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -747,9 +747,12 @@ async def signup( has_users = await Users.has_users(db=db) if WEBUI_AUTH: - if not request.app.state.config.ENABLE_SIGNUP or not request.app.state.config.ENABLE_LOGIN_FORM: - if has_users or not ENABLE_INITIAL_ADMIN_SIGNUP: + if has_users: + if not request.app.state.config.ENABLE_SIGNUP or not request.app.state.config.ENABLE_LOGIN_FORM: raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + # Don't gate the first admin on ENABLE_SIGNUP: it auto-disables and can persist stale across a DB reset. + elif not request.app.state.config.ENABLE_LOGIN_FORM and not ENABLE_INITIAL_ADMIN_SIGNUP: + raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) else: if has_users: raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)