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("secrets error: {0}")]
33 Secrets(#[from] SecretsResolverError),
34
35 #[error("authentication error: {0}")]
36 Authentication(String),
37
38 #[error("unauthorized: {0}")]
39 Unauthorized(String),
40
41 #[error("forbidden: {0}")]
42 Forbidden(String),
43
44 #[error("step-up required: {0}")]
56 StepUpRequired(String),
57
58 #[error("approval required: {code}")]
78 ApprovalRequired {
79 code: &'static str,
80 details: serde_json::Value,
81 },
82
83 #[error("validation error: {0}")]
84 Validation(String),
85
86 #[error("request is missing required Trust-Task header")]
91 TrustTaskMissing,
92
93 #[error("Trust-Task header does not match handler (expected {expected})")]
98 TrustTaskMismatch {
99 expected: String,
100 received: Option<String>,
101 },
102
103 #[error("malformed Trust-Task identifier: {0}")]
107 TrustTaskMalformed(String),
108
109 #[error("Idempotency-Key conflict: same key, different request body")]
115 IdempotencyKeyConflict,
116
117 #[error("invalid pagination cursor")]
124 InvalidCursor,
125
126 #[error("resource limit exceeded: {0}")]
134 ResourceExhausted(String),
135
136 #[error("{message}")]
139 ServiceError { status: StatusCode, message: String },
140
141 #[error("{operation} failed: {source}")]
147 Vsock {
148 operation: &'static str,
149 #[source]
150 source: std::io::Error,
151 },
152}
153
154impl AppError {
155 pub fn vsock(operation: &'static str) -> impl FnOnce(std::io::Error) -> AppError {
160 move |source| AppError::Vsock { operation, source }
161 }
162}
163
164impl From<crate::auth::backend::AuthError> for AppError {
181 fn from(e: crate::auth::backend::AuthError) -> Self {
182 use crate::auth::backend::AuthError as A;
183 match e {
184 A::Forbidden | A::DidMethodRejected => AppError::Forbidden(e.to_string()),
185 A::PendingChallengeLimitReached => AppError::Validation(e.to_string()),
186 A::SessionNotFound
187 | A::SessionStateMismatch
188 | A::ChallengeMismatch
189 | A::ChallengeExpired
190 | A::SignerMismatch
191 | A::StaleMessage
192 | A::RefreshTokenInvalid
193 | A::RefreshTokenExpired => AppError::Authentication(e.to_string()),
194 A::AttestationFailed(msg) => AppError::Internal(format!("tee attestation: {msg}")),
195 A::Internal(msg) => AppError::Internal(msg),
196 }
197 }
198}
199
200impl IntoResponse for AppError {
201 fn into_response(self) -> Response {
202 let status = match &self {
203 AppError::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
204 AppError::Io(_) => StatusCode::INTERNAL_SERVER_ERROR,
205 AppError::Store(_) => StatusCode::INTERNAL_SERVER_ERROR,
206 AppError::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
207 AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
208 AppError::SecretStore(_) => StatusCode::INTERNAL_SERVER_ERROR,
209 AppError::NotFound(_) => StatusCode::NOT_FOUND,
210 AppError::Conflict(_) => StatusCode::CONFLICT,
211 AppError::Secrets(_) => StatusCode::INTERNAL_SERVER_ERROR,
212 AppError::Authentication(_) => StatusCode::UNAUTHORIZED,
213 AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
214 AppError::Forbidden(_) => StatusCode::FORBIDDEN,
215 AppError::StepUpRequired(_) => StatusCode::FORBIDDEN,
216 AppError::ApprovalRequired { .. } => StatusCode::FORBIDDEN,
217 AppError::Validation(_) => StatusCode::BAD_REQUEST,
218 AppError::TrustTaskMissing => StatusCode::BAD_REQUEST,
219 AppError::TrustTaskMismatch { .. } => StatusCode::UNSUPPORTED_MEDIA_TYPE,
220 AppError::TrustTaskMalformed(_) => StatusCode::BAD_REQUEST,
221 AppError::IdempotencyKeyConflict => StatusCode::UNPROCESSABLE_ENTITY,
222 AppError::InvalidCursor => StatusCode::BAD_REQUEST,
223 AppError::ResourceExhausted(_) => StatusCode::SERVICE_UNAVAILABLE,
224 AppError::ServiceError { status, .. } => *status,
225 AppError::Vsock { .. } => StatusCode::INTERNAL_SERVER_ERROR,
226 };
227
228 if status.is_server_error() {
229 warn!(status = %status.as_u16(), error = %self, "server error");
230 } else {
231 debug!(status = %status.as_u16(), error = %self, "client error");
232 }
233
234 let body = match &self {
239 AppError::TrustTaskMissing => serde_json::json!({
240 "error": "TrustTaskMissing",
241 "message": self.to_string(),
242 }),
243 AppError::TrustTaskMismatch { expected, received } => serde_json::json!({
244 "error": "TrustTaskMismatch",
245 "message": self.to_string(),
246 "expected": expected,
247 "received": received,
248 }),
249 AppError::TrustTaskMalformed(value) => serde_json::json!({
250 "error": "TrustTaskMalformed",
251 "message": self.to_string(),
252 "received": value,
253 }),
254 AppError::IdempotencyKeyConflict => serde_json::json!({
255 "error": "IdempotencyKeyConflict",
256 "message": self.to_string(),
257 }),
258 AppError::StepUpRequired(msg) => serde_json::json!({
259 "error": "step_up_required",
260 "message": msg,
261 "requiredAcr": "aal2",
262 }),
263 AppError::ApprovalRequired { code, details } => {
269 let mut body = match details {
270 serde_json::Value::Object(map) => map.clone(),
271 _ => serde_json::Map::new(),
272 };
273 body.insert("error".to_string(), serde_json::json!(code));
274 serde_json::Value::Object(body)
275 }
276 _ => serde_json::json!({ "error": self.to_string() }),
277 };
278 (status, axum::Json(body)).into_response()
279 }
280}
281
282pub fn key_derivation_error(msg: impl Into<String>) -> AppError {
284 AppError::ServiceError {
285 status: StatusCode::BAD_REQUEST,
286 message: format!("key derivation error: {}", msg.into()),
287 }
288}
289
290pub fn bad_gateway_error(msg: impl Into<String>) -> AppError {
292 AppError::ServiceError {
293 status: StatusCode::BAD_GATEWAY,
294 message: format!("bad gateway: {}", msg.into()),
295 }
296}
297
298pub fn tee_attestation_error(msg: impl Into<String>) -> AppError {
300 AppError::ServiceError {
301 status: StatusCode::SERVICE_UNAVAILABLE,
302 message: format!("TEE attestation error: {}", msg.into()),
303 }
304}
305
306#[cfg(test)]
307mod approval_required_tests {
308 use super::*;
309 use axum::body::to_bytes;
310 use axum::response::IntoResponse;
311
312 async fn body_of(err: AppError) -> serde_json::Value {
313 let resp = err.into_response();
314 let bytes = to_bytes(resp.into_body(), usize::MAX).await.expect("body");
315 serde_json::from_slice(&bytes).expect("json body")
316 }
317
318 #[tokio::test]
321 async fn details_are_merged_alongside_the_code() {
322 let body = body_of(AppError::ApprovalRequired {
323 code: "auth:step_up_required",
324 details: serde_json::json!({
325 "requiredAcr": "aal2",
326 "approveRequest": { "id": "urn:uuid:abc" },
327 }),
328 })
329 .await;
330
331 assert_eq!(body["error"], "auth:step_up_required");
332 assert_eq!(body["requiredAcr"], "aal2");
333 assert_eq!(body["approveRequest"]["id"], "urn:uuid:abc");
334 }
335
336 #[tokio::test]
337 async fn consent_details_survive_the_round_trip() {
338 let body = body_of(AppError::ApprovalRequired {
339 code: "auth:consent_required",
340 details: serde_json::json!({
341 "approverSet": "ops",
342 "minApprovals": 2,
343 "excludeRequester": true,
344 }),
345 })
346 .await;
347
348 assert_eq!(body["error"], "auth:consent_required");
349 assert_eq!(body["minApprovals"], 2);
350 assert_eq!(body["excludeRequester"], true);
351 }
352
353 #[tokio::test]
356 async fn details_cannot_overwrite_the_code() {
357 let body = body_of(AppError::ApprovalRequired {
358 code: "auth:consent_required",
359 details: serde_json::json!({ "error": "allow", "approverSet": "ops" }),
360 })
361 .await;
362
363 assert_eq!(body["error"], "auth:consent_required");
364 assert_eq!(body["approverSet"], "ops");
365 }
366
367 #[tokio::test]
370 async fn a_non_object_details_still_renders_the_code() {
371 let body = body_of(AppError::ApprovalRequired {
372 code: "auth:step_up_required",
373 details: serde_json::Value::Null,
374 })
375 .await;
376
377 assert_eq!(body["error"], "auth:step_up_required");
378 assert!(body.as_object().is_some_and(|m| m.len() == 1));
379 }
380
381 #[tokio::test]
382 async fn renders_as_forbidden() {
383 let resp = AppError::ApprovalRequired {
384 code: "auth:consent_required",
385 details: serde_json::json!({}),
386 }
387 .into_response();
388 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
389 }
390}