Skip to content

feat(openapi): redesign auth pipeline with per-token-type routing#36693

Merged
wylswz merged 13 commits into
mainfrom
feat/openapi-auth-pipeline
May 27, 2026
Merged

feat(openapi): redesign auth pipeline with per-token-type routing#36693
wylswz merged 13 commits into
mainfrom
feat/openapi-auth-pipeline

Conversation

@GareArc
Copy link
Copy Markdown
Contributor

@GareArc GareArc commented May 26, 2026

Summary

  • Introduces a TokenType discriminator (oauth_account, oauth_external_sso) on bearer tokens, enabling correct routing for future token kinds (e.g. PATs) that share the same subject type
  • Each token type now has its own auth pipeline with distinct prepare and verification steps, replacing a single shared pipeline that required branching on token identity inside each step
  • Edition gating (CE/EE/SaaS) is enforced at the router level rather than scattered across individual steps, so pipelines themselves are edition-agnostic
  • Adds WORKSPACE_READ scope used by the /workspaces endpoint

Test plan

  • pytest api/tests/unit_tests/controllers/openapi/ — all pipeline, condition, and endpoint tests
  • pytest api/tests/unit_tests/libs/test_oauth_bearer_rate_limit_ordering.py api/tests/unit_tests/libs/test_workspace_member_helper.py
  • Manually verify account token flow: GET /openapi/v1/account returns account info
  • Manually verify external SSO token rejected on account-only endpoints with 403 unsupported_token_type
  • Verify external SSO token rejected on EE-only endpoints when running CE with 403 external_sso_requires_ee

GareArc added 3 commits May 26, 2026 03:16
…ith PipelineRouter

Replace the single mutable-context Pipeline with a two-phase, condition-driven
system dispatched by token type.

New architecture:
- TokenType(StrEnum) replaces source: str on AuthContext / TokenKind
- AuthPipeline: pure prepare→auth step runner; no guard()
- PipelineRoute: binds AuthPipeline to an optional required_edition gate
- PipelineRouter: single guard() entry point; runs edition/license/token-type
  pre-gates then dispatches to the registered pipeline for the token type
- Cond / When: composable predicates for conditional step dispatch
- AuthData: frozen Pydantic model produced by the prepare phase; carries
  token_id so endpoints don't need to call get_auth_ctx() for identity fields
- Edition enum + current_edition(): CE / EE / SAAS discriminator

Two pipelines in composition.py:
- account_pipeline  — OAUTH_ACCOUNT tokens
- external_sso_pipeline — OAUTH_EXTERNAL_SSO tokens (EE enforced at route level)

All /openapi/v1 endpoints migrated to auth_router.guard().
Old context.py, steps.py, strategies.py, surface_gate.py deleted.
WORKSPACE_READ scope added; cached_verdicts renamed to membership_cache.
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. refactor labels May 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

Pyrefly Diff

base → PR
--- /tmp/pyrefly_base.txt	2026-05-27 11:22:24.013629207 +0000
+++ /tmp/pyrefly_pr.txt	2026-05-27 11:22:09.885514490 +0000
@@ -2189,58 +2189,6 @@
    --> tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py:342:35
 ERROR Missing argument `payload` in function `protected_view` [missing-argument]
    --> tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py:348:36
-ERROR Object of class `Step` has no attribute `_mounters` [missing-attribute]
-  --> tests/unit_tests/controllers/openapi/auth/test_composition.py:52:31
-ERROR Argument `Literal['apps:run']` is not assignable to parameter `required_scope` with type `Scope` in function `controllers.openapi.auth.context.Context.__init__` [bad-argument-type]
- --> tests/unit_tests/controllers/openapi/auth/test_context.py:5:34
-ERROR Argument `Literal['apps:run']` is not assignable to parameter `required_scope` with type `Scope` in function `controllers.openapi.auth.context.Context.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_context.py:19:34
-ERROR `frozenset[str]` is not assignable to attribute `scopes` with type `frozenset[Scope]` [bad-assignment]
-  --> tests/unit_tests/controllers/openapi/auth/test_context.py:20:18
-ERROR Argument `Literal['x']` is not assignable to parameter `required_scope` with type `Scope` in function `controllers.openapi.auth.context.Context.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_pipeline.py:18:65
-ERROR Argument `Literal['x']` is not assignable to parameter `required_scope` with type `Scope` in function `controllers.openapi.auth.context.Context.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_pipeline.py:34:61
-ERROR Argument `Literal['apps:run']` is not assignable to parameter `scope` with type `Scope` in function `controllers.openapi.auth.pipeline.Pipeline.guard` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_pipeline.py:49:27
-ERROR Argument `Literal['apps:run']` is not assignable to parameter `required_scope` with type `Scope` in function `controllers.openapi.auth.context.Context.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_app_resolver.py:13:35
-ERROR Object of class `NoneType` has no attribute `id` [missing-attribute]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_app_resolver.py:63:12
-ERROR Object of class `NoneType` has no attribute `id` [missing-attribute]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_app_resolver.py:64:12
-ERROR Argument `Literal['apps:run']` is not assignable to parameter `required_scope` with type `Scope` in function `controllers.openapi.auth.context.Context.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_authz.py:14:32
-ERROR `str | Unknown` is not assignable to attribute `account_id` with type `UUID | None` [bad-assignment]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_authz.py:17:20
-ERROR `SimpleNamespace` is not assignable to attribute `app` with type `App | None` [bad-assignment]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_authz.py:18:13
-ERROR `SimpleNamespace` is not assignable to attribute `tenant` with type `Tenant | None` [bad-assignment]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_authz.py:19:16
-ERROR Argument `() -> SimpleNamespace` is not assignable to parameter `resolve_strategy` with type `() -> AppAuthzStrategy` in function `controllers.openapi.auth.steps.AppAuthzCheck.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_authz.py:70:23
-ERROR Argument `() -> SimpleNamespace` is not assignable to parameter `resolve_strategy` with type `() -> AppAuthzStrategy` in function `controllers.openapi.auth.steps.AppAuthzCheck.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_authz.py:76:19
-ERROR Argument `Literal['apps:run']` is not assignable to parameter `required_scope` with type `Scope` in function `controllers.openapi.auth.context.Context.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_bearer.py:22:35
-ERROR Argument `Literal['apps:read']` is not assignable to parameter `required_scope` with type `Scope` in function `controllers.openapi.auth.context.Context.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_layer0.py:18:32
-ERROR `SimpleNamespace | None` is not assignable to attribute `tenant` with type `Tenant | None` [bad-assignment]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_layer0.py:21:16
-ERROR Argument `Literal['apps:run']` is not assignable to parameter `required_scope` with type `Scope` in function `controllers.openapi.auth.context.Context.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_mount.py:15:32
-ERROR `SimpleNamespace` is not assignable to attribute `app` with type `App | None` [bad-assignment]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_mount.py:19:13
-ERROR `SimpleNamespace` is not assignable to attribute `tenant` with type `Tenant | None` [bad-assignment]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_mount.py:20:16
-ERROR Argument `test_caller_mount_dispatches_by_subject_type.Fake` is not assignable to parameter `*mounters` with type `CallerMounter` in function `controllers.openapi.auth.steps.CallerMount.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_mount.py:68:9
-ERROR Argument `test_caller_mount_dispatches_by_subject_type.Fake` is not assignable to parameter `*mounters` with type `CallerMounter` in function `controllers.openapi.auth.steps.CallerMount.__init__` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/auth/test_step_mount.py:69:9
-ERROR `in` is not supported between `Literal['wrong_surface']` and `None` [not-iterable]
-  --> tests/unit_tests/controllers/openapi/auth/test_surface_gate.py:93:20
-ERROR `in` is not supported between `Literal['/openapi/v1/apps']` and `None` [not-iterable]
-  --> tests/unit_tests/controllers/openapi/auth/test_surface_gate.py:96:20
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
   --> tests/unit_tests/controllers/openapi/test_account.py:40:12
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
@@ -2297,24 +2245,26 @@
   --> tests/unit_tests/controllers/openapi/test_workspaces.py:49:12
 ERROR `in` is not supported between `Literal['GET']` and `None` [not-iterable]
   --> tests/unit_tests/controllers/openapi/test_workspaces.py:50:12
+ERROR Could not find name `AuthData` [unknown-name]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:107:42
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:164:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:176:12
 ERROR `in` is not supported between `Literal['POST']` and `None` [not-iterable]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:165:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:177:12
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:170:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:182:12
 ERROR `in` is not supported between `Literal['GET']` and `None` [not-iterable]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:171:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:183:12
 ERROR `in` is not supported between `Literal['POST']` and `None` [not-iterable]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:172:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:184:12
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:177:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:189:12
 ERROR `in` is not supported between `Literal['DELETE']` and `None` [not-iterable]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:178:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:190:12
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:183:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:195:12
 ERROR `in` is not supported between `Literal['PUT']` and `None` [not-iterable]
-   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:184:12
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:196:12
 ERROR Object of class `NoneType` has no attribute `json`
 ERROR Object of class `NoneType` has no attribute `json`
 ERROR Cannot index into `Iterable[bytes]` [bad-index]
@@ -6129,13 +6079,13 @@
 ERROR `SimpleNamespace` is not assignable to attribute `_current_tenant` with type `Tenant | None` [bad-assignment]
    --> tests/unit_tests/libs/test_login.py:247:35
 ERROR Argument `Literal['apps:read']` is not assignable to parameter `scope` with type `Scope` in function `libs.oauth_bearer.require_scope` [bad-argument-type]
-  --> tests/unit_tests/libs/test_oauth_bearer_require_scope.py:61:20
+  --> tests/unit_tests/libs/test_oauth_bearer_require_scope.py:62:20
 ERROR Argument `Literal['apps:write']` is not assignable to parameter `scope` with type `Scope` in function `libs.oauth_bearer.require_scope` [bad-argument-type]
-  --> tests/unit_tests/libs/test_oauth_bearer_require_scope.py:70:20
+  --> tests/unit_tests/libs/test_oauth_bearer_require_scope.py:71:20
 ERROR Argument `Literal['apps:write']` is not assignable to parameter `scope` with type `Scope` in function `libs.oauth_bearer.require_scope` [bad-argument-type]
-  --> tests/unit_tests/libs/test_oauth_bearer_require_scope.py:81:20
+  --> tests/unit_tests/libs/test_oauth_bearer_require_scope.py:82:20
 ERROR Argument `Literal['apps:read']` is not assignable to parameter `scope` with type `Scope` in function `libs.oauth_bearer.require_scope` [bad-argument-type]
-  --> tests/unit_tests/libs/test_oauth_bearer_require_scope.py:90:20
+  --> tests/unit_tests/libs/test_oauth_bearer_require_scope.py:91:20
 ERROR Argument `_FakeRedis` is not assignable to parameter `redis_client` with type `_RateLimiterRedisClient` in function `libs.helper.RateLimiter.__init__` [bad-argument-type]
   --> tests/unit_tests/libs/test_rate_limiter.py:44:22
 ERROR Argument `dict[@_, @_]` is not assignable to parameter `headers` with type `Message` in function `python_http_client.exceptions.HTTPError.__init__` [bad-argument-type]

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 45.75% 45.75% +0.00%
Strict coverage 45.27% 45.27% +0.00%
Typed symbols 24,445 24,456 +11
Untyped symbols 29,295 29,304 +9
Modules 2736 2735 -1

@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

❌ Patch coverage is 87.46667% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.84%. Comparing base (b2710b8) to head (75f6d2c).

Files with missing lines Patch % Lines
api/controllers/openapi/auth/pipeline.py 74.07% 14 Missing and 7 partials ⚠️
api/controllers/openapi/auth/verify.py 80.43% 6 Missing and 3 partials ⚠️
api/controllers/openapi/account.py 68.75% 5 Missing ⚠️
api/controllers/openapi/apps.py 72.72% 3 Missing ⚠️
api/controllers/openapi/auth/prepare.py 94.00% 2 Missing and 1 partial ⚠️
api/controllers/openapi/auth/data.py 96.00% 1 Missing and 1 partial ⚠️
api/controllers/openapi/workspaces.py 91.66% 2 Missing ⚠️
api/controllers/openapi/app_run.py 87.50% 1 Missing ⚠️
api/controllers/openapi/files.py 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #36693      +/-   ##
==========================================
- Coverage   85.85%   85.84%   -0.02%     
==========================================
  Files        4534     4536       +2     
  Lines      220457   220490      +33     
  Branches    40660    40666       +6     
==========================================
+ Hits       189284   189288       +4     
- Misses      27601    27636      +35     
+ Partials     3572     3566       -6     
Flag Coverage Δ
api 85.37% <87.46%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread api/controllers/openapi/auth/prepare.py Outdated
@wylswz
Copy link
Copy Markdown
Contributor

wylswz commented May 27, 2026

There are some conflicts.

GareArc and others added 4 commits May 27, 2026 04:00
…rier

- Remove frozen=True from AuthData; add path_params field
- Construct AuthData directly in _run(), inline ExternalIdentity from identity.subject_email
- Delete _init_builder, pop() calls, and AuthData(**builder) splat
- Prepare steps take (data: AuthData) instead of (builder: dict)
- Remove build_external_identity step; composition and tests updated
- Add InternalServerError guard in load_tenant when app not loaded
- Use explicit None checks in resolve_external_user for type narrowing
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label May 27, 2026
@wylswz wylswz enabled auto-merge May 27, 2026 12:43
@wylswz wylswz disabled auto-merge May 27, 2026 12:44
@wylswz wylswz added this pull request to the merge queue May 27, 2026
Merged via the queue into main with commit d2788d7 May 27, 2026
35 checks passed
@wylswz wylswz deleted the feat/openapi-auth-pipeline branch May 27, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer refactor size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants