Skip to main content

aven_protocol/
refusal.rs

1//! Stable admission refusal codes. HTTP dispatch and refusal precedence are backend-owned.
2/// Error codes of one router, each prefixed with its family name.
3pub struct Codes {
4    /// 415: the body is not uncompressed JSON.
5    pub content_type: &'static str,
6    /// 401: a required bearer credential is missing or malformed.
7    pub credential: &'static str,
8    /// 413: the body exceeds its limit.
9    pub limit: &'static str,
10    /// 400: the body does not parse as a request.
11    pub malformed: &'static str,
12    /// 400: the operation refused a well-formed request.
13    pub refused: &'static str,
14    /// 403: the credential may not perform the operation.
15    pub unauthorized: &'static str,
16    /// 408: the body or the operation did not finish in time.
17    pub timeout: &'static str,
18    /// 500: storage failed or the reply exceeded its limit.
19    pub server_error: &'static str,
20    /// 503: every permit is taken; retry after `Retry-After`.
21    pub busy: &'static str,
22}
23
24/// The [`Codes`] of a router family, e.g. `codes!("enrollment")`.
25#[macro_export]
26macro_rules! refusal_codes {
27    ($family:literal) => {
28        $crate::refusal::Codes {
29            content_type: concat!($family, "-content-type"),
30            credential: concat!($family, "-credential"),
31            limit: concat!($family, "-limit"),
32            malformed: concat!($family, "-malformed"),
33            refused: concat!($family, "-refused"),
34            unauthorized: concat!($family, "-unauthorized"),
35            timeout: concat!($family, "-timeout"),
36            server_error: concat!($family, "-server-error"),
37            busy: concat!($family, "-busy"),
38        }
39    };
40}
41
42/// Endpoint-specific client interpretation, independent of HTTP execution.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum Bootstrap {
45    Claimed,
46    SetupRejected,
47    SetupExpired,
48    Quota,
49    Unknown,
50}
51
52impl Bootstrap {
53    pub fn classify(code: Option<&str>, claim: bool) -> Self {
54        match code {
55            Some("bootstrap-storage-already-claimed") => Self::Claimed,
56            Some("bootstrap-setup-invitation-rejected") if claim => Self::SetupRejected,
57            Some("bootstrap-setup-invitation-expired") if claim => Self::SetupExpired,
58            Some("attachment-quota-exceeded") => Self::Quota,
59            _ => Self::Unknown,
60        }
61    }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum Enrollment {
66    Stale,
67    Busy,
68    Unauthorized,
69    Timeout,
70    Refused,
71    Server,
72}
73
74impl Enrollment {
75    pub fn classify(status: u16, code: Option<&str>) -> Self {
76        match code {
77            Some("membership-stale") => Self::Stale,
78            Some("enrollment-busy") => Self::Busy,
79            Some("enrollment-unauthorized") => Self::Unauthorized,
80            Some("enrollment-timeout") => Self::Timeout,
81            Some(_) if (400..500).contains(&status) => Self::Refused,
82            _ => Self::Server,
83        }
84    }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum Tail {
89    Malformed,
90    BatchKnown,
91    Stale,
92    Unauthorized,
93    PrefixIdentityCollision,
94    Quota,
95    Unknown,
96}
97
98impl Tail {
99    pub fn classify(code: Option<&str>) -> Self {
100        match code {
101            Some("encrypted-tail-malformed") => Self::Malformed,
102            Some("encrypted-tail-batch-known") => Self::BatchKnown,
103            Some("membership-stale") => Self::Stale,
104            Some("encrypted-tail-unauthorized") => Self::Unauthorized,
105            Some("encrypted-tail-prefix-identity-collision") => Self::PrefixIdentityCollision,
106            Some("attachment-quota-exceeded") => Self::Quota,
107            _ => Self::Unknown,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn endpoint_refusals_preserve_access_and_outcome_uncertainty() {
118        assert_eq!(
119            Bootstrap::classify(Some("bootstrap-setup-invitation-expired"), true),
120            Bootstrap::SetupExpired
121        );
122        assert_eq!(
123            Bootstrap::classify(Some("bootstrap-setup-invitation-expired"), false),
124            Bootstrap::Unknown
125        );
126        assert_eq!(
127            Enrollment::classify(401, Some("enrollment-credential")),
128            Enrollment::Refused
129        );
130        assert_eq!(
131            Enrollment::classify(503, Some("enrollment-busy")),
132            Enrollment::Busy
133        );
134        assert_eq!(
135            Enrollment::classify(403, Some("enrollment-unauthorized")),
136            Enrollment::Unauthorized
137        );
138        assert_eq!(Enrollment::classify(403, None), Enrollment::Server);
139        assert_eq!(
140            Tail::classify(Some("encrypted-tail-credential")),
141            Tail::Unknown
142        );
143        assert_eq!(
144            Tail::classify(Some("encrypted-image-unauthorized")),
145            Tail::Unknown
146        );
147        assert_eq!(
148            Tail::classify(Some("encrypted-tail-unauthorized")),
149            Tail::Unauthorized
150        );
151        assert_eq!(
152            Tail::classify(Some("attachment-quota-exceeded")),
153            Tail::Quota
154        );
155    }
156}
157
158/// Hosting policy is not device authority. Each code belongs to exactly one
159/// endpoint family; tail batches use the tail family. Unknown codes or status
160/// combinations carry no hosting or membership assertion.
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub enum Hosting {
163    Blocked,
164    Deleted,
165    Quota,
166    Unavailable,
167}
168
169/// Endpoint-family hosting refusals for backend dispatch and client mapping.
170pub struct HostingCodes {
171    /// 403: reversible hosting block, without altering device membership.
172    pub blocked: &'static str,
173    /// 403: the vault was deleted for good; devices must set up sync again.
174    pub deleted: &'static str,
175    /// 429: hosting capacity exhausted; no accepted history is discarded.
176    pub quota: &'static str,
177    /// 503: hosting temporarily unavailable, not a credential refusal.
178    pub unavailable: &'static str,
179}
180
181impl HostingCodes {
182    pub fn classify(&self, status: u16, code: Option<&str>) -> Option<Hosting> {
183        match (status, code) {
184            (403, Some(code)) if code == self.blocked => Some(Hosting::Blocked),
185            (403, Some(code)) if code == self.deleted => Some(Hosting::Deleted),
186            (429, Some(code)) if code == self.quota => Some(Hosting::Quota),
187            (503, Some(code)) if code == self.unavailable => Some(Hosting::Unavailable),
188            _ => None,
189        }
190    }
191}
192
193pub const BOOTSTRAP_HOSTING: HostingCodes = HostingCodes {
194    blocked: "bootstrap-hosting-blocked",
195    deleted: "bootstrap-hosting-deleted",
196    quota: "bootstrap-hosting-quota",
197    unavailable: "bootstrap-hosting-unavailable",
198};
199pub const ENROLLMENT_HOSTING: HostingCodes = HostingCodes {
200    blocked: "enrollment-hosting-blocked",
201    deleted: "enrollment-hosting-deleted",
202    quota: "enrollment-hosting-quota",
203    unavailable: "enrollment-hosting-unavailable",
204};
205pub const TAIL_HOSTING: HostingCodes = HostingCodes {
206    blocked: "encrypted-tail-hosting-blocked",
207    deleted: "encrypted-tail-hosting-deleted",
208    quota: "encrypted-tail-hosting-quota",
209    unavailable: "encrypted-tail-hosting-unavailable",
210};
211pub const IMAGE_HOSTING: HostingCodes = HostingCodes {
212    blocked: "encrypted-image-hosting-blocked",
213    deleted: "encrypted-image-hosting-deleted",
214    quota: "encrypted-image-hosting-quota",
215    unavailable: "encrypted-image-hosting-unavailable",
216};
217
218#[cfg(test)]
219mod hosting_tests {
220    use super::*;
221
222    #[test]
223    fn hosting_codes_require_their_endpoint_family_and_status() {
224        let families = [
225            &BOOTSTRAP_HOSTING,
226            &ENROLLMENT_HOSTING,
227            &TAIL_HOSTING,
228            &IMAGE_HOSTING,
229        ];
230        for (i, family) in families.iter().enumerate() {
231            for (status, code, expected) in [
232                (403, family.blocked, Hosting::Blocked),
233                (403, family.deleted, Hosting::Deleted),
234                (429, family.quota, Hosting::Quota),
235                (503, family.unavailable, Hosting::Unavailable),
236            ] {
237                assert_eq!(family.classify(status, Some(code)), Some(expected));
238                assert_eq!(family.classify(401, Some(code)), None);
239                for (j, other) in families.iter().enumerate() {
240                    if i != j {
241                        assert_eq!(other.classify(status, Some(code)), None);
242                    }
243                }
244            }
245            assert_eq!(family.classify(403, Some("unknown")), None);
246            assert_eq!(family.classify(503, None), None);
247        }
248    }
249}