Skip to content

refactor(security): converge anonymous-deny into one shared function + source-enumerating ratchet (#2567 Phase 2)#2987

Merged
os-zhuang merged 2 commits into
mainfrom
claude/anonymous-posture-inconsistency-j43xco
Jul 16, 2026
Merged

refactor(security): converge anonymous-deny into one shared function + source-enumerating ratchet (#2567 Phase 2)#2987
os-zhuang merged 2 commits into
mainfrom
claude/anonymous-posture-inconsistency-j43xco

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Phase 2 of #2567, built on merged Phase 1 (#2973). Pure refactor + test-infra — no runtime behavior change intended.

Why

Phase 1 gated every HTTP surface (REST /data, dispatcher /graphql + /meta, raw-hono /data) against the secure-by-default requireAuth posture, but each seam hand-rolled the same !userId && !isSystem → 401 check, and the anti-regression guarantee was three conformance rows a human had to remember to keep in sync. Phase 2 converges the decision into one shared, tested function and makes the ratchet enumerate the surfaces from source, so a new ungated entry point fails CI instead of shipping open.

Changes

1. One shared decision — shouldDenyAnonymous in @objectstack/core (security/anonymous-deny.ts)
Mirrors the established auth-gate.ts pattern: a pure decision function + shared 401 body/constants, so the seams "can never drift." All five seams now delegate to it:

  • REST enforceAuth (rest-server.ts) — the ADR-0069 authGate branch is untouched; only the anonymous block delegates. Covers all ~50 enforceAuth call sites incl. the /meta guarded registrar.
  • Dispatcher handleGraphQL / handleMetadata / handleAI (http-dispatcher.ts).
  • Hono denyAnonymous (hono-plugin.ts).

Identity resolution and the dynamic exemptions (public-form grants, share-link tokens) are deliberately not moved — verified they run upstream and only ever hand the seam an already-resolved context (publicFormGrant's route never calls enforceAuth; share-links build a system context downstream).

2. A source-enumerating ratchet on the authz-conformance matrix (authz-conformance.test.ts)
checkLedger already supports discover() + per-row covers. A curated per-file probe table statically enumerates the data/meta/graphql HTTP entry points from source (control-plane routes excluded, but pattern-based within each file so a genuinely new /data route is auto-discovered) and asserts each is classified by a covers key. A new/removed/stale surface now fails CI as UNCLASSIFIED / STALE. A companion negative test proves the ratchet bites (drop a covers → fail; inject a fake route → fail; stale key → fail).

Design trap guarded: isAuthGateAllowlisted(undefined) returns true. A body-routed seam (GraphQL has no request path) must pass no path — the shared function's non-empty-path guard then denies anonymous unconditionally instead of falling through to the control-plane allowlist and silently reopening the hole. Locked by a unit test.

Verification

  • @objectstack/core security: 114/114 (9 new shouldDenyAnonymous cases incl. the undefined-path trap guard)
  • runtime http-dispatcher.requireauth: 13/13; rest rest-auth-gate: 4/4; hono hono-anonymous-deny: 5/5
  • dogfood authz-conformance: 5/5 (soundness + the 3 ratchet-bites negatives + baseline)
  • dogfood anonymous-deny surfaces e2e (platform default): 10/10
  • check:single-authz-resolver intact; lint clean on all changed files; changeset added

Out of scope (noted for a future pass): the dispatcher's handleSecurity admin gate uses the same inline shape but is not a #2567 data/meta/graphql surface, so it's left for a follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ShknNUYoSTspJLBiqorD8V


Generated by Claude Code

…+ source-enumerating ratchet (#2567 Phase 2)

Phase 1 gated every HTTP surface against the requireAuth posture, but each seam
hand-rolled the same anonymous check. Phase 2 removes the duplication and pins
the surfaces so a new ungated entry point fails CI.

- New shouldDenyAnonymous in @objectstack/core (security/anonymous-deny.ts): the
  single anonymous-deny decision + shared 401 body/constants, mirroring the
  auth-gate.ts pattern. All five seams delegate to it — REST enforceAuth,
  dispatcher handleGraphQL/handleMetadata/handleAI, hono denyAnonymous. Pure
  refactor, no runtime behavior change; identity resolution and the dynamic
  exemptions (public-form grants, share-link tokens) are untouched.
- discover() ratchet on the authz-conformance matrix: statically enumerates the
  data/meta/graphql HTTP entry points from source (curated per-file probes,
  control-plane excluded) and asserts each is classified by a covers key. A new
  route or a removed/stale covers now fails CI as UNCLASSIFIED/STALE. A negative
  test proves the ratchet bites.

Guards the isAuthGateAllowlisted(undefined)===true trap: body-routed seams
(GraphQL) pass no path, so the non-empty-path guard denies anonymous
unconditionally rather than falling through to the control-plane allowlist.

Verified: core security 114/114 (9 new), runtime requireauth gate 13/13, rest
auth-gate 4/4, hono anonymous-deny 5/5, dogfood conformance 5/5 (ratchet bites)
+ anonymous-deny surfaces e2e 10/10, single-authz-resolver guard intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ShknNUYoSTspJLBiqorD8V
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Canceled Canceled Jul 15, 2026 4:23pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling and removed size/m labels Jul 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/core, @objectstack/dogfood, @objectstack/plugin-hono-server, @objectstack/rest, @objectstack/runtime.

35 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/api/error-catalog.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/automation/webhooks.mdx (via @objectstack/core)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core)
  • content/docs/kernel/services.mdx (via @objectstack/core)
  • content/docs/permissions/authentication.mdx (via @objectstack/core, @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/dogfood, packages/rest)
  • content/docs/permissions/delegated-administration.mdx (via packages/dogfood)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/plugin-hono-server, @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/plugin-hono-server, @objectstack/rest, @objectstack/runtime)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core)
  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/plugin-hono-server, @objectstack/rest, @objectstack/runtime)
  • content/docs/releases/v12.mdx (via @objectstack/rest)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Comment thread packages/plugins/plugin-hono-server/src/hono-plugin.ts Fixed
The anonymous-deny import edit touched the import line, surfacing a
pre-existing unused `IHttpServer` import (referenced only in comments).
Remove it to clear the CodeQL finding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ShknNUYoSTspJLBiqorD8V
@os-zhuang
os-zhuang marked this pull request as ready for review July 16, 2026 01:08
@os-zhuang
os-zhuang merged commit 99755b5 into main Jul 16, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/anonymous-posture-inconsistency-j43xco branch July 16, 2026 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants