1use std::{collections::BTreeMap, sync::Arc};
2
3use aep_core::{
4 AgentStatus, AssertionOperation, Authentication, AuthenticationMethod, Binding, Bindings,
5 Command, Commands, CoreConfiguration, EnrollRequest, EnrollResponse, EnrollmentDecisionStatus,
6 ErrorCode, Extensions, GrantType, HttpConfiguration, Identity, InspectClaims, InspectDocument,
7 RevokeResponse, ServiceIdentity, StatusResponse, StringBoolean, VERSION,
8 missing_required_claim_names, parse_enroll_request, parse_grant_request, parse_revoke_request,
9 validate_inspect_document,
10};
11use futures::FutureExt as _;
12use http::{HeaderMap, HeaderValue, header};
13use serde::Serialize;
14use serde_json::Value;
15use sha2::{Digest as _, Sha256};
16use time::format_description::well_known::Rfc3339;
17
18use crate::{
19 AuthenticatedCommandOptions, AuthenticatedPrincipal, AuthenticationKind,
20 ClientAssertionReplayStore, Clock, CommandIdempotencyInput, CommandIdempotencyResult,
21 CommandIdempotencyStore, EnrollmentPolicy, EnrollmentRecord, EnrollmentStore, GrantContext,
22 IdempotentCommand, IdempotentCommandOptions, MemoryClientAssertionReplayStore,
23 MemoryCommandIdempotencyStore, MemoryEnrollmentStore, ProtectedResourceAuthentication,
24 ProtectedResourceRequest, ResponseBody, ServiceConfiguration, ServiceError, ServiceOptions,
25 ServiceResponse, StaticEnrollmentPolicy, SystemClock,
26 authentication::{
27 AssertionAuthentication, SelectedAuthorization, authenticate_assertion,
28 select_authorization,
29 },
30 problem,
31};
32
33pub struct Service {
34 clock: Arc<dyn Clock>,
35 configuration: ServiceConfiguration,
36 enrollment_policy: Arc<dyn EnrollmentPolicy>,
37 enrollment_store: Arc<dyn EnrollmentStore>,
38 idempotency_store: Arc<dyn CommandIdempotencyStore>,
39 replay_store: Arc<dyn ClientAssertionReplayStore>,
40 verifier: Arc<dyn crate::ClientAssertionVerifier>,
41 allow_insecure_loopback: bool,
42}
43
44impl Service {
45 pub fn new(options: ServiceOptions) -> Result<Arc<Self>, ServiceError> {
46 let configuration = build_configuration(&options)?;
47 let clock = options.clock.unwrap_or_else(|| Arc::new(SystemClock));
48 Ok(Arc::new(Self {
49 allow_insecure_loopback: options.allow_insecure_loopback,
50 configuration,
51 enrollment_policy: options
52 .enrollment_policy
53 .unwrap_or_else(|| Arc::new(StaticEnrollmentPolicy::default())),
54 enrollment_store: options
55 .enrollment_store
56 .unwrap_or_else(|| Arc::new(MemoryEnrollmentStore::default())),
57 idempotency_store: options.idempotency_store.unwrap_or_else(|| {
58 Arc::new(MemoryCommandIdempotencyStore::with_clock(clock.clone()))
59 }),
60 replay_store: options
61 .replay_store
62 .unwrap_or_else(|| Arc::new(MemoryClientAssertionReplayStore::default())),
63 verifier: options.verifier,
64 clock,
65 }))
66 }
67
68 pub fn inspect_document(&self) -> InspectDocument {
69 self.configuration.inspect_document.clone()
70 }
71
72 pub async fn enroll(
73 self: &Arc<Self>,
74 request: &[u8],
75 options: IdempotentCommandOptions,
76 ) -> Result<ServiceResponse, ServiceError> {
77 let Some(claims) = self
78 .authenticate_command(
79 &options.client_assertion,
80 AssertionOperation::Enroll,
81 Some(&options.idempotency_key),
82 )
83 .await?
84 else {
85 return Ok(problem(ErrorCode::NotRecognized, 401));
86 };
87 let Ok(request) = parse_enroll_request(request) else {
88 return Ok(problem(ErrorCode::InvalidRequest, 400));
89 };
90 if request.agent_did != claims.sub || options.idempotency_key.is_empty() {
91 return Ok(if options.idempotency_key.is_empty() {
92 problem(ErrorCode::InvalidRequest, 400)
93 } else {
94 problem(ErrorCode::NotRecognized, 401)
95 });
96 }
97 if request
98 .idempotency_key
99 .as_ref()
100 .is_some_and(|key| key != &options.idempotency_key)
101 {
102 return Ok(problem(ErrorCode::InvalidRequest, 400));
103 }
104 if !within_claim_value_limits(
107 request.claims.as_ref(),
108 self.configuration.claim_value_limits,
109 ) {
110 return Ok(problem(ErrorCode::InvalidRequest, 400));
111 }
112 let input = idempotency_input(
113 &claims.sub,
114 IdempotentCommand::Enroll,
115 &options.idempotency_key,
116 &request,
117 )?;
118 let service = self.clone();
119 let result = self
120 .idempotency_store
121 .execute(
122 input,
123 Box::new(move || async move { service.execute_enroll(request).await }.boxed()),
124 )
125 .await?;
126 Ok(idempotency_response(result))
127 }
128
129 pub async fn status(
130 &self,
131 options: AuthenticatedCommandOptions,
132 ) -> Result<ServiceResponse, ServiceError> {
133 let Some(claims) = self
134 .authenticate_command(&options.client_assertion, AssertionOperation::Status, None)
135 .await?
136 else {
137 return Ok(problem(ErrorCode::NotRecognized, 401));
138 };
139 let Some(enrollment) = self.enrollment_store.find(&claims.sub).await? else {
140 return Ok(problem(ErrorCode::NotRecognized, 401));
141 };
142 Ok(success(ResponseBody::Status(status_response(&enrollment))))
143 }
144
145 pub async fn grant(
146 self: &Arc<Self>,
147 request: &[u8],
148 options: IdempotentCommandOptions,
149 ) -> Result<ServiceResponse, ServiceError> {
150 let Some(claims) = self
151 .authenticate_command(
152 &options.client_assertion,
153 AssertionOperation::Grant,
154 Some(&options.idempotency_key),
155 )
156 .await?
157 else {
158 return Ok(problem(ErrorCode::NotRecognized, 401));
159 };
160 let Ok(request) = parse_grant_request(request) else {
161 return Ok(problem(ErrorCode::InvalidRequest, 400));
162 };
163 if options.idempotency_key.is_empty() {
164 return Ok(problem(ErrorCode::InvalidRequest, 400));
165 }
166 let Some(enrollment) = self.enrollment_store.find(&claims.sub).await? else {
167 return Ok(problem(ErrorCode::NotRecognized, 401));
168 };
169 if let Some(response) = blocked_enrollment(&enrollment) {
170 return Ok(response);
171 }
172 let Some(handler) = self
173 .configuration
174 .grant_handlers
175 .get(request.grant_type.as_str())
176 .cloned()
177 else {
178 return Ok(problem(ErrorCode::UnsupportedGrantType, 400));
179 };
180 let input = idempotency_input(
181 &claims.sub,
182 IdempotentCommand::Grant,
183 &options.idempotency_key,
184 &request,
185 )?;
186 let agent_did = claims.sub;
187 let now = self.clock.now();
188 let result = self
189 .idempotency_store
190 .execute(
191 input,
192 Box::new(move || {
193 async move {
194 let context = GrantContext {
195 agent_did,
196 enrollment,
197 grant_type: request.grant_type.clone(),
198 now,
199 };
200 let response = handler.grant(&request, &context).await?;
201 if !response.as_object().is_some_and(|response| {
202 response
203 .get("credential_id")
204 .and_then(Value::as_str)
205 .is_some_and(|credential_id| !credential_id.is_empty())
206 }) {
207 return Err(ServiceError::Handler(
208 "Grant handler response requires a non-empty credential_id"
209 .to_owned(),
210 ));
211 }
212 Ok(success(ResponseBody::Grant(response)))
213 }
214 .boxed()
215 }),
216 )
217 .await?;
218 Ok(idempotency_response(result))
219 }
220
221 pub async fn revoke(
222 self: &Arc<Self>,
223 request: &[u8],
224 options: IdempotentCommandOptions,
225 ) -> Result<ServiceResponse, ServiceError> {
226 let Some(claims) = self
227 .authenticate_command(
228 &options.client_assertion,
229 AssertionOperation::Revoke,
230 Some(&options.idempotency_key),
231 )
232 .await?
233 else {
234 return Ok(problem(ErrorCode::NotRecognized, 401));
235 };
236 let Ok(request) = parse_revoke_request(request) else {
237 return Ok(problem(ErrorCode::InvalidRequest, 400));
238 };
239 if options.idempotency_key.is_empty() {
240 return Ok(problem(ErrorCode::InvalidRequest, 400));
241 }
242 let Some(enrollment) = self.enrollment_store.find(&claims.sub).await? else {
243 return Ok(problem(ErrorCode::NotRecognized, 401));
244 };
245 let handlers = match &request.grant_type {
246 Some(grant_type) => {
247 let Some(handler) = self
248 .configuration
249 .grant_handlers
250 .get(grant_type.as_str())
251 .cloned()
252 else {
253 return Ok(problem(ErrorCode::UnsupportedGrantType, 400));
254 };
255 vec![(grant_type.clone(), handler)]
256 }
257 None => self
258 .configuration
259 .grant_handlers
260 .iter()
261 .map(|(grant_type, handler)| (GrantType::from(grant_type.clone()), handler.clone()))
262 .collect(),
263 };
264 let input = idempotency_input(
265 &claims.sub,
266 IdempotentCommand::Revoke,
267 &options.idempotency_key,
268 &request,
269 )?;
270 let agent_did = claims.sub;
271 let now = self.clock.now();
272 let result = self
273 .idempotency_store
274 .execute(
275 input,
276 Box::new(move || {
277 async move {
278 for (grant_type, handler) in handlers {
279 let context = GrantContext {
280 agent_did: agent_did.clone(),
281 enrollment: enrollment.clone(),
282 grant_type,
283 now,
284 };
285 handler.revoke(&request, &context).await?;
286 }
287 Ok(success(ResponseBody::Revoke(RevokeResponse::default())))
288 }
289 .boxed()
290 }),
291 )
292 .await?;
293 Ok(idempotency_response(result))
294 }
295
296 pub async fn authenticate_protected_resource(
297 &self,
298 request: ProtectedResourceRequest,
299 ) -> Result<ProtectedResourceAuthentication, ServiceError> {
300 let selected = match select_authorization(&request.headers) {
301 Ok(selected) => selected,
302 Err(()) => return Ok(self.authentication_failure(ErrorCode::NotRecognized, &request)),
303 };
304 if let SelectedAuthorization::Aep(assertion) = selected {
305 if !self
306 .configuration
307 .authentication_methods
308 .contains(&AuthenticationMethod::AepJwt)
309 {
310 return Ok(self
311 .authentication_failure(ErrorCode::UnsupportedAuthenticationMethod, &request));
312 }
313 let Some(claims) = self
314 .authenticate_assertion(
315 &assertion,
316 AssertionOperation::Authenticate,
317 None,
318 Some(&request.url),
319 )
320 .await?
321 else {
322 return Ok(self.authentication_failure(ErrorCode::NotRecognized, &request));
323 };
324 let Some(enrollment) = self.enrollment_store.find(&claims.sub).await? else {
325 return Ok(self.authentication_failure(ErrorCode::NotRecognized, &request));
326 };
327 if enrollment.status != AgentStatus::Active {
328 return Ok(self.authentication_failure(ErrorCode::NotRecognized, &request));
329 }
330 return Ok(ProtectedResourceAuthentication::Authenticated(
331 AuthenticatedPrincipal {
332 agent_did: claims.sub,
333 authentication_kind: AuthenticationKind::ClientAssertion,
334 authentication_method: AuthenticationMethod::AepJwt,
335 credential_id: None,
336 grant_type: None,
337 scopes: Vec::new(),
338 },
339 ));
340 }
341 if let SelectedAuthorization::Session(method) = &selected
342 && !self.configuration.authentication_methods.contains(method)
343 {
344 return Ok(
345 self.authentication_failure(ErrorCode::UnsupportedAuthenticationMethod, &request)
346 );
347 }
348 let input = crate::CredentialAuthenticationInput {
349 headers: request.headers.clone(),
350 now: self.clock.now(),
351 };
352 let mut presented = matches!(selected, SelectedAuthorization::Session(_));
353 for method in &self.configuration.authentication_methods {
354 if method == &AuthenticationMethod::AepJwt {
355 continue;
356 }
357 let Some(handler) = self.configuration.grant_handlers.get(method.as_str()) else {
358 continue;
359 };
360 presented |= handler.has_presentation(&input).await?;
361 if let Some(principal) = handler.authenticate(&input).await? {
362 if principal.agent_did.is_empty()
363 || principal.authentication_kind != AuthenticationKind::SessionCredential
364 || principal.authentication_method != *method
365 || principal.grant_type.as_ref().map(GrantType::as_str) != Some(method.as_str())
366 {
367 return Ok(self.authentication_failure(ErrorCode::NotRecognized, &request));
368 }
369 let Some(enrollment) = self.enrollment_store.find(&principal.agent_did).await?
370 else {
371 return Ok(self.authentication_failure(ErrorCode::NotRecognized, &request));
372 };
373 if enrollment.status != AgentStatus::Active {
374 return Ok(self.authentication_failure(ErrorCode::NotRecognized, &request));
375 }
376 return Ok(ProtectedResourceAuthentication::Authenticated(principal));
377 }
378 }
379 let code = if presented {
380 ErrorCode::NotRecognized
381 } else {
382 ErrorCode::AuthenticationRequired
383 };
384 Ok(self.authentication_failure(code, &request))
385 }
386
387 async fn execute_enroll(
388 &self,
389 request: EnrollRequest,
390 ) -> Result<ServiceResponse, ServiceError> {
391 if let Some(existing) = self.enrollment_store.find(&request.agent_did).await? {
392 return Ok(success(ResponseBody::Enroll(enroll_response(&existing))));
393 }
394 let missing = missing_required_claim_names(
395 &self.configuration.claims.required,
396 request.claims.as_ref(),
397 );
398 if !missing.is_empty() {
399 let mut response = problem(ErrorCode::RequirementsUnmet, 422);
401 let ResponseBody::Problem(details) = &mut response.body else {
402 unreachable!();
403 };
404 details.requirements_pending = Some(
405 missing
406 .into_iter()
407 .map(|claim| claim.as_str().to_owned())
408 .collect(),
409 );
410 return Ok(response);
411 }
412 let now = self.clock.now();
413 let decision = self.enrollment_policy.decide(&request, now).await?;
414 let record = self
415 .enrollment_store
416 .save(EnrollmentRecord {
417 agent_did: request.agent_did,
418 claims: request.claims.unwrap_or_default(),
419 created_at: now,
420 owner_action_required: decision.owner_action_required,
421 requirements_pending: decision.requirements_pending,
422 since: now,
423 status: decision_status(decision.status),
424 updated_at: now,
425 verification_pending: decision.verification_pending,
426 })
427 .await?;
428 Ok(success(ResponseBody::Enroll(enroll_response(&record))))
429 }
430
431 async fn authenticate_command(
432 &self,
433 assertion: &str,
434 operation: AssertionOperation,
435 idempotency_key: Option<&str>,
436 ) -> Result<Option<aep_core::ClientAssertionClaims>, ServiceError> {
437 self.authenticate_assertion(assertion, operation, idempotency_key, None)
438 .await
439 }
440
441 async fn authenticate_assertion(
442 &self,
443 assertion: &str,
444 operation: AssertionOperation,
445 idempotency_key: Option<&str>,
446 resource: Option<&url::Url>,
447 ) -> Result<Option<aep_core::ClientAssertionClaims>, ServiceError> {
448 authenticate_assertion(AssertionAuthentication {
449 allow_insecure_loopback: self.allow_insecure_loopback,
450 assertion,
451 clock: self.clock.as_ref(),
452 idempotency_key,
453 identity_methods: &self.configuration.inspect_document.identity.methods,
454 maximum_clock_skew: self.configuration.maximum_clock_skew,
455 operation,
456 replay_store: &self.replay_store,
457 resource,
458 service_did: &self.configuration.service_did,
459 signing_algorithms: &self.configuration.signing_algorithms,
460 verifier: &self.verifier,
461 })
462 .await
463 }
464
465 fn authentication_failure(
466 &self,
467 code: ErrorCode,
468 request: &ProtectedResourceRequest,
469 ) -> ProtectedResourceAuthentication {
470 let mut response = problem(code.clone(), 401);
471 let inspect = self.configuration.inspect_url.clone().unwrap_or_else(|| {
472 let mut inspect = request.url.clone();
473 inspect.set_path(aep_core::WELL_KNOWN_PATH);
474 inspect.set_query(None);
475 inspect.set_fragment(None);
476 inspect
477 });
478 let challenge = format!(
479 "{} service_did=\"{}\", inspect=\"{}\", reason=\"{}\"",
480 aep_core::AUTHORIZATION_SCHEME,
481 self.configuration.service_did,
482 inspect,
483 code.as_str()
484 );
485 if let Ok(value) = HeaderValue::from_str(&challenge) {
486 response.headers.insert(header::WWW_AUTHENTICATE, value);
487 }
488 ProtectedResourceAuthentication::Rejected(response)
489 }
490}
491
492fn build_configuration(options: &ServiceOptions) -> Result<ServiceConfiguration, ServiceError> {
493 if !options.service_did.starts_with("did:") {
494 return Err(ServiceError::InvalidConfiguration(
495 "Service DID must be a DID".to_owned(),
496 ));
497 }
498 require_unique(
499 options.identity_methods.iter().map(|value| value.as_str()),
500 "identity methods",
501 )?;
502 require_unique(
503 options
504 .grant_types
505 .iter()
506 .map(|value| value.grant_type.as_str()),
507 "grant types",
508 )?;
509 require_unique(
510 options
511 .authentication_methods
512 .iter()
513 .map(|value| value.as_str()),
514 "authentication methods",
515 )?;
516 require_unique(
517 options
518 .signing_algorithms
519 .iter()
520 .map(|value| value.as_str()),
521 "signing algorithms",
522 )?;
523 if options.identity_methods.is_empty() || options.signing_algorithms.is_empty() {
524 return Err(ServiceError::InvalidConfiguration(
525 "identity methods and signing algorithms must not be empty".to_owned(),
526 ));
527 }
528 let mut grant_handlers = BTreeMap::new();
529 let mut grant_types_config = BTreeMap::new();
530 for definition in &options.grant_types {
531 let Some(handler) = &definition.handler else {
532 return Err(ServiceError::InvalidConfiguration(format!(
533 "grant type {} requires a handler",
534 definition.grant_type.as_str()
535 )));
536 };
537 grant_handlers.insert(definition.grant_type.as_str().to_owned(), handler.clone());
538 if let Some(config) = &definition.config {
539 grant_types_config.insert(definition.grant_type.as_str().to_owned(), config.clone());
540 }
541 }
542 for method in &options.authentication_methods {
543 if method != &AuthenticationMethod::AepJwt && !grant_handlers.contains_key(method.as_str())
544 {
545 return Err(ServiceError::InvalidConfiguration(format!(
546 "authentication method {} requires a matching grant type handler",
547 method.as_str()
548 )));
549 }
550 }
551 let commands = if options.grant_types.is_empty() {
552 vec![Command::Enroll, Command::Inspect, Command::Status]
553 } else {
554 vec![
555 Command::Enroll,
556 Command::Grant,
557 Command::Inspect,
558 Command::Revoke,
559 Command::Status,
560 ]
561 };
562 let inspect_document = InspectDocument {
563 aep_version: VERSION.to_owned(),
564 authentication: (!options.authentication_methods.is_empty()).then(|| Authentication {
565 methods: options.authentication_methods.clone(),
566 }),
567 bindings: Bindings {
568 supported: vec![Binding::Http],
569 additional: BTreeMap::new(),
570 },
571 claims: Some(InspectClaims {
572 optional: options.claims.optional.clone(),
573 preferred: options.claims.preferred.clone(),
574 required: options.claims.required.clone(),
575 additional: BTreeMap::new(),
576 }),
577 commands: Commands {
578 grant_types: options
579 .grant_types
580 .iter()
581 .map(|definition| definition.grant_type.clone())
582 .collect(),
583 grant_types_config,
584 supported: commands,
585 additional: BTreeMap::new(),
586 },
587 core: CoreConfiguration {
588 signing_algorithms: options.signing_algorithms.clone(),
589 additional: BTreeMap::new(),
590 },
591 extensions: Some(Extensions {
592 supported: options.extensions.clone(),
593 additional: BTreeMap::new(),
594 }),
595 http: HttpConfiguration {
596 endpoint_base: options.endpoint_base.clone(),
597 openapi: options
598 .openapi
599 .as_ref()
600 .map(|openapi| aep_core::OpenApiReference {
601 path_matching: aep_core::OpenApiPathMatching {
602 trailing_slash: openapi.trailing_slash,
603 },
604 url: openapi.url.clone(),
605 }),
606 additional: BTreeMap::new(),
607 },
608 identity: Identity {
609 methods: options.identity_methods.clone(),
610 additional: BTreeMap::new(),
611 },
612 service: ServiceIdentity {
613 did: options.service_did.clone(),
614 additional: BTreeMap::new(),
615 },
616 additional: BTreeMap::new(),
617 };
618 validate_inspect_document(&inspect_document).map_err(aep_core::CoreError::from)?;
619 Ok(ServiceConfiguration {
620 authentication_methods: options.authentication_methods.clone(),
621 claim_value_limits: options.claim_value_limits.validate()?,
622 claims: options.claims.clone(),
623 grant_handlers,
624 inspect_document,
625 inspect_url: options.inspect_url.clone(),
626 maximum_clock_skew: options.maximum_clock_skew,
627 service_did: options.service_did.clone(),
628 signing_algorithms: options.signing_algorithms.clone(),
629 })
630}
631
632fn within_claim_value_limits(
635 claims: Option<&aep_core::ClaimValues>,
636 limits: crate::ClaimValueLimits,
637) -> bool {
638 let Some(claims) = claims else {
639 return true;
640 };
641 let Ok(encoded) = serde_json::to_vec(claims) else {
642 return false;
643 };
644 if encoded.len() > limits.maximum_encoded_bytes {
645 return false;
646 }
647 let Ok(value) = serde_json::from_slice::<Value>(&encoded) else {
648 return false;
649 };
650 let mut members = 0;
651 within_value_limits(&value, 1, limits, &mut members)
652}
653
654fn within_value_limits(
655 value: &Value,
656 depth: usize,
657 limits: crate::ClaimValueLimits,
658 members: &mut usize,
659) -> bool {
660 match value {
661 Value::String(value) => value.chars().count() <= limits.maximum_string_length,
662 Value::Null | Value::Bool(_) | Value::Number(_) => true,
663 Value::Array(values) => {
664 depth <= limits.maximum_object_depth
665 && values
666 .iter()
667 .all(|value| within_value_limits(value, depth + 1, limits, members))
668 }
669 Value::Object(object) => {
670 if depth > limits.maximum_object_depth {
671 return false;
672 }
673 object.iter().all(|(name, value)| {
674 *members += 1;
675 *members <= limits.maximum_member_count
676 && name.chars().count() <= limits.maximum_string_length
677 && within_value_limits(value, depth + 1, limits, members)
678 })
679 }
680 }
681}
682
683fn enroll_response(record: &EnrollmentRecord) -> EnrollResponse {
684 EnrollResponse {
685 additional: BTreeMap::new(),
686 owner_action_required: record.owner_action_required.then_some(StringBoolean::True),
687 requirements_pending: non_empty(&record.requirements_pending),
688 status: record.status,
689 verification_pending: non_empty(&record.verification_pending),
690 }
691}
692
693fn status_response(record: &EnrollmentRecord) -> StatusResponse {
694 StatusResponse {
695 additional: BTreeMap::new(),
696 owner_action_required: record.owner_action_required.then_some(StringBoolean::True),
697 requirements_pending: non_empty(&record.requirements_pending),
698 since: record.since.format(&Rfc3339).ok(),
699 status: record.status,
700 verification_pending: non_empty(&record.verification_pending),
701 }
702}
703
704fn non_empty(values: &[String]) -> Option<Vec<String>> {
705 (!values.is_empty()).then(|| values.to_vec())
706}
707
708fn decision_status(status: EnrollmentDecisionStatus) -> AgentStatus {
709 match status {
710 EnrollmentDecisionStatus::Active => AgentStatus::Active,
711 EnrollmentDecisionStatus::Pending => AgentStatus::Pending,
712 EnrollmentDecisionStatus::Rejected => AgentStatus::Rejected,
713 }
714}
715
716fn blocked_enrollment(enrollment: &EnrollmentRecord) -> Option<ServiceResponse> {
717 let code = match enrollment.status {
718 AgentStatus::Active => return None,
719 AgentStatus::Pending => ErrorCode::VerificationPending,
720 AgentStatus::Rejected => ErrorCode::NotRecognized,
721 AgentStatus::Suspended => ErrorCode::IdentitySuspended,
722 AgentStatus::Terminated => ErrorCode::IdentityTerminated,
723 AgentStatus::Unavailable => ErrorCode::IdentityUnavailable,
724 };
725 let status = if code == ErrorCode::NotRecognized {
726 401
727 } else {
728 403
729 };
730 let mut response = problem(code, status);
731 if enrollment.status == AgentStatus::Pending {
732 let ResponseBody::Problem(details) = &mut response.body else {
733 unreachable!();
734 };
735 details.owner_action_required = enrollment
736 .owner_action_required
737 .then_some(StringBoolean::True);
738 details.requirements_pending = non_empty(&enrollment.requirements_pending);
739 details.verification_pending = non_empty(&enrollment.verification_pending);
740 }
741 Some(response)
742}
743
744fn idempotency_input(
745 agent_did: &str,
746 command: IdempotentCommand,
747 idempotency_key: &str,
748 request: &impl Serialize,
749) -> Result<CommandIdempotencyInput, ServiceError> {
750 let canonical = serde_json::to_vec(request)?;
751 let digest: String = Sha256::digest(canonical)
752 .iter()
753 .map(|byte| format!("{byte:02x}"))
754 .collect();
755 Ok(CommandIdempotencyInput {
756 agent_did: agent_did.to_owned(),
757 command,
758 idempotency_key: idempotency_key.to_owned(),
759 request_hash: format!("sha256:{digest}"),
760 })
761}
762
763fn idempotency_response(result: CommandIdempotencyResult) -> ServiceResponse {
764 match result {
765 CommandIdempotencyResult::Created(response)
766 | CommandIdempotencyResult::Replayed(response) => response,
767 CommandIdempotencyResult::Conflict => problem(ErrorCode::IdempotencyConflict, 409),
768 }
769}
770
771fn success(body: ResponseBody) -> ServiceResponse {
772 let mut headers = HeaderMap::new();
773 headers.insert(
774 header::CONTENT_TYPE,
775 HeaderValue::from_static(body.content_type()),
776 );
777 ServiceResponse {
778 body,
779 headers,
780 status: 200,
781 }
782}
783
784fn require_unique<'a>(
785 values: impl IntoIterator<Item = &'a str>,
786 name: &str,
787) -> Result<(), ServiceError> {
788 let mut unique = std::collections::BTreeSet::new();
789 if values.into_iter().any(|value| !unique.insert(value)) {
790 return Err(ServiceError::InvalidConfiguration(format!(
791 "{name} must not contain duplicates"
792 )));
793 }
794 Ok(())
795}