fix: sites resolve PLG gating#2309
Conversation
Replace the generic 404 on the siteId-path branch of resolveSite with
three explicit failures so the UI can distinguish "not enrolled" from
"site doesn't exist" and stop falling through to PLG onboarding:
- no_entitlement_for_product: org has no entitlement for x-product
- aso_pre_onboard: entitlement tier not in CUSTOMER_VISIBLE_TIERS
- site_not_enrolled: entitlement visible but no SiteEnrollment
HTTP status stays 404. Body shape: { message, errorCause, details }.
Also sets x-error-cause header. Success payload unchanged.
Adds three unit tests covering the new branches; updates the existing
PRE_ONBOARD-via-siteId test to assert the new envelope.
Spec: experience-success-studio-ui/specs/sites-resolve-plg-gating.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removed the `details ? { details } : {}` conditional spread in the
resolveFailure helper — all three callers always pass details, so the
false branch was unreachable. Including `details` unconditionally keeps
the response shape stable and gets branch coverage back to 100%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…via callerImsOrg Internal/demo Adobe orgs (listed in ASO_PLG_EXCLUDED_ORGS) should never see the PLG onboarding wizard, even when accessing customer data via backoffice URLs (?organizationId=customer_uuid). The check is keyed off the caller's identity, not the site's org, so it works regardless of the URL params used. - Adds optional 'callerImsOrg' query param representing the caller's AEC shell IMS org (separate from imsOrg, which the frontend may drop when organizationId is also sent). - At the top of resolveSite, looks up the caller's Spacecat UUID via Organization.findByImsOrgId(callerImsOrg) and computes a single callerIsInternal flag using isInternalOrg(orgId, env). - siteId path remaps no_entitlement_for_product / aso_pre_onboard → site_not_enrolled when callerIsInternal so internal callers see the "No site onboarded" UI instead of the PLG wizard. site_not_enrolled itself is unchanged. organizationId / imsOrg paths need no remap — their generic 404 already maps to "No site onboarded" in the UI. - Reuses isInternalOrg / parseCommaSeparatedEnvList from support/utils.js (shared with plg-onboarding.js). Backwards compatible: missing callerImsOrg → callerIsInternal = false → original behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- OpenAPI: add callerImsOrg query parameter to /sites-resolve with description of the internal/demo org bypass behavior. - package.json: point deploy-dev at sugandhg lambda alias for developer-isolated dev deploys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…decide Previously, internal/demo callers (ASO_PLG_EXCLUDED_ORGS) with a PRE_ONBOARD data org always got site_not_enrolled regardless of enrollment state. The intent was to suppress the PLG wizard — but it also prevented internal callers from seeing the site dashboard when the site was actually enrolled. Fix: for internal callers, skip the non-CUSTOMER_VISIBLE tier check entirely and fall through to the enrollment check. If enrolled → 200 dashboard; if not enrolled → site_not_enrolled. Customer callers are unaffected. The no-entitlement branch is unchanged (login fails before reaching the dashboard anyway, so site_not_enrolled is correct for both caller types). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Missing integration tests in test/it/ CLAUDE.md requires IT tests for modified endpoints. No new cases were added to test/it/ for the structured 404 responses or the internal-caller remap. The existing unit tests stub findByImsOrgId entirely, so a model-method rename, No test for Organization.findByImsOrgId throwing All 13 new test cases stub findByImsOrgId to resolve. None reject. Combined with issue 2, this is the highest-risk untested branch. Add a case where findByImsOrgId rejects and assert the response matches the expected badRequest('Failed to callerImsOrg trust model should be documented The parameter is read from the query string with no server-side check that the caller actually belongs to that IMS org. A client can pass any internal org's IMS ID and get the "internal" response shape. The security impact is bounded - the resolveFailure helper naming invites misuse src/controllers/sites.js:1200-1204 - The OpenAPI spec says generic 404s omit resolveStatus and details, but the helper always includes both. If a future maintainer reaches for this helper for a generic not-found case, the contract silently Minor (Nice to Have)
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…min bypass - When Organization.findByImsOrgId returns null on the imsOrg path, return no_entitlement_for_product (external caller) or site_not_enrolled (internal caller) instead of falling to the generic notFound. Covers brand-new customers whose org is not yet in the Spacecat DB and personal-dev environments where the caller org may not exist. - Add !hasAdminAccess() guard to the aso_pre_onboard tier check so admin users with a PRE_ONBOARD entitlement still reach the dashboard. d25ce6e4 accidentally blocked this path by inserting the tier check before the admin+enrolled success branch. - Update two tests: assert no_entitlement_for_product on null-org path; update stale internal-caller test to expect site_not_enrolled (d25ce6e4 changed this). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Organization.findByImsOrgId(callerImsOrg) was outside the main try/catch, so a DB driver error or network blip would propagate as an unhandled exception and return a raw 500 instead of the expected error shape. Wrap the lookup in a local try/catch that fails open: a lookup failure leaves callerIsInternal=false (external-caller default), which is the safe fallback — an unverifiable caller never receives internal bypass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Site - Add test: callerIsInternal=true + imsOrg not in DB → site_not_enrolled (covers the true branch of callerIsInternal ternary at the org-not-found guard) - Add test: callerImsOrg lookup throws → warn and treat as non-internal (covers the new try/catch catch block) - Add test: imsOrg path, visible entitlement, no enrolled site → site_not_enrolled (covers the site_not_enrolled fallback at end of imsOrg block) - Add test: non-admin, visible tier, enrolled site → 200 (covers the CUSTOMER_VISIBLE_TIERS branch of the enrolledSite OR condition, which was short-circuited when hasAdminAccess returns true in test env) - Drop imsOrgEntitlement?. optional chain at enrollment check: entitlement is guaranteed non-null at that point (null already returned early), so the ?. was an untestable dead branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… per-request log Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## [1.476.2](v1.476.1...v1.476.2) (2026-05-06) ### Bug Fixes * sites resolve PLG gating ([#2309](#2309)) ([a0ad339](a0ad339))
|
🎉 This PR is included in version 1.476.2 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Please ensure your pull request adheres to the following guidelines:
describe here the problem you're solving.
If the PR is changing the API specification:
yet. Ideally, return a 501 status code with a message explaining the feature is not implemented yet.
If the PR is changing the API implementation or an entity exposed through the API:
If the PR is introducing a new audit type:
Related Issues
Thanks for contributing!