#![deny(missing_docs)]
use serde::{Deserialize, Serialize};
use crate::types::{AuthorityCapability, EgressRule};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeclaredAuthoritySurface {
#[serde(default)]
pub egress_rules: Vec<EgressRule>,
#[serde(default)]
pub secret_refs: Vec<String>,
#[serde(default)]
pub dns_queries: Vec<String>,
}
impl DeclaredAuthoritySurface {
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.egress_rules.is_empty() && self.secret_refs.is_empty() && self.dns_queries.is_empty()
}
}
pub fn validate_declared_authority_surface(
declared: &DeclaredAuthoritySurface,
authorized: &AuthorityCapability,
) -> Result<(), String> {
for dr in &declared.egress_rules {
let matched = authorized.egress_rules.iter().any(|ar| {
ar.host.eq_ignore_ascii_case(&dr.host)
&& ar.port == dr.port
&& ar.protocol == dr.protocol
});
if !matched {
return Err(format!(
"declared egress rule {host}:{port} (protocol={proto:?}) is not authorized",
host = dr.host,
port = dr.port,
proto = dr.protocol,
));
}
}
for ds in &declared.secret_refs {
if !authorized.secret_refs.iter().any(|asr| asr == ds) {
return Err(format!("declared secret ref {ds:?} is not authorized",));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn egress(host: &str, port: u16, proto: Option<&str>) -> EgressRule {
EgressRule {
host: host.into(),
port,
protocol: proto.map(str::to_string),
dns_egress_justification: None,
}
}
fn authorized_with(
egress_rules: Vec<EgressRule>,
secret_refs: Vec<String>,
) -> AuthorityCapability {
AuthorityCapability {
egress_rules,
secret_refs,
}
}
#[test]
fn declared_within_authorized_returns_ok() {
let authorized = authorized_with(
vec![
egress("api.example.com", 443, Some("tcp")),
egress("db.example.com", 5432, Some("tcp")),
],
vec!["api-key".into(), "db-pass".into()],
);
let declared = DeclaredAuthoritySurface {
egress_rules: vec![egress("api.example.com", 443, Some("tcp"))],
secret_refs: vec!["api-key".into()],
dns_queries: vec!["api.example.com".into()],
};
validate_declared_authority_surface(&declared, &authorized)
.expect("declared subset must validate");
}
#[test]
fn declared_exceeds_egress_returns_err() {
let authorized = authorized_with(
vec![egress("api.example.com", 443, Some("tcp"))],
vec!["api-key".into()],
);
let declared = DeclaredAuthoritySurface {
egress_rules: vec![
egress("api.example.com", 443, Some("tcp")),
egress("evil.example.com", 443, Some("tcp")),
],
secret_refs: vec!["api-key".into()],
dns_queries: vec![],
};
let err = validate_declared_authority_surface(&declared, &authorized)
.expect_err("declared egress overreach must fail");
assert!(
err.contains("evil.example.com"),
"diagnostic should name offending host; got {err}"
);
assert!(
err.contains("not authorized"),
"diagnostic should explain failure; got {err}"
);
}
#[test]
fn declared_exceeds_secrets_returns_err() {
let authorized = authorized_with(vec![], vec!["api-key".into()]);
let declared = DeclaredAuthoritySurface {
egress_rules: vec![],
secret_refs: vec!["api-key".into(), "root-token".into()],
dns_queries: vec![],
};
let err = validate_declared_authority_surface(&declared, &authorized)
.expect_err("declared secret overreach must fail");
assert!(
err.contains("root-token"),
"diagnostic should name offending secret ref; got {err}"
);
}
#[test]
fn empty_declared_is_ok() {
let authorized = AuthorityCapability::default();
let declared = DeclaredAuthoritySurface::empty();
assert!(declared.is_empty());
validate_declared_authority_surface(&declared, &authorized)
.expect("empty declared surface always validates");
let authorized = authorized_with(
vec![egress("api.example.com", 443, Some("tcp"))],
vec!["api-key".into()],
);
validate_declared_authority_surface(&DeclaredAuthoritySurface::empty(), &authorized)
.expect("empty declared surface validates against any authorized");
}
#[test]
fn egress_match_is_host_case_insensitive() {
let authorized = authorized_with(vec![egress("API.Example.com", 443, Some("tcp"))], vec![]);
let declared = DeclaredAuthoritySurface {
egress_rules: vec![egress("api.example.com", 443, Some("tcp"))],
secret_refs: vec![],
dns_queries: vec![],
};
validate_declared_authority_surface(&declared, &authorized)
.expect("host comparison must be case-insensitive");
}
#[test]
fn egress_protocol_mismatch_fails() {
let authorized = authorized_with(vec![egress("api.example.com", 443, Some("tcp"))], vec![]);
let declared = DeclaredAuthoritySurface {
egress_rules: vec![egress("api.example.com", 443, Some("udp"))],
secret_refs: vec![],
dns_queries: vec![],
};
let err = validate_declared_authority_surface(&declared, &authorized)
.expect_err("protocol mismatch must fail");
assert!(err.contains("api.example.com"));
}
#[test]
fn dns_queries_are_not_subset_checked_against_egress() {
let authorized = authorized_with(vec![], vec![]);
let declared = DeclaredAuthoritySurface {
egress_rules: vec![],
secret_refs: vec![],
dns_queries: vec!["api.example.com".into(), "telemetry.example.com".into()],
};
validate_declared_authority_surface(&declared, &authorized)
.expect("dns_queries are not subset-checked in F2 surface");
}
}