Exempt auth controllers from read-only organization enforcement

What does this MR do and why?

Re-lands the authentication-controller read-only-organization exemptions from the closed !242344 (closed) that were not covered by the merged !242419 (merged), as part of the Org Read-Only Mode work tracked in #602813 (closed).

Problem

!241161 (merged) added enforce_read_only_organization as a before_action in ApplicationController. It blocks all POST/PATCH/PUT/DELETE requests when Current.organization is read-only and the organization_read_only_enforcement feature flag is enabled.

Because authentication controllers inherit from ApplicationController, sign-in / SSO / password-reset POSTs are blocked when an organization is read-only — wrongly locking users out of obtaining a session to READ a read-only organization's data.

ADR 010 (Organization Read-Only Mode, section "Authentication exemption") requires authentication endpoints to remain available during read-only.

!242344 (closed) fixed this but was closed as superseded by !242419 (merged). In practice, !242419 (merged) only added the skip to OmniauthCallbacksController (plus the new-user INSERT block in Gitlab::Auth::OAuth::User#save and the Organizations::Organization#read_only_enforced? helper). Every other exemption from !242344 (closed) is still missing from master — this MR re-lands them.

Solution

Add skip_before_action :enforce_read_only_organization to the remaining authentication controllers, mirroring OmniauthCallbacksController (and GraphqlController). Skips are action-scoped (only:) wherever a controller mixes auth-plane and non-auth actions.

Controllers exempted in this MR:

Controller Scope Reason
SessionsController whole controller Sign-in / sign-out — session row, audit event, last-used timestamp
JwtController whole controller Container registry JWT auth (GET-only today; preventive)
PasswordsController :create, :update Password reset is required to regain access to read
ConfirmationsController :create Resending the confirmation email is part of completing authentication
UnlocksController :create Requesting unlock instructions lets a locked-out user regain access to sign in (new GitLab subclass of the Devise default)
Users::TermsController :accept, :decline Enforcement runs before enforce_terms!, so users get wedged: they can't accept terms and can't read anything until they do
Users::BaseIdentityVerificationController (EE) :verify_email_code, :send_phone_verification_code, :verify_phone_verification_code, :verify_credit_card_captcha Identity verification is a sign-in gate
Groups::SsoController (EE) :saml only Render-only SSO sign-in landing POST. #unlink is a real identity write and stays enforced (negative spec included)
SmartcardController (EE) :auth Smartcard authentication entrypoint

Controllers NOT exempted (and why):

Controller Reason
OmniauthCallbacksController Already exempted on master via !242419 (merged)
Oauth::TokensController Inherits from Doorkeeper::ApplicationMetalController via Gitlab::BaseDoorkeeperController, NOT from ApplicationController — the before_action is never registered there, so no skip is needed
Groups::SsoController#unlink (EE) Unlinking an identity is an org-owned write, not part of completing authentication — remains enforced

Every exemption ships with a regression spec. HTML write endpoints assert on the read-only flash (a blocked request produces a 302 redirect with a flash, not a 4xx/5xx status), controller specs use spies on enforce_read_only_organization, and JwtController (GET-only) asserts the callback chain directly. The LDAP and Group SAML callback controllers inherit the skip from OmniauthCallbacksController; their specs guard that inheritance.

Rollout safety

The rollout caveat from !242344 (closed) no longer applies: the new-user INSERT block (MR B) is already merged via !242419 (merged), so exempting these controllers cannot cause new users rows to be created on a read-only source Cell during SSO sign-in.

References

Screenshots or screen recordings

N/A — backend-only change, no UI impact.

How to set up and validate locally

  1. Enable the feature flag in rails console:

    Feature.enable(:organization_read_only_enforcement)
  2. Put an organization into read-only mode:

    org = Organizations::Organization.default_organization
    org.start_read_only!(read_only_reason: 'migration')
    org.confirm_read_only!
  3. Sign out, then sign back in via POST /users/sign_in — it should succeed instead of redirecting with a read-only error flash.

  4. Verify a non-auth write (e.g. creating an issue) is still blocked with the read-only flash.

Edited by Chen Zhang

Merge request reports

Loading
Loading