1use affinidi_tdk::secrets_resolver::errors::SecretsResolverError;
2use axum::http::StatusCode;
3use axum::response::{IntoResponse, Response};
4use tracing::{debug, warn};
5
6#[derive(Debug, thiserror::Error)]
7pub enum AppError {
8 #[error("configuration error: {0}")]
9 Config(String),
10
11 #[error("io error: {0}")]
12 Io(#[from] std::io::Error),
13
14 #[error("store error: {0}")]
15 Store(#[from] fjall::Error),
16
17 #[error("serialization error: {0}")]
18 Serialization(#[from] serde_json::Error),
19
20 #[error("internal error: {0}")]
21 Internal(String),
22
23 #[error("secret store error: {0}")]
24 SecretStore(String),
25
26 #[error("not found: {0}")]
27 NotFound(String),
28
29 #[error("conflict: {0}")]
30 Conflict(String),
31
32 #[error("gone: {0}")]
42 Gone(String),
43
44 #[error("secrets error: {0}")]
45 Secrets(#[from] SecretsResolverError),
46
47 #[error("authentication error: {0}")]
48 Authentication(String),
49
50 #[error("unauthorized: {0}")]
51 Unauthorized(String),
52
53 #[error("forbidden: {0}")]
54 Forbidden(String),
55
56 #[error("step-up required: {0}")]
68 StepUpRequired(String),
69
70 #[error("approval required: {code}")]
90 ApprovalRequired {
91 code: &'static str,
92 details: serde_json::Value,
93 },
94
95 #[error("validation error: {0}")]
96 Validation(String),
97
98 #[error("request is missing required Trust-Task header")]
103 TrustTaskMissing,
104
105 #[error("Trust-Task header does not match handler (expected {expected})")]
110 TrustTaskMismatch {
111 expected: String,
112 received: Option<String>,
113 },
114
115 #[error("malformed Trust-Task identifier: {0}")]
119 TrustTaskMalformed(String),
120
121 #[error("Idempotency-Key conflict: same key, different request body")]
127 IdempotencyKeyConflict,
128
129 #[error("invalid pagination cursor")]
136 InvalidCursor,
137
138 #[error("resource limit exceeded: {0}")]
146 ResourceExhausted(String),
147
148 #[error("{message}")]
151 ServiceError { status: StatusCode, message: String },
152
153 #[error("{operation} failed: {source}")]
159 Vsock {
160 operation: &'static str,
161 #[source]
162 source: std::io::Error,
163 },
164}
165
166impl AppError {
167 pub fn vsock(operation: &'static str) -> impl FnOnce(std::io::Error) -> AppError {
172 move |source| AppError::Vsock { operation, source }
173 }
174}
175
176impl From<crate::auth::backend::AuthError> for AppError {
193 fn from(e: crate::auth::backend::AuthError) -> Self {
194 use crate::auth::backend::AuthError as A;
195 match e {
196 A::Forbidden | A::DidMethodRejected => AppError::Forbidden(e.to_string()),
197 A::PendingChallengeLimitReached => AppError::Validation(e.to_string()),
198 A::SessionNotFound
199 | A::SessionStateMismatch
200 | A::ChallengeMismatch
201 | A::ChallengeExpired
202 | A::SignerMismatch
203 | A::StaleMessage
204 | A::RefreshTokenInvalid
205 | A::RefreshTokenExpired => AppError::Authentication(e.to_string()),
206 A::AttestationFailed(msg) => AppError::Internal(format!("tee attestation: {msg}")),
207 A::Internal(msg) => AppError::Internal(msg),
208 }
209 }
210}
211
212impl IntoResponse for AppError {
213 fn into_response(self) -> Response {
214 let status = match &self {
215 AppError::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
216 AppError::Io(_) => StatusCode::INTERNAL_SERVER_ERROR,
217 AppError::Store(_) => StatusCode::INTERNAL_SERVER_ERROR,
218 AppError::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
219 AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
220 AppError::SecretStore(_) => StatusCode::INTERNAL_SERVER_ERROR,
221 AppError::NotFound(_) => StatusCode::NOT_FOUND,
222 AppError::Conflict(_) => StatusCode::CONFLICT,
223 AppError::Gone(_) => StatusCode::GONE,
224 AppError::Secrets(_) => StatusCode::INTERNAL_SERVER_ERROR,
225 AppError::Authentication(_) => StatusCode::UNAUTHORIZED,
226 AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
227 AppError::Forbidden(_) => StatusCode::FORBIDDEN,
228 AppError::StepUpRequired(_) => StatusCode::FORBIDDEN,
229 AppError::ApprovalRequired { .. } => StatusCode::FORBIDDEN,
230 AppError::Validation(_) => StatusCode::BAD_REQUEST,
231 AppError::TrustTaskMissing => StatusCode::BAD_REQUEST,
232 AppError::TrustTaskMismatch { .. } => StatusCode::UNSUPPORTED_MEDIA_TYPE,
233 AppError::TrustTaskMalformed(_) => StatusCode::BAD_REQUEST,
234 AppError::IdempotencyKeyConflict => StatusCode::UNPROCESSABLE_ENTITY,
235 AppError::InvalidCursor => StatusCode::BAD_REQUEST,
236 AppError::ResourceExhausted(_) => StatusCode::SERVICE_UNAVAILABLE,
237 AppError::ServiceError { status, .. } => *status,
238 AppError::Vsock { .. } => StatusCode::INTERNAL_SERVER_ERROR,
239 };
240
241 if status.is_server_error() {
242 warn!(status = %status.as_u16(), error = %self, "server error");
243 } else {
244 debug!(status = %status.as_u16(), error = %self, "client error");
245 }
246
247 let body = match &self {
252 AppError::TrustTaskMissing => serde_json::json!({
253 "error": "TrustTaskMissing",
254 "message": self.to_string(),
255 }),
256 AppError::TrustTaskMismatch { expected, received } => serde_json::json!({
257 "error": "TrustTaskMismatch",
258 "message": self.to_string(),
259 "expected": expected,
260 "received": received,
261 }),
262 AppError::TrustTaskMalformed(value) => serde_json::json!({
263 "error": "TrustTaskMalformed",
264 "message": self.to_string(),
265 "received": value,
266 }),
267 AppError::IdempotencyKeyConflict => serde_json::json!({
268 "error": "IdempotencyKeyConflict",
269 "message": self.to_string(),
270 }),
271 AppError::StepUpRequired(msg) => serde_json::json!({
272 "error": "step_up_required",
273 "message": msg,
274 "requiredAcr": "aal2",
275 }),
276 AppError::ApprovalRequired { code, details } => {
282 let mut body = match details {
283 serde_json::Value::Object(map) => map.clone(),
284 _ => serde_json::Map::new(),
285 };
286 body.insert("error".to_string(), serde_json::json!(code));
287 serde_json::Value::Object(body)
288 }
289 _ => serde_json::json!({ "error": self.to_string() }),
290 };
291 (status, axum::Json(body)).into_response()
292 }
293}
294
295pub fn key_derivation_error(msg: impl Into<String>) -> AppError {
297 AppError::ServiceError {
298 status: StatusCode::BAD_REQUEST,
299 message: format!("key derivation error: {}", msg.into()),
300 }
301}
302
303pub fn bad_gateway_error(msg: impl Into<String>) -> AppError {
305 AppError::ServiceError {
306 status: StatusCode::BAD_GATEWAY,
307 message: format!("bad gateway: {}", msg.into()),
308 }
309}
310
311pub fn tee_attestation_error(msg: impl Into<String>) -> AppError {
313 AppError::ServiceError {
314 status: StatusCode::SERVICE_UNAVAILABLE,
315 message: format!("TEE attestation error: {}", msg.into()),
316 }
317}
318
319#[cfg(test)]
320mod approval_required_tests {
321 use super::*;
322 use axum::body::to_bytes;
323 use axum::response::IntoResponse;
324
325 async fn body_of(err: AppError) -> serde_json::Value {
326 let resp = err.into_response();
327 let bytes = to_bytes(resp.into_body(), usize::MAX).await.expect("body");
328 serde_json::from_slice(&bytes).expect("json body")
329 }
330
331 #[tokio::test]
334 async fn details_are_merged_alongside_the_code() {
335 let body = body_of(AppError::ApprovalRequired {
336 code: "auth:step_up_required",
337 details: serde_json::json!({
338 "requiredAcr": "aal2",
339 "approveRequest": { "id": "urn:uuid:abc" },
340 }),
341 })
342 .await;
343
344 assert_eq!(body["error"], "auth:step_up_required");
345 assert_eq!(body["requiredAcr"], "aal2");
346 assert_eq!(body["approveRequest"]["id"], "urn:uuid:abc");
347 }
348
349 #[tokio::test]
350 async fn consent_details_survive_the_round_trip() {
351 let body = body_of(AppError::ApprovalRequired {
352 code: "auth:consent_required",
353 details: serde_json::json!({
354 "approverSet": "ops",
355 "minApprovals": 2,
356 "excludeRequester": true,
357 }),
358 })
359 .await;
360
361 assert_eq!(body["error"], "auth:consent_required");
362 assert_eq!(body["minApprovals"], 2);
363 assert_eq!(body["excludeRequester"], true);
364 }
365
366 #[tokio::test]
369 async fn details_cannot_overwrite_the_code() {
370 let body = body_of(AppError::ApprovalRequired {
371 code: "auth:consent_required",
372 details: serde_json::json!({ "error": "allow", "approverSet": "ops" }),
373 })
374 .await;
375
376 assert_eq!(body["error"], "auth:consent_required");
377 assert_eq!(body["approverSet"], "ops");
378 }
379
380 #[tokio::test]
383 async fn a_non_object_details_still_renders_the_code() {
384 let body = body_of(AppError::ApprovalRequired {
385 code: "auth:step_up_required",
386 details: serde_json::Value::Null,
387 })
388 .await;
389
390 assert_eq!(body["error"], "auth:step_up_required");
391 assert!(body.as_object().is_some_and(|m| m.len() == 1));
392 }
393
394 #[tokio::test]
395 async fn renders_as_forbidden() {
396 let resp = AppError::ApprovalRequired {
397 code: "auth:consent_required",
398 details: serde_json::json!({}),
399 }
400 .into_response();
401 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
402 }
403}
404
405#[cfg(test)]
406mod gone_tests {
407 use super::*;
408
409 #[test]
414 fn renders_as_410_gone() {
415 let resp =
416 AppError::Gone("TEE first-boot carve-out has already been used".into()).into_response();
417 assert_eq!(resp.status(), StatusCode::GONE);
418 }
419
420 #[tokio::test]
421 async fn body_carries_the_message() {
422 use axum::body::to_bytes;
423
424 let resp = AppError::Gone("carve-out closed".into()).into_response();
425 let bytes = to_bytes(resp.into_body(), usize::MAX).await.expect("body");
426 let body: serde_json::Value = serde_json::from_slice(&bytes).expect("json body");
427 assert_eq!(body["error"], "gone: carve-out closed");
428 }
429}