use async_trait::async_trait;
use url::Url;
#[derive(Debug, Clone, PartialEq)]
pub enum VouchValidationResult {
Valid,
Invalid(String),
Error(VouchError),
}
#[derive(Debug, Clone, PartialEq, thiserror::Error, miette::Diagnostic)]
pub enum VouchError {
#[error("Network error: {0}")]
#[diagnostic(code(vouch::network_error))]
Network(String),
#[error("Invalid URL: {0}")]
#[diagnostic(code(vouch::invalid_url))]
InvalidUrl(String),
#[error("HTTP error: {0}")]
#[diagnostic(code(vouch::http_error))]
Http(String),
}
#[derive(Debug, Clone)]
pub struct VouchConfig {
pub requirement: VouchRequirement,
pub max_vouch_chain_length: usize,
pub vouch_timeout_seconds: u64,
}
impl Default for VouchConfig {
fn default() -> Self {
Self {
requirement: VouchRequirement::Optional,
max_vouch_chain_length: 5,
vouch_timeout_seconds: 30,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum VouchRequirement {
Required,
Optional,
Disabled,
}
#[async_trait]
pub trait VouchValidator: Send + Sync {
async fn validate_vouch(&self, vouch_url: &Url, sender_authority: &str) -> Result<VouchValidationResult, VouchError>;
}
#[async_trait]
pub trait DomainApprover: Send + Sync {
async fn is_authority_approved(&self, authority: &str) -> Result<bool, VouchError>;
}
#[async_trait]
pub trait VouchDiscoverer: Send + Sync {
async fn find_vouch_for(&self, sender_authority: &str, receiver_authority: &str) -> Result<Option<Url>, VouchError>;
}
pub struct AlwaysAllow;
#[async_trait]
impl DomainApprover for AlwaysAllow {
async fn is_authority_approved(&self, _authority: &str) -> Result<bool, VouchError> {
Ok(true)
}
}
pub fn extract_authority(url: &Url) -> String {
format!("{}://{}{}",
url.scheme(),
url.host_str().unwrap_or(""),
url.port().map(|p| format!(":{p}")).unwrap_or_default()
)
}
pub async fn validate_vouch_url(
validator: &impl VouchValidator,
vouch_url: &Url,
sender_authority: &str,
) -> Result<VouchValidationResult, VouchError> {
validator.validate_vouch(vouch_url, sender_authority).await
}
pub async fn is_authority_approved(
approver: &impl DomainApprover,
authority: &str,
) -> Result<bool, VouchError> {
approver.is_authority_approved(authority).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_authority() {
let url = Url::parse("https://example.com:8080/path").unwrap();
assert_eq!(extract_authority(&url), "https://example.com:8080");
let url = Url::parse("http://example.com/path").unwrap();
assert_eq!(extract_authority(&url), "http://example.com");
}
#[tokio::test]
async fn test_always_allow() {
let approver = AlwaysAllow;
assert!(approver.is_authority_approved("https://example.com").await.unwrap());
assert!(approver.is_authority_approved("http://unknown.com").await.unwrap());
}
}