use std::collections::HashSet;
use kdl::KdlNode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BusAccess {
Open,
Full,
Scoped(HashSet<String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BusAuth {
pub access: BusAccess,
}
impl BusAuth {
#[must_use]
pub fn open() -> Self {
Self {
access: BusAccess::Open,
}
}
#[must_use]
pub fn allows_module(&self, module: &str) -> bool {
match &self.access {
BusAccess::Open | BusAccess::Full => true,
BusAccess::Scoped(modules) => modules.contains(module),
}
}
#[must_use]
pub fn permits_global_probe(&self) -> bool {
matches!(self.access, BusAccess::Open | BusAccess::Full)
}
}
#[must_use]
pub fn token_from_env() -> Option<String> {
crate::security::bus_token()
}
#[must_use]
pub fn credentials_configured() -> bool {
crate::security::bus_credentials_configured()
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
fn resolve_access(provided: &str) -> Result<BusAccess, String> {
if let Some(expected) = token_from_env() {
if constant_time_eq(expected.as_bytes(), provided.as_bytes()) {
return Ok(BusAccess::Full);
}
}
let scoped = crate::security::bus_scoped_tokens();
for (token, modules) in &scoped {
if constant_time_eq(token.as_bytes(), provided.as_bytes()) {
return Ok(BusAccess::Scoped(modules.clone()));
}
}
Err("invalid bus token".into())
}
pub fn authenticate<'a>(
nodes: &'a [&'a KdlNode],
) -> Result<(BusAuth, &'a KdlNode, Vec<&'a KdlNode>), String> {
let Some(head) = nodes.first() else {
return Err("empty KDL document".into());
};
if !credentials_configured() {
return Ok((BusAuth::open(), head, nodes[1..].to_vec()));
}
if head.name().value() != "auth" {
return Err("missing auth wrapper (auth token=\"…\" { … })".into());
}
let Some(provided) = head
.get("token")
.and_then(|v| v.as_string().map(str::to_string))
else {
return Err("auth node requires token=\"…\" property".into());
};
let access = resolve_access(&provided)?;
let Some(children) = head.children() else {
return Err("auth wrapper requires a nested command".into());
};
let inner: Vec<_> = children.nodes().iter().collect();
let Some(command) = inner.first().copied() else {
return Err("auth wrapper requires a nested command".into());
};
Ok((
BusAuth { access },
command,
inner[1..].to_vec(),
))
}
pub fn unwrap_command<'a>(
expected: Option<&str>,
node: &'a KdlNode,
) -> Result<(&'a KdlNode, Vec<&'a KdlNode>), String> {
let Some(expected) = expected else {
return Ok((node, Vec::new()));
};
if node.name().value() != "auth" {
return Err("missing auth wrapper (auth token=\"…\" { … })".into());
}
let Some(provided) = node
.get("token")
.and_then(|v| v.as_string().map(str::to_string))
else {
return Err("auth node requires token=\"…\" property".into());
};
if !constant_time_eq(expected.as_bytes(), provided.as_bytes()) {
return Err("invalid bus token".into());
}
let Some(children) = node.children() else {
return Err("auth wrapper requires a nested command".into());
};
let inner: Vec<_> = children.nodes().iter().collect();
let Some(head) = inner.first().copied() else {
return Err("auth wrapper requires a nested command".into());
};
Ok((head, inner[1..].to_vec()))
}
#[cfg(test)]
mod tests {
use super::*;
use kdl::KdlDocument;
#[test]
fn passthrough_when_no_token_configured() {
let doc: KdlDocument = "filter status".parse().unwrap();
let nodes: Vec<_> = doc.nodes().iter().collect();
let (auth, head, tail) = authenticate(&nodes).unwrap();
assert_eq!(auth.access, BusAccess::Open);
assert_eq!(head.name().value(), "filter");
assert!(tail.is_empty());
}
#[test]
fn rejects_missing_auth_when_token_required() {
let doc: KdlDocument = "filter status".parse().unwrap();
let nodes: Vec<_> = doc.nodes().iter().collect();
assert!(unwrap_command(Some("secret"), nodes[0]).is_err());
}
#[test]
fn unwraps_authenticated_command() {
let doc: KdlDocument = r#"auth token="secret" {
filter {
status
}
}"#
.parse()
.unwrap();
let node = doc.nodes().first().unwrap();
let (head, tail) = unwrap_command(Some("secret"), node).unwrap();
assert_eq!(head.name().value(), "filter");
assert!(tail.is_empty());
}
#[test]
fn rejects_wrong_token_without_leaking_length() {
let doc: KdlDocument = r#"auth token="wrong" {
ping
}"#
.parse()
.unwrap();
let node = doc.nodes().first().unwrap();
assert!(unwrap_command(Some("secret"), node).is_err());
}
#[test]
fn scoped_access_allows_only_listed_modules() {
let auth = BusAuth {
access: BusAccess::Scoped(HashSet::from(["filter".into()])),
};
assert!(auth.allows_module("filter"));
assert!(!auth.allows_module("tls"));
}
}