axess_identity/
resolver.rs1use std::collections::BTreeMap;
24
25use crate::TenantId;
26
27use crate::{IdentityError, Issuer, Principal, TrustDomain, WorkloadId, WorkloadPrincipal};
28
29pub trait PrincipalResolver: Send + Sync {
32 fn resolve(&self)
37 -> impl std::future::Future<Output = Result<Principal, IdentityError>> + Send;
38}
39
40#[derive(Debug, Clone)]
53pub struct CliResolver {
54 workload: WorkloadPrincipal,
55}
56
57impl CliResolver {
58 pub fn builder() -> CliResolverBuilder {
61 CliResolverBuilder::default()
62 }
63
64 pub fn workload(&self) -> &WorkloadPrincipal {
68 &self.workload
69 }
70
71 pub fn as_workload_principal(&self) -> &WorkloadPrincipal {
76 &self.workload
77 }
78}
79
80impl PrincipalResolver for CliResolver {
81 async fn resolve(&self) -> Result<Principal, IdentityError> {
82 Ok(Principal::Workload(self.workload.clone()))
83 }
84}
85
86#[derive(Debug, Default, Clone)]
89pub struct CliResolverBuilder {
90 trust_domain: Option<TrustDomain>,
91 service_name: Option<String>,
92 tenant_id: Option<TenantId>,
93 tenant_slug: Option<String>,
94 attributes: BTreeMap<String, serde_json::Value>,
95}
96
97impl CliResolverBuilder {
98 pub fn trust_domain(mut self, value: TrustDomain) -> Self {
100 self.trust_domain = Some(value);
101 self
102 }
103
104 pub fn service_name(mut self, value: impl Into<String>) -> Self {
106 self.service_name = Some(value.into());
107 self
108 }
109
110 pub fn tenant_id(mut self, value: TenantId) -> Self {
112 self.tenant_id = Some(value);
113 self
114 }
115
116 pub fn tenant_slug(mut self, value: impl Into<String>) -> Self {
118 self.tenant_slug = Some(value.into());
119 self
120 }
121
122 pub fn attribute(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
124 self.attributes.insert(key.into(), value);
125 self
126 }
127
128 pub fn build(self) -> Result<CliResolver, IdentityError> {
131 let trust_domain = self
132 .trust_domain
133 .ok_or_else(|| IdentityError::InvalidComponent("trust_domain not set".to_string()))?;
134 let service_name = self
135 .service_name
136 .ok_or_else(|| IdentityError::InvalidComponent("service_name not set".to_string()))?;
137 let tenant_id = self
138 .tenant_id
139 .ok_or_else(|| IdentityError::InvalidComponent("tenant_id not set".to_string()))?;
140 let tenant_slug = self
141 .tenant_slug
142 .ok_or_else(|| IdentityError::InvalidComponent("tenant_slug not set".to_string()))?;
143 let workload_id = WorkloadId::build(&trust_domain, &service_name, &tenant_slug)?;
144 let workload = WorkloadPrincipal {
145 workload_id,
146 trust_domain,
147 issuer: Issuer::Cli,
148 tenant_id,
149 tenant_slug,
150 service_name,
151 attributes: self.attributes,
152 };
153 Ok(CliResolver { workload })
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 fn sample_tenant() -> TenantId {
162 TenantId::from_bytes([9u8; 16])
163 }
164
165 #[tokio::test]
166 async fn cli_resolver_builds_workload_principal() {
167 let trust = TrustDomain::new("gnomes.local").unwrap();
168 let resolver = CliResolver::builder()
169 .trust_domain(trust.clone())
170 .service_name("compute-worker")
171 .tenant_id(sample_tenant())
172 .tenant_slug("ekekrantz")
173 .build()
174 .unwrap();
175 let principal = resolver.resolve().await.unwrap();
176 match principal {
177 Principal::Workload(w) => {
178 assert_eq!(
179 w.workload_id.as_str(),
180 "spiffe://gnomes.local/compute-worker/ekekrantz"
181 );
182 assert_eq!(w.trust_domain, trust);
183 assert_eq!(w.issuer, Issuer::Cli);
184 assert_eq!(w.tenant_id, sample_tenant());
185 assert_eq!(w.tenant_slug, "ekekrantz");
186 assert_eq!(w.service_name, "compute-worker");
187 assert!(w.attributes.is_empty());
188 }
189 Principal::Human(_) => panic!("expected Workload principal"),
190 }
191 }
192
193 #[tokio::test]
194 async fn cli_resolver_resolve_is_idempotent() {
195 let trust = TrustDomain::new("gnomes.local").unwrap();
196 let resolver = CliResolver::builder()
197 .trust_domain(trust)
198 .service_name("feed-worker")
199 .tenant_id(sample_tenant())
200 .tenant_slug("ekekrantz")
201 .build()
202 .unwrap();
203 let a = resolver.resolve().await.unwrap();
204 let b = resolver.resolve().await.unwrap();
205 assert_eq!(a, b);
206 }
207
208 #[test]
209 fn cli_resolver_builder_rejects_missing_trust_domain() {
210 let err = CliResolver::builder()
211 .service_name("compute-worker")
212 .tenant_id(sample_tenant())
213 .tenant_slug("ekekrantz")
214 .build()
215 .unwrap_err();
216 assert!(matches!(err, IdentityError::InvalidComponent(_)));
217 }
218
219 #[test]
220 fn cli_resolver_builder_rejects_missing_service_name() {
221 let trust = TrustDomain::new("gnomes.local").unwrap();
222 let err = CliResolver::builder()
223 .trust_domain(trust)
224 .tenant_id(sample_tenant())
225 .tenant_slug("ekekrantz")
226 .build()
227 .unwrap_err();
228 assert!(matches!(err, IdentityError::InvalidComponent(_)));
229 }
230
231 #[test]
232 fn cli_resolver_builder_rejects_missing_tenant_id() {
233 let trust = TrustDomain::new("gnomes.local").unwrap();
234 let err = CliResolver::builder()
235 .trust_domain(trust)
236 .service_name("compute-worker")
237 .tenant_slug("ekekrantz")
238 .build()
239 .unwrap_err();
240 assert!(matches!(err, IdentityError::InvalidComponent(_)));
241 }
242
243 #[test]
244 fn cli_resolver_builder_rejects_missing_tenant_slug() {
245 let trust = TrustDomain::new("gnomes.local").unwrap();
246 let err = CliResolver::builder()
247 .trust_domain(trust)
248 .service_name("compute-worker")
249 .tenant_id(sample_tenant())
250 .build()
251 .unwrap_err();
252 assert!(matches!(err, IdentityError::InvalidComponent(_)));
253 }
254
255 #[test]
256 fn cli_resolver_builder_rejects_invalid_service_name() {
257 let trust = TrustDomain::new("gnomes.local").unwrap();
258 let err = CliResolver::builder()
259 .trust_domain(trust)
260 .service_name("compute worker")
261 .tenant_id(sample_tenant())
262 .tenant_slug("ekekrantz")
263 .build()
264 .unwrap_err();
265 assert!(matches!(err, IdentityError::InvalidComponent(_)));
266 }
267
268 #[tokio::test]
269 async fn cli_resolver_carries_attributes() {
270 let trust = TrustDomain::new("gnomes.local").unwrap();
271 let resolver = CliResolver::builder()
272 .trust_domain(trust)
273 .service_name("compute-worker")
274 .tenant_id(sample_tenant())
275 .tenant_slug("ekekrantz")
276 .attribute("worker_pid", serde_json::json!(12345))
277 .attribute("hostname", serde_json::json!("worker-1.local"))
278 .build()
279 .unwrap();
280 let workload = resolver.workload();
281 assert_eq!(workload.attributes.len(), 2);
282 assert_eq!(workload.attributes["worker_pid"], serde_json::json!(12345));
283 }
284}