Skip to content

feat(permissions): add POST /sites/:siteId/permissions/check/deploy - #3038

Open
ravverma wants to merge 3 commits into
mainfrom
feat/site-deploy-permission-check
Open

feat(permissions): add POST /sites/:siteId/permissions/check/deploy#3038
ravverma wants to merge 3 commits into
mainfrom
feat/site-deploy-permission-check

Conversation

@ravverma

Copy link
Copy Markdown
Contributor

What

Adds a new internal endpoint POST /sites/:siteId/permissions/check/deploy — a site-scoped probe for the "deploy" capability (apply / roll back optimizations). It returns 200 { "hasPermission": true } when the caller may deploy and 403 otherwise, so UI flows can show/hide a deploy control before the user acts.

Authorization

The route is FACS-governed for both products — mapped to llmo/can_deploy and aso/can_deploy in facs-capabilities.js. For FACS‑enrolled orgs (ReBAC enabled) facsWrapper enforces can_deploy before the handler runs (ASO via the :siteId primary resource, LLMO via the site→brands secondary resolver — established in #3030), so a 200 already means the caller is authorized.

The controller only adds the ReBAC‑disabled fallback the wrapper leaves to it for non‑FACS orgs:

  • LLMOhasLlmoCapabilityForSite(site) (falls back to the legacy isLLMOAdministrator() claim).
  • ASOhasAccess(site, 'auto_fix') only when facs_enabled is not set (the legacy ASO deploy gate; auto_fix maps to dx_aem_perf_auto_fix, minted only for ASO logins). This mirrors the facs_enabled gate already used by edge-routing-auth / hasLlmoCapabilityForSite.

It is an internal route — intentionally absent from routeRequiredCapabilities (INTERNAL_ROUTES), so it is never exposed to S2S consumers.

Changes

  • src/controllers/sites.js — new checkDeployPermission method (+ import X_PRODUCT_HEADER; removed a now‑duplicate local X_PRODUCT_HEADER in resolveSite).
  • src/routes/index.js — route → handler.
  • src/routes/facs-capabilities.jsllmo/can_deploy + aso/can_deploy.
  • src/routes/required-capabilities.js — added to INTERNAL_ROUTES.
  • docs/openapi/{site-api,api}.yaml — OpenAPI path (docs/index.html is gitignored; regenerate via npm run docs:build).

Testing

  • 8 unit tests (test/controllers/sites.test.js): LLMO grant/deny, ASO ReBAC‑disabled grant/deny (asserts the auto_fix scope), ASO facs_enabled bypass (asserts the legacy check does not run), no‑header default, 404.
  • Route‑wiring tests (test/routes/index.test.js) + FACS union‑equality / required‑capabilities coverage invariants pass.
  • npm run lint, npm run type-check, npm run docs:lint (valid, 0 new warnings) all green.

Note: no test/it/ integration test — the endpoint's logic is FACS/JWT‑claim‑driven and the IT harness personas (admin bypasses FACS; user is a plain org member) can't exercise the facs_enabled bypass or capability branches; unit tests cover those.

Relates to LLMO-6938.

🤖 Generated with Claude Code

Site-scoped deploy-permission probe: returns 200 { hasPermission: true } when the
caller may deploy (apply / roll back optimizations), 403 otherwise. Intended for
UI flows that show or hide a deploy/apply control before the user acts.

- FACS-governed for both products: mapped to llmo/can_deploy and aso/can_deploy,
  so facsWrapper enforces the capability for FACS-enrolled orgs (ASO via the
  :siteId primary resource, LLMO via the site->brands secondary resolver).
- Internal route: absent from routeRequiredCapabilities, so it is never exposed
  to S2S consumers.
- Controller adds the ReBAC-disabled fallback the wrapper leaves to it:
  * LLMO -> hasLlmoCapabilityForSite (legacy isLLMOAdministrator fallback).
  * ASO  -> hasAccess(site, 'auto_fix') only when facs_enabled is not set.

Includes unit tests, route-wiring tests, and the OpenAPI path. Relates to LLMO-6938.

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

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ravverma,

⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.

Verdict: Request changes - the missing x-product header validation creates a path where the permission probe returns a false-positive for non-FACS orgs.
Complexity: HIGH - medium diff; auth + ReBAC + API surface signals.
Changes: Adds a deploy-permission probe endpoint (POST /sites/:siteId/permissions/check/deploy) that UI flows call to decide whether to show deploy controls (8 files).
Note: Recommend a human read before merge - this change modifies a shared contract (OpenAPI spec). The bot review is a complement to, not a replacement for, a human read here.

Must fix before merge

  1. [Important] Missing x-product header validation allows a weaker permission check for non-FACS orgs - src/controllers/sites.js:~2328 (details inline)
Non-blocking (3): minor issues and suggestions
  • nit: OpenAPI spec does not document x-product as a required request header parameter, so consumers reading the spec will not know to send it - docs/openapi/site-api.yaml:25
  • suggestion: The LLMO branch delegates FACS-awareness entirely to hasLlmoCapabilityForSite, while the ASO branch inlines the facs_enabled check. Consider extracting the ASO FACS+legacy check into a method on AccessControlUtil for symmetry - src/controllers/sites.js:~2339
  • suggestion: If more /permissions/check/{capability} probes are planned, define the URL scheme, response shape, and header contract now to avoid divergent one-offs

Comment thread src/controllers/sites.js
* - ASO (and any non-LLMO product) -> when ReBAC is disabled (`facs_enabled` not set), require
* org access carrying the `auto_fix` sub-service scope — the legacy ASO deploy gate. The
* `auto_fix` scope maps to `dx_aem_perf_auto_fix`, minted only for ASO logins.
*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): When x-product is absent or unrecognized (e.g. undefined, empty, lowercase 'llmo'), the handler falls into the ASO/fallback branch and sets autoFixSubService = '' because xProduct === 'ASO' is false. This means hasAccess(site, '') performs a plain org-membership check with no capability scoping - a weaker gate than the actual deploy endpoints enforce.

The permission probe then returns { hasPermission: true } for a user who will get a 403 on the real deploy call (which enforces the header and the auto_fix scope). This creates a misleading UI state where the deploy button appears but clicking it fails.

For FACS-enrolled orgs the wrapper compensates, but for non-FACS orgs the handler IS the enforcement layer and the fallback is too permissive.

Fix: Normalize and validate at the top of the handler:

const xProduct = context.pathInfo?.headers?.[X_PRODUCT_HEADER]?.toUpperCase?.();
if (xProduct !== 'LLMO' && xProduct !== 'ASO') {
  return badRequest('x-product header is required and must be LLMO or ASO');
}

This aligns with the resolveSite pattern in the same controller.

@MysticatBot MysticatBot added ai-reviewed Reviewed by AI complexity:high AI-assessed PR complexity: HIGH needs-human-review AI reviewer recommends a human read before merge labels Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

Address PR review (#3038): without x-product validation the ReBAC-disabled
fallback fell into the ASO branch with an empty sub-service and reported
hasPermission:true via a plain org-membership check — a false positive relative
to the real deploy endpoints, and inconsistent with facsWrapper, which lowercases
x-product before its route lookup.

- Normalize x-product to upper case and require it to be LLMO or ASO (400 else).
- Extract the ASO FACS + legacy deploy check into
  AccessControlUtil.hasAsoDeployCapabilityForSite, symmetric with
  hasLlmoCapabilityForSite.
- Document x-product as a required header parameter in the OpenAPI spec.

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

@lucianfelix lucianfelix left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned, implementation of this API is not necessary.

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ravverma,

⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.

Verdict: Approve - the prior blocking finding (x-product validation gap) is properly addressed; no new blocking issues found.
Complexity: HIGH - medium diff; auth + ReBAC + API surface signals.
Changes: Adds a deploy-permission probe endpoint with x-product validation, case normalization, and a symmetric ASO deploy-capability helper (11 files).
Note: Recommend a human read before merge - this change modifies a shared contract (OpenAPI spec). The bot review is a complement to, not a replacement for, a human read here.

Non-blocking (3): minor issues and suggestions
  • suggestion: hasAsoDeployCapabilityForSite returns true unconditionally when facs_enabled is set, relying on the invariant that ASO's wrapper never defers (the site is the primary resource). Consider adding a defensive log.warn for the unexpected-deferral case so it fails closed rather than open if the FACS topology ever evolves - src/support/access-control-util.js:309
  • nit: OpenAPI xProduct parameter enum lists values in upper case but description says case-insensitive; consumers reading the schema strictly may not realize lowercase is accepted - docs/openapi/parameters.yaml:430
  • suggestion: The ASO 403 message is a static string while the LLMO branch uses a FACS-aware llmoForbiddenMessage() helper. Consider a symmetric asoForbiddenMessage() so FACS-enrolled users get a contextual denial reason - src/controllers/sites.js:2353

Previously flagged, now resolved

  • x-product header validation now required and normalized (commit 629e3ae); the false-positive permission probe for non-FACS orgs is eliminated

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 3m 2s | Cost: $8.66 | Commit: 629e3ae6e06f4877645076c1678ef5138a17ad75
If this code review was useful, please react with 👍. Otherwise, react with 👎.

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

Labels

ai-reviewed Reviewed by AI complexity:high AI-assessed PR complexity: HIGH needs-human-review AI reviewer recommends a human read before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants