fix: prevent first-user admin race in LDAP and OAuth registration (#23626)
Both LDAP and OAuth registration checked user count before insert to determine whether to assign admin role. With multiple workers, concurrent first-user registrations could each see zero users and both create admin accounts. Applies the insert-first-check-after pattern already used by signup_handler: insert with DEFAULT_USER_ROLE, then atomically check get_num_users()==1 and promote only the sole user to admin.
This commit is contained in:
@@ -479,19 +479,25 @@ async def ldap_auth(
|
|||||||
user = Users.get_user_by_email(email, db=db)
|
user = Users.get_user_by_email(email, db=db)
|
||||||
if not user:
|
if not user:
|
||||||
try:
|
try:
|
||||||
role = 'admin' if not Users.has_users(db=db) else request.app.state.config.DEFAULT_USER_ROLE
|
# Insert with default role first to avoid TOCTOU race on
|
||||||
|
# first-user registration. Matches signup_handler pattern.
|
||||||
user = Auths.insert_new_auth(
|
user = Auths.insert_new_auth(
|
||||||
email=email,
|
email=email,
|
||||||
password=str(uuid.uuid4()),
|
password=str(uuid.uuid4()),
|
||||||
name=cn,
|
name=cn,
|
||||||
role=role,
|
role=request.app.state.config.DEFAULT_USER_ROLE,
|
||||||
db=db,
|
db=db,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||||
|
|
||||||
|
# Atomically check if this is the only user *after* the
|
||||||
|
# insert. Only the single user present should become admin.
|
||||||
|
if Users.get_num_users(db=db) == 1:
|
||||||
|
Users.update_user_role_by_id(user.id, 'admin', db=db)
|
||||||
|
user = Users.get_user_by_id(user.id, db=db)
|
||||||
|
|
||||||
apply_default_group_assignment(
|
apply_default_group_assignment(
|
||||||
request.app.state.config.DEFAULT_GROUP_ID,
|
request.app.state.config.DEFAULT_GROUP_ID,
|
||||||
user.id,
|
user.id,
|
||||||
|
|||||||
@@ -1109,9 +1109,12 @@ class OAuthManager:
|
|||||||
log.debug('Assigning the only user the admin role')
|
log.debug('Assigning the only user the admin role')
|
||||||
return 'admin'
|
return 'admin'
|
||||||
if not user and user_count == 0:
|
if not user and user_count == 0:
|
||||||
# If there are no users, assign the role "admin", as the first user will be an admin
|
# First-user bootstrap: skip role management gating so the
|
||||||
log.debug('Assigning the first user the admin role')
|
# instance can be initialized. We intentionally return the
|
||||||
return 'admin'
|
# default role here (not 'admin') — admin promotion happens
|
||||||
|
# race-safely *after* insert via get_num_users() == 1.
|
||||||
|
log.debug('First user bootstrap: using default role (admin promotion deferred to post-insert)')
|
||||||
|
return auth_manager_config.DEFAULT_USER_ROLE
|
||||||
|
|
||||||
if auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT:
|
if auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT:
|
||||||
log.debug('Running OAUTH Role management')
|
log.debug('Running OAUTH Role management')
|
||||||
@@ -1577,6 +1580,16 @@ class OAuthManager:
|
|||||||
db=db,
|
db=db,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||||
|
|
||||||
|
# Atomically check if this is the only user *after* the
|
||||||
|
# insert to avoid TOCTOU race on first-user registration.
|
||||||
|
# Matches signup_handler pattern.
|
||||||
|
if Users.get_num_users(db=db) == 1:
|
||||||
|
Users.update_user_role_by_id(user.id, 'admin', db=db)
|
||||||
|
user = Users.get_user_by_id(user.id, db=db)
|
||||||
|
|
||||||
if auth_manager_config.WEBHOOK_URL:
|
if auth_manager_config.WEBHOOK_URL:
|
||||||
await post_webhook(
|
await post_webhook(
|
||||||
WEBUI_NAME,
|
WEBUI_NAME,
|
||||||
|
|||||||
Reference in New Issue
Block a user