1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(tag = "method", rename_all = "snake_case")]
8pub enum AuthMethod {
9 Bearer {
11 token_hash: String,
13 },
14 ApiKey {
16 key_name: String,
18 key_hash: String,
20 },
21 Cookie {
23 cookie_name: String,
25 cookie_hash: String,
27 },
28 MtlsCertificate {
30 subject_dn: String,
32 fingerprint: String,
34 },
35 Anonymous,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct CallerIdentity {
45 pub subject: String,
48
49 pub auth_method: AuthMethod,
51
52 #[serde(default)]
56 pub verified: bool,
57
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub tenant: Option<String>,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub agent_id: Option<String>,
65}
66
67impl CallerIdentity {
68 #[must_use]
70 pub fn anonymous() -> Self {
71 Self {
72 subject: "anonymous".to_string(),
73 auth_method: AuthMethod::Anonymous,
74 verified: false,
75 tenant: None,
76 agent_id: None,
77 }
78 }
79
80 pub fn identity_hash(&self) -> chio_core_types::Result<String> {
83 let bytes = chio_core_types::canonical_json_bytes(self)?;
84 Ok(chio_core_types::sha256_hex(&bytes))
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn anonymous_identity() {
94 let id = CallerIdentity::anonymous();
95 assert_eq!(id.subject, "anonymous");
96 assert!(!id.verified);
97 assert!(matches!(id.auth_method, AuthMethod::Anonymous));
98 }
99
100 #[test]
101 fn identity_hash_deterministic() {
102 let id = CallerIdentity {
103 subject: "user-123".to_string(),
104 auth_method: AuthMethod::Bearer {
105 token_hash: "abc123".to_string(),
106 },
107 verified: true,
108 tenant: Some("acme".to_string()),
109 agent_id: None,
110 };
111 let h1 = id.identity_hash().unwrap();
112 let h2 = id.identity_hash().unwrap();
113 assert_eq!(h1, h2);
114 assert_eq!(h1.len(), 64); }
116
117 #[test]
118 fn serde_roundtrip() {
119 let id = CallerIdentity {
120 subject: "svc-agent".to_string(),
121 auth_method: AuthMethod::ApiKey {
122 key_name: "X-API-Key".to_string(),
123 key_hash: "deadbeef".to_string(),
124 },
125 verified: true,
126 tenant: None,
127 agent_id: Some("agent-42".to_string()),
128 };
129 let json = serde_json::to_string(&id).unwrap();
130 let back: CallerIdentity = serde_json::from_str(&json).unwrap();
131 assert_eq!(back.subject, "svc-agent");
132 assert_eq!(back.agent_id.as_deref(), Some("agent-42"));
133 }
134
135 #[test]
136 fn mtls_certificate_serde_roundtrip() {
137 let id = CallerIdentity {
138 subject: "CN=service.internal".to_string(),
139 auth_method: AuthMethod::MtlsCertificate {
140 subject_dn: "CN=service.internal,O=Acme".to_string(),
141 fingerprint: "abcdef1234567890".to_string(),
142 },
143 verified: true,
144 tenant: Some("acme-corp".to_string()),
145 agent_id: None,
146 };
147 let json = serde_json::to_string(&id).unwrap();
148 let back: CallerIdentity = serde_json::from_str(&json).unwrap();
149 assert_eq!(back.subject, "CN=service.internal");
150 assert!(back.verified);
151 assert_eq!(back.tenant.as_deref(), Some("acme-corp"));
152 match &back.auth_method {
153 AuthMethod::MtlsCertificate {
154 subject_dn,
155 fingerprint,
156 } => {
157 assert_eq!(subject_dn, "CN=service.internal,O=Acme");
158 assert_eq!(fingerprint, "abcdef1234567890");
159 }
160 other => panic!("expected MtlsCertificate, got {other:?}"),
161 }
162 }
163
164 #[test]
165 fn cookie_auth_method_serde_roundtrip() {
166 let id = CallerIdentity {
167 subject: "cookie-user".to_string(),
168 auth_method: AuthMethod::Cookie {
169 cookie_name: "session_id".to_string(),
170 cookie_hash: "cookiehash123".to_string(),
171 },
172 verified: false,
173 tenant: None,
174 agent_id: None,
175 };
176 let json = serde_json::to_string(&id).unwrap();
177 let back: CallerIdentity = serde_json::from_str(&json).unwrap();
178 match &back.auth_method {
179 AuthMethod::Cookie {
180 cookie_name,
181 cookie_hash,
182 } => {
183 assert_eq!(cookie_name, "session_id");
184 assert_eq!(cookie_hash, "cookiehash123");
185 }
186 other => panic!("expected Cookie, got {other:?}"),
187 }
188 }
189
190 #[test]
191 fn different_identities_produce_different_hashes() {
192 let id1 = CallerIdentity {
193 subject: "user-a".to_string(),
194 auth_method: AuthMethod::Bearer {
195 token_hash: "hash1".to_string(),
196 },
197 verified: true,
198 tenant: None,
199 agent_id: None,
200 };
201 let id2 = CallerIdentity {
202 subject: "user-b".to_string(),
203 auth_method: AuthMethod::Bearer {
204 token_hash: "hash2".to_string(),
205 },
206 verified: true,
207 tenant: None,
208 agent_id: None,
209 };
210 let h1 = id1.identity_hash().unwrap();
211 let h2 = id2.identity_hash().unwrap();
212 assert_ne!(h1, h2);
213 }
214
215 #[test]
216 fn anonymous_identity_serde_omits_optional_fields() {
217 let id = CallerIdentity::anonymous();
218 let json = serde_json::to_string(&id).unwrap();
219 assert!(!json.contains("tenant"));
221 assert!(!json.contains("agent_id"));
222 }
223}