use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum Credentials {
Anonymous,
Bearer { token: String },
Certificate {
subject: String,
issuer: String,
fingerprint: String,
der: Vec<u8>,
},
PeerCredentials { uid: u32, gid: u32, pid: u32 },
Signature {
key_id: String,
signature: String,
payload: Vec<u8>,
},
Multiple(Vec<Credentials>),
}
impl Credentials {
pub fn is_anonymous(&self) -> bool {
matches!(self, Credentials::Anonymous)
}
pub fn description(&self) -> String {
match self {
Credentials::Anonymous => "anonymous".to_string(),
Credentials::Bearer { .. } => "bearer token".to_string(),
Credentials::Certificate { subject, .. } => format!("certificate: {}", subject),
Credentials::PeerCredentials { uid, .. } => format!("peer uid:{}", uid),
Credentials::Signature { key_id, .. } => format!("signature key:{}", key_id),
Credentials::Multiple(creds) => {
format!("multiple ({} methods)", creds.len())
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Identity {
pub subject: String,
pub identity_type: String,
pub tenant: Option<String>,
pub auth_method: String,
pub claims: HashMap<String, serde_json::Value>,
pub roles: Vec<String>,
pub authenticated_at: chrono::DateTime<chrono::Utc>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub trust_level: TrustLevel,
}
impl Identity {
pub fn new(subject: &str, identity_type: &str, auth_method: &str) -> Self {
Self {
subject: subject.to_string(),
identity_type: identity_type.to_string(),
tenant: None,
auth_method: auth_method.to_string(),
claims: HashMap::new(),
roles: vec![],
authenticated_at: chrono::Utc::now(),
expires_at: None,
trust_level: TrustLevel::Standard,
}
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
pub fn is_expired(&self) -> bool {
if let Some(expires) = self.expires_at {
chrono::Utc::now() > expires
} else {
false
}
}
pub fn get_claim<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
self.claims
.get(key)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum TrustLevel {
Untrusted = 0,
Low = 1,
Standard = 2,
Elevated = 3,
Full = 4,
}
impl Default for TrustLevel {
fn default() -> Self {
TrustLevel::Standard
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthzDecision {
pub allowed: bool,
pub reason: String,
pub policy: String,
pub constraints: Option<AuthzConstraints>,
pub audit_info: Option<AuditInfo>,
}
impl AuthzDecision {
pub fn allow(reason: &str, policy: &str) -> Self {
Self {
allowed: true,
reason: reason.to_string(),
policy: policy.to_string(),
constraints: None,
audit_info: None,
}
}
pub fn deny(reason: &str, policy: &str) -> Self {
Self {
allowed: false,
reason: reason.to_string(),
policy: policy.to_string(),
constraints: None,
audit_info: None,
}
}
pub fn with_constraints(mut self, constraints: AuthzConstraints) -> Self {
self.constraints = Some(constraints);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuthzConstraints {
pub max_output_bytes: Option<u64>,
pub max_duration_ms: Option<u64>,
pub filtered_paths: Vec<String>,
pub rate_limit_rpm: Option<u32>,
pub redact_output: bool,
pub custom: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditInfo {
pub audit_id: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub context: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub enum AuthResult {
Success(Identity),
Failed {
reason: String,
permanent: bool,
},
Challenge {
challenge_type: String,
challenge_data: serde_json::Value,
},
}
impl AuthResult {
pub fn is_success(&self) -> bool {
matches!(self, AuthResult::Success(_))
}
pub fn identity(&self) -> Option<&Identity> {
match self {
AuthResult::Success(id) => Some(id),
_ => None,
}
}
}
#[async_trait]
pub trait AuthProvider: Send + Sync {
fn name(&self) -> &str;
fn auth_method(&self) -> &str;
fn can_authenticate(&self, creds: &Credentials) -> bool;
async fn authenticate(&self, creds: &Credentials) -> Result<AuthResult>;
async fn authorize(
&self,
identity: &Identity,
capability: &str,
params: &serde_json::Value,
) -> Result<AuthzDecision>;
async fn validate_config(&self) -> Result<()>;
async fn stats(&self) -> AuthProviderStats;
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuthProviderStats {
pub auth_attempts: u64,
pub auth_successes: u64,
pub auth_failures: u64,
pub authz_checks: u64,
pub authz_allowed: u64,
pub authz_denied: u64,
pub avg_auth_latency_ms: f64,
}
pub struct AuthProviderChain {
providers: Vec<Box<dyn AuthProvider>>,
}
impl AuthProviderChain {
pub fn new() -> Self {
Self {
providers: Vec::new(),
}
}
pub fn add_provider(mut self, provider: Box<dyn AuthProvider>) -> Self {
self.providers.push(provider);
self
}
pub async fn authenticate(&self, creds: &Credentials) -> Result<AuthResult> {
for provider in &self.providers {
if provider.can_authenticate(creds) {
let result = provider.authenticate(creds).await?;
if result.is_success() {
return Ok(result);
}
}
}
Ok(AuthResult::Failed {
reason: "No provider could authenticate the credentials".to_string(),
permanent: true,
})
}
}
impl Default for AuthProviderChain {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_credentials_anonymous() {
let creds = Credentials::Anonymous;
assert!(creds.is_anonymous());
assert_eq!(creds.description(), "anonymous");
}
#[test]
fn test_credentials_bearer() {
let creds = Credentials::Bearer {
token: "test-token".to_string(),
};
assert!(!creds.is_anonymous());
assert_eq!(creds.description(), "bearer token");
}
#[test]
fn test_credentials_certificate() {
let creds = Credentials::Certificate {
subject: "CN=test-user".to_string(),
issuer: "CN=test-ca".to_string(),
fingerprint: "abc123".to_string(),
der: vec![1, 2, 3],
};
assert!(!creds.is_anonymous());
assert_eq!(creds.description(), "certificate: CN=test-user");
}
#[test]
fn test_credentials_peer_credentials() {
let creds = Credentials::PeerCredentials {
uid: 1000,
gid: 1000,
pid: 12345,
};
assert!(!creds.is_anonymous());
assert_eq!(creds.description(), "peer uid:1000");
}
#[test]
fn test_credentials_signature() {
let creds = Credentials::Signature {
key_id: "key-123".to_string(),
signature: "sig-abc".to_string(),
payload: vec![1, 2, 3],
};
assert!(!creds.is_anonymous());
assert_eq!(creds.description(), "signature key:key-123");
}
#[test]
fn test_credentials_multiple() {
let creds = Credentials::Multiple(vec![
Credentials::Anonymous,
Credentials::Bearer {
token: "test".to_string(),
},
]);
assert!(!creds.is_anonymous());
assert_eq!(creds.description(), "multiple (2 methods)");
}
#[test]
fn test_identity_new() {
let identity = Identity::new("user-123", "user", "jwt");
assert_eq!(identity.subject, "user-123");
assert_eq!(identity.identity_type, "user");
assert_eq!(identity.auth_method, "jwt");
assert!(identity.tenant.is_none());
assert!(identity.roles.is_empty());
assert_eq!(identity.trust_level, TrustLevel::Standard);
}
#[test]
fn test_identity_has_role() {
let mut identity = Identity::new("user-123", "user", "jwt");
identity.roles = vec!["admin".to_string(), "user".to_string()];
assert!(identity.has_role("admin"));
assert!(identity.has_role("user"));
assert!(!identity.has_role("superuser"));
}
#[test]
fn test_identity_is_expired_none() {
let identity = Identity::new("user-123", "user", "jwt");
assert!(!identity.is_expired());
}
#[test]
fn test_identity_is_expired_future() {
let mut identity = Identity::new("user-123", "user", "jwt");
identity.expires_at = Some(chrono::Utc::now() + chrono::Duration::hours(1));
assert!(!identity.is_expired());
}
#[test]
fn test_identity_is_expired_past() {
let mut identity = Identity::new("user-123", "user", "jwt");
identity.expires_at = Some(chrono::Utc::now() - chrono::Duration::hours(1));
assert!(identity.is_expired());
}
#[test]
fn test_identity_get_claim() {
let mut identity = Identity::new("user-123", "user", "jwt");
identity
.claims
.insert("email".to_string(), serde_json::json!("user@example.com"));
identity
.claims
.insert("level".to_string(), serde_json::json!(5));
let email: Option<String> = identity.get_claim("email");
assert_eq!(email, Some("user@example.com".to_string()));
let level: Option<i32> = identity.get_claim("level");
assert_eq!(level, Some(5));
let missing: Option<String> = identity.get_claim("nonexistent");
assert!(missing.is_none());
}
#[test]
fn test_trust_level_ordering() {
assert!(TrustLevel::Untrusted < TrustLevel::Low);
assert!(TrustLevel::Low < TrustLevel::Standard);
assert!(TrustLevel::Standard < TrustLevel::Elevated);
assert!(TrustLevel::Elevated < TrustLevel::Full);
}
#[test]
fn test_trust_level_default() {
let level = TrustLevel::default();
assert_eq!(level, TrustLevel::Standard);
}
#[test]
fn test_trust_level_values() {
assert_eq!(TrustLevel::Untrusted as i32, 0);
assert_eq!(TrustLevel::Low as i32, 1);
assert_eq!(TrustLevel::Standard as i32, 2);
assert_eq!(TrustLevel::Elevated as i32, 3);
assert_eq!(TrustLevel::Full as i32, 4);
}
#[test]
fn test_authz_decision_allow() {
let decision = AuthzDecision::allow("User is authorized", "default-policy");
assert!(decision.allowed);
assert_eq!(decision.reason, "User is authorized");
assert_eq!(decision.policy, "default-policy");
assert!(decision.constraints.is_none());
}
#[test]
fn test_authz_decision_deny() {
let decision = AuthzDecision::deny("Access forbidden", "security-policy");
assert!(!decision.allowed);
assert_eq!(decision.reason, "Access forbidden");
assert_eq!(decision.policy, "security-policy");
}
#[test]
fn test_authz_decision_with_constraints() {
let constraints = AuthzConstraints {
max_output_bytes: Some(1024),
max_duration_ms: Some(5000),
rate_limit_rpm: Some(60),
..Default::default()
};
let decision = AuthzDecision::allow("Allowed with limits", "rate-limit-policy")
.with_constraints(constraints);
assert!(decision.constraints.is_some());
let c = decision.constraints.unwrap();
assert_eq!(c.max_output_bytes, Some(1024));
assert_eq!(c.max_duration_ms, Some(5000));
assert_eq!(c.rate_limit_rpm, Some(60));
}
#[test]
fn test_authz_constraints_default() {
let constraints = AuthzConstraints::default();
assert!(constraints.max_output_bytes.is_none());
assert!(constraints.max_duration_ms.is_none());
assert!(constraints.filtered_paths.is_empty());
assert!(constraints.rate_limit_rpm.is_none());
assert!(!constraints.redact_output);
assert!(constraints.custom.is_empty());
}
#[test]
fn test_auth_result_success() {
let identity = Identity::new("user-123", "user", "jwt");
let result = AuthResult::Success(identity);
assert!(result.is_success());
assert!(result.identity().is_some());
assert_eq!(result.identity().unwrap().subject, "user-123");
}
#[test]
fn test_auth_result_failed() {
let result = AuthResult::Failed {
reason: "Invalid token".to_string(),
permanent: true,
};
assert!(!result.is_success());
assert!(result.identity().is_none());
}
#[test]
fn test_auth_result_challenge() {
let result = AuthResult::Challenge {
challenge_type: "totp".to_string(),
challenge_data: serde_json::json!({"message": "Enter 2FA code"}),
};
assert!(!result.is_success());
assert!(result.identity().is_none());
}
#[test]
fn test_auth_provider_chain_new() {
let chain = AuthProviderChain::new();
assert!(chain.providers.is_empty());
}
#[test]
fn test_auth_provider_chain_default() {
let chain = AuthProviderChain::default();
assert!(chain.providers.is_empty());
}
#[test]
fn test_auth_provider_stats_default() {
let stats = AuthProviderStats::default();
assert_eq!(stats.auth_attempts, 0);
assert_eq!(stats.auth_successes, 0);
assert_eq!(stats.auth_failures, 0);
assert_eq!(stats.authz_checks, 0);
assert_eq!(stats.authz_allowed, 0);
assert_eq!(stats.authz_denied, 0);
assert_eq!(stats.avg_auth_latency_ms, 0.0);
}
#[test]
fn test_identity_serialization() {
let identity = Identity::new("user-123", "service", "mtls");
let json = serde_json::to_string(&identity).unwrap();
let deserialized: Identity = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.subject, identity.subject);
assert_eq!(deserialized.identity_type, identity.identity_type);
assert_eq!(deserialized.auth_method, identity.auth_method);
}
#[test]
fn test_trust_level_serialization() {
let level = TrustLevel::Elevated;
let json = serde_json::to_string(&level).unwrap();
let deserialized: TrustLevel = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, TrustLevel::Elevated);
}
#[test]
fn test_authz_decision_serialization() {
let decision = AuthzDecision::allow("test reason", "test-policy");
let json = serde_json::to_string(&decision).unwrap();
let deserialized: AuthzDecision = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.allowed, decision.allowed);
assert_eq!(deserialized.reason, decision.reason);
assert_eq!(deserialized.policy, decision.policy);
}
#[test]
fn test_audit_info_creation() {
let audit = AuditInfo {
audit_id: "audit-123".to_string(),
timestamp: chrono::Utc::now(),
context: HashMap::new(),
};
assert_eq!(audit.audit_id, "audit-123");
assert!(audit.context.is_empty());
}
}