use std::collections::{HashMap, HashSet};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::monotonic::MonotonicSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SubjectType {
User,
Agent,
Service,
System,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SubjectExtension {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject_type: Option<SubjectType>,
#[serde(default)]
pub roles: HashSet<String>,
#[serde(default)]
pub permissions: HashSet<String>,
#[serde(default)]
pub teams: HashSet<String>,
#[serde(default)]
pub claims: HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ObjectSecurityProfile {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub managed_by: Option<String>,
#[serde(default)]
pub permissions: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trust_domain: Option<String>,
#[serde(default)]
pub data_scope: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RetentionPolicy {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_age_seconds: Option<u64>,
#[serde(default)]
pub policy: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delete_after: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DataPolicy {
#[serde(default)]
pub apply_labels: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_actions: Option<Vec<String>>,
#[serde(default)]
pub denied_actions: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retention: Option<RetentionPolicy>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClientTrustLevel {
FirstParty,
ThirdParty,
Internal,
#[serde(untagged)]
Custom(String),
}
impl Default for ClientTrustLevel {
fn default() -> Self {
ClientTrustLevel::ThirdParty
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientExtension {
pub client_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_name: Option<String>,
#[serde(default)]
pub trust_level: ClientTrustLevel,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authorized_scopes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authorized_audiences: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub roles: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub permissions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub teams: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub claims: HashMap<String, Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WorkloadIdentity {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spiffe_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trust_domain: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attested_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attestor: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub selectors: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SecurityExtension {
#[serde(default)]
pub labels: MonotonicSet<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub classification: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<SubjectExtension>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client: Option<ClientExtension>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub caller_workload: Option<WorkloadIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub this_workload: Option<WorkloadIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_method: Option<String>,
#[serde(default)]
pub objects: HashMap<String, ObjectSecurityProfile>,
#[serde(default)]
pub data: HashMap<String, DataPolicy>,
}
impl SecurityExtension {
pub fn add_label(&mut self, label: impl Into<String>) {
self.labels.add_label(label);
}
pub fn has_label(&self, label: &str) -> bool {
self.labels.has_label(label)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_labels_monotonic() {
let mut sec = SecurityExtension::default();
sec.add_label("PII");
sec.add_label("HIPAA");
assert!(sec.has_label("PII"));
assert!(sec.has_label("pii")); assert!(sec.has_label("HIPAA"));
assert!(!sec.has_label("SOX"));
}
#[test]
fn test_security_classification() {
let sec = SecurityExtension {
classification: Some("confidential".into()),
..Default::default()
};
assert_eq!(sec.classification.as_deref(), Some("confidential"));
}
#[test]
fn test_subject_extension() {
let subject = SubjectExtension {
id: Some("alice".into()),
subject_type: Some(SubjectType::User),
roles: ["admin".to_string(), "hr".to_string()].into(),
permissions: ["read_all".to_string()].into(),
teams: ["engineering".to_string()].into(),
claims: [("iss".to_string(), "auth.example.com".to_string())].into(),
};
assert_eq!(subject.id.as_deref(), Some("alice"));
assert_eq!(subject.subject_type, Some(SubjectType::User));
assert!(subject.roles.contains("admin"));
assert!(subject.permissions.contains("read_all"));
assert!(subject.teams.contains("engineering"));
assert_eq!(subject.claims.get("iss").unwrap(), "auth.example.com");
}
#[test]
fn test_workload_identity() {
let w = WorkloadIdentity {
spiffe_id: Some("spiffe://example.com/ns/team1/sa/weather-tool".into()),
trust_domain: Some("example.com".into()),
attestor: Some("spire-agent".into()),
selectors: vec!["k8s:ns:team1".into(), "k8s:sa:weather-tool".into()],
client_id: Some("weather-agent".into()),
..Default::default()
};
assert_eq!(
w.spiffe_id.as_deref(),
Some("spiffe://example.com/ns/team1/sa/weather-tool")
);
assert_eq!(w.trust_domain.as_deref(), Some("example.com"));
assert_eq!(w.attestor.as_deref(), Some("spire-agent"));
assert_eq!(w.selectors.len(), 2);
assert_eq!(w.client_id.as_deref(), Some("weather-agent"));
}
#[test]
fn test_workload_identity_default() {
let w = WorkloadIdentity::default();
assert!(w.spiffe_id.is_none());
assert!(w.trust_domain.is_none());
assert!(w.attested_at.is_none());
assert!(w.attestor.is_none());
assert!(w.selectors.is_empty());
assert!(w.client_id.is_none());
}
#[test]
fn test_security_with_this_workload_and_subject() {
let sec = SecurityExtension {
labels: {
let mut l = super::super::MonotonicSet::new();
l.add_label("PII");
l
},
classification: Some("confidential".into()),
subject: Some(SubjectExtension {
id: Some("alice".into()),
subject_type: Some(SubjectType::User),
..Default::default()
}),
this_workload: Some(WorkloadIdentity {
spiffe_id: Some("spiffe://corp.com/hr-agent".into()),
trust_domain: Some("corp.com".into()),
client_id: Some("hr-agent".into()),
..Default::default()
}),
auth_method: Some("jwt".into()),
..Default::default()
};
assert_eq!(sec.subject.as_ref().unwrap().id.as_deref(), Some("alice"));
assert_eq!(
sec.this_workload.as_ref().unwrap().client_id.as_deref(),
Some("hr-agent")
);
assert_eq!(
sec.this_workload.as_ref().unwrap().trust_domain.as_deref(),
Some("corp.com")
);
assert_eq!(sec.auth_method.as_deref(), Some("jwt"));
assert!(sec.has_label("PII"));
}
#[test]
fn test_security_serde_roundtrip() {
let mut sec = SecurityExtension::default();
sec.add_label("PII");
sec.classification = Some("internal".into());
sec.this_workload = Some(WorkloadIdentity {
client_id: Some("my-agent".into()),
..Default::default()
});
sec.auth_method = Some("mtls".into());
let json = serde_json::to_string(&sec).unwrap();
let deserialized: SecurityExtension = serde_json::from_str(&json).unwrap();
assert!(deserialized.has_label("PII"));
assert_eq!(deserialized.classification.as_deref(), Some("internal"));
assert_eq!(
deserialized
.this_workload
.as_ref()
.unwrap()
.client_id
.as_deref(),
Some("my-agent")
);
assert_eq!(deserialized.auth_method.as_deref(), Some("mtls"));
}
#[test]
fn test_object_security_profile() {
let profile = ObjectSecurityProfile {
managed_by: Some("hr-system".into()),
permissions: vec!["read".into(), "write".into()],
trust_domain: Some("corp.com".into()),
data_scope: vec!["employee_data".into()],
};
assert_eq!(profile.managed_by.as_deref(), Some("hr-system"));
assert_eq!(profile.permissions.len(), 2);
}
#[test]
fn test_data_policy() {
let policy = DataPolicy {
apply_labels: vec!["PII".into()],
allowed_actions: Some(vec!["read".into()]),
denied_actions: vec!["delete".into()],
retention: Some(RetentionPolicy {
max_age_seconds: Some(86400),
policy: "30-day".into(),
delete_after: Some("2026-05-01".into()),
}),
};
assert_eq!(policy.apply_labels[0], "PII");
assert!(policy.retention.is_some());
assert_eq!(
policy.retention.as_ref().unwrap().max_age_seconds,
Some(86400)
);
}
}