POC: Organization-scoped read-only mode
What does this MR do and why?
Implements the POC for Organization-scoped read-only mode, as tracked in #594327 (closed). Covers Steps 1, 2, and portions of Steps 3-5 from the implementation plan.
This replaces the namespace-scoped approach from !226399 (closed).
Step 1: Organization maintenance state machine
Adds a state column (smallint, default 0) to the organizations table and an Organizations::Maintenable concern:
start_maintenance!/complete_maintenance!— state transitionsmaintenance?/active?— query methods
Step 2: Central controller enforcement via Current.organization
Adds EnforcesReadOnlyOrganization concern included in ApplicationController with a before_action that runs after set_current_organization. Uses Current.organization directly — no per-controller overrides needed.
| Request type | Org in maintenance | Result |
|---|---|---|
| GET/HEAD | Yes | Allowed |
| POST/PATCH/PUT/DELETE | No | Allowed |
| POST/PATCH/PUT/DELETE (JSON) | Yes | 503 + Retry-After header |
| POST/PATCH/PUT/DELETE (HTML) | Yes | Redirect with flash alert |
Step 3 (partial): API enforcement
Adds check_organization_read_only! in API::Helpers#set_current_organization. Covers all API endpoints that call set_current_organization (Projects, Groups, Snippets, Topics, etc.). Endpoints that don't call set_current_organization are a known gap for follow-up.
Step 5: GraphQL mutation enforcement
Adds disallow_mutations_for_organization_read_only in GraphqlController. Blocks mutations when Current.organization is in maintenance. Queries (reads) pass through. Skips the HTML-level enforce_read_only_organization since GraphQL has its own check.
Bonus: Git push enforcement
Adds check_organization_read_only! in Gitlab::GitAccess#check_db_accessibility!. Blocks git push when the project's organization is in maintenance. git pull is unaffected. Uses respond_to?(:organization) guard since Wiki containers don't implement organization.
Changes
Step 1 — State machine:
db/migrate/20260324230000_add_state_to_organizations.rbapp/models/concerns/organizations/maintenable.rb(new)app/models/organizations/organization.rbspec/models/concerns/organizations/maintenable_spec.rb(new)
Step 2 — Controller enforcement:
app/controllers/concerns/enforces_read_only_organization.rb(new)app/controllers/application_controller.rb— include + before_action
Step 3 — API enforcement:
lib/api/helpers.rb—check_organization_read_only!spec/requests/api/organization_read_only_mode_spec.rb(new)
Step 5 — GraphQL enforcement:
app/controllers/graphql_controller.rb— mutation blocking + skip HTML enforcementspec/controllers/graphql_controller_organization_read_only_spec.rb(new)
Git push enforcement:
lib/gitlab/git_access.rb—check_organization_read_only!spec/lib/gitlab/git_access_organization_read_only_spec.rb(new)
Other:
ee/spec/requests/search_controller_spec.rb—:with_current_organizationtraitee/spec/requests/search_default_scope_spec.rb— sameee/spec/features/search/zoekt/search_spec.rb— same
Known gaps (follow-up)
- API endpoints that don't call
set_current_organization(e.g. Issues API) find_group!/find_project!level enforcement for broader API coverage- Admin UI/API toggle for maintenance mode
- Background write paths (Sidekiq workers, cron jobs)
- GET requests with side effects
How to validate locally
org = Organizations::Organization.find(id)
org.start_maintenance!
# POST to a group/project endpoint → blocked with 503
# GET a group/project page → works normally
org.complete_maintenance!
# Writes work againRelated issues
- Task: #594670 (closed)
- Parent: #594327 (closed)
- Epic: gitlab-org&20404
- Supersedes: !226399 (closed)
- Related POC: !215821 (closed) (@dblessing)
MR acceptance checklist
Evaluate against the MR acceptance checklist.