Add namespace-scoped read-only middleware (Step 2)

What does this MR do and why?

This MR implements Step 2 of the POC for read-only mode for top-level groups, as tracked in #590009 (closed) (task #591689 (closed)).

It adds Gitlab::Middleware::NamespaceReadOnly, a Rack middleware that blocks write HTTP requests (POST/PATCH/PUT/DELETE) for namespaces in maintenance state. This is the core enforcement mechanism for TLG read-only mode on web and REST API routes.

Depends on: !226399 (closed) (Step 1 - state machine transitions)

Changes

Middleware (lib/gitlab/middleware/namespace_read_only.rb)

  • Entry point that delegates to Controller, mirroring the existing Gitlab::Middleware::ReadOnly pattern

Controller (lib/gitlab/middleware/namespace_read_only/controller.rb)

  • Blocks POST/PATCH/PUT/DELETE requests targeting namespaces in :maintenance state
  • Namespace resolution: Extracts top-level namespace from request path using route_hash (for accurate Rails route matching) with fallback to path-based extraction (stripping known prefixes like /api/v4/, /groups/, etc.)
  • Allowlist: Reuses the same patterns as the existing ReadOnly middleware (internal API routes, sessions, sidekiq admin, GraphQL queries)
  • Response handling:
    • JSON requests: returns 503 with Retry-After: 900 header and maintenance message
    • HTML requests: sets flash alert and redirects to last visited URL (301)
  • Caching: Uses RequestStore to cache namespace lookups within the same request cycle

Registration (config/application.rb)

  • Added require_dependency for the new middleware
  • Inserted after Gitlab::Middleware::ReadOnly in the Rack stack

Design decisions

  • Standalone middleware (Option A from the issue) for clean separation from instance-level read-only. Does not couple with or modify the existing ReadOnly middleware.
  • GraphQL queries are allowlisted intentionally. All GraphQL requests are POST to /api/graphql, so the middleware cannot determine the target namespace from the URL. GraphQL mutation enforcement is Step 4 (#591691 (closed)).

How to set up and validate locally

  1. Apply Step 1 changes first (!226399 (closed))
  2. Open Rails console
  3. Put a namespace in maintenance: ns = Namespace.find_by_path('my-group'); ns.start_maintenance!(transition_user: User.first)
  4. Try a write request to that namespace (e.g., create an issue) - should get blocked with maintenance message
  5. Try a read request (e.g., view the group page) - should work normally
  6. Exit maintenance: ns.complete_maintenance!
  7. Write requests should work again

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Merge request reports

Loading