Block new-user INSERT during organization read-only mode

What does this MR do and why?

Keeps SSO/SAML/JIT authentication available while an organization is in read_only_initialization or read_only state, but blocks creating a brand-new users row on that path, while existing-user sign-ins (timestamp updates) remain permitted.

Problem: ApplicationController's before_action :enforce_read_only_organization blocks all write requests (including the OAuth callback POST) when the organization is read-only. Authentication must stay available during read-only, but simply exempting the whole auth controller would leave the new-user INSERT path unguarded: a new users row created during read-only would live only on the source Cell, would not be part of the cutover snapshot, and would be lost when traffic moves to the destination Cell (ADR 010).

Fix: Two parts, both in this MR:

  1. Add skip_before_action :enforce_read_only_organization to OmniauthCallbacksController so authentication stays available during read-only.
  2. Add a narrower new_user_blocked_by_read_only_organization? guard in Gitlab::Auth::OAuth::User#save that raises NewUserOrganizationReadOnlyError when:
    • the user does not yet exist in the database (new record / INSERT path);
    • the owning Organization is read-only and the organization_read_only_enforcement flag is enabled (via Organizations::Organization#read_only_enforced?).

OmniauthCallbacksController#sign_in_user_flow rescues the new error and surfaces a read-only flash message to the user.

References

Screenshots or screen recordings

N/A — backend-only change.

How to set up and validate locally

  1. Enable the feature flag for an organization in read-only state:
    org = Organizations::Organization.find(<id>)
    org.start_read_only!(read_only_reason: 'migration')
    Feature.enable(:organization_read_only_enforcement, org)
  2. Attempt to sign in via SSO/SAML with a new (non-existing) user account.
  3. Verify the sign-in is blocked with the read-only flash message.
  4. Attempt to sign in with an existing user — verify it succeeds.

Database review

This MR introduces a single new query in the sign-in path: ::Organizations::Organization.find_by_id(organization_id) in Gitlab::Auth::OAuth::User#new_user_blocked_by_read_only_organization?.

It is a primary-key lookup on organizations.id, only executed on the new-user (INSERT) branch of OAuth/SAML sign-in, and short-circuits before the query when organization_id is absent.

Raw SQL:

SELECT "organizations".* FROM "organizations" WHERE "organizations"."id" = $1 LIMIT 1;

Query plan (PK lookup via the implicit unique index organizations_pkey):

Index Scan using organizations_pkey on organizations  (cost=0.28..8.30 rows=1 width=...)
  Index Cond: (id = $1)

The lookup uses the primary-key index, returns at most one row, and adds a single constant-time query to the new-user sign-in path only.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Chen Zhang

Merge request reports

Loading
Loading