website-htmx-rustelo-code/crates/build-config/src/rbac_config.rs
2026-07-10 03:44:13 +01:00

57 lines
1.8 KiB
Rust

//! RBAC policy loader for build-time auth pattern extraction.
//!
//! Reads `site/rbac.ncl` (via `nickel export`) and returns the URL patterns
//! that are denied for anonymous users. These patterns are the **single source
//! of truth** for route protection — no duplication in `config.ncl`.
use anyhow::{Context, Result};
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Deserialize)]
struct RbacConfig {
defaults: RbacDefaults,
}
#[derive(Debug, Deserialize)]
struct RbacDefaults {
unauthenticated_allow: Vec<RbacEntry>,
}
#[derive(Debug, Deserialize)]
struct RbacEntry {
resource: String,
pattern: String,
outcome: String,
}
/// Return URL patterns denied for anonymous users (from `defaults.unauthenticated_allow`).
///
/// Filters to `resource = "page"` + `outcome = "deny"` entries in the order they
/// appear in the policy — the order matters for display but not for the generated
/// pattern matcher, which checks all entries.
///
/// Returns an empty vec if `rbac.ncl` is absent (non-fatal; no protected routes).
pub fn load_rbac_anonymous_deny_patterns(manifest_dir: &Path) -> Result<Vec<String>> {
let rbac_ncl = manifest_dir.join("site").join("rbac.ncl");
if !rbac_ncl.exists() {
return Ok(Vec::new());
}
let json = rustelo_config::nickel::export_to_json(&rbac_ncl)
.with_context(|| format!("Nickel export failed for {}", rbac_ncl.display()))?;
let config: RbacConfig = serde_json::from_str(&json)
.with_context(|| format!("JSON deserialisation failed for {}", rbac_ncl.display()))?;
let patterns = config
.defaults
.unauthenticated_allow
.into_iter()
.filter(|e| e.resource == "page" && e.outcome == "deny")
.map(|e| e.pattern)
.collect();
Ok(patterns)
}