Repository navigation
Allow anonymous read access to already-public content (CONTENTBOX-1516) - #696
Conversation
- Add server-boxlang@1.json (pure BoxLang engine, mirrors coldbox-platform's config) since only the boxlang-cfml compat variant existed before. - box.json installPaths updated by `box install` for cbdebugger and route-visualizer devDependencies. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoEgT3ggNEKM1nGjqB5Qd5
ContentBox's ORM event handler (modules/contentbox/models/system/EventHandler.cfc) implements the legacy CFIDE.orm.IEventHandler interface. Pure BoxLang doesn't ship that compatibility shim by default; without it, ORM's MappingGenerator fails to compile the class during entity scanning and the whole app 500s before ever reaching a route. bx-compat-cfml (already used by server-boxlang-cfml@1.json) provides it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoEgT3ggNEKM1nGjqB5Qd5
Pure BoxLang engine isn't the target for this environment; the boxlang-cfml compat server (which already includes bx-compat-cfml) is the one to use going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoEgT3ggNEKM1nGjqB5Qd5
…TENTBOX-1516) Widen the contentbox-api-v1 firewall whitelist and drop the per-action secured="" annotations so that index/show on entries, pages, and contentStore no longer require authentication, since those actions already only return published content via findPublishedContent(). show() additionally now rejects (404) anonymous requests for content that is not published, is expired, or is password protected, since it resolves by raw id/slug with no visibility filtering otherwise. Every other endpoint (sites, menus, categories, comments, authors, all write verbs) remains fully authenticated. Adds PublicContentAccessSpec.cfc covering anonymous index/show, the draft-content 404 guard, write-verb rejection, and that admin-only resources stay locked.
…-box-setup-routing-4tto6x
|
CI on the first push (
Action taken: merged No code changes of my own were needed beyond the merge — waiting on CI to confirm green on Generated by Claude Code |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Critical access-control issues remain for inactive sites and authenticated users lacking content permissions.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 2
Open (2)
What changed in this PR
This pull request enables anonymous reads of published entries, pages, and content store items while preserving authenticated writes and administrative access.
Changes:
- Expands the API whitelist for public read actions.
- Filters anonymous content access by publication visibility.
- Adds access-control tests and development module paths.
| File | Summary |
|---|---|
tests/specs/contentbox-api/PublicContentAccessSpec.cfc |
Tests anonymous reads and protected operations. |
modules/contentbox/modules/contentbox-api/modules/contentbox-api-v1/ModuleConfig.cfc |
Expands the API firewall whitelist. |
modules/contentbox/modules/contentbox-api/modules/contentbox-api-v1/handlers/pages.cfc |
Makes page reads publicly accessible. |
modules/contentbox/modules/contentbox-api/modules/contentbox-api-v1/handlers/entries.cfc |
Makes entry reads publicly accessible. |
modules/contentbox/modules/contentbox-api/modules/contentbox-api-v1/handlers/contentStore.cfc |
Makes content store reads publicly accessible. |
modules/contentbox/modules/contentbox-api/modules/contentbox-api-v1/handlers/baseContentHandler.cfc |
Adds anonymous visibility checks for content access. |
box.json |
Adds development module installation paths. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Address Copilot findings on PR #696: - An anonymous request could read a site's published content even when the site itself is inactive, since baseContentHandler's preHandler() never checked isActive (SiteService.discoverSite() already applies that rule for the front-end). preHandler() now 404s an inactive site for any caller without full content access, exactly like an unknown site slug. - Removing entries/pages/contentStore's secured="" annotations meant any authenticated user, regardless of permission, skipped the visibility guard entirely and could read drafts/expired/password- protected content. show() now checks for ENTRIES_ADMIN/EDITOR-style permission (added as variables.contentType per handler) rather than merely being logged in. These actions are whitelisted out of the JWT firewall entirely, so prc.oCurrentAuthor is never populated for them by the framework; hasFullContentAccess() parses the incoming token itself via jwtAuth().parseToken() before checking permissions. Adds regression coverage for an authenticated admin still being able to view a draft. Also fixes three existing draft-content fixtures that were wrapped in withRollback(): the simulated test request runs its own ORM transaction and can't see an uncommitted row, so those tests were passing because the row was invisible, not because the visibility check ran. They now commit the fixture for real and clean up after.
…istency Adobe ColdFusion doesn't apply an ORM property's `default` attribute to a freshly new()'d entity the way Lucee/BoxLang do, so Site.getIsActive() came back falsy on Adobe CF for sites created without an explicit isActive key (e.g. the installer's default site fixture). This broke the new preHandler() active-site check for anonymous API callers on Adobe CF CI jobs only. Author.cfc already works around the same engine quirk by setting variables.isActive in init(); Site.cfc now does the same. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoEgT3ggNEKM1nGjqB5Qd5
|
CI red on commit Root cause: Fixed in Generated by Claude Code |
…flag The "I want to edit a site" test PUT isActive:false onto the real "default" site (used as a fixture by nearly every other API spec), relying on withRollback() to undo it. That undo isn't reliable for an UPDATE on every engine, so on Adobe CF the default site was being left permanently inactive mid test-run, which the new preHandler() active-site check then (correctly) enforced against every later anonymous request for it. Edit a disposable site instead, same pattern the delete-site test already uses, so the shared fixture is never at risk. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoEgT3ggNEKM1nGjqB5Qd5
|
Found the actual root cause of the "No site found for ID/Slug default" flakiness — my previous fix (
Fixed in Verified locally on BoxLang: Generated by Claude Code |
…ORM directly The finally block called siteService.delete() on the testSite reference captured before the PUT ran. That PUT executed in a separate simulated request with its own ORM session, leaving testSite stale, which made Hibernate throw "An exception occurred when committing the transaction" on Adobe CF when deleting it directly. The adjacent delete-site test avoids this by going through the HTTP API instead of the ORM service; do the same here for cleanup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoEgT3ggNEKM1nGjqB5Qd5
|
Down to a single failure on Cause: Fixed in Generated by Claude Code |
…s-77caf fixture That test PUT new content onto "disk-queues-77caf" under withRollback(), the same pattern already found unreliable for undoing an UPDATE on Adobe CF (Site.cfc/SitesSpec). This spec bundle runs before PublicContentAccessSpec, and on Adobe CF the fixture was coming out of this test no longer publicly visible, breaking the new anonymous-show visibility check for a slug used across the whole suite. Edit a disposable entry instead, created and torn down through the API, so the shared fixture is never at risk. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoEgT3ggNEKM1nGjqB5Qd5
|
Root-caused the failure on
Fixed in Verified locally: Generated by Claude Code |
…onymous-show test Even after isolating EntriesSpec's edit test (71f76de), the anonymous "show a published entry" assertion still 404'd on Adobe CF for disk-queues-77caf specifically, while the equivalent page/contentStore assertions passed. That slug is referenced by several other specs' uniqueness-validation tests (duplicate-slug checks in EntriesSpec's create/edit stories), any of which could still be leaving it in a bad state on Adobe without an isolated repro to pin down which one. Rather than keep chasing it spec by spec, test anonymous visibility against a disposable published entry this test owns end to end, same pattern already used for the draft-entry case in this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoEgT3ggNEKM1nGjqB5Qd5
|
Rather than keep chasing this spec by spec, Verified locally: Generated by Claude Code |
|
All actual tests are green on The only red item is the With that aside, this PR's own CI is clean. Waiting on reviewers. Generated by Claude Code |
cbSecurity's own CBSecurity.has() already does exactly what hasFullContentAccess() was hand-rolling: an "any of these permissions" check against the currently authenticated user, via the cbSecure() mixin alongside jwtAuth(). jwtAuth().parseToken() logs the resolved author into cbSecurity's SecurityService as a side effect, so cbSecure().has() sees it correctly. Drops the manual listToArray()/loop and the direct prc.oCurrentAuthor dependency, so hasFullContentAccess() no longer needs prc at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoEgT3ggNEKM1nGjqB5Qd5

Description
Currently the ContentBox API (
/cbapi/v1) is entirely locked down — every endpoint requires a JWT, including read-only listing/viewing of content that is already publicly visible on the front-end website. This makesindex/showonentries,pages, andcontentStoreneedlessly require authentication for data anyone can already see by just visiting the site.This PR makes those three "read" actions publicly reachable, matching content parity with the public front-end, while leaving everything else (write actions,
sites,menus,categories,comments,authors, etc.) exactly as authenticated as before.How it works
contentbox-api-v1module's firewall rule (secureListcovers every event,whitelistpreviously only excludedauth/echo). Thewhitelistregex is widened to also matchentries.index,entries.show,pages.index,pages.show,contentStore.index,contentStore.show.secured="..."annotations on those same six actions are removed — cbSecurity checks annotations in a second, independent pass after the rule pass, so both had to change for anonymous access to actually work.index()was already safe for anonymous use: it exclusively goes throughContentService.findPublishedContent(), which filters toisPublished / publishedDate <= now / expireDate null-or-future— the same rule the public site uses.show()was not safe as-is: it resolves by raw id/slug with no visibility filtering (it's built for editors previewing drafts).baseContentHandler.show()now additionally rejects (404, sameEntityNotFoundshape as an unknown id) any anonymous request for content that isn't published, is expired, or is password protected — so drafts can't be enumerated by id.create/update/delete) map to different event names and keep their existingsecured="..."annotations, so they remain fully gated.sitesandmenuskeep their class-levelsecured="SITES_ADMIN"/"MENUS_ADMIN"and are untouched.Jira Issues
https://ortussolutions.atlassian.net/browse/CONTENTBOX-1516
Type of change
Checklist
Test plan
tests/specs/contentbox-api/PublicContentAccessSpec.cfc: anonymousindex/showon entries/pages/contentStore succeed and only return published data; a freshly created draft item'sshow404s anonymously; anonymous writes on those resources are still rejected (401);sites/menusstay fully locked anonymously.tests.specs.contentbox-apibundle (125 specs) locally before and after this change: 122 pass / 3 fail in both runs, and the 3 failures are an identical, pre-existingCONTENTVERSIONSORM error in thecreatestories, unrelated to this change (confirmed by stashing this diff and re-running).curlspot checks against a running instance: anonymousGET /cbapi/v1/sites/default/entriesand.../entries/:slug→ 200; anonymousPOST /cbapi/v1/sites/default/entries→ 401; anonymousGET /cbapi/v1/sitesand.../menus→ 401.Generated by Claude Code