-
Bug
-
Resolution: Unresolved
-
Major
-
None
-
quay-v3.16.2
Summary:
When an LDAP user with LDAP_RESTRICTED_USER_FILTER attempts to create a new organization, the backend correctly returns HTTP 403 Forbidden, but the new UI incorrectly displays a success notification saying "Successfully created organization <name>" even though the organization was not created.

This causes confusion for users who believe they successfully created an organization when the operation actually failed due to access restrictions.
Environment:
- FEATURE_RESTRICTED_USERS: true
- LDAP_RESTRICTED_USER_FILTER: (postalCode=3000)
- Authentication: LDAP
Steps to Reproduce:
- Configure Quay with LDAP authentication and LDAP_RESTRICTED_USER_FILTER
- Login as an LDAP user matching the restricted user filter (e.g., postalCode=3000)
- Navigate to Organizations page in new UI
- Click "Create New Organization"
- Enter organization name and email
- Click "Create" button
Expected Behavior:
- Error notification: "Unable to create organization" with appropriate error message
- Modal remains open to allow user to correct issue
- Organization is NOT created
Actual Behavior:
- Success notification: "Successfully created organization <name>"
- Modal closes
- Organization is NOT created (backend correctly blocks it)
- User believes organization was created successfully
Root Cause Analysis:
Backend (Working Correctly)
File: endpoints/api/organization.py:155-174
The backend correctly validates restricted users and returns HTTP 403:
@require_user_admin(disallow_for_restricted_users=features.RESTRICTED_USERS) @nickname("createOrganization") @validate_json_request("NewOrg") def post(self): # Lines 172-174: Restricted user check if not usermanager.is_superuser(user.username): if features.RESTRICTED_USERS and usermanager.is_restricted_user(user.username): raise Unauthorized() # Returns HTTP 403
Frontend Bug
File: web/src/hooks/UseOrganizations.ts:256-257
The bug is in the createOrganization function which uses .mutate() instead of .mutateAsync():
Unable to find source-code formatter for language: typescript. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yaml// BUG: Uses .mutate() which returns void immediately createOrganization: async (name: string, email: string) => createOrganizationMutator.mutate({name, email}),
File: web/src/routes/OrganizationsList/CreateOrganizationModal.tsx:80-99
Unable to find source-code formatter for language: typescript. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yamlconst createOrganizationHandler = async () => { try { await createOrganization(organizationName, organizationEmail); // Line 82 // BUG: This ALWAYS executes because mutate() returns void addAlert({ variant: AlertVariant.Success, title: `Successfully created organization ${organizationName}`, }); props.handleModalToggle(); } catch (err) { // This catch block NEVER executes const errorMessage = addDisplayError("Unable to create organization", err); setErr(errorMessage); addAlert({ variant: AlertVariant.Failure, title: "Unable to create organization", message: errorMessage, }); } };
Why the Bug Occurs
React Query .mutate() vs .mutateAsync():
- .mutate() - Returns void immediately, does NOT throw errors (requires onError callback)
- .mutateAsync() - Returns Promise, throws errors on failure (works with try-catch)
Timeline of events:
- User clicks "Create" button
- createOrganization() calls .mutate() which returns void immediately
- await completes instantly (nothing to await)
- Success alert shown BEFORE API call completes
- Modal closes
- (Background) API request sent to /api/v1/organization/
- (Background) Backend returns HTTP 403 Forbidden
- (Background) Error silently ignored (no onError callback defined)
Recommended Fix:
File: web/src/hooks/UseOrganizations.ts:257
Change one line to use .mutateAsync() instead of .mutate():
Unable to find source-code formatter for language: diff. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yamlcreateOrganization: async (name: string, email: string) =>
- createOrganizationMutator.mutate({name, email}),
+ createOrganizationMutator.mutateAsync({name, email}),
This makes it consistent with the delete operations (line 259) which already use .mutateAsync():
Unable to find source-code formatter for language: typescript. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yamldeleteOrganizations: async (names: string[]) =>
deleteOrganizationMutator.mutateAsync(names),
Impact:
- Severity: Medium
- User confusion - users believe organization was created when it was not
- Affects all LDAP users with LDAP_RESTRICTED_USER_FILTER configured
- Regression in new UI compared to old UI (old UI showed correct error)
- Potential support tickets from confused users
Affected Files:
- web/src/hooks/UseOrganizations.ts:257 - Bug location (one line fix)
- web/src/routes/OrganizationsList/CreateOrganizationModal.tsx:80-99 - Error handling that cannot work due to bug