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

vercelBot commented Jul 15, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specCanceledCanceledJul 15, 2026 4:23pm

Request Review

@github-actionsgithub-actionsBot 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 threadpackages/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 mainJul 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-zhuang@github-advanced-security@claude