diff --git a/.changeset/docs-2567-anonymous-posture-surfaces.md b/.changeset/docs-2567-anonymous-posture-surfaces.md new file mode 100644 index 0000000000..a4ac6f2c62 --- /dev/null +++ b/.changeset/docs-2567-anonymous-posture-surfaces.md @@ -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.) diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx index 62cf10f0e1..95008453f5 100644 --- a/content/docs/permissions/authorization.mdx +++ b/content/docs/permissions/authorization.mdx @@ -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 | diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index c6f37442aa..2b7bdd76a5 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -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 | diff --git a/packages/spec/src/api/rest-server.zod.ts b/packages/spec/src/api/rest-server.zod.ts index 9704cdeaf8..480936e5e0 100644 --- a/packages/spec/src/api/rest-server.zod.ts +++ b/packages/spec/src/api/rest-server.zod.ts @@ -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