Skip to content

feat: add /sites/:siteId/permissions/check for LLMO deploy authz (LLMO-6848) - #3035

Closed
jeddiec wants to merge 5 commits into
mainfrom
feat/llmo-6848-permissions-check-endpoint
Closed

feat: add /sites/:siteId/permissions/check for LLMO deploy authz (LLMO-6848)#3035
jeddiec wants to merge 5 commits into
mainfrom
feat/llmo-6848-permissions-check-endpoint

Conversation

@jeddiec

@jeddiec jeddiec commented Aug 12, 2026

Copy link
Copy Markdown

Why (LLMO-6848)

The LLMO UI carries a dx_llmo IMS token that reaches Mystique /v1/apply but carries no per-resource authorization signal. Rather than re-implement SpaceCat's entitlement + ReBAC in Python (MacGiver, facs_access_mappings, TierClient, legacy-vs-ReBAC branching), Mystique can call back into SpaceCat and reuse the authorization that already runs in prod on the llmo/* routes.

This PR adds that endpoint. It's the durable Option 1 from the LLMO-6848 decision (route authz through SpaceCat) — put up as a reviewable diff so the team can make an explicit call:

Decision for the thread: accept the coarse, time-boxed #654 edge widening as the interim, or ship this and skip the throwaway widen (once verified fail-closed, the gateway strips to identity-only and supersedes #630/#654).

⚠️ Identity model — being reworked (review follow-up)

Original assumption (Mystique forwards the user's raw IMS bearer, endpoint reads authInfo) does not hold — thanks @baranovskyalexandr for catching it:

Planned rework: use the existing promise-token path in src/support/edge-routing-auth.jsgetImsTokenFromPromiseToken (reads x-promise-token → user's IMS token) + authorizeEdgeCdnRouting-style authorization (paid = LLMO product context; trial = LLMO Admin group; + FACS for the site). Ideally extract that core so LLMO deploy-authz lives in one place. Open question for owners: is the canonical deploy model that edge-routing one, or the entitlement + can_deploy state-layer grant used below? Building to whichever you pick.

What it does (current diff)

POST /sites/:siteId/permissions/check — header x-product, body { capability? } (defaults to <product>/can_deploy). Returns 200 { allowed, reason, capability } — it reports the decision rather than 403-ing, so Mystique can surface a clean deny.

Composes two building blocks already in prod:

  • AccessControlUtil.validateEntitlement — entitlement / enrollment / tier
  • site → brand resolution + the can_deploy state-layer grant (listBrandIdsForSite + listResourceIdsWithCapability, which checks both user and org subject scopes)

Rule (LLMO-6848 AC): allowed = entitlement OK AND caller/org holds <capability> on any brand of the site AND tier ∈ { PAID, FREE_TRIAL }. The FREE_TRIAL case is admitted only when the grant is explicitly present — i.e. the per-customer trial exception IS a state-layer can_deploy grant. Trial without a grant is default-denied; PLG / non-visible tiers are denied by validateEntitlement.

Wired as llmo/can_view (+ aso/can_view) in facs-capabilities.js so facsWrapper requires the caller be at least a viewer of the site; the can_deploy answer is computed inside.

Owner decisions

  1. The exact PRODUCT_CODES value validateEntitlement expects for LLMOresolved (EntitlementModel.PRODUCT_CODES.LLMO === 'LLMO'; uppercase header passes through correctly).
  2. Hard tier === PAID gate with the grant purely as the trial exception, vs. using the grant as the paid/trial gate (marked where to branch).
  3. Identity/authz model — see the ⚠️ section above.

Status

Draft. Reworking the identity model per review; the entitlement/brand-grant composition itself is verified against prod signatures. Test file test/controllers/permissions.test.js mirrors the state-access-mappings esmock/sinon pattern.

Jira: LLMO-6848

🤖 Generated with Claude Code

… deploy authz (LLMO-6848)

Resource-scoped authorization introspection ("may this caller <capability>
this site?") so Mystique /v1/apply can reuse SpaceCat's authorization instead
of re-implementing entitlement + ReBAC in Python.

Composes two building blocks already running in prod on the llmo/* routes:
- AccessControlUtil.validateEntitlement (entitlement/enrollment/tier)
- site -> brand resolution + the can_deploy state-layer grant
  (listBrandIdsForSite + listResourceIdsWithCapability)

Rule (LLMO-6848 AC): allowed = entitlement OK AND can_deploy grant on the
site's brand AND tier in {PAID, FREE_TRIAL}; the trial exception IS the
state-layer grant. Two internal-contract decisions are flagged in-code as
TODO(spacecat) for the owners (productCode value; optional hard tier===PAID
gate). Draft for the #654-interim vs ship-this-durable decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeddiec jeddiec changed the title LLMO-6848: POST /sites/:siteId/permissions/check — reuse SpaceCat authz for Mystique /v1/apply (DRAFT) feat: add /sites/:siteId/permissions/check for LLMO deploy authz (LLMO-6848) Aug 12, 2026
@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

@baranovskyalexandr

Copy link
Copy Markdown
Contributor

Verified the building blocks compose correctly (validateEntitlement, listBrandIdsForSite, listResourceIdsWithCapability signatures all match) — nice touch closing TODO #1 yourself for free: EntitlementModel.PRODUCT_CODES.LLMO === 'LLMO', so passing the uppercase header value straight through is already correct, nothing to confirm there.

One thing that looks broken as currently described, though — the identity model:

"the endpoint authorizes the caller (Mystique forwards the end user's IMS bearer)... It does not authorize a service account acting on the user's behalf."

This PR's base (a2179c9f, 2026-08-12) is after #3022 (merged 2026-08-10), which removed AdobeImsHandler from the global AUTH_HANDLERS chain — the only IMS auth path left is ApiKeyImsHandler, and it's route-scoped to /tools/api-keys/* only. POST /sites/:siteId/permissions/check isn't in that allowlist, so forwarding a raw dx_llmo IMS bearer as Authorization: Bearer <token> to this endpoint will 401 — the auth chain has nowhere left to accept it.

The alternative — Mystique calling through its existing S2S client instead — doesn't satisfy the stated model either: that authenticates as the S2S service account, so ctx.attributes.authInfo won't be the real user, and subjectId in the listResourceIdsWithCapability call will resolve to the wrong identity for the per-user capability check (only the org-level scope would still work).

Given #3022's own migration note says "delegated flows keep using x-promise-token" — is the intent that Mystique exchanges the promise token for a SpaceCat JWT session first, then calls this endpoint with that session token? If so, that's worth spelling out explicitly in the PR description (and probably needs its own small addition here or on the Mystique side), since as written today there's no path that actually produces "the caller's" identity on this route.

…uct, null-sub branches

Lifts branch coverage on src/controllers/permissions.js past the 90% gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeddiec

jeddiec commented Aug 12, 2026

Copy link
Copy Markdown
Author

@baranovskyalexandr — great catch. Verified: post-#3022 the only IMS surface left is ApiKeyImsHandler (scoped to /tools/api-keys/*), so a raw dx_llmo bearer to this route 401s, and the S2S path authenticates as the service account (wrong subjectId). The authInfo-from-bearer model in my description doesn't hold. And thanks for closing TODO #1 — good to know EntitlementModel.PRODUCT_CODES.LLMO === 'LLMO'.

Better news from digging: the repo already has the pattern this should follow — src/support/edge-routing-auth.js:

  • getImsTokenFromPromiseToken(context) reads x-promise-token/cookie → exchangePromiseToken → the user's IMS token, and
  • authorizeEdgeCdnRouting(...) authorizes an LLMO action for a site (paid: LLMO product context in the profile; trial: LLMO Admin group; + a FACS capability lookup for siteId).

That's essentially the CDN-routing instance of exactly this deploy check. So instead of my authInfo + validateEntitlement + listResourceIdsWithCapability path, I'll rework this to: read x-promise-tokengetImsTokenFromPromiseToken → resolve the profile → authorize — ideally extracting the core of authorizeEdgeCdnRouting so LLMO deploy-authz lives in one place, not two.

One for you since you own this area: which model is canonical for deploy — the edge-routing one (paid = product context; trial = LLMO Admin group) + FACS, or the entitlement + can_deploy state-layer grant I used? I'll build to whichever and update the description accordingly. Leaning toward reusing authorizeEdgeCdnRouting's so there's a single implementation.

…ntories

Adds the new route to required-capabilities.js INTERNAL_ROUTES (mirroring its
sibling /user/capabilities, FACS-gated via facs-capabilities.js) and to the
expected dynamic-route list in test/routes/index.test.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@solaris007 solaris007 added the enhancement New feature or request label Aug 13, 2026
@solaris007
solaris007 marked this pull request as ready for review August 13, 2026 07:40
@solaris007
solaris007 requested a review from MysticatBot August 13, 2026 07:40
@MysticatBot

Copy link
Copy Markdown

Mysticat review failed: Claude CLI crashed (exit 1): stderr= stdout={"type":"result","subtype":"error_max_budget_usd","duration_ms":521253,"duration_api_ms":1456497,"is_error":true,"num_turns":55,"stop_reason":"tool_use","session_id":"e9366c3e-3bc0-4d38-9715-b5a75f9ea1d7","total_cost_usd":10.008379,"usage":{"input_tokens":30,"cache_creation_input_tokens":178568,"cac

@solaris007
solaris007 marked this pull request as draft August 13, 2026 08:34
@ravverma

Copy link
Copy Markdown
Contributor

Closing this PR and moving forward with #3038
as we have now enhanced RBAC to check site based end point access control in FACS layer itself for LLMO

@ravverma ravverma closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants