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:
- Web controllers — a new
EnforcesReadOnlyOrganizationconcern blocksPOST/PATCH/PUT/DELETErequests. For JSON requests the HTTP status depends on the read-only reason: time-bounded reasons (migration,incident) return503+Retry-After: 60; indefinite reasons (isolation,billing,legal) return403with noRetry-After. HTML requests are redirected back with a flash alert in both cases. - REST (Grape) API —
lib/api/helpers.rbblocks writes inset_current_organization,find_project!, andfind_group!. Time-bounded reasons return503+Retry-After: 60; indefinite reasons return403. - GraphQL mutations —
GraphqlController#disallow_mutations_for_organization_read_onlyraisesGitlab::Graphql::Errors::OrganizationReadOnlyError(mapped to HTTP503) for mutating operations; read queries are unaffected. - Git push —
Gitlab::GitAccess#check_organization_read_only!blocksgit-receive-packwith aForbiddenErrorregardless of reason (git has noRetry-After);git pullis 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.rbspec/controllers/graphql_controller_organization_read_only_spec.rbspec/lib/gitlab/git_access_organization_read_only_spec.rbspec/requests/api/organization_read_only_mode_spec.rbspec/models/concerns/organizations/stateful_spec.rb(reason classifier)
All passing locally. RuboCop and LSP clean on all changed files.
Related
- Design reference: ADR 010 (Organization Read-Only Mode)
- Feature issue: #603366 (closed)
- Rollout issue: #603377