1use chrono::{DateTime, Utc};
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::crypto::{KeyPair, signable, verify_signature};
10use crate::errors::{AAPError, Result};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Identity {
16 pub aap_version: String,
17 pub id: String,
18 pub public_key: String,
19 pub parent: String,
20 pub scope: Vec<String>,
21 pub issued_at: DateTime<Utc>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub expires_at: Option<DateTime<Utc>>,
24 pub revoked: bool,
25 #[serde(skip_serializing_if = "HashMap::is_empty", default)]
26 pub metadata: HashMap<String, String>,
27 pub signature: String,
28}
29
30impl Identity {
31 pub fn new(
34 id: &str,
35 scope: Vec<String>,
36 agent_kp: &KeyPair,
37 parent_kp: &KeyPair,
38 parent_did: &str,
39 ) -> Result<Self> {
40 let id_re = Regex::new(r"^aap://[a-z0-9\-\.]+/[a-z0-9\-]+/[a-z0-9\-\.]+@\d+\.\d+\.\d+$")
42 .unwrap();
43 if !id_re.is_match(id) {
44 return Err(AAPError::Validation {
45 field: "id".into(),
46 message: format!("invalid format: {id:?} — expected aap://org/type/name@semver"),
47 });
48 }
49 if scope.is_empty() {
50 return Err(AAPError::Validation {
51 field: "scope".into(),
52 message: "must contain at least one item".into(),
53 });
54 }
55 let scope_re = Regex::new(r"^[a-z]+:[a-z0-9_\-\*]+$").unwrap();
56 for s in &scope {
57 if !scope_re.is_match(s) {
58 return Err(AAPError::Validation {
59 field: "scope".into(),
60 message: format!("invalid item {s:?} — expected verb:resource"),
61 });
62 }
63 }
64
65 let mut identity = Self {
66 aap_version: "0.1".into(),
67 id: id.into(),
68 public_key: agent_kp.public_key_b64(),
69 parent: parent_did.into(),
70 scope,
71 issued_at: Utc::now(),
72 expires_at: None,
73 revoked: false,
74 metadata: HashMap::new(),
75 signature: String::new(),
76 };
77
78 let v = serde_json::to_value(&identity)?;
79 let data = signable(&v)?;
80 identity.signature = parent_kp.sign(&data);
81 Ok(identity)
82 }
83
84 pub fn allows_action(&self, action: &str) -> bool {
86 let (verb, resource) = action.split_once(':').unwrap_or((action, ""));
87 self.scope.iter().any(|s| {
88 let (sv, sr) = s.split_once(':').unwrap_or((s.as_str(), ""));
89 sv == verb && (sr == "*" || sr == resource)
90 })
91 }
92
93 pub fn is_expired(&self) -> bool {
95 self.expires_at.map(|e| Utc::now() > e).unwrap_or(false)
96 }
97
98 pub fn verify(&self, parent_public_key_b64: &str) -> Result<()> {
100 if self.revoked {
101 return Err(AAPError::Revocation { id: self.id.clone() });
102 }
103 let v = serde_json::to_value(self)?;
104 let data = signable(&v)?;
105 verify_signature(parent_public_key_b64, &data, &self.signature)
106 }
107}