Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/docs-2567-anonymous-posture-surfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@objectstack/spec': patch
---

docs(security): document that `requireAuth` denies anonymous across ALL HTTP surfaces (#2567)

The `api.requireAuth` schema description and JSDoc said the anonymous-deny
posture applied to REST `/data/*` only. Post-#2567 the same value is threaded to
every entry point that reaches object data — REST `/data`, the metadata
endpoints (`/meta`), the dispatcher GraphQL endpoint (`/graphql`), and the
raw-hono standard `/data` routes — sharing one decision (`shouldDenyAnonymous`).
The description now reflects the uniform, by-surface posture and the single
opt-out (`requireAuth: false`). Doc-only; no behavior change.

(Accompanying hand-written docs — `permissions/authorization.mdx` and the
regenerated `references/api/rest-server.mdx` — are updated to match.)
2 changes: 1 addition & 1 deletion content/docs/permissions/authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ site — the file you read when behavior surprises you.

| # | Gate | What it decides | Enforcement site | Failure direction |
|---|---|---|---|---|
| 1 | **Anonymous deny** | No identity → HTTP 401 on `/data/*`. **Default-on** (ADR-0056 D2): public data serving requires an explicit `api.requireAuth: false` opt-out, which logs a boot warning. Control plane (`/auth`, `/health`, `/discovery`) is exempt; share-links validate their token then read as SYSTEM. | `packages/rest/src/rest-server.ts` `enforceAuth` (default in `packages/spec/src/api/rest-server.zod.ts`) | fail-closed |
| 1 | **Anonymous deny** | No identity → HTTP 401. **Uniform across every HTTP surface that reaches object data** (#2567): REST `/data`, the metadata endpoints (`/meta`), the dispatcher GraphQL endpoint (`/graphql`), and the raw-hono standard `/data` routes — one shared decision, so a caller denied on `/data` can't read the same rows through a sibling door. **Default-on** (ADR-0056 D2): public serving requires an explicit `api.requireAuth: false` opt-out, which logs a boot warning. Control plane (`/auth`, `/health`, `/discovery`) is exempt; share-links validate their token then read as SYSTEM. | `packages/core/src/security/anonymous-deny.ts` `shouldDenyAnonymous` — called by `rest-server.ts` `enforceAuth`, the dispatcher `handleGraphQL`/`handleMetadata`/`handleAI`, and `plugin-hono-server` `denyAnonymous` (default in `packages/spec/src/api/rest-server.zod.ts`); a source-enumerating ratchet in `authz-conformance.test.ts` fails CI if a new surface ships ungated | fail-closed |
| 2 | **Public-form grant** | An anonymous form submission carries a declaration-derived `publicFormGrant` authorizing ONLY create + read-back on the form's declared target object — never anything else (ADR-0056 Option A). No guest-portal configuration needed (anonymous principals hold the `guest` position). | `packages/plugins/plugin-security/src/security-plugin.ts` (ObjectQL middleware) | scope-limited allow |
| 3 | **Object CRUD** | `allowRead/Create/Edit/Delete` (+ the destructive lifecycle class `allowTransfer/Restore/Purge`, gated ahead of the M2 operations — #1883) resolved across the caller's permission sets. | `packages/plugins/plugin-security/src/permission-evaluator.ts` `checkObjectPermission` | fail-closed 403 |
| 4 | **OWD / sharing** | Org-wide default (`private` / `public_read` / `public_read_write` / `controlled_by_parent`; **unset or unknown ⇒ `private`, fail-closed** — ADR-0090 D1) plus the external dial (`externalSharingModel`, ADR-0090 D11), manual record shares, criteria sharing rules (owner-type rules are declared but seed-skipped — [not enforced](/docs/permissions/sharing-rules#owner-based-sharing-rules)), business-unit hierarchy widening (ADR-0057 D5: scope-depth hierarchy lives on `sys_business_unit`, not positions). | `packages/plugins/plugin-sharing/src/sharing-service.ts` + `sharing-rule-service.ts` | fail-closed to owner-only |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/rest-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ const result = BatchEndpointsConfig.parse(data);
| **enableOpenApi** | `boolean` | ✅ | Enable OpenAPI 3.1 spec & docs viewer endpoints |
| **enableProjectScoping** | `boolean` | ✅ | Enable project-scoped routing for data/meta/AI APIs |
| **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | ✅ | Project ID resolution strategy |
| **requireAuth** | `boolean` | ✅ | Reject anonymous requests on /data/* with HTTP 401 (secure-by-default; set false to serve data publicly) |
| **requireAuth** | `boolean` | ✅ | Reject anonymous requests on ALL HTTP surfaces that reach object data (REST /data, /meta, GraphQL, raw-hono /data) with HTTP 401 (secure-by-default; set false to serve them publicly) |
| **documentation** | `Object` | optional | OpenAPI/Swagger documentation config |
| **responseFormat** | `Object` | optional | Response format options |

Expand Down
42 changes: 24 additions & 18 deletions packages/spec/src/api/rest-server.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,27 +113,33 @@ export const RestApiConfigSchema = lazySchema(() => z.object({
.describe('Project ID resolution strategy'),

/**
* When `true`, all `/data/*` CRUD + batch endpoints reject anonymous
* requests with HTTP 401 before reaching ObjectQL. This is the REST-layer
* counterpart to the security plugin's RBAC enforcement and the
* **only** defense for deployments where the security plugin is not
* mounted (legacy bare-runtime hosts) or where it would otherwise fall
* through anonymous traffic.
* When `true`, anonymous requests are rejected with HTTP 401 before reaching
* ObjectQL. This is the transport-layer counterpart to the security plugin's
* RBAC enforcement and the **only** defense for deployments where the
* security plugin is not mounted (legacy bare-runtime hosts) or where it
* would otherwise fall through anonymous traffic.
*
* **Default `true` — secure by default (ADR-0056 D2).** Anonymous access
* to the data API must be an explicit deployment decision
* (`requireAuth: false`), not a silent fallthrough. Legitimate anonymous
* surfaces survive the deny posture without any opt-out: the control
* plane (`/auth`, `/health`, `/discovery`) is exempt, share-links read
* under a system context after token validation, and public-form
* submission is self-authorizing via the declaration-derived
* `publicFormGrant` (create + read-back on the declared target object
* only). Demo/playground deployments that intentionally serve data
* publicly must now set `requireAuth: false` explicitly — the REST
* plugin logs a boot warning when they do.
* **Applies to every HTTP surface, not just REST `/data`** (#2567). The same
* `requireAuth` value is threaded to every entry point that reaches object
* data, so the anonymous-deny posture is UNIFORM by surface: REST `/data`
* CRUD + batch, the metadata endpoints (`/meta`), the dispatcher's GraphQL
* endpoint (`/graphql`), and the raw-hono standard `/data` routes. All share
* one decision (`shouldDenyAnonymous` in `@objectstack/core`). Before #2567 a
* caller denied on `/data` could read the same rows through a sibling door.
*
* **Default `true` — secure by default (ADR-0056 D2).** Anonymous access must
* be an explicit deployment decision (`requireAuth: false`), not a silent
* fallthrough. Legitimate anonymous surfaces survive the deny posture without
* any opt-out: the control plane (`/auth`, `/health`, `/discovery`) is exempt,
* share-links read under a system context after token validation, and
* public-form submission is self-authorizing via the declaration-derived
* `publicFormGrant` (create + read-back on the declared target object only).
* Demo/playground deployments that intentionally serve data publicly must set
* `requireAuth: false` explicitly — the REST plugin (and the dispatcher / hono
* plugins) log a boot warning when they do.
*/
requireAuth: z.boolean().default(true)
.describe('Reject anonymous requests on /data/* with HTTP 401 (secure-by-default; set false to serve data publicly)'),
.describe('Reject anonymous requests on ALL HTTP surfaces that reach object data (REST /data, /meta, GraphQL, raw-hono /data) with HTTP 401 (secure-by-default; set false to serve them publicly)'),

/**
* API documentation configuration
Expand Down