feat(auth): admin role with cookie-only RequireAdmin extractor (0.37.0)
Adds an `is_admin` flag on users plus the substrate every later PR in the admin feature builds on: - migration 0018 adds the column with default false - `repo::user::bootstrap_admin` creates or promotes the user named by `ADMIN_USERNAME` at startup, hashing `ADMIN_PASSWORD` only when the row is new — never overwriting an existing hash, so an operator can rotate the admin password via the UI without env-var conflict - `CurrentSessionUser` extractor accepts only the session cookie; `RequireAdmin` composes over it and additionally requires `user.is_admin`. Bearer tokens are intentionally excluded so an admin's bot token never inherits admin authority (privilege-escalation surface that bites every "API keys reuse user perms" auth design) - demotion is instant: `RequireAdmin` re-reads the user row each request `/api/v1/auth/me` now exposes `is_admin`; no other response embeds `User`, so no privacy fanout to audit.
This commit is contained in:
@@ -59,6 +59,13 @@ pub struct Config {
|
||||
pub upload: UploadConfig,
|
||||
pub cors_allowed_origins: Vec<String>,
|
||||
pub crawler: CrawlerConfig,
|
||||
/// `(username, password)` for the admin user provisioned at startup
|
||||
/// when both `ADMIN_USERNAME` and `ADMIN_PASSWORD` are set. `None`
|
||||
/// skips the bootstrap entirely. See `repo::user::bootstrap_admin`
|
||||
/// for the create-vs-promote semantics — notably the password here
|
||||
/// is used only when creating a new row, never to overwrite an
|
||||
/// existing one.
|
||||
pub admin_bootstrap: Option<(String, String)>,
|
||||
}
|
||||
|
||||
/// All crawler-daemon knobs read from env. Mirrors the env vars the
|
||||
@@ -158,10 +165,21 @@ impl Config {
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
crawler: CrawlerConfig::from_env()?,
|
||||
admin_bootstrap: admin_bootstrap_from_env(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `Some((username, password))` only when BOTH `ADMIN_USERNAME`
|
||||
/// and `ADMIN_PASSWORD` are set and non-empty. Half-set configuration is
|
||||
/// treated as "no bootstrap" rather than a hard error, so an operator
|
||||
/// can comment out one env var without crashing the server.
|
||||
fn admin_bootstrap_from_env() -> Option<(String, String)> {
|
||||
let username = std::env::var("ADMIN_USERNAME").ok().filter(|s| !s.is_empty())?;
|
||||
let password = std::env::var("ADMIN_PASSWORD").ok().filter(|s| !s.is_empty())?;
|
||||
Some((username, password))
|
||||
}
|
||||
|
||||
impl CrawlerConfig {
|
||||
pub fn from_env() -> anyhow::Result<Self> {
|
||||
// Parse CRAWLER_DAILY_AT (HH:MM, 24h). Invalid → fail fast.
|
||||
|
||||
Reference in New Issue
Block a user