Skip to content

fix: sites resolve PLG gating#2309

Merged
Sugandhgyl merged 64 commits intomainfrom
fix/sites-resolve-plg-gating
May 6, 2026
Merged

fix: sites resolve PLG gating#2309
Sugandhgyl merged 64 commits intomainfrom
fix/sites-resolve-plg-gating

Conversation

@Sugandhgyl
Copy link
Copy Markdown
Contributor

@Sugandhgyl Sugandhgyl commented May 4, 2026

Please ensure your pull request adheres to the following guidelines:

  • make sure to link the related issues in this description. Or if there's no issue created, make sure you
    describe here the problem you're solving.
  • when merging / squashing, make sure the fixed issue references are visible in the commits, for easy compilation of release notes

If the PR is changing the API specification:

  • make sure you add a "Not implemented yet" note the endpoint description, if the implementation is not ready
    yet. Ideally, return a 501 status code with a message explaining the feature is not implemented yet.
  • make sure you add at least one example of the request and response.

If the PR is changing the API implementation or an entity exposed through the API:

  • make sure you update the API specification and the examples to reflect the changes.

If the PR is introducing a new audit type:

  • make sure you update the API specification with the type, schema of the audit result and an example
Screenshot 2026-05-05 at 11 16 14 AM Screenshot 2026-05-05 at 10 05 37 AM

Related Issues

Thanks for contributing!

Sugandhgyl and others added 30 commits November 14, 2025 21:48
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>
Sugandhgyl and others added 4 commits May 4, 2026 21:55
…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>
Sugandhgyl and others added 2 commits May 5, 2026 11:31
…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>
Comment thread package.json Outdated
Comment thread src/controllers/sites.js
@Kanishkavijay39
Copy link
Copy Markdown
Contributor

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,
column-mapping change, or DTO drift would silently break this feature. Add at minimum: external caller + no entitlement, external caller + PRE_ONBOARD tier, and internal caller + no entitlement remapped to site_not_enrolled.

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
resolve site') shape (not an unhandled 500).

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
remap only narrows 404 detail and cannot widen data access (the 200 path is still gated on accessControlUtil.hasAccess(organization) for the requested org). But this trust assumption should be documented. Either: (a) add a note in the OpenAPI
description that this param is a UX hint only and not an authorization signal, or (b) consider deriving callerIsInternal from the authenticated principal if the caller's IMS org is available in context.auth. Also add a test confirming that a
spoofed callerImsOrg does not change any 200-OK response.

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
breaks. Rename to resolveStatusFailure to make scope explicit, or have it omit undefined fields.

Minor (Nice to Have)

  • src/controllers/sites.js:1252, 1262 - The remap branch log.info lines fire in production on every internal-caller remap. Downgrade to log.debug once the feature is confirmed working.
  • src/controllers/sites.js:1213, 1215 - Log callerImsOrg via a structured object rather than string interpolation to avoid CRLF log injection: log.info('[resolveSite] caller resolved', { callerImsOrg, callerIsInternal }).
  • src/support/utils.js:1944 - The orphan @returns {Array} JSDoc line above the new helpers looks like it belonged to the function below. Delete or reposition.
  • docs/openapi/sites-api.yaml:302-309 - The description exposes internal implementation details (ASO_PLG_EXCLUDED_ORGS, "Spacecat UUID"). Trim to the client-facing behavior: "IMS Org of the calling shell. Used by internal Adobe surfaces to
    suppress the PLG onboarding wizard for demo/internal orgs. External callers can omit."
  • src/controllers/sites.js - The aso_pre_onboard status value is product-prefixed in an otherwise product-agnostic discriminator set. pre_onboard with the product code in details.productCode would avoid a parallel llmo_pre_onboard appearing
    when the next product reuses this endpoint. Cheaper to change now than after UI clients ship against the string.
  • test/controllers/sites.test.js - Add a test for callerImsOrg: '' confirming it behaves identically to omitting the param.
  • test/controllers/sites.test.js - Add a test for accessControlUtil.hasAccess(organization) returning false to confirm the response is the generic 404 with no details.

Sugandhgyl and others added 4 commits May 5, 2026 21:29
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>
@Sugandhgyl Sugandhgyl merged commit a0ad339 into main May 6, 2026
18 checks passed
@Sugandhgyl Sugandhgyl deleted the fix/sites-resolve-plg-gating branch May 6, 2026 09:23
solaris007 pushed a commit that referenced this pull request May 6, 2026
## [1.476.2](v1.476.1...v1.476.2) (2026-05-06)

### Bug Fixes

* sites resolve PLG gating ([#2309](#2309)) ([a0ad339](a0ad339))
@solaris007
Copy link
Copy Markdown
Member

🎉 This PR is included in version 1.476.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants