Skip to main content

trusty_memory/transport/
api_error.rs

1//! The failure type every folded method reports, and its one mapping onto the
2//! wire (#6286).
3//!
4//! Why: these handlers used to be axum handlers, and `ApiError` used to be an
5//! HTTP status plus a message with an `IntoResponse` impl. ADR-0032 retired the
6//! listener, so the status code has nothing left to set. What the status
7//! ENCODED is still worth keeping — "the palace is not there" and "the palace
8//! could not be read" are different answers and #5549 is the issue that proves
9//! a caller acts on the difference — so the type survives as a failure KIND and
10//! the single [`From<ApiError> for RpcError`] below is where a kind becomes a
11//! code.
12//!
13//! What: [`ApiError`] carries an [`ErrorKind`] and a message; [`open_handle`]
14//! is the one palace lookup every method that names a palace by id runs.
15//!
16//! Test: `api_error_kind_maps_to_its_rpc_code`,
17//! `api_error_message_survives_the_conversion`.
18
19use trusty_common::memory_core::palace::PalaceId;
20use trusty_common::memory_core::PalaceRegistry;
21#[cfg_attr(not(test), allow(unused_imports))]
22use trusty_common::uds::server::{
23    RpcError, CODE_INTERNAL_ERROR, CODE_INVALID_PARAMS, CODE_METHOD_NOT_FOUND,
24};
25
26use crate::AppState;
27
28/// The request named something that is not there.
29///
30/// Why: `CODE_METHOD_NOT_FOUND` says the METHOD is unknown, which is a
31/// different fact from a well-formed call naming a palace, drawer or session
32/// that does not exist. trusty-analyze took the same code for the same
33/// distinction (#5049), and a caller that already reads `-32004` from one
34/// daemon reads it the same way here.
35pub const CODE_NOT_FOUND: i64 = -32004;
36
37/// The request is well formed and refused anyway.
38///
39/// Covers both a state conflict — deleting a palace that still holds drawers
40/// without `force` — and a refusal by the multi-tenant authz seam (#1714). The
41/// two were 409 and 403 over HTTP; on this wire the caller's next move is the
42/// same for both, which is to read the message.
43pub const CODE_REFUSED: i64 = -32006;
44
45/// Why one call failed, at the granularity a caller acts on.
46///
47/// Why: not the HTTP status set it replaces. `NotFound` and `Internal` were 404
48/// and 500 and stay distinct because #5549 turns on exactly that distinction;
49/// `Conflict` and `Forbidden` were 409 and 403 and collapse into `Refused`
50/// because nothing downstream branched on which.
51/// Test: `api_error_kind_maps_to_its_rpc_code`.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum ErrorKind {
54    /// The caller's arguments do not describe a request this method can run.
55    BadRequest,
56    /// Well formed, and it names something absent.
57    NotFound,
58    /// Well formed, understood, and refused — a state conflict or an authz
59    /// denial.
60    Refused,
61    /// The daemon could not complete the call.
62    Internal,
63}
64
65impl ErrorKind {
66    /// The JSON-RPC code this kind crosses the wire as.
67    fn code(self) -> i64 {
68        match self {
69            Self::BadRequest => CODE_INVALID_PARAMS,
70            Self::NotFound => CODE_NOT_FOUND,
71            Self::Refused => CODE_REFUSED,
72            Self::Internal => CODE_INTERNAL_ERROR,
73        }
74    }
75}
76
77/// One folded method's failure.
78///
79/// Why: the handlers below return `Result<Value, ApiError>` rather than
80/// `Result<Value, RpcError>` so the code mapping lives in one place instead of
81/// at every `return`. That is the shape trusty-analyze's `ApiError` took
82/// through the same migration.
83/// What: a kind and a message. The message reaches the caller verbatim.
84/// Test: `api_error_message_survives_the_conversion`.
85#[derive(Debug, Clone)]
86pub struct ApiError {
87    /// Which failure this is.
88    pub kind: ErrorKind,
89    /// What to tell the caller.
90    pub message: String,
91}
92
93impl ApiError {
94    /// The caller's arguments are wrong.
95    pub fn bad_request(msg: impl Into<String>) -> Self {
96        Self {
97            kind: ErrorKind::BadRequest,
98            message: msg.into(),
99        }
100    }
101
102    /// The thing named is not there.
103    pub fn not_found(msg: impl Into<String>) -> Self {
104        Self {
105            kind: ErrorKind::NotFound,
106            message: msg.into(),
107        }
108    }
109
110    /// Understood, and refused. Formerly 409 Conflict.
111    pub fn conflict(msg: impl Into<String>) -> Self {
112        Self {
113            kind: ErrorKind::Refused,
114            message: msg.into(),
115        }
116    }
117
118    /// Understood, and denied. Formerly 403 Forbidden (#1714).
119    pub fn forbidden(msg: impl Into<String>) -> Self {
120        Self {
121            kind: ErrorKind::Refused,
122            message: msg.into(),
123        }
124    }
125
126    /// Structurally valid and semantically unacceptable — content too short to
127    /// be worth storing (#466). Formerly 422.
128    pub fn unprocessable(msg: impl Into<String>) -> Self {
129        Self {
130            kind: ErrorKind::BadRequest,
131            message: msg.into(),
132        }
133    }
134
135    /// The daemon could not complete the call.
136    pub fn internal(msg: impl Into<String>) -> Self {
137        Self {
138            kind: ErrorKind::Internal,
139            message: msg.into(),
140        }
141    }
142}
143
144impl std::fmt::Display for ApiError {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        f.write_str(&self.message)
147    }
148}
149
150impl std::error::Error for ApiError {}
151
152/// The one place a failure kind becomes a JSON-RPC code (#6286).
153///
154/// Every folded method's `?` runs through here, so a change to the mapping is a
155/// change to the whole surface rather than to whichever handler was edited last.
156///
157/// Test: `api_error_kind_maps_to_its_rpc_code`.
158impl From<ApiError> for RpcError {
159    fn from(e: ApiError) -> Self {
160        RpcError::new(e.kind.code(), e.message)
161    }
162}
163
164impl From<crate::service::ServiceError> for ApiError {
165    fn from(e: crate::service::ServiceError) -> Self {
166        match e {
167            crate::service::ServiceError::BadRequest(m) => ApiError::bad_request(m),
168            crate::service::ServiceError::NotFound(m) => ApiError::not_found(m),
169            crate::service::ServiceError::Conflict(m) => ApiError::conflict(m),
170            crate::service::ServiceError::Internal(m) => ApiError::internal(m),
171            crate::service::ServiceError::Forbidden(m) => ApiError::forbidden(m),
172        }
173    }
174}
175
176/// Open a palace handle by id, telling absence apart from an open that failed.
177///
178/// Why: every method that references a palace by id runs the same registry
179/// lookup, and #5549 (ADR-0045) is what makes the distinction load-bearing:
180/// mapping every open failure to "not found" sent an operator looking for a
181/// deleted palace when what they had was a denied read or a jammed redb lock.
182/// What: calls `PalaceRegistry::open_palace`, then asks
183/// `PalaceRegistry::open_error_is_absent` which failure it got —
184/// [`ApiError::not_found`] only for a genuine absence, [`ApiError::internal`]
185/// otherwise.
186/// Test: `unreadable_palace_is_internal_not_not_found_at_open_handle`.
187pub fn open_handle(
188    state: &AppState,
189    id: &str,
190) -> Result<std::sync::Arc<trusty_common::memory_core::PalaceHandle>, ApiError> {
191    state
192        .registry
193        .open_palace(&state.data_root, &PalaceId::new(id))
194        .map_err(|e| {
195            if PalaceRegistry::open_error_is_absent(&e) {
196                ApiError::not_found(format!("palace not found: {id} ({e:#})"))
197            } else {
198                ApiError::internal(format!("palace could not be loaded: {id} ({e:#})"))
199            }
200        })
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    /// Why: the mapping is the whole contract this type exists to hold, and a
208    /// silent change to it would look identical on the happy path. The two
209    /// codes that are not JSON-RPC standard are the ones a consumer has to
210    /// learn, so both are asserted by value rather than by name.
211    /// Test: itself.
212    #[test]
213    fn api_error_kind_maps_to_its_rpc_code() {
214        let cases = [
215            (ApiError::bad_request("x"), CODE_INVALID_PARAMS),
216            (ApiError::unprocessable("x"), CODE_INVALID_PARAMS),
217            (ApiError::not_found("x"), -32004),
218            (ApiError::conflict("x"), -32006),
219            (ApiError::forbidden("x"), -32006),
220            (ApiError::internal("x"), CODE_INTERNAL_ERROR),
221        ];
222        for (error, expected) in cases {
223            let kind = error.kind;
224            let rpc: RpcError = error.into();
225            assert_eq!(rpc.code, expected, "{kind:?} must map to {expected}");
226        }
227        // A method-not-found is the router's to report, never a handler's; this
228        // asserts no kind claims that code.
229        assert!(
230            !cases_map_to(CODE_METHOD_NOT_FOUND),
231            "no handler failure may impersonate method_not_found"
232        );
233    }
234
235    fn cases_map_to(code: i64) -> bool {
236        [
237            ErrorKind::BadRequest,
238            ErrorKind::NotFound,
239            ErrorKind::Refused,
240            ErrorKind::Internal,
241        ]
242        .iter()
243        .any(|k| k.code() == code)
244    }
245
246    /// Why: the message is what an operator reads; a conversion that dropped
247    /// or rewrote it would leave every failure looking the same on the wire.
248    /// Test: itself.
249    #[test]
250    fn api_error_message_survives_the_conversion() {
251        let rpc: RpcError = ApiError::not_found("palace not found: alpha").into();
252        assert_eq!(rpc.message, "palace not found: alpha");
253    }
254
255    /// Why (#5549, ADR-0045): mapping every open failure to `NotFound` sent an
256    /// operator looking for a deleted palace when what they had was a palace
257    /// that could not be READ. The two answers call for different next moves,
258    /// and the only thing separating them is this branch.
259    /// What: creates a palace directory the process cannot enter (`0o000`),
260    /// then opens it by id and asserts the failure is `Internal` rather than
261    /// `NotFound` — the palace is plainly there.
262    /// Test: itself.
263    #[tokio::test]
264    async fn unreadable_palace_is_internal_not_not_found_at_open_handle() {
265        use std::os::unix::fs::PermissionsExt as _;
266
267        // Running as root defeats the mode bits — the open would succeed and
268        // the test would assert nothing.
269        if unsafe { libc::geteuid() } == 0 {
270            eprintln!("SKIP: running as root, so 0o000 does not deny this process");
271            return;
272        }
273
274        let tmp = tempfile::tempdir().expect("tempdir");
275        let state = AppState::new(tmp.path().to_path_buf());
276        let dir = tmp.path().join("unreadable");
277        std::fs::create_dir_all(&dir).expect("create the palace directory");
278        std::fs::write(dir.join("palace.json"), "{}").expect("seed metadata");
279        std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o000))
280            .expect("deny access");
281
282        let failure = open_handle(&state, "unreadable")
283            .err()
284            .expect("an unreadable palace cannot be opened");
285
286        // Restore before the tempdir drop, or the cleanup cannot descend.
287        let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700));
288
289        assert_eq!(
290            failure.kind,
291            ErrorKind::Internal,
292            "a palace that is present but unreadable must not report as absent: {}",
293            failure.message
294        );
295    }
296}