Skip to main content

zvault_server/
error.rs

1//! HTTP error types for `VaultRS` server.
2//!
3//! Maps domain errors from `zvault-core` into appropriate HTTP responses.
4//! Every error variant produces a JSON body with a machine-readable `error`
5//! field and a human-readable `message`.
6
7use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9use serde::Serialize;
10
11use zvault_core::error::{AppRoleError, BarrierError, DatabaseError, EngineError, LeaseError, MountError, PkiError, PolicyError, SealError, TokenError};
12
13/// Application-level error returned from HTTP handlers.
14#[derive(Debug)]
15pub enum AppError {
16    /// The vault is sealed — reject all secret operations.
17    Sealed,
18    /// Authentication failed or token invalid.
19    Unauthorized(String),
20    /// Policy denied the operation.
21    Forbidden(String),
22    /// Requested resource not found.
23    NotFound(String),
24    /// Client sent invalid input.
25    BadRequest(String),
26    /// A conflict (e.g., already initialized, already mounted).
27    Conflict(String),
28    /// Internal server error.
29    Internal(String),
30}
31
32/// JSON error response body.
33#[derive(Serialize)]
34struct ErrorBody {
35    error: &'static str,
36    message: String,
37}
38
39impl IntoResponse for AppError {
40    fn into_response(self) -> Response {
41        let (status, error_type, message) = match self {
42            Self::Sealed => (
43                StatusCode::SERVICE_UNAVAILABLE,
44                "sealed",
45                "vault is sealed".to_owned(),
46            ),
47            Self::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, "unauthorized", msg),
48            Self::Forbidden(msg) => (StatusCode::FORBIDDEN, "forbidden", msg),
49            Self::NotFound(msg) => (StatusCode::NOT_FOUND, "not_found", msg),
50            Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg),
51            Self::Conflict(msg) => (StatusCode::CONFLICT, "conflict", msg),
52            Self::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, "internal_error", msg),
53        };
54
55        let body = ErrorBody {
56            error: error_type,
57            message,
58        };
59
60        (status, axum::Json(body)).into_response()
61    }
62}
63
64impl From<SealError> for AppError {
65    fn from(err: SealError) -> Self {
66        match err {
67            SealError::AlreadyInitialized
68            | SealError::AlreadyUnsealed
69            | SealError::AlreadySealed => Self::Conflict(err.to_string()),
70
71            SealError::NotInitialized
72            | SealError::InvalidConfig { .. }
73            | SealError::InvalidShare { .. }
74            | SealError::RecoveryFailed { .. }
75            | SealError::RootKeyDecryption { .. } => Self::BadRequest(err.to_string()),
76
77            SealError::Crypto(_) | SealError::Barrier(_) | SealError::Storage(_) => {
78                Self::Internal(err.to_string())
79            }
80        }
81    }
82}
83
84impl From<BarrierError> for AppError {
85    fn from(err: BarrierError) -> Self {
86        match err {
87            BarrierError::Sealed => Self::Sealed,
88            BarrierError::Crypto(_) | BarrierError::Storage(_) => Self::Internal(err.to_string()),
89        }
90    }
91}
92
93impl From<TokenError> for AppError {
94    fn from(err: TokenError) -> Self {
95        match err {
96            TokenError::NotFound => Self::Unauthorized("invalid token".to_owned()),
97            TokenError::Expired { .. } => Self::Unauthorized(err.to_string()),
98            TokenError::NotRenewable | TokenError::MaxTtlExceeded { .. } => {
99                Self::BadRequest(err.to_string())
100            }
101            TokenError::Barrier(ref inner) => match inner {
102                BarrierError::Sealed => Self::Sealed,
103                BarrierError::Crypto(_) | BarrierError::Storage(_) => {
104                    Self::Internal(err.to_string())
105                }
106            },
107        }
108    }
109}
110
111impl From<PolicyError> for AppError {
112    fn from(err: PolicyError) -> Self {
113        match err {
114            PolicyError::NotFound { .. } => Self::NotFound(err.to_string()),
115            PolicyError::Invalid { .. } => Self::BadRequest(err.to_string()),
116            PolicyError::BuiltIn { .. } | PolicyError::Denied { .. } => {
117                Self::Forbidden(err.to_string())
118            }
119            PolicyError::Barrier(ref inner) => match inner {
120                BarrierError::Sealed => Self::Sealed,
121                BarrierError::Crypto(_) | BarrierError::Storage(_) => {
122                    Self::Internal(err.to_string())
123                }
124            },
125        }
126    }
127}
128
129impl From<MountError> for AppError {
130    fn from(err: MountError) -> Self {
131        match err {
132            MountError::AlreadyMounted { .. } => Self::Conflict(err.to_string()),
133            MountError::NotFound { .. } => Self::NotFound(err.to_string()),
134            MountError::InvalidPath { .. } | MountError::UnknownEngineType { .. } => {
135                Self::BadRequest(err.to_string())
136            }
137            MountError::Barrier(ref inner) => match inner {
138                BarrierError::Sealed => Self::Sealed,
139                BarrierError::Crypto(_) | BarrierError::Storage(_) => {
140                    Self::Internal(err.to_string())
141                }
142            },
143        }
144    }
145}
146
147impl From<EngineError> for AppError {
148    fn from(err: EngineError) -> Self {
149        match err {
150            EngineError::NotFound { .. } => Self::NotFound(err.to_string()),
151            EngineError::InvalidRequest { .. } => Self::BadRequest(err.to_string()),
152            EngineError::Barrier(ref inner) => match inner {
153                BarrierError::Sealed => Self::Sealed,
154                BarrierError::Crypto(_) | BarrierError::Storage(_) => {
155                    Self::Internal(err.to_string())
156                }
157            },
158            EngineError::Internal { .. } => Self::Internal(err.to_string()),
159        }
160    }
161}
162
163impl From<LeaseError> for AppError {
164    fn from(err: LeaseError) -> Self {
165        match err {
166            LeaseError::NotFound { .. } => Self::NotFound(err.to_string()),
167            LeaseError::Expired { .. } | LeaseError::NotRenewable { .. } => {
168                Self::BadRequest(err.to_string())
169            }
170            LeaseError::Barrier(ref inner) => match inner {
171                BarrierError::Sealed => Self::Sealed,
172                BarrierError::Crypto(_) | BarrierError::Storage(_) => {
173                    Self::Internal(err.to_string())
174                }
175            },
176        }
177    }
178}
179
180impl From<DatabaseError> for AppError {
181    fn from(err: DatabaseError) -> Self {
182        match err {
183            DatabaseError::NotFound { .. } | DatabaseError::RoleNotFound { .. } => {
184                Self::NotFound(err.to_string())
185            }
186            DatabaseError::InvalidConfig { .. } => Self::BadRequest(err.to_string()),
187            DatabaseError::Internal { .. } => Self::Internal(err.to_string()),
188            DatabaseError::Barrier(ref inner) => match inner {
189                BarrierError::Sealed => Self::Sealed,
190                BarrierError::Crypto(_) | BarrierError::Storage(_) => {
191                    Self::Internal(err.to_string())
192                }
193            },
194        }
195    }
196}
197
198impl From<PkiError> for AppError {
199    fn from(err: PkiError) -> Self {
200        match err {
201            PkiError::NoRootCa => Self::NotFound(err.to_string()),
202            PkiError::RoleNotFound { .. } => Self::NotFound(err.to_string()),
203            PkiError::InvalidRequest { .. } => Self::BadRequest(err.to_string()),
204            PkiError::CertGeneration { .. } | PkiError::Internal { .. } => {
205                Self::Internal(err.to_string())
206            }
207            PkiError::Barrier(ref inner) => match inner {
208                BarrierError::Sealed => Self::Sealed,
209                BarrierError::Crypto(_) | BarrierError::Storage(_) => {
210                    Self::Internal(err.to_string())
211                }
212            },
213        }
214    }
215}
216
217impl From<AppRoleError> for AppError {
218    fn from(err: AppRoleError) -> Self {
219        match err {
220            AppRoleError::RoleNotFound { .. } => Self::NotFound(err.to_string()),
221            AppRoleError::InvalidSecretId { .. } => {
222                Self::Unauthorized(err.to_string())
223            }
224            AppRoleError::InvalidConfig { .. } => Self::BadRequest(err.to_string()),
225            AppRoleError::Internal { .. } => Self::Internal(err.to_string()),
226            AppRoleError::Barrier(ref inner) => match inner {
227                BarrierError::Sealed => Self::Sealed,
228                BarrierError::Crypto(_) | BarrierError::Storage(_) => {
229                    Self::Internal(err.to_string())
230                }
231            },
232        }
233    }
234}