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"));
}