Implement the granular token interface on OauthAccessToken
What does this MR do and why?
This MR makes OauthAccessToken include Authz::GranularTokenInterface, task T1 of the fine-grained OAuth token sequence. It's stacked on !255384 (merged), which adds the consent grant tables, and targets that branch until it merges.
granular? is true when the token has the granular scope. granular_scopes come from the user's active consent grant for the application, found with the authorized and not_duo_session enum scopes. The grant is read at request time, so revoking it denies every outstanding token for that application immediately.
The interface gets a subject_to_granular_enforcement? hook, defaulting to legacy?. OauthAccessToken returns false, so legacy OAuth tokens aren't denied in namespaces that enforce granular tokens: granular OAuth tokens can't be created yet, so there'd be no migration path. Authz::Tokens::AuthorizeGranularScopesService and the GraphQL boundary preloader call this hook.
No user-facing change, no migration, no feature flag, so no changelog entry.
Database review
No migrations or writes: two new read queries. The consent grant lookup uses authorized.not_duo_session, whose predicate (status = 0 AND source != 3) matches the partial unique index idx_oauth_consent_grants_on_user_id_and_application_id_active, so at most one row matches.
The tables come from the parent MR and don't exist in Database Lab yet, so they were created on a Joe clone of gitlab-production-main with the DDL from db/structure.sql and seeded with about 1.5 million rows each. The gitlab-qa user has an active grant plus revoked and duo_session siblings for the same application, and 4 granular scopes. Plans: https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/58290 (pgai joe result 500488 and pgai joe result 500490).
Query 1: consent grant lookup
Ruby: Authz::OauthConsentGrant.authorized.not_duo_session.find_by(user_id: resource_owner_id, application_id: application_id)
SELECT "oauth_consent_grants".*
FROM "oauth_consent_grants"
WHERE "oauth_consent_grants"."status" = 0
AND "oauth_consent_grants"."source" != 3
AND "oauth_consent_grants"."user_id" = 1614863
AND "oauth_consent_grants"."application_id" = 5
LIMIT 1Index Scan on idx_oauth_consent_grants_on_user_id_and_application_id_active, returning 1 row with 7 shared buffer hits. Execution 0.123 ms, planning 1.120 ms.
Plan: https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/58290 (CLI command 500488, full output below)
EXPLAIN ANALYZE output (command 500488)
Limit (cost=0.43..3.45 rows=1 width=52) (actual time=0.077..0.078 rows=1 loops=1)
Buffers: shared hit=7
I/O Timings: read=0.000 write=0.000
-> Index Scan using idx_oauth_consent_grants_on_user_id_and_application_id_active on public.oauth_consent_grants (cost=0.43..3.45 rows=1 width=52) (actual time=0.075..0.076 rows=1 loops=1)
Index Cond: ((oauth_consent_grants.user_id = 1614863) AND (oauth_consent_grants.application_id = 5))
Buffers: shared hit=7
I/O Timings: read=0.000 write=0.000
Settings: random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off'
Query ID: 2839077724862359413Query 2: granular scopes of the grant
Ruby: consent_grant.granular_scopes
SELECT "granular_scopes".*
FROM "granular_scopes"
INNER JOIN "oauth_consent_grant_granular_scopes" ON "granular_scopes"."id" = "oauth_consent_grant_granular_scopes"."granular_scope_id"
WHERE "oauth_consent_grant_granular_scopes"."oauth_consent_grant_id" = 1500001Nested loop: Index Only Scan on idx_oauth_consent_grant_granular_scopes_on_grant_id_scope_id (4 rows, 0 heap fetches), then Index Scan on granular_scopes_pkey (4 loops). 4 rows returned, 23 shared buffer hits. Execution 0.128 ms, planning 1.520 ms.
Plan: https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/58290 (CLI command 500490, full output below)
EXPLAIN ANALYZE output (command 500490)
Nested Loop (cost=0.85..5.39 rows=1 width=111) (actual time=0.069..0.076 rows=4 loops=1)
Buffers: shared hit=23
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using idx_oauth_consent_grant_granular_scopes_on_grant_id_scope_id on public.oauth_consent_grant_granular_scopes (cost=0.43..1.95 rows=1 width=8) (actual time=0.052..0.053 rows=4 loops=1)
Index Cond: (oauth_consent_grant_granular_scopes.oauth_consent_grant_id = 1500001)
Heap Fetches: 0
Buffers: shared hit=7
I/O Timings: read=0.000 write=0.000
-> Index Scan using granular_scopes_pkey on public.granular_scopes (cost=0.43..3.45 rows=1 width=111) (actual time=0.004..0.004 rows=1 loops=4)
Index Cond: (granular_scopes.id = oauth_consent_grant_granular_scopes.granular_scope_id)
Buffers: shared hit=16
I/O Timings: read=0.000 write=0.000
Settings: random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off'
Query ID: 571384802602357391A legacy OAuth token issues neither query. spec/models/oauth_access_token_spec.rb asserts these query counts.
References
- Closes #608635 (closed)
- Stacked on !255384 (merged)
- Epic: #468924
- Sequence: https://gitlab.com/gitlab-org/gitlab/-/work_items/605869
- Design proposal: https://gitlab.com/gitlab-org/architecture/auth-architecture/design-doc/-/merge_requests/136