Apply organization scoping to module-level URL helpers
What does this MR do and why?
GitLab is rolling out organization-scoped URLs: every route also exists under /o/:organization_path. Routing::OrganizationsHelper::MappedHelpers makes URL helpers organization-aware by prepending an override module onto Rails' url_helpers, so that when a request has organization context, helpers like project_path return the scoped /o/<org>/... variant.
That prepend only reaches instance dispatch (controllers, views, presenters). Module-level calls such as Gitlab::Routing.url_helpers.foo_path and Rails.application.routes.url_helpers.foo_path (around 455 call sites) dispatch through the module's singleton class and bypassed the override, returning unscoped paths. A related gap: API requests are never nested under /o/, so REST/GraphQL responses stayed unscoped even after fixing the above, unlike HAML-rendered pages. This MR closes both gaps.
- Prepend the override module onto
url_helpers.singleton_class(and mirror theunscoped_*aliases there) so module-level helper calls get scoped too. - Fall back to resolving the organization from the
X-GitLab-Organization-IDrequest header when the URL itself doesn't name one, since the frontend already sends this header on API calls. - Prepend the same override onto Rails' internal named-routes proxy modules so module-level
url_for/polymorphic_url(e.g.WorkItemPresenter#web_path) are covered too.
All new behavior is behind the extended_organization_url_scoping derisk feature flag (disabled by default, rollout: #629134 (closed)). Instance dispatch — the pre-existing behavior — is not gated.
Implementation details
- Commit 1 — module-level dispatch:
MappedHelpers.installnow also prepends the override tourl_helpers.singleton_class, and copies theunscoped_*aliases (unscoped_root_path, etc.) onto the singleton so module-level callers keep the global-path escape hatch. - Commit 2 — header fallback:
MappedHelpers.scoped_path_forfalls back to the organization resolved fromX-GitLab-Organization-IDwhen the URL doesn't name one.- Guarded by
Organization#scoped_paths?(false for the default organization), because the frontend sends the header on every request regardless of organization. An explicitly named/o/URL is still echoed back even for the default organization (unchanged behavior). - Grape's
set_current_organizationnow stores the resolver inCurrent.organization_resolver— previously it only lived in a local variable, so REST requests had no resolver available at all. Gitlab::Current::Organization#from_headersis memoized, since URL helpers can invoke it many times per request.
- Commit 3 — polymorphic helpers: the override is also prepended to
named_routes.url_helpers_moduleandnamed_routes.path_helpers_module, covering Rails' internal proxy object used by module-levelurl_for/polymorphic_url. - Feature flag: the prepends happen once at boot and cannot be gated themselves, so the new dispatch paths (singleton, Rails proxy) get a gated variant of the override module that checks
extended_organization_url_scopingper call — only after an organization context was found, so boot-time helper calls never trigger aFeaturelookup. The header fallback is gated by the same flag.Feature.current_requestis the actor, so one response never mixes scoped and unscoped URLs during percentage rollout. - Resulting precedence for choosing the organization path (order unchanged, header added last):
Current.data_context, when it resolves to an isolated organization (cannot be overridden)- explicit
organization_path:keyword argument (nilforces the global path) - the request's
/o/:organization_pathURL segment - otherwise, the
X-GitLab-Organization-IDheader, only for organizations with scoped paths
References
- Resolves #605739 (closed)
- Related to #608081
- Related epic: &23234
Screenshots or screen recordings
Not applicable, backend URL generation only (no UI change).
How to set up and validate locally
We need an organization and a group in that organization.
This transfers group 'twitter' to a new organization:
Feature.enable(:ui_for_organizations)
Feature.enable(:extended_organization_url_scoping)
my_org = Organizations::Organization.find_or_create_by!(path: 'my-org') { |org| org.name = 'My Org' }
group = Group.find_by_path('twitter')
user = User.find_by_username('root')
Organizations::Transfer::GroupsService.new(group: group, new_organization: my_org, current_user: user).executeTest some url's using master branch nad then switch to this branch:
| URL | Description | master |
608081-patch-singleton-url-helpers |
|---|---|---|---|
/o/my-org/groups/twitter |
Link to Typehead project | ||
/o/my-org/twitter/Typeahead.Js |
Links in the file browser | ||
/o/my-org/twitter/Typeahead.Js/-/work_items |
Links to the work items | ||
/o/my-org/twitter/Typeahead.Js/-/commits/master |
All the links to commits are unscoped |
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.