Commit f0049bce authored by Alex Pooley's avatar Alex Pooley 🔴
Browse files

Add Request Context blueprint (Org/User/Nil) and ADR 016

parent a6e4164d
Loading
Loading
Loading
Loading
+11 −2
Original line number Diff line number Diff line
@@ -85,6 +85,12 @@ organization is non-isolated, or isolated, before enforcing organization boundar
See [ADR 008: Non-isolated organizations on GitLab.com](decisions/008_non_isolated_organizations_gitlab_com.md)
for full details.

## Request Context

Every request, job, and task resolves to one of three contexts:
Organization context, User context, or Nil context. Isolation is the
only thing that changes which one applies. See [Request Context](contexts.md) for the full model, and [ADR 016: `organization_id` and Organization-scoped query filtering](decisions/016_user_context.md) for how it is encoded today.

## Impact of the Organization on Other Domains

Here is a growing list of pages that describe in more detail how
@@ -95,6 +101,7 @@ Organization affects other parts of the system.
- [Settings](settings.md)
- [Lifecycle](lifecycle.md)
- [Users](users.md)
- [Request Context](contexts.md)
- [Login](login.md)
- [OAuth - GitLab as SP](oauth_client_auth.md)

@@ -275,7 +282,8 @@ target. In some cases we have intentionally restricted the problem scope and int
  - Organizations can be public or private.
  - The Default Organization is public.
  - Requests made to non-Organization specific endpoints such as `/explore` will
    default to the default organization.
    default to the default organization. Whether such endpoints should instead
    resolve to Nil context is an open, per-route decision — see [Request Context](contexts.md).
  - Public Organizations can be seen by everyone. They can contain public and private Groups and Projects.
  - Private Organizations can only be seen by the Users that are part of the Organization. They can only contain private or internal Groups and Projects.
- **Users**
@@ -283,7 +291,7 @@ target. In some cases we have intentionally restricted the problem scope and int
  - The creation of an Organization appoints the creating User as the Organization Administrator.
  - Organization Administrators can update the existing user type of a user from Regular User to Administrator or vice versa.
  - There must be at least one Organization Administrator per Organization.
  - A User can only be part of one Organization. A new account needs to be created for each Organization a User wants to be part of.
  - A User can be part of multiple non-isolated Organizations with the same account. Once a User's identity is owned by an isolated Organization, they belong to that Organization only. See [Request Context](contexts.md).
  - Organization Administrators can delete users within their own Organization.
  - When a user becomes a member of a group or project they are also added as an Organization user. They receive an email informing them that they have been added to the Organization.
  - Removing a user from their last group or project should not remove them from the Organization.
@@ -353,6 +361,7 @@ Based on this analysis we expect to see similar behavior when rolling out Organi
- [013: Warn when creating a Top-Level-Group inside an organization](decisions/013_warn_on_tlg_creation.md)
- [014: Organization roles renamed to Organization user types](decisions/014_organization_roles_renamed_to_organization_user_type.md)
- [015: Non-isolation is a permanent Organization state](decisions/015_non_isolation_is_permanent.md)
- [016: `organization_id` and Organization-scoped query filtering](decisions/016_user_context.md)

## Links

+117 −0
Original line number Diff line number Diff line
---
title: "Request Context"
description: "Names the three request contexts GitLab has always had  Organization, User, and Nil  as a single model, and how isolation determines which one applies."
owning-stage: "~devops::tenant scale"
group: Organizations
toc_hide: true
---

## What is a context?

A context is a data boundary. It sets what data an execution path can
reach. A path can also have an **anchor** — the entity it names, such
as an Organization or a User. An anchor is not itself a context; see
"Organizations anchor a path," below, for the distinction.

This document describes how the GitLab product resolves context today,
and how Organizations extend that model.

## The three contexts

GitLab has three contexts:

1. **Organization context.** The execution path is bound to one isolated
   Organization.
1. **User context.** The execution path is bound to one User. It reaches
   every Organization that User can reach.
1. **Nil context.** The execution path is bound to nothing. It has no
   Organization and no User.

## The context matrix

Two facts decide the context:

1. Is an Isolated Organization present?
2. Is a User present?

| | No User | User present |
|---|---|---|
| **No organization** | Nil context | User context |
| **Non-isolated organization** | Nil context | User context |
| **Isolated organization** | Organization context | Organization context |

The "No organization" row and the "Non-isolated organization" row are
identical. Isolation is the only fact that changes the answer.

## Examples

Routes are the clearest examples. Some also carry an anchor — the
entity the path names:

| Example route | Context | Anchor |
|---|---|---|
| `/dashboard/...` | User context | None |
| `/explore` | Nil context | None |
| `/o/acme/...`, Acme isolated | Organization context | Acme (Organization) |
| `/o/acme/...`, Acme not isolated, user signed in | User context | Acme (Organization) |
| `/o/acme/...`, Acme not isolated, signed out | Nil context | Acme (Organization) |

The same route, `/o/acme/...`, can carry two different contexts, but
the same anchor. Isolation decides which context applies, not the URL.
See "Organizations anchor a path," below, for why.

Every process resolves to one of these three contexts, not just routes.
The same three apply to background jobs, scheduled tasks, GraphQL
queries, and ActionCable connections.

## One service, more than one context

A service can accept more than one context. It does not need a separate
code path per context. For example, a to-do list service takes a context
as input. Given User context, it returns to-do items from every
Organization. Given Organization context, it returns to-do items from
one Organization.

## Organizations anchor a path. Isolation makes it a boundary

A URL path can be about something without that something being the
request's context. An **anchor** names which entity a path is about —
for example, an Organization, or a User. An anchor does not, by itself,
set a data boundary. Only a context does that.

An anchor is a property of a path, not of every execution path. A
background job or a scheduled task has a context, the same as a route
does, but it has no path to anchor it.

Organizations add a new fact: a URL path can now name an Organization.
On its own, this does not reach the "Isolated organization" row. A path
naming a non-isolated Organization still resolves to User context or
Nil context. This is why the "No organization" and "Non-isolated
organization" rows in the context matrix are identical, and why
`/o/acme/...` can carry two different contexts.

Isolation is the fact that reaches the third row. An isolated
Organization keeps all of its data inside its own boundary. No data
crosses that boundary, in or out. Only then does Organization context
exist for that Organization. Only then do its anchor and its context
become the same thing.

The table above makes the distinction concrete. Neither
`/dashboard/...` nor `/explore` names an entity in the URL, so neither
has an anchor. Only `/o/acme/...` does, which is exactly why its
context can diverge from what it names.

## Isolation removes an Organization from User and Nil context

Isolation is one rule about the Organization boundary: no data crosses
it. It is not a separate rule for User context, and another separate
rule for Nil context. Two facts follow from the one rule:

1. An isolated Organization's Users no longer belong to User context.
   Their identity now belongs to one Organization only.
1. An isolated Organization has no data outside its own boundary. Nil
   context has nothing left to find there.

## Related decisions

- [ADR 016: `organization_id` and Organization-scoped query filtering](decisions/016_user_context.md) decides how this model is encoded today.
+8 −2
Original line number Diff line number Diff line
@@ -15,9 +15,15 @@ For web requests the current organization will be determined in the following or

1. Path params. E.g. `/o/my-org/my-group`, `/my-group/my-project`
1. Header field (`X-GitLab-Organization-ID`)
1. Users Home Organization (`user.organization`)
1. Users Home Organization (`user.organization_id`).
1. The default organization (ID = 1).

This precedence resolves Organization context. **User context** and
**Nil context** are separate, independent contexts — see [Request Context](contexts.md) — and a route can resolve to either of them
instead of falling back to the Default Organization. Which routes should
do that is an explicit, per-route decision not yet made; this page does
not resolve it.

### Path Params

The current Organization will be the parent Organization for the resource requested.
@@ -39,7 +45,7 @@ For example:

### Users Home Organization

Every user belongs to one Organization. This organization is known as the Home Organization.
A User's Home Organization (`organization_id`) always points at exactly one Organization — ownership is always exclusive. Whether membership is exclusive to that Organization too depends on whether it is isolated. See [The User's Home Organization](users.md#the-users-home-organization).

Used as a fallback for any ambiguous authenticated request.

+116 −0
Original line number Diff line number Diff line
---
owning-stage: "~devops::tenant scale"
title: 'Organizations ADR 016: organization_id and Organization-scoped query filtering'
description: "ADR that defines the isolation-based rule for when organization_id creates exclusive membership, and the Organization filter rule, closing a gap the isolated-Organization work knowingly left open."
creation-date: "2026-07-27"
authors: [ "@alexpooley" ]
toc_hide: true
---

## Context

See [Request Context](../contexts.md) for the three contexts every
request or process resolves to: Organization context, User context, and
Nil context.

Today, `organization_id` is required, and membership in that
Organization is always treated as exclusive, whether or not the
Organization is isolated. This blocks User context for every User:
every User's identity is always bound to one Organization.

Today, routing also treats "no Organization" the same as "Default
Organization." This blocks Nil context the same way, and causes bugs
(see [issue #605747](https://gitlab.com/gitlab-org/gitlab/-/issues/605747)).

[ADR 015: Non-isolation is a permanent Organization state](015_non_isolation_is_permanent.md)
established that some Organizations stay non-isolated indefinitely. User
context and Nil context only make sense given that fact: they need
Users whose membership is not exclusive, not just Users who have not
isolated yet.

The isolated-Organization work moved straight from "every User belongs
to one Organization" to "every Organization is isolated." It did not
define the states in between. This decision fills that gap.

## Decision

### Isolation, not presence, decides whether membership is exclusive

`organization_id` stays required. It is also the `users` table's
Cells sharding key, so every User needs a concrete value; that cannot
change.

```text
User.organization_id: org_id
```

What changes is the rule for what that value means. Isolation gates
User context, not whether `organization_id` points at an
Organization — it always does. This field only affects User context —
Nil context has no User to apply it to.

Ownership — `organization_id` itself — is always exclusive: it points
at exactly one Organization. What isolation decides is whether
membership is exclusive too:

1. Points at a **non-isolated** Organization — the User has User
   context. Membership is not exclusive: the User can be a member of
   any number of other non-isolated Organizations too, with the same
   account, and User context aggregates across all of them.
1. Points at an **isolated** Organization — required for every member.
   The User has no User context: the Organization is a real boundary,
   and the User's identity does not exist outside it.

Invariant: membership in an isolated Organization implies
`organization_id` points at it, with no exceptions. A User's
`organization_id` points at, at most, one isolated Organization at a
time. It is reassigned once, at isolation, for members not already
pointing at it. It does not change as memberships change afterward.

### Query scoping

Org-scoped finders apply an Organization filter when the Organization
acts as a real boundary:

```ruby
apply_org_filter = org.present? && org.isolated?
```

This is the core rule. There is a separate, secondary optimization for
instances with only one Organization: when `Organization.count == 1`,
filtering is a no-op, so we skip it. This is a performance detail, not
part of the scoping rule.

Self-managed and Dedicated instances have exactly one Organization, the
Default Organization, so `Organization.count == 1` is true for every
install there, and the filter stays a no-op, at no cost.

### Routing must decide, per route, which context applies

Today, a path with no Organization prefix (for example
`/dashboard/...`) implies the Default Organization. A path with an
Organization prefix (for example `/o/acme/...`) implies that
Organization instead.

User context and Nil context are real, separate values now, not
variants of "no Organization" that fall back to the Default
Organization. An unprefixed path like `/dashboard/...` could mean User
context, or it could still mean Organization context bound to the
Default Organization. A path like `/explore` could mean Nil context, or
it could likewise still mean the Default Organization today.

We must choose, one route at a time, which context applies. Some routes
(for example, a personal to-do list) likely want User context. Some
routes (for example, `/explore`) likely want Nil context. Other routes
may still want the Default Organization. This decision does not resolve
that choice. It only makes the three contexts real enough to choose
between.

## Consequences

On .com, every existing User's `organization_id` already points at
the Default Organization, which is non-isolated. No backfill is needed.
User context is available: a User's to-do list, and other User-context
views, span every non-isolated Organization they are a member of — for
example, an open-source contributor who is also a member of a company
Organization.
+2 −2
Original line number Diff line number Diff line
@@ -23,9 +23,9 @@ Our goal is to minimize disruption to Self Managed and Dedicated while we try to

In the longer term, we anticipate faster feature development by having Organizations as a common top level entity across all platforms.

## Can a user belong to multiple Organizations?
## Can a user be a member of multiple Organizations?

Currently, our model is designed for one user to belong to one Organization. This restriction was implemented because allowing users to belong to multiple Organizations created significant technical challenges and raised many product questions. We expect usage of multiple Organization membership to be relatively low, so this limitation will likely remain for the foreseeable future.
Yes, as long as those Organizations are non-isolated. Ownership (`organization_id`) always points at exactly one Organization, but membership is only exclusive to that Organization once it isolates (see [The User's Home Organization](users.md#the-users-home-organization)); before that, the same account can be a member of any number of non-isolated Organizations. See [Request Context](contexts.md) for the full model. Membership in a single isolated Organization is the long-term goal once isolation ships for a given Organization.

## Do we expect to be able to configure alternate GitLab domain names for Organizations (such as `customer.gitlab.com`)?

Loading