Hive
fix(server): attach existing Tuist users on SCIM POST instead of returning 409
GitHub issue · Closed
What changed
Tuist.SCIM.provision_user/2 no longer returns {:error, :email_taken} (HTTP 409) when an Okta SCIM POST /Users targets an email that matches an existing Tuist user not yet a member of the calling org. Instead the user is attached to the org with the requested role and receives an email notification so they can audit or leave if the attachment was unexpected.
server/lib/tuist/scim.ex—provisionable_user/1now returns{:ok, user, :existing | :created}so the caller can distinguish “user we just created” from “existing user we just attached.”provision_user/2was split intoapply_provision/4+attach_active_user/4to keep cyclomatic complexity within credo’s bounds. The notification is dispatched after the transaction commits.server/lib/tuist/accounts/user_notifier.ex— newdeliver_scim_organization_attachment/2.server/test/tuist/scim_test.exsandserver/test/tuist_web/controllers/scim/users_controller_test.exs— the tests that codified the rejection ("rejects existing users outside the organization"/"returns conflict when the email belongs to a user outside the organization") were updated to assert the new attach behavior at both the context and controller layers.server/priv/gettext/dashboard_account.pot— new gettext strings for the email body.
Why
Customers who had members in Tuist before enabling SCIM hit 409 on every Okta POST /Users for a pre-existing email, blocking IdP rollouts. Recovery currently requires either pre-seeding Okta with internal Tuist user IDs (not practical) or manually attaching members through the dashboard. This was observed in production via repeated SCIM 409 bursts on a customer org immediately after enabling SCIM.
Root cause
provisionable_user/2 had a defensive branch: if get_user_by_email/1 matched a user that didn’t already belongs_to_organization?, it returned {:error, :email_taken}. The defense was meant to prevent one tenant’s Okta from claiming another tenant’s users. In practice it broke the only path that lets a customer roll SCIM out after their team already exists in Tuist.
Why attach rather than the alternatives
Two alternatives were considered and rejected:
- Attach only if the user is in no other orgs — defensible, but breaks the contractor-in-two-orgs case which is real for enterprise customers.
- Attach only if the user already authenticated via this org’s SSO — defeats the point of SCIM, which exists to provision before first login. Would not have fixed customers’ current state.
The SCIM token is org-scoped (account:scim:write on an org-owned AccountToken), so the IdP is already authoritative for the calling org. The notification email gives the attached user a clear signal and an account-settings link to leave, matching how comparable SCIM service providers handle the same case.
Impact
- Customers enabling SCIM after Tuist adoption see their existing members attached on the first Okta sync instead of repeated 409s.
- Existing Tuist users whose email is POSTed by a SCIM-enabled org receive an email so they can audit the attachment.
- No change for the “user already in org” and “brand-new user” paths — both remain idempotent / creating respectively.
- The
:email_takenerror path is still reachable fromreplace_user/patch_user(email-change uniqueness conflicts) and from the create-user race inprovisionable_user/1, so the controller’s 409 handler stays.
How to test locally
mise run installif dependencies are not installed.mix test test/tuist/scim_test.exs test/tuist_web/controllers/scim/users_controller_test.exs— 38 tests, 0 failures.- Manual flow: create an org with a SCIM token via
Tuist.SCIM.create_token/2, create a standalone Tuist user, thenPOST /scim/v2/Userswith that user’s email. Pre-fix: HTTP 409. Post-fix: HTTP 201, user attached, notification email sent.
No GitHub comments yet.