Enforce organization read-only mode across write surfaces

What does this MR do and why?

Implements the enforcement layer for Organization Read-Only Mode. When an organization is in a read-only state, write operations are blocked across four surfaces while reads always pass through:

  1. Web controllers — a new EnforcesReadOnlyOrganization concern blocks POST/PATCH/PUT/DELETE requests. For JSON requests the HTTP status depends on the read-only reason: time-bounded reasons (migration, incident) return 503 + Retry-After: 60; indefinite reasons (isolation, billing, legal) return 403 with no Retry-After. HTML requests are redirected back with a flash alert in both cases.
  2. REST (Grape) APIlib/api/helpers.rb blocks writes in set_current_organization, find_project!, and find_group!. Time-bounded reasons return 503 + Retry-After: 60; indefinite reasons return 403.
  3. GraphQL mutationsGraphqlController#disallow_mutations_for_organization_read_only raises Gitlab::Graphql::Errors::OrganizationReadOnlyError (mapped to HTTP 503) for mutating operations; read queries are unaffected.
  4. Git pushGitlab::GitAccess#check_organization_read_only! blocks git-receive-pack with a ForbiddenError regardless of reason (git has no Retry-After); git pull is unaffected.

The 503-vs-403 split follows ADR 010: time-bounded reasons are retryable, indefinite reasons are not.

All four layers gate on the single Organizations::Organization#read_only? predicate and are wrapped in the default-off organization_read_only_enforcement feature flag, so the behaviour ships dark and is a complete no-op when the flag is disabled.

The model layer (read-only states and predicate) was delivered separately in !240492 (merged) and is not modified here.

Out of scope (tracked separately)

Container Registry, Git LFS, authentication exemption, GET-with-side-effects audit, read-only UI banner, and CI/CD pipeline blocking.

Feature flag

  • Name: organization_read_only_enforcement (gitlab_com_derisk, default-off)
  • Env- and organization-scoped, enabling cohort-by-cohort rollout.

Testing

Specs added for every surface, including flag on/off and the 503-vs-403 reason split:

  • spec/controllers/concerns/enforces_read_only_organization_spec.rb
  • spec/controllers/graphql_controller_organization_read_only_spec.rb
  • spec/lib/gitlab/git_access_organization_read_only_spec.rb
  • spec/requests/api/organization_read_only_mode_spec.rb
  • spec/models/concerns/organizations/stateful_spec.rb (reason classifier)

All passing locally. RuboCop and LSP clean on all changed files.

Edited by Chen Zhang

Merge request reports

Loading