Skip to content

Commit eb3e1d6

Browse files
committed
docs(security): document requireAuth anonymous-deny across all HTTP surfaces (#2567)
The api.requireAuth schema description + JSDoc claimed the anonymous-deny posture applied to REST /data/* only. Post-#2567 the same value governs every entry point that reaches object data (REST /data, /meta, GraphQL, raw-hono /data) via the shared shouldDenyAnonymous decision. Update the source-of-truth zod describe/JSDoc (regenerating references/api/rest-server.mdx) and the hand-written permissions/authorization.mdx "Anonymous deny" enforcement-chain row to reflect the uniform, by-surface posture and the single requireAuth:false opt-out. Doc-only; no behavior change. spec rest-server tests 54/54, doc-authoring guard clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShknNUYoSTspJLBiqorD8V
1 parent e1484bc commit eb3e1d6

4 files changed

Lines changed: 42 additions & 20 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(security): document that `requireAuth` denies anonymous across ALL HTTP surfaces (#2567)
6+
7+
The `api.requireAuth` schema description and JSDoc said the anonymous-deny
8+
posture applied to REST `/data/*` only. Post-#2567 the same value is threaded to
9+
every entry point that reaches object data — REST `/data`, the metadata
10+
endpoints (`/meta`), the dispatcher GraphQL endpoint (`/graphql`), and the
11+
raw-hono standard `/data` routes — sharing one decision (`shouldDenyAnonymous`).
12+
The description now reflects the uniform, by-surface posture and the single
13+
opt-out (`requireAuth: false`). Doc-only; no behavior change.
14+
15+
(Accompanying hand-written docs — `permissions/authorization.mdx` and the
16+
regenerated `references/api/rest-server.mdx` — are updated to match.)

content/docs/permissions/authorization.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ site — the file you read when behavior surprises you.
4848

4949
| # | Gate | What it decides | Enforcement site | Failure direction |
5050
|---|---|---|---|---|
51-
| 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 |
51+
| 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 |
5252
| 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 |
5353
| 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 |
5454
| 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 |

content/docs/references/api/rest-server.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ const result = BatchEndpointsConfig.parse(data);
192192
| **enableOpenApi** | `boolean` || Enable OpenAPI 3.1 spec & docs viewer endpoints |
193193
| **enableProjectScoping** | `boolean` || Enable project-scoped routing for data/meta/AI APIs |
194194
| **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` || Project ID resolution strategy |
195-
| **requireAuth** | `boolean` || Reject anonymous requests on /data/* with HTTP 401 (secure-by-default; set false to serve data publicly) |
195+
| **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) |
196196
| **documentation** | `Object` | optional | OpenAPI/Swagger documentation config |
197197
| **responseFormat** | `Object` | optional | Response format options |
198198

packages/spec/src/api/rest-server.zod.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -113,27 +113,33 @@ export const RestApiConfigSchema = lazySchema(() => z.object({
113113
.describe('Project ID resolution strategy'),
114114

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

138144
/**
139145
* API documentation configuration

0 commit comments

Comments
 (0)