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
- Work item: #602813 (closed)
- Original attempt (closed): !242344 (closed)
- Merged partial fix (
OmniauthCallbacksController+ INSERT block): !242419 (merged) - Original enforcement MR: !241161 (merged)
- ADR 010: https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/organization/decisions/010_organization_read_only_mode/
Screenshots or screen recordings
N/A — backend-only change, no UI impact.
How to set up and validate locally
-
Enable the feature flag in rails console:
Feature.enable(:organization_read_only_enforcement) -
Put an organization into read-only mode:
org = Organizations::Organization.default_organization org.start_read_only!(read_only_reason: 'migration') org.confirm_read_only! -
Sign out, then sign back in via
POST /users/sign_in— it should succeed instead of redirecting with a read-only error flash. -
Verify a non-auth write (e.g. creating an issue) is still blocked with the read-only flash.