Skip to content

UN-4016 [FEAT] Resolve the organisation from a platform API key via a whoami endpoint - #2269

Open
pk-zipstack wants to merge 6 commits into
mainfrom
UN-4016-platform-key-whoami
Open

UN-4016 [FEAT] Resolve the organisation from a platform API key via a whoami endpoint#2269
pk-zipstack wants to merge 6 commits into
mainfrom
UN-4016-platform-key-whoami

Conversation

@pk-zipstack

Copy link
Copy Markdown
Contributor

What

GET /api/v1/unstract/whoami/ — an organisation-less endpoint that authenticates with a platform API key and answers {organization_id, organization_name, permission, key_name}, read off the key row itself. It reaches the generated OpenAPI spec, so the published client and the CLI pick it up.

Builds on the spec pipeline from #2237 (UN-4009), which merged on 31 Aug — this was written against that branch and has been rebased onto main since, with the spec regenerated and re-verified against the merged result.

Why

Before a caller can use any organisation-scoped endpoint they need org_id, and it has no documented source — it is the first path segment of every web-app URL (useMainAppRoutes.js, <Route path=":orgName">) and nothing says so. A platform key already carries its organisation by construction: PlatformApiKey inherits DefaultOrganizationMixin, whose FK is stamped at mint time from the minting admin's active org, and key is globally unique, so a bearer token maps to exactly one row and therefore one organisation. Nothing exposed that association over HTTP.

Every comparable CLI that predates OAuth (doctl, twilio, sentry-cli) issues one opaque token that carries its own scope and is validated the moment it is supplied. This is the endpoint that lets ours do the same.

How

  • platform_api/whoami_views.py — a field read. permission_classes = [] because CustomAuthMiddleware has already resolved the token to a key row, bound the service account to request.user and enforced the tier against the method; a permission class would re-ask a question already answered. authentication_classes is deliberately not set — the project configures no DEFAULT_AUTHENTICATION_CLASSES that resolves a user, so DRF's default returns None and the middleware's request.user survives into the view. No extra query: custom_auth_middleware.py already select_relateds organization.
  • platform_api/whoami_urls.py + a mount in base_urls.py — its own module because the spec's urlconf selector can only pick out a mount declared with a dotted module path (the tenant and public mounts pass a list, so urlconf_name.__name__ is None for them). Mounted ahead of the tenant urlconf so resolution is deterministic rather than relying on fall-through; organisation-scoped paths are rewritten before routing and none of those rewrites can produce whoami/.
  • Two middleware changes, and they are the reason this is not a one-file PR. OrganizationMiddleware matches ^/api/(v[12])/unstract/(?P<org_id>[^/]+)/, so /api/v1/unstract/whoami/ parsed whoami as the organisation and rewrote path_info to /api/v1/unstract/404, with request.organization_id = "whoami" making the org-match guard at custom_auth_middleware.py:94 403 every valid key. The whitelist escape hatch (ORGANIZATION_MIDDLEWARE_WHITELISTED_PATHS, previously []) returns early without ever setting request.organization_id, which that same line reads by bare attribute access → AttributeError500. So: the path is added to that list, and the whitelist branch now sets the attribute. The second half is a latent-bug fix that protects any future organisation-less path — test_whoami.py fails with exactly that AttributeError if it is removed.
    This is not WHITELISTED_PATHS: that list skips authentication entirely, which for this endpoint would mean answering with someone else's organisation or none at all. A test asserts it stays out.
  • platform_api/openapi_schema.py — mirrors api_v2/openapi_schema.py: a spec-only serializer that never builds a response, with permission sourced from ApiKeyPermission.choices so a new tier cannot reach the API without reaching the spec. auth=[{"platformKey": []}] is mandatory, not decorative — an operation that omits it regresses to cookieAuth/basicAuth, because DRF's unset authentication default gets introspected as a decision.
  • Three spec gates widened, each as narrowly as possible since UN-4009 [MISC] Generate and commit the API deployment OpenAPI spec in-repo #2237 is in review: the published-prefix check accepts the tenant mount alongside the deployment one (still rejecting an overridden API_DEPLOYMENT_PATH_PREFIX); SPEC_URLCONFS gains the new urlconf; and the two tests that looped over every operation asserting deployment-specific facts now pin those to the deployment operations and assert only genuinely universal ones (401/403/500, and that each operation names exactly one declared bearer scheme) globally. Declaring 400/404 on an operation that carries no body and names no resource would hand clients a dead branch — the same sin test_only_the_execution_endpoint_... already guards against.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why.

The two middleware changes are the risk surface, and both are additive:

  • ORGANIZATION_MIDDLEWARE_WHITELISTED_PATHS goes from [] to one regex matching only /api/v1/unstract/whoami/. Cloud and on-prem .append() to this list rather than reassigning it, so their existing entries are unaffected.
  • Setting request.organization_id = None in the whitelist branch can only turn an AttributeError into the skip the early return already intended. No path reached that branch before this PR, since the list was empty.

The new URL mount sits ahead of the tenant urlconf but cannot shadow it: OrganizationMiddleware rewrites every organisation-scoped path to /api/v1/unstract/<rest>/ before routing, and no rewrite yields whoami/. Verified by hand against a running server — /api/v1/unstract/acme/api/deployment/ still returns 200 with the same key, and a key from another organisation still gets 403 there.

Cloud and on-prem inherit both the mount and the setting with no change in unstract-cloud: cloud_base_urls.py and onprem_base_urls.py both from .base_urls import urlpatterns and append.

The regenerated spec is +160/−0 — the deployment operations are byte-identical, so nothing generated from it changes.

Database Migrations

None. No model changes.

Env Config

None. ORGANIZATION_MIDDLEWARE_WHITELISTED_PATHS is a settings constant, not an env var, and is not intended to be overridden per installation.

Relevant Docs

None in this repo. unstract-docs has a Platform API Keys page that could gain a whoami entry once this lands; not blocking.

Related Issues or PRs

Dependencies Versions

None added or changed.

Notes on Testing

46 passed in the two suites this touches (platform_api/, api_v2/tests/test_docstudio_spec.py), of which 10 + 3 subtests are new endpoint tests and 6 are new spec anchors. Full backend suite baselined against the unmodified parent commit: identical failure set, +16 passing.

platform_api/tests/test_whoami.py goes through the real URLconf and a real middleware chain, because everything that makes this endpoint work happens before the view. It covers: the organisation resolving from the key and not the URL (two organisations, one URL, two answers); every tier reading its own identity; missing/malformed/unknown/inactive keys; that the route actually resolve()s, so a 404 cannot masquerade as a passing 401; and that the path is not in WHITELISTED_PATHS.

Also exercised by hand against a running server with two seeded organisations — whoami answers each key with its own organisation over the same URL, all four rejection paths return 401, and the organisation-scoped regression checks above pass.

Screenshots

n/a — no user-facing surface.

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K92qFRNecJ5kNkKcmJ8qkM

pk-zipstack and others added 2 commits September 1, 2026 16:45
… whoami endpoint

A caller holding a platform API key knows the secret but not what it is scoped
to: `organization_id` is only discoverable by reading it out of a web-app URL,
and every organisation-scoped endpoint takes it as a path segment. This adds
`GET /api/v1/unstract/whoami/`, which returns `{organization_id,
organization_name, permission, key_name}` read off the key row itself.

The route carries no organisation segment, and that is what made it more than a
view. `OrganizationMiddleware` matches `^/api/v1/unstract/<org>/`, so it parsed
`whoami` as the organisation and rewrote the path to a 404. Its whitelist escape
hatch then returned without ever setting `request.organization_id`, which
`CustomAuthMiddleware` reads by bare attribute access -- a 500 rather than a
skip. Both are fixed: the path is whitelisted for the organisation middleware
only (it still authenticates), and the whitelist branch now sets the attribute,
which protects any future organisation-less path.

The endpoint also flows through the committed OpenAPI spec, which needed three
gates widened: the published-prefix check now accepts the tenant mount as well
as the deployment one, `SPEC_URLCONFS` gains the new urlconf, and the two spec
tests that looped over every operation asserting deployment-specific facts now
pin those facts to the deployment operations and check only genuinely universal
ones globally. The regenerated spec is +160/-0 -- the deployment contract is
byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K92qFRNecJ5kNkKcmJ8qkM
…endpoint sends

Iteration 1 of unstract:remediation against PR #2269. Fifteen of sixteen
findings; F6 is handed back (see below).

The two High findings were both invisible from inside the change:

F1 — the spec published `ErrorResponse` for 401/403, but `whoami` is
deliberately not whitelisted, so `CustomAuthMiddleware` answers every rejection
itself with a bare `{"message": ...}` and DRF's exception handler is never
reached. A generated client branching on `errors[0].code` would raise on the
most common failure the endpoint has. Now declares a `PlatformKeyError` shape
that matches the wire, with the 500 carrying no body schema because a Django
HTML 500 has none.

F2 — `@pytest.mark.critical_path("platform-key-whoami")` named an id in no
registry, and `tests/rig/cli.py` sets `overall_exit = 1` on an unknown marker.
The rig would have failed the build on every run. CI never caught it because
`test` skips on draft PRs. Now registered.

Also fixed: the view returned 403 where the spec said 401 (DRF coerces
`NotAuthenticated` unless the first authenticator offers a WWW-Authenticate
header, and SessionAuthentication offers none) — it now returns 401 explicitly,
and the branch has a test, which it never had because every other rejection is
answered before the view runs. The whitelist regex is anchored, so an
organisation named `whoami` no longer has its whole API treated as
organisation-less. The organisation-scoped alias `/<org>/whoami/`, which the
mount comment wrongly claimed could not exist, is now documented and tested in
both directions.

The dominant defect class was not any single bug: seven of sixteen findings
were confidently-worded comments asserting mechanisms the code does not
implement — that DRF resolves no user, that the org FK is non-null by
construction, that no rewrite can produce `whoami/`, that a test observes
middleware it never invokes. Each is corrected to what the code does, or
deleted.

Every fix is mutation-checked: reverting it fails a named test. That check also
caught one of the new tests passing vacuously — it matched an error string the
OpenAPI-validity gate produces, so it went green with the gate it was written
for removed.

F6 is not fixed here and needs a decision: setting `organization_id = None`
makes `whoami` bypass `SubscriptionMiddleware` in the enterprise tree, where
every other org-less path has an explicit `SUBSCRIPTION_WHITELISTED_PATHS_LIST`
entry. The fix belongs in unstract-cloud and the intent is a product call.

Findings: F1 F2 F3 F4 F5 F7(partial) F8 F9 F10 F11 F12 F13 F14 F15 F16

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K92qFRNecJ5kNkKcmJ8qkM
@pk-zipstack

Copy link
Copy Markdown
Contributor Author

@chandrasekharan-zipstack @hari-kuriakose — one finding from this PR's review needs a decision from you, because the fix isn't in this repo.

whoami silently bypasses the subscription gate on cloud

To serve an organisation-less route, this PR adds ^/api/v1/unstract/whoami/$ to ORGANIZATION_MIDDLEWARE_WHITELISTED_PATHS and sets request.organization_id = None on that branch (backend/middleware/organization_middleware.py:23).

In the enterprise tree that has a second, unintended effect. SubscriptionMiddleware runs immediately after CustomAuthMiddleware for every non-/deployment path, and takes the org from UserSessionUtils.get_organization_id, which returns request.organization_id verbatim — so it gets None. get_subscription(None) finds no row and verify_subscription falls through to get_response.

Two consequences:

  1. The endpoint is exempt from the trial/subscription gate — by accident, not by decision.
  2. It issues a Subscription.objects.get(organization_id=None, is_active=True) query on every call.

What makes this worth a decision rather than a silent fix: every previously org-less cloud path was given a matching entry in SUBSCRIPTION_WHITELISTED_PATHS_LIST with a comment explaining why — marketplace/webhook/, tackle/webhook/, marketplace/claim/, marketplace/claim-status/ (unstract-cloud backend/backend/settings/cloud.py:273-286). whoami/ is in the org-middleware list and in neither of those.

The call

Exemption is probably right — an identity probe that tells you which org your key belongs to arguably should answer regardless of billing state, and it reads nothing billable. But right now it's a side effect of the org id being None, and it will change the day get_subscription stops tolerating a null org.

  • If whoami should answer regardless of subscription state: add "whoami/" to SUBSCRIPTION_WHITELISTED_PATHS_LIST in unstract-cloud, with the same style of comment as its neighbours, so it's a decision on the record.
  • If it should not: SubscriptionMiddleware needs the key's own organisation rather than the URL's — CustomAuthMiddleware already puts it in StateStore at custom_auth_middleware.py:140.

Either way it's an unstract-cloud change, so it can't ride on this PR. Flagging rather than guessing.

Found by unstract:remediation (iteration 1). The other 15 findings from that review are fixed in b64b6f9 on this branch.

pk-zipstack and others added 4 commits September 1, 2026 18:18
Iteration 2. The adversarial verifier re-found F1: iteration 1 changed the
401/403 `$ref` to `PlatformKeyError` but left the `examples` block beside it
showing the handler's `{type, errors[]}` body, so the artifact contradicted
itself in a single media-type object -- schema requiring `message`, example
showing `errors[0].code`. A client author reads the example, not the `$ref`, so
the defect F1 was filed against survived its own fix.

Cause: `drf_standardized_errors` appends an example of the handler body to
every 4xx/5xx keyed on the status code alone, never consulting the declared
serializer (`openapi.py:343-356`), so overriding `responses` was never going to
be enough. `PlatformKeyAutoSchema` suppresses that injection for this view
only; the deployment operations keep their examples, which are correct there
because those really do return the handler body.

Also fixed, all four raised by the same verifier against iteration 1's own work:
the module docstring claimed the middleware answers *every* rejection, which the
401 iteration 1 added to the view had just made false; the new spec test lacked
the `assert reads` guard that the same commit added twelve lines below it, so it
passed vacuously on an operationId rename; the alias test's docstring claimed
mount-order coverage a mutation disproved (swapping the mount leaves every test
green); and the 403 body was declared but asserted nowhere, which is the exact
"status asserted, body unasserted" gap that let the original spec lie.

`test_no_published_example_contradicts_its_own_schema` now pins the property
structurally for every operation. It found three pre-existing instances in the
merged deployment spec -- `status` 406/500 and `execute` 500 all declare a
non-error body beside a handler-shaped example. Those are recorded in
`_KNOWN_EXAMPLE_DIVERGENCES` rather than fixed here: they predate this branch
(verified against c49968e) and belong to whoever owns that spec. The check
fails on any new instance.

Findings: F1(re-fixed) N2 N3 N4 N5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K92qFRNecJ5kNkKcmJ8qkM
…claiming it cannot

Iteration 3. The verifier re-found N2 with a counter-example rounds 1 and 2
both missed: `POST /api/v1/unstract/whoami/` with a `read_write` key -- the
model default -- is passed by the middleware, refused by DRF, and comes back
405 in the exception handler's `{type, errors[]}`. Four comments across three
files asserted that shape was unreachable here, and that claim was the stated
justification for both not reusing `ErrorResponse` and suppressing every error
example on the view.

So the endpoint has always returned two error shapes and the spec described
one. 405 is now declared with `ErrorResponse`, and `PlatformKeyAutoSchema`
narrows its suppression to the statuses the middleware actually answers (401,
403) rather than blanket-stripping, so the 405 keeps the handler example that
is correct for it. Verified on the wire: a `read_write` key POSTing gets 405
`{type, errors[]}`; a `read` key gets 403 `{message}` from the middleware
instead. Both are now pinned, against each other, so neither path can look like
the only one.

The absolute clauses are deleted rather than rewritten a third time. Two
rounds of rewriting them produced two more false statements; what remains says
only what is enforced.

Also from the same verifier: `_KNOWN_EXAMPLE_DIVERGENCES` was keyed on
`(operationId, code)`, which exempted those coordinates forever -- a new
divergent example injected at `status` 406 passed. Now keyed on the example
name too, so the three recorded rows stay exempt and a fourth fails; the
comment claiming the check catches new instances is true as written for the
first time. Dropped a dead exemption branch that could never match, and a
comment misattributing which commit added a guard and how far below it sits.

Findings: N2(re-fixed) + 4 new from verifier round 2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K92qFRNecJ5kNkKcmJ8qkM
…not return

Iteration 4, and the last: N2 is escalated rather than attempted a fourth time.

Round 3 declared 405 under the `get` operation. A verifier's 42-cell matrix
(3 tiers x 7 methods x 2 routes) shows GET returns 200, 401 or 403 and never
405 -- so the declaration was unreachable at the coordinate it was written, and
the 405s that do occur (POST/PUT/PATCH at read_write+, DELETE at full_access)
have no operation in the spec to attach to. The kept example made it worse: it
published `Method "get" not allowed.` under the GET operation, a hardcoded
placeholder from drf_standardized_errors, not derived from the route.

The declaration is withdrawn. The behaviour is real and is now stated in the
operation description in prose, which is where a fact about methods the route
does not serve can honestly live.

Also corrected, all introduced by round 3 and all found by the same verifier:
an I001 failure of the repo's PINNED ruff 0.3.4 hook, which round 3 introduced
and the installed ruff 0.15 does not report -- verified clean at 7eb41bc and
failing at 32ec142, then confirmed the fix satisfies both versions; a comment
asserting everything else "keeps the handler shape", contradicted by the 500
declared eight lines below it; a true clause round 3 deleted while rewriting
the sentence around it, restored verbatim; and a test whose name claimed a
relationship to the spec that nothing in it tested -- renamed to what it
actually asserts, with the gap it does not cover named in its docstring.

WHAT IS ESCALATED, and why this loop stops here:

N2 has survived three fix attempts. Each attempt was a correct reading of the
previous failure and each introduced a new defect: round 2 rewrote a false
claim into a stronger false one, round 3 fixed the claim and declared the
status in the wrong place. Three consecutive rounds introducing new defects is
the convergence tripwire, and the discriminator applies -- the fixes keep
needing an exception to a shared rule, which means the rule is wrong, not the
wording.

The rule that is wrong: this path serves GET, but answers non-GET methods with
a status and a body shape that no `get` operation can describe. OpenAPI
attaches responses to operations, not paths. Options, none of which a
remediation loop should pick unilaterally: declare stub operations for the
methods purely to carry a 405; accept prose (what this commit does); or change
the view so every method the tier permits is answered in one shape.

Findings: NEW-1..NEW-5 from verifier round 3 fixed; N2 ESCALATE (attempts=3)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K92qFRNecJ5kNkKcmJ8qkM
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

pk-zipstack added a commit to Zipstack/unstract-cli that referenced this pull request Sep 1, 2026
…the default points

Iteration 1 of unstract:remediation against PR #3. Nineteen findings across
fifteen classes; one escalated (below).

The headline defect had four faces and one cause. `config.py` stated a
relationship in a comment -- "the same host as docstudio: one deployment serves
both the platform API and the deployments it manages" -- and implemented it as
a constant on the very next line. So `platform.base_url` ignored a profile
written before the `platform` block existed, ignored `docstudio --base-url`,
and left `--api-key` inert for `deployment ls`, sending an organisation-admin
platform key to `us-central.unstract.com` after the operator had explicitly
named their own host. A reviewer proved it by pointing `--base-url` at
127.0.0.1:9 and getting a real 401 back: a closed port cannot answer, so the
request reached the SaaS default. `platform_base_url()` now falls back to the
resolved docstudio host, which fixes all four sites at once. The security scan
rates it 3/10 as a vulnerability -- vendor host, TLS, and `requests` strips the
auth header across hosts -- so it lands as a correctness defect, not a leak.

The other four High findings:

- `_store_organisation` re-derived the profile ladder and dropped the
  `$UNSTRACT_PROFILE` tier, so the key resolved from one profile and the org
  was written into another; the next command then failed after a `whoami` that
  reported `saved: true`. It now takes `ResolvedConfig.active_profile`, the same
  chain every read uses. It also refuses to *create* a profile that is not in
  the file: `setdefault` was materialising a typo, permanently disarming the
  "Profile not found" guard so every later command silently resolved production
  defaults.
- A failed config write threw away an identity the network call had already
  returned, exiting 1 ("check your disk") or 2 ("usage error") with `data: null`.
  `ExitCode.SAVE_FAILED` exists for exactly this and `poll.py` already uses it;
  the identity now reaches stdout in `details` either way.
- `config init` wrote `api_key = "env:UNSTRACT_PLATFORM_KEY"` into every starter
  profile, and an `env:` reference to an unset variable is a `config doctor`
  problem -- so doctor exited 1 for every user without a platform key, which
  this PR's own comment calls the common case. The key is dropped from the
  starter blocks.
- `core/platform.py` -- the only wire-facing new code -- had no test executing
  it at all, because every command test replaces the factory. Two mutations
  (breaking the whoami URL, forcing every listing to organisation "") left the
  suite green. `tests/test_platform.py` now exercises the real class; both
  mutations fail it.

Also fixed: a 204 or non-dict body raised an AttributeError that matched no arm
in `__main__`, so the caller got a traceback and no envelope -- the one thing
this CLI promises never to do; `requests` transport errors (a scheme-less
base_url, a proxy's HTML on a 200) reached the entry point's full-disk handler
and were reported as "Check the path and disk."; `PlatformAPIError` folded up to
2KB of server body into `error.message`, which `emit_error` documents as a
one-line summary; `--transport-timeout` was accepted on `deployment ls` and
ignored (8.65s elapsed against a 1s flag), and `auth` had no such flag at all;
`api_path_prefix` was hard-wired, so whoami and ls were unreachable on precisely
the self-hosted installs the onprem-example profile caters to, while `clone`
worked; `whoami --save` rewrote a discovered project-local `.unstract.toml`,
dropping its comments and narrowing its mode; the write was invisible outside
`-o json`; the 401 hint talked about deployments on a command that has none; and
the README claimed a deployment key "runs one deployment", contradicted by three
other statements including this file's own KEY_SOURCES.

Every fix is mutation-checked: reverting it fails a named test, nine of nine.

ESCALATED, needs a decision: `GET /api/v1/unstract/whoami/` is served only by
Zipstack/unstract#2269, which is unmerged. Against any released Unstract the
README's documented first command 404s, and `hint_for(404)` sends the reader
after a resource id that does not exist. `config doctor --probe` inherits it and
exits non-zero on a good setup. Whether this CLI ships before the backend, and
what it should say when it does, is a release call rather than a fix.

Findings: A B C D E F G H I J K L M N O(escalated) P

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K92qFRNecJ5kNkKcmJ8qkM
@pk-zipstack
pk-zipstack marked this pull request as ready for review September 2, 2026 03:11
@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds an organization-less, platform-key-authenticated whoami endpoint and publishes it through the generated OpenAPI contract.

  • Resolves organization and permission metadata from the authenticated platform API key.
  • Updates organization middleware handling and top-level routing for the organization-less path.
  • Extends spec generation, contract tests, endpoint tests, and critical-path coverage.

Confidence Score: 5/5

The PR appears safe to merge, with no actionable changed-code defects identified.

The new route remains behind platform-key authentication, derives identity from the authenticated key row, preserves tenant scoping on the organization-qualified alias, and keeps the generated contract aligned with runtime behavior.

Important Files Changed

Filename Overview
backend/platform_api/whoami_views.py Adds the organization-less identity response using the platform key already resolved by authentication middleware.
backend/account_v2/custom_auth_middleware.py Not changed directly; investigation confirmed it remains the authentication and platform-key organization boundary used by the new endpoint.
backend/middleware/organization_middleware.py Ensures organization-less whitelisted requests still define organization_id before downstream middleware executes.
backend/backend/base_urls.py Mounts the dedicated whoami URL configuration ahead of the tenant URL set under the shared API prefix.
backend/backend/settings/base.py Adds the narrowly anchored organization-middleware exception and platform-key OpenAPI metadata.
backend/platform_api/openapi_schema.py Defines the identity operation, response fields, platform-key security scheme, and middleware-specific error shapes.
backend/api_v2/management/commands/generate_docstudio_spec.py Broadens spec path validation to the two intended public mounts while retaining rejection of other prefixes.
specs/docstudio-oss.json Publishes the generated whoami operation and its response and authentication components.

Sequence Diagram

sequenceDiagram
  participant C as API Client
  participant O as OrganizationMiddleware
  participant A as CustomAuthMiddleware
  participant W as WhoAmIView
  participant D as PlatformApiKey / Organization

  C->>O: "GET /api/v1/unstract/whoami/<br/>Bearer platform key"
  O->>O: "Preserve org-less path<br/>organization_id = None"
  O->>A: Continue middleware chain
  A->>D: Resolve active key and organization
  D-->>A: Key, service account, organization
  A->>W: Bind platform_api_key and request.user
  W->>D: Read key and organization fields
  W-->>C: "organization_id, organization_name,<br/>permission, key_name"
Loading

Reviews (1): Last reviewed commit: "UN-4016 [FIX] Withdraw the 405 declarati..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 17.6
e2e-coowners e2e 1 0 0 0 1.4
e2e-etl e2e 1 0 0 0 8.3
e2e-login e2e 2 0 0 0 1.6
e2e-prompt-studio e2e 1 0 0 0 4.5
e2e-smoke e2e 2 0 0 0 1.4
e2e-workflow e2e 1 0 0 0 16.4
frontend unit 0 1 0 0 0.0
integration-backend integration 330 0 0 26 46.0
integration-connectors integration 1 0 0 7 7.7
integration-workers integration 157 0 0 1 50.5
ui e2e 0 1 0 0 0.0
unit-backend unit 1167 0 0 1 42.5
unit-connectors unit 63 0 0 0 9.7
unit-core unit 33 0 0 0 1.3
unit-platform-service unit 15 0 0 0 2.6
unit-rig unit 120 0 0 0 4.7
unit-runner unit 5 0 0 0 3.0
unit-sdk1 unit 563 0 0 0 29.5
unit-workers unit 1397 0 0 1 123.8
TOTAL 3862 2 0 36 372.3

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • platform-key-whoami — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant