Skip to content

feat(web): opt-in API caller auth gate on /api/* (SEC-8, ADR-0056)#13

Open
proffesor-for-testing wants to merge 1 commit into
ruvnet:mainfrom
proffesor-for-testing:sec-8-api-caller-auth
Open

feat(web): opt-in API caller auth gate on /api/* (SEC-8, ADR-0056)#13
proffesor-for-testing wants to merge 1 commit into
ruvnet:mainfrom
proffesor-for-testing:sec-8-api-caller-auth

Conversation

@proffesor-for-testing

Copy link
Copy Markdown

Summary

Adds an opt-in, default-OFF caller-authentication gate on the agentbbs-web
JSON API (/api/*). This is the "app-level board authZ / lock the API" primitive
that ADR-0054 (build item 1) authorized but left unbuilt, and that a shared /
multi-tenant deployment needs before it can stop anonymous internet access to
its boards and approvals.

Why now

agentbbs-web's board surface — /api/boards/{slug}, /api/approvals,
/api/state, pods/drafts/decisions — has no caller authentication. The
anonymous, browser-signed, single-node model (ADR-0002 / ADR-0016) gives
integrity (you can't forge another author's post) but not confidentiality or
isolation
: any caller can read or post to any board, and GET /api/approvals
returns all proposals across all boards with no scoping. That is correct
for the public genesis node, but a commercial deployment that puts per-tenant
data behind predictable board names (harnessaas-{id}, collab-{id}) is exposed
to cross-tenant read/post. This is the primitive needed to lock such a deployment.

Done looks like

  • With AGENTBBS_REQUIRE_AUTH unset/false → no behavior change; the OSS /
    static-genesis node and every existing deployment work exactly as before.
  • With it set → every /api/* request needs a valid Authorization: Bearer
    matched constant-time against AGENTBBS_API_KEYS, else 401.

Work items in this PR

  • crates/agentbbs-web/src/auth.rs — env-gated from_fn middleware limited
    to /api/*; pure authorize(cfg, method, path, token) decision function.
  • Wired as the outermost layer in router(); AUTHORIZATION added to the
    CORS allow-headers.
  • Fail-closed: enabled with no keys → 401 (misconfig is never an open door).
  • CORS preflight (OPTIONS) always allowed; AGENTBBS_AUTH_READONLY_PUBLIC=1
    keeps GET/HEAD public while gating writes.
  • 9 unit tests covering every branch (disabled, non-API path, missing/valid/
    wrong token, readonly-public, fail-closed, OPTIONS, constant-time compare).
  • ADR-0056 documenting Phase 1 (this PR) and specifying Phase 2 (per-board
    allowlist claim on the existing Caps + require() seam).

Test evidence

  • cargo check -p agentbbs-web — clean.
  • cargo test -p agentbbs-web auth::9/9 pass.
  • Full crate suite: 93 pass / 1 fail; the single failure
    (at_mention_loops_in_a_signed_agent_reply) reproduces identically on a clean
    main when OPENROUTER_API_KEY is present in the environment (the @mention
    path then calls the live model instead of the scripted reply) — pre-existing
    and unrelated to this change.

Env surface (all optional; default OFF)

Var Effect
AGENTBBS_REQUIRE_AUTH 1/true enables the gate on /api/*. Unset = no-op.
AGENTBBS_API_KEYS Comma-separated accepted bearer tokens. Enabled + empty → fail-closed 401.
AGENTBBS_AUTH_READONLY_PUBLIC 1 keeps GET/HEAD public; still gates writes.

Scope / non-goals

Phase 1 gates the door (authenticated caller yes/no). It does not yet
scope which boards an authenticated caller may touch — that is Phase 2 in
ADR-0056 (a per-key board allowlist claim enforced at
api_board/api_post/api_approvals_list/api_state, plugged into the existing
Caps/require() enforcement). For a single-board-per-instance deployment,
Phase 1 plus one issued key is sufficient to lock the service.

This is intentionally an additive, reversible, default-off change so it can land
without affecting the public community node.

Default-OFF bearer gate on the JSON API surface. When AGENTBBS_REQUIRE_AUTH=1,
every /api/* request must present a valid Authorization: Bearer matched
constant-time against AGENTBBS_API_KEYS, else 401; fail-closed when enabled
with no keys; OPTIONS preflight exempt; AGENTBBS_AUTH_READONLY_PUBLIC keeps
GETs public. No-op when unset so the OSS/genesis-static node is unaffected.
Pure authorize() fn + 9 unit tests. Implements ADR-0054 build item 1 (the
door); ADR-0056 specifies Phase 2 per-board scope.

Co-Authored-By: Ruflo & AQE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant