use std::path::Path;
use std::sync::{Arc, Mutex};
use gunnar_grpc::{AdminService, Caller, ControlPlane, EventBus, ServerConfig, ServiceContext, AUDIT_TARGET};
use gunnar_proto::admin_server::Admin as _;
use gunnar_proto::*;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use tonic::{Code, Request};
use tracing_subscriber::layer::SubscriberExt as _;
#[derive(Clone, Default)]
struct AuditLog(Arc<Mutex<Vec<String>>>);
struct AuditLayer(AuditLog);
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for AuditLayer {
fn on_event(&self, event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
if event.metadata().target() != AUDIT_TARGET {
return;
}
struct Visit(String);
impl tracing::field::Visit for Visit {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0.push_str(&format!("{}={:?} ", field.name(), value));
}
}
let mut visit = Visit(String::new());
event.record(&mut visit);
self.0 .0.lock().unwrap().push(visit.0);
}
}
fn caller(id: &str, machine: bool) -> Caller {
Caller { principal_id: id.to_owned(), fingerprint: format!("SHA256:{id}"), machine, authenticated: true }
}
fn authed<T>(caller: &Caller, body: T) -> Request<T> {
let mut r = Request::new(body);
r.extensions_mut().insert(caller.clone());
r
}
fn admin_at(dir: &Path) -> AdminService {
let stores = Arc::new(ServerStores::open(dir).expect("stores"));
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
if plane.add_principal("gunnar-ui", "gunnar-ui", PrincipalKind::Machine).is_ok() {
plane.grant("gunnar-ui", "", "", GrantLevel::Admin).expect("the console is a server admin");
plane.add_principal("acme", "acme", PrincipalKind::User).expect("an account");
plane.grant("acme", "acme", "", GrantLevel::Admin).expect("acme administers its own account only");
}
let control: Arc<dyn ControlPlane> = plane;
AdminService::new(ServiceContext::new(control, Arc::new(EventBus::new(64)), ServerConfig::default()))
}
#[test]
fn a_setting_is_server_admin_only_audited_bounded_and_kept() {
let log = AuditLog::default();
let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().expect("runtime");
let subscriber = tracing_subscriber::registry().with(AuditLayer(log.clone()));
tracing::subscriber::with_default(subscriber, || runtime.block_on(inner(log.clone())));
}
async fn inner(log: AuditLog) {
let dir = tempfile::tempdir().unwrap();
let console = caller("gunnar-ui", true);
let acme = caller("acme", false);
let name = "console.a-switch";
{
let admin = admin_at(dir.path());
let unset = admin.get_setting(authed(&console, GetSettingRequest { name: name.into() })).await.expect("a read").into_inner();
assert_eq!((unset.present, unset.value.as_slice()), (false, &b""[..]), "nothing stored yet");
for refused in [
admin.get_setting(authed(&acme, GetSettingRequest { name: name.into() })).await.map(|_| ()),
admin.set_setting(authed(&acme, SetSettingRequest { name: name.into(), value: b"on".to_vec() })).await.map(|_| ()),
admin.set_setting(Request::new(SetSettingRequest { name: name.into(), value: b"on".to_vec() })).await.map(|_| ()),
] {
let code = refused.expect_err("refused").code();
assert!(matches!(code, Code::PermissionDenied | Code::Unauthenticated), "{code:?}");
}
assert!(!log.0.lock().unwrap().iter().any(|l| l.contains("admin.setting_set")), "a refused write writes and audits nothing");
let first = admin.set_setting(authed(&console, SetSettingRequest { name: name.into(), value: b"on".to_vec() })).await.expect("the console writes").into_inner();
assert!(!first.had && first.previous.is_empty());
let second = admin.set_setting(authed(&console, SetSettingRequest { name: name.into(), value: b"off".to_vec() })).await.expect("again").into_inner();
assert_eq!((second.had, second.previous.as_slice()), (true, &b"on"[..]), "the value it replaced");
for (n, v, bound) in [
("Console.X".to_owned(), b"1".to_vec(), "[a-z0-9._-]"),
("a".repeat(129), b"1".to_vec(), "128"),
("console.big".to_owned(), vec![b'x'; 4097], "4096"),
] {
let s = admin.set_setting(authed(&console, SetSettingRequest { name: n, value: v })).await.expect_err("out of bounds");
assert_eq!(s.code(), Code::InvalidArgument, "{s:?}");
assert!(s.message().contains(bound), "the refusal names the bound {bound}: {}", s.message());
}
}
let admin = admin_at(dir.path());
let kept = admin.get_setting(authed(&console, GetSettingRequest { name: name.into() })).await.expect("a read").into_inner();
assert_eq!((kept.present, kept.value.as_slice()), (true, &b"off"[..]));
let lines: Vec<String> = log.0.lock().unwrap().iter().filter(|l| l.contains("admin.setting_set")).cloned().collect();
assert_eq!(lines.len(), 2, "one audit line per write: {lines:#?}");
assert!(lines[0].contains("principal=gunnar-ui") && lines[0].contains(&format!("name={name}")) && lines[0].contains("old=(unset)") && lines[0].contains("new=\"on\""), "{}", lines[0]);
assert!(lines[1].contains("old=\"on\"") && lines[1].contains("new=\"off\""), "{}", lines[1]);
}
#[test]
fn the_server_crates_name_no_console_feature() {
let crates = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let words = ["public-site", "public_site", "gunnar-mcp", "gunnar_mcp", "console.mcp", "indexnow", "llms.txt", "seo"];
let mut hits = Vec::new();
for krate in ["gunnar-server/src", "gunnar-core/src", "gunnar-grpc/src", "gunnar-proto/proto"] {
walk(&crates.join(krate), &mut |path, text| {
for (i, line) in text.lines().enumerate() {
let lower = line.to_ascii_lowercase();
if words.iter().any(|w| contains_word(&lower, w)) || contains_word(&lower, "mcp") {
hits.push(format!("{}:{}: {}", path.display(), i + 1, line.trim()));
}
}
});
}
assert!(hits.is_empty(), "the server crates name a console feature:\n{}", hits.join("\n"));
let mut seen = 0;
walk(&crates.join("gunnar-core/src"), &mut |_, _| seen += 1);
assert!(seen > 10, "the walk read the sources");
}
fn contains_word(line: &str, word: &str) -> bool {
line.match_indices(word).any(|(i, _)| {
let before = line[..i].chars().next_back().is_none_or(|c| !c.is_ascii_alphanumeric());
let after = line[i + word.len()..].chars().next().is_none_or(|c| !c.is_ascii_alphanumeric());
before && after
})
}
fn walk(dir: &Path, seen: &mut dyn FnMut(&Path, &str)) {
let Ok(entries) = std::fs::read_dir(dir) else { return };
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(&path, seen);
} else if path.extension().is_some_and(|e| e == "rs" || e == "proto") {
if let Ok(text) = std::fs::read_to_string(&path) {
seen(&path, &text);
}
}
}
}
#[test]
fn the_word_test_needs_the_whole_word() {
assert!(contains_word("the seo pages", "seo") && contains_word("(mcp)", "mcp"));
assert!(!contains_word("museo", "seo") && !contains_word("mcpx", "mcp"));
}