feat: add /sites/:siteId/permissions/check for LLMO deploy authz (LLMO-6848) - #3035
feat: add /sites/:siteId/permissions/check for LLMO deploy authz (LLMO-6848)#3035jeddiec wants to merge 5 commits into
Conversation
… 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>
|
This PR will trigger a minor release when merged. |
|
Verified the building blocks compose correctly ( One thing that looks broken as currently described, though — the identity model:
This PR's base ( 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 Given |
…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>
|
@baranovskyalexandr — great catch. Verified: post-#3022 the only IMS surface left is Better news from digging: the repo already has the pattern this should follow —
That's essentially the CDN-routing instance of exactly this deploy check. So instead of my 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 + |
…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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
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 |
|
Closing this PR and moving forward with #3038 |
Why (LLMO-6848)
The LLMO UI carries a
dx_llmoIMS token that reaches Mystique/v1/applybut 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 thellmo/*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:
Original assumption (Mystique forwards the user's raw IMS bearer, endpoint reads
authInfo) does not hold — thanks @baranovskyalexandr for catching it:ApiKeyImsHandler, route-scoped to/tools/api-keys/*, so a rawdx_llmobearer to this route 401s.subjectIdcapability lookup resolves the wrong identity.Planned rework: use the existing promise-token path in
src/support/edge-routing-auth.js—getImsTokenFromPromiseToken(readsx-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_deploystate-layer grant used below? Building to whichever you pick.What it does (current diff)
POST /sites/:siteId/permissions/check— headerx-product, body{ capability? }(defaults to<product>/can_deploy). Returns200 { 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 / tiercan_deploystate-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-layercan_deploygrant. Trial without a grant is default-denied; PLG / non-visible tiers are denied byvalidateEntitlement.Wired as
llmo/can_view(+aso/can_view) infacs-capabilities.jssofacsWrapperrequires the caller be at least a viewer of the site; thecan_deployanswer is computed inside.Owner decisions
The exact— resolved (PRODUCT_CODESvaluevalidateEntitlementexpects for LLMOEntitlementModel.PRODUCT_CODES.LLMO === 'LLMO'; uppercase header passes through correctly).tier === PAIDgate with the grant purely as the trial exception, vs. using the grant as the paid/trial gate (marked where to branch).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.jsmirrors thestate-access-mappingsesmock/sinon pattern.Jira: LLMO-6848
🤖 Generated with Claude Code