Skip to main content

khive_gate/
error.rs

1use thiserror::Error;
2
3/// Validation error for gate wire types.
4///
5/// Returned by `try_new` constructors and custom `Deserialize` impls when
6/// invariants are violated (empty fields, zero rate-limit values).
7#[derive(Error, Debug, Clone, PartialEq, Eq)]
8pub enum GateValidationError {
9    #[error("actor kind must not be empty")]
10    EmptyActorKind,
11    #[error("actor id must not be empty")]
12    EmptyActorId,
13    #[error("verb must not be empty")]
14    EmptyVerb,
15    #[error("invalid deny_writes_for configuration: {0}")]
16    InvalidWriteDenyPatterns(String),
17    #[error("deny reason must not be empty")]
18    EmptyDenyReason,
19    #[error("audit tag must not be empty")]
20    EmptyAuditTag,
21    #[error("rate limit window_secs must be > 0")]
22    ZeroRateLimitWindow,
23    #[error("rate limit max must be > 0")]
24    ZeroRateLimitMax,
25}
26
27/// Errors returned by [`crate::Gate::check`].
28#[derive(Error, Debug)]
29pub enum GateError {
30    #[error("policy error: {0}")]
31    Policy(String),
32    #[error("internal gate error: {0}")]
33    Internal(String),
34    #[error("validation error: {0}")]
35    Validation(#[from] GateValidationError),
36}
37
38impl GateError {
39    /// Stable, caller-safe classification of this error's failure category.
40    ///
41    /// This is what a `Gate::check` caller is permitted to forward to an
42    /// untrusted requester. It never includes this error's `Display` text:
43    /// a gate backend's error message can embed connection details,
44    /// addresses, or credentials, and that text must stay in server-side
45    /// logs only (log the full error separately with its `Display` impl).
46    ///
47    /// `Internal` is a transient backend-availability failure — safe to
48    /// retry. `Policy` and `Validation` are non-transient: the gate backend
49    /// is reachable but the request or its configured policy cannot be
50    /// evaluated, and retrying the identical request will not change the
51    /// outcome.
52    pub fn wire_reason(&self) -> &'static str {
53        match self {
54            Self::Internal(_) => "gate backend unavailable",
55            Self::Policy(_) => "gate policy evaluation failed",
56            Self::Validation(_) => "gate request validation failed",
57        }
58    }
59}
60
61#[cfg(test)]
62mod wire_reason_tests {
63    use super::{GateError, GateValidationError};
64
65    #[test]
66    fn wire_reason_never_echoes_backend_display_text() {
67        let canary = "postgres://svc:not-a-real-secret@internal-host";
68        let error = GateError::Internal(canary.to_string());
69
70        assert!(!error.wire_reason().contains(canary));
71        assert_eq!(error.wire_reason(), "gate backend unavailable");
72    }
73
74    #[test]
75    fn policy_and_internal_classify_into_distinguishable_stable_text() {
76        let internal = GateError::Internal("connection refused".to_string());
77        let policy = GateError::Policy("rule set has no allow clause".to_string());
78        let validation = GateError::Validation(GateValidationError::EmptyVerb);
79
80        assert_eq!(internal.wire_reason(), "gate backend unavailable");
81        assert_eq!(policy.wire_reason(), "gate policy evaluation failed");
82        assert_eq!(validation.wire_reason(), "gate request validation failed");
83        assert_ne!(internal.wire_reason(), policy.wire_reason());
84        assert_ne!(policy.wire_reason(), validation.wire_reason());
85    }
86}