fix(api): derive require_auth_for_reads from api_key when unset#2448
Conversation
#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)).
|
Addressed review feedback in 808711e. Doc/code mismatch — chose Option B (fix the doc). After reading Changes:
Verification:
|
There was a problem hiding this comment.
💡 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".
| let require_auth_for_reads = match configured_require_auth_for_reads { | ||
| Some(explicit) => explicit, | ||
| None => any_auth, | ||
| }; |
There was a problem hiding this comment.
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 👍 / 👎.
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`:
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