wire/reception/
admission.rs1use kernel::{AxCode, AxError, TimeMs};
16
17use super::BindFace;
18use super::sessions::Sessions;
19use crate::auth::{self, Pairing};
20
21#[derive(Debug, Clone, Copy)]
26pub struct Keys<'a> {
27 pub face: &'a BindFace,
28 pub sessions: &'a Sessions,
29 pub now: TimeMs,
30}
31
32impl Keys<'_> {
33 #[must_use]
35 pub fn pairing(&self, offered: Option<&str>) -> Pairing {
36 let Some(offered) = offered else {
37 return Pairing::Absent;
38 };
39 if auth::verify(Some(offered), self.face.key()) || self.sessions.holds(offered, self.now) {
40 Pairing::Held
41 } else {
42 Pairing::Absent
43 }
44 }
45}
46
47#[derive(Debug)]
51pub enum Standing {
52 Stands,
53 Ends(Box<AxError>),
54}
55
56#[must_use]
60pub fn decide_standing(pairing: Pairing) -> Standing {
61 match pairing {
62 Pairing::Held => Standing::Stands,
63 Pairing::Absent => Standing::Ends(Box::new(
64 AxError::failure(
65 AxCode::GateDenied,
66 "keep a browser session open",
67 "this browser was forgotten, or its session token has ended",
68 )
69 .with_recovery(
70 "pair this browser again with the pairing code the terminal shows; a browser \
71 the city still knows reconnects by itself",
72 ),
73 )),
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum Door {
85 Transcribe,
87 Enroll,
89 Acp,
91 Drop,
93}
94
95#[derive(Debug)]
97pub enum Admission {
98 Admit(Pairing),
101 Refuse(AxError),
102}
103
104#[must_use]
115pub fn decide_admission(door: Door, pairing: Pairing) -> Admission {
116 let action = match (pairing, door) {
117 (Pairing::Held, _) => return Admission::Admit(Pairing::Held),
118 (Pairing::Absent, Door::Acp) => return Admission::Admit(Pairing::Absent),
119 (Pairing::Absent, Door::Transcribe) => "transcribe a recording",
120 (Pairing::Absent, Door::Enroll) => "enrol a credential",
121 (Pairing::Absent, Door::Drop) => "keep a dropped file",
122 };
123 Admission::Refuse(
124 AxError::failure(
125 AxCode::GateDenied,
126 action,
127 "the request carried neither this city's key nor a live session token",
128 )
129 .with_recovery(
130 "send the session token or the city's key as an `Authorization: Bearer` header; \
131 a program on this machine reads the key from the city's key file",
132 ),
133 )
134}
135
136#[must_use]
145pub fn offered_pairing(header: Option<&str>) -> Option<&str> {
146 let raw = header?.trim();
147 let (scheme, token) = raw.split_once(' ')?;
148 scheme
149 .eq_ignore_ascii_case("Bearer")
150 .then(|| token.trim())
151 .filter(|token| !token.is_empty())
152}
153
154#[cfg(test)]
155#[allow(clippy::panic, reason = "test code")]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn the_acting_doors_refuse_an_empty_hand_and_the_editor_door_carries_the_bit() {
161 for door in [Door::Transcribe, Door::Enroll, Door::Drop] {
162 let Admission::Refuse(err) = decide_admission(door, Pairing::Absent) else {
163 panic!("{door:?} acts on a request, so it refuses an unpaired one");
164 };
165 assert_eq!(*err.code(), AxCode::GateDenied);
166 }
167 assert!(matches!(
168 decide_admission(Door::Acp, Pairing::Absent),
169 Admission::Admit(Pairing::Absent)
170 ));
171 }
172
173 #[test]
174 fn the_key_and_nothing_else_is_held_on_a_loopback_face() {
175 let face = BindFace::Loopback {
176 key: kernel::B3Hash::digest(b"native-key"),
177 };
178 let sessions = Sessions::default();
179 let keys = Keys {
180 face: &face,
181 sessions: &sessions,
182 now: kernel::TimeMs::new(0),
183 };
184 assert_eq!(keys.pairing(Some("native-key")), Pairing::Held);
185 assert_eq!(keys.pairing(Some("guess")), Pairing::Absent);
186 assert_eq!(keys.pairing(None), Pairing::Absent);
187 }
188
189 #[test]
190 fn only_a_bearer_header_offers_a_token() {
191 assert_eq!(offered_pairing(Some("Bearer abc")), Some("abc"));
192 assert_eq!(offered_pairing(Some("bearer abc ")), Some("abc"));
193 assert_eq!(offered_pairing(Some("Basic abc")), None);
194 assert_eq!(offered_pairing(Some("abc")), None);
195 assert_eq!(offered_pairing(Some("Bearer ")), None);
196 assert_eq!(offered_pairing(None), None);
197 }
198}