Skip to content

fix(api): derive require_auth_for_reads from api_key when unset#2448

Merged
houko merged 4 commits into
mainfrom
fix/auth-reads-default-on
Apr 14, 2026
Merged

fix(api): derive require_auth_for_reads from api_key when unset#2448
houko merged 4 commits into
mainfrom
fix/auth-reads-default-on

Conversation

@houko

@houko houko commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Summary

#2398 added `require_auth_for_reads` as an opt-in flag that collapses the dashboard public-read allowlist when set to `true`. In practice that meant an operator who set `api_key = "…"` but forgot the separate `require_auth_for_reads = true` was still leaking agent IDs, config, budget, sessions, and pending approvals to any host on the LAN — because the default was `false` and the flag name wasn't obvious.

The dashboard SPA already handles 401 via its existing login flow (`setOnUnauthorized` / `getStoredApiKey` / `Authorization: Bearer` injection), so the safe default is "close the allowlist whenever auth is configured", not "closed only when two separate knobs agree".

Fix

Switch `KernelConfig.require_auth_for_reads` from `bool` to `Option`:

  • `None` (default) → derive from whether any authentication is configured (non-empty `api_key`, per-user API keys, or dashboard credentials). Setting an `api_key` now auto-collapses the reads allowlist, matching operator expectations.
  • `Some(true)` → force closed even if auth looks misconfigured. Catches an accidental `api_key = ""` redeploy.
  • `Some(false)` → explicit escape hatch for deployments fronted by an external auth proxy that want the in-tree dashboard to keep rendering before the reverse proxy attaches credentials.

The middleware allowlist itself is unchanged — `/` static assets, OAuth flow endpoints, and `/api/health*` stay reachable in every mode. When the auto-enable path kicks in the daemon logs an `info!` line so operators see reads have been locked.

Dashboard impact

None. The SPA in `crates/librefang-api/dashboard` already reads a `librefang-api-key` entry from `localStorage`, injects `Authorization: Bearer` on every request, and intercepts 401 through `setOnUnauthorized` to trigger the login screen. An unauthenticated visitor lands on the login form, types their key, and resumes — same flow operators already use today with `require_auth_for_reads = true`.

Migration

Operators who explicitly want open reads with an `api_key` set (reverse-proxy auth in front of the daemon, say) must now add `require_auth_for_reads = false` to `config.toml`. Previously they didn't need any flag at all. The one `info!` line on boot makes the transition discoverable.

Test plan

  • `cargo check -p librefang-api` — clean (verifies the `Option` lands without breaking other readers)
  • CI: `cargo test -p librefang-api --lib middleware` (tests still construct `AuthState { require_auth_for_reads: false }` directly — those stay compatible because `AuthState` still carries a plain `bool`; only the config-level type changed)
  • CI: `cargo clippy -p librefang-api -p librefang-types --all-targets -- -D warnings`
  • CI full workspace build

#2398 added `require_auth_for_reads` as an opt-in flag that collapses
the dashboard public-read allowlist when set to `true`. In practice
that means an operator who set `api_key = "…"` but forgot the
separate `require_auth_for_reads = true` was still leaking agent
IDs, config, budget, sessions, and pending approvals to any host on
the LAN — because the default was `false` and the flag name wasn't
obvious. The dashboard SPA already handles 401 via its existing
login flow (`setOnUnauthorized` / `getStoredApiKey` / bearer
injection), so the safe default is "close the allowlist whenever
auth is configured", not "closed only when two separate knobs
agree".

Switch `KernelConfig.require_auth_for_reads` from `bool` to
`Option<bool>`:
- `None` (default) → derive from whether any authentication is
  configured: non-empty `api_key`, per-user API keys, or dashboard
  credentials. Setting an `api_key` now auto-collapses the reads
  allowlist, matching operator expectations.
- `Some(true)` → force closed even if auth looks misconfigured.
  Useful for catching an accidental `api_key = ""` redeploy.
- `Some(false)` → explicit escape hatch for deployments fronted by
  an external auth proxy that want the in-tree dashboard to keep
  rendering before the reverse proxy attaches its own credentials.

When the auto-enable path kicks in, log a one-line `info!` so
operators see that reads are now locked. When the operator forces
`require_auth_for_reads = true` but has no auth configured at all,
keep the existing `warn!` so a misconfiguration still surfaces.

All existing public, OAuth, and health endpoints stay reachable
regardless of the resolved value — that list is hard-coded in
`middleware.rs::is_always_public` and unchanged.

Dashboard impact: none. The SPA in `crates/librefang-api/dashboard`
already injects `Authorization: Bearer` from `localStorage` on
every request and intercepts 401 to trigger the login screen, so
flipping the default on for `api_key`-having deployments does not
break rendering — an unauthenticated visitor lands on the login
form, types their key, and resumes.

Migration: operators who explicitly *want* open reads with an
`api_key` set (fronted by reverse-proxy auth, say) must now add
`require_auth_for_reads = false` to `config.toml`. Previously they
didn't need any flag at all.
The original doc claimed Some(true) would force the reads allowlist closed
even when no auth was configured, but the middleware deliberately short-
circuits in that case (middleware.rs:423-426) and falls through to the
unauthenticated local-dev bypass at lines 438-443. That bypass is
intentional — it keeps the flag from silently no-oping when operators use
per-user keys or dashboard credentials instead of a global api_key, which
is explained in the comment above the check.

Rather than ripping out the documented bypass, fix the doc to match
reality: Some(true) logs a boot-time warning when no auth is configured,
but only actually enforces once some form of auth is present. Drop the
stray 'auto' TOML literal from the doc too — Option<bool> never accepted
a string value.

Also simplify the redundant info-log guard in server.rs (the outer match
already narrows to the None branch) and extract the derivation to a
small pure function with unit tests covering all four input combinations
(None + auth, None + no auth, Some(false), Some(true)).
@houko

houko commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review feedback in 808711e.

Doc/code mismatch — chose Option B (fix the doc). After reading middleware.rs:417-443, the bypass at 423-426 (enforce_auth_on_reads = require_auth_for_reads && auth_configured) is deliberate: the comment above it explicitly explains that gating on api_key.is_empty() alone would silently no-op the flag whenever operators configure only per-user keys or dashboard credentials — which is the common production setup. Removing that short-circuit would regress the very case the comment protects, so the fix is to make the docs honest instead.

Changes:

  • crates/librefang-types/src/config/types.rs:1791-1811 — rewrote the Some(true) bullet to state that the daemon logs a boot-time warning when no auth is configured but does not enforce until some form of auth is also present. Dropped the stray auto TOML literal (Option<bool> never accepted a string).
  • crates/librefang-api/src/server.rs:697-729 — extracted the derivation to a derive_require_auth_for_reads(configured, any_auth) -> bool helper and simplified the info-log guard to configured_require_auth_for_reads.is_none() (the outer if require_auth_for_reads + match-None already covers the any_auth branch).
  • crates/librefang-api/src/server.rs:1362-1386 — added a derive_require_auth_for_reads_tests module with four cases: None + auth → true, None + no auth → false, Some(false) preserved, Some(true) preserved.

Verification:

  • cargo check -p librefang-api -p librefang-types --lib — clean
  • cargo test -p librefang-api --lib — 305 passed (including 4 new derivation tests)
  • cargo test -p librefang-types --lib — 550 passed
  • cargo clippy -p librefang-api -p librefang-types --all-targets -- -D warnings — clean

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 044ddb87d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/librefang-api/src/server.rs Outdated
Comment on lines +710 to +713
let require_auth_for_reads = match configured_require_auth_for_reads {
Some(explicit) => explicit,
None => any_auth,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recompute auto read-auth when auth config changes

Deriving require_auth_for_reads from any_auth only once during router construction makes the new auto mode stale after runtime credential updates. In the same file, change_password calls reload_config() and updates api_key_lock, so a daemon that starts with no auth (None -> false) and later gets an API key will still keep dashboard-read endpoints public until restart because this cached bool never flips to true. That defeats the intended “auto-close when auth is configured” behavior and can leave agent/config/budget reads exposed in live deployments that set credentials post-boot.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot added the area/docs Documentation and guides label Apr 14, 2026
@houko
houko enabled auto-merge (squash) April 14, 2026 07:51
@houko
houko merged commit 64dc924 into main Apr 14, 2026
12 checks passed
@houko
houko deleted the fix/auth-reads-default-on branch April 14, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation and guides

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant