Skip to content

fix(containers): serve container content only to users who can read it - #38321

Open
AP2300 wants to merge 4 commits into
mainfrom
container-contentlet-lookup-read-check
Open

AP2300 wants to merge 4 commits into
mainfrom
container-contentlet-lookup-read-check

Conversation

@AP2300

@AP2300 AP2300 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

GET /api/v1/containers/{containerId}/content/{contentletId} and GET /api/v1/containers/content/{contentletId}?containerId= render a contentlet inside a container. When the contentlet is given by identifier, it is now resolved with the caller's permissions, the same way the inode form already was.

Changes

  • ContainerResource.containerContent, identifier form:
    • The version to serve comes from the contentlet's version info, skipping archived versions, with the DEFAULT variant preferred:
      • the working version for back-end users in edit mode;
      • the live version for any other user.
    • That version is loaded with contentletAPI.find(inode, user, true).
    • Front-end roles, including CMS Anonymous, count towards READ. The permission API applies CMS Anonymous only to live versions, so it never grants access to a working version.
    • While a Time Machine date is set (as the page API sets for a future publishDate), a caller served the live version must also be able to read the working version, because the container can then render the working version of scheduled content.
    • No READ answers 403. No version to serve answers 404.
  • Inode form: unchanged permission check. An inode that belongs to no contentlet now answers 404 instead of failing on a null contentlet.
  • A 403 from these endpoints carries a generic message ("The user does not have the permissions required for this request") instead of the underlying exception text.
  • VersionableAPI, already a constructor argument, is now kept as a field.
  • Javadoc added or updated on containerContent, the new helper and the testing constructor. No @Operation/@ApiResponse changes, so openapi.yaml is unchanged.

Tests

  • ContainerResourceTest (unit, new, 11 tests):
    • identifier form without READ → 403, nothing rendered;
    • inode form without READ → 403;
    • the 403 body and error-message header carry no contentlet title, identifier or inode;
    • no version → 404;
    • a live version found in another language/variant is the one read as the caller;
    • no live version → 404;
    • unknown inode → 404;
    • archived versions are skipped;
    • the DEFAULT variant is preferred;
    • a back-end caller is checked on the working version;
    • with a Time Machine date, READ on the working version is also required.
  • ContainerResourceContentReadAccessTest (integration, new, 19 tests, registered in MainSuite3a with just add-it-test). It runs with real users, roles, content and permissions; only WebResource and the Velocity merge are mocked. It covers:
    • back-end and front-end users without READ, both URL forms → 403;
    • the inode form → 403;
    • a generic 403 body;
    • a back-end user with READ, on published content and on content never published → rendered;
    • content the anonymous role can read, for back-end and front-end users → rendered, including a front-end user on published content that has a newer draft;
    • a front-end user on content never published → 404;
    • a back-end user whose READ comes only from the anonymous role → 403 on content with a newer draft and on content never published;
    • the query-parameter form with a front-end user without READ → 403;
    • a front-end user with a Time Machine date on content with a newer draft → 403;
    • a reader on content with a newer draft, an administrator, and a reader by inode → rendered;
    • archived content → 404.
  • Checked against a local build: the same requests before and after, as back-end, front-end, authorized non-admin, admin and anonymous users.

Suite placement: per docs/testing/INTEGRATION_TESTS.md, the new JUnit 4 class goes in the MainSuite* that just suite-timings names (8 runs sampled, 2026-10-29):

sampled: https://github.com/dotCMS/core/actions/runs/40867467903
sampled: https://github.com/dotCMS/core/actions/runs/40841635863
sampled: https://github.com/dotCMS/core/actions/runs/40832712086
sampled: https://github.com/dotCMS/core/actions/runs/40824774206
sampled: https://github.com/dotCMS/core/actions/runs/40822410726
sampled: https://github.com/dotCMS/core/actions/runs/40819766829
sampled: https://github.com/dotCMS/core/actions/runs/40809770137
sampled: https://github.com/dotCMS/core/actions/runs/40805197024

MainSuite    median  samples   each run (min)
3a           15.8m       7   16, 17, 16, 16, 16, 16, 16
2a           15.9m       7   16, 16, 17, 16, 15, 16, 16
2b           19.1m       7   19, 17, 22, 16, 19, 19, 19
1a           19.5m       7   20, 20, 20, 18, 19, 18, 19
1b           26.9m       7   28, 27, 23, 26, 27, 21, 30

-> register new coverage in MainSuite3a (15.8 min median)
   slowest suite is MainSuite1b (26.9 min median)

Risk

  • Callers who can read the contentlet see no change.
  • Identifier form, user without READ: 403 (previously 200).
  • A back-end user who can read published content only through CMS Anonymous gets 403 when that content has a newer working version, since back-end users are served the working version. Front-end users keep getting the live version.
  • Identifier form with no version to serve (e.g. a front-end user and content never published, or archived content): 404 instead of an empty render or a 400.
  • While a Time Machine date is set, a caller served live content also needs READ on the working version.
  • Inode form, unknown inode: 404 instead of 500.
  • 403 bodies no longer include the underlying exception message.
  • Rollback-safe: no schema, index or API contract change.

Fixes dotCMS/private-issues#734

🤖 Generated with Claude Code

The container content endpoints now resolve a contentlet given by
identifier with the caller's permissions: back-end users in edit mode
are checked on the working version, any other user on the live
version. A user without READ gets 403 with a generic message; a
contentlet with no version to serve gets 404.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label Oct 28, 2026
@AP2300
AP2300 marked this pull request as ready for review October 29, 2026 13:21
AP2300 and others added 3 commits October 29, 2026 09:23
…lookup-read-check

# Conflicts:
#	dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java
… under Time Machine

- Content given by identifier no longer resolves archived versions,
  as before this change.
- While a Time Machine date is set, a caller served live content must
  also be able to read the working version, since the container can
  render it.
- Tests for the DEFAULT-variant preference, the working version served
  to back-end users, admin and inode access, and anonymous-only READ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lookup-read-check

# Conflicts:
#	dotCMS/src/main/java/com/dotcms/rest/api/v1/container/ContainerResource.java

This branch has not been deployed

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

Labels

Area : Backend PR changes Java/Maven backend code

1 participant