use kernel::{AxCode, AxError};
use super::BindFace;
use crate::auth::{self, Pairing};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Door {
Transcribe,
Enroll,
Acp,
Drop,
}
#[derive(Debug)]
pub enum Admission {
Admit(Pairing),
Refuse(AxError),
}
#[must_use]
pub fn decide_admission(door: Door, offered: Option<&str>, face: &BindFace) -> Admission {
let Some(expected) = face.token_digest() else {
return Admission::Admit(Pairing::Held);
};
if auth::verify(offered, expected) {
return Admission::Admit(Pairing::Held);
}
let action = match door {
Door::Transcribe => "transcribe a recording",
Door::Enroll => "enrol a credential",
Door::Drop => "keep a dropped file",
Door::Acp => return Admission::Admit(Pairing::Absent),
};
Admission::Refuse(
AxError::failure(
AxCode::GateDenied,
action,
"the request did not carry this city's pairing token",
)
.with_recovery(
"open the control surface from the link that carries the pairing code, or send the \
code as an `Authorization: Bearer` header",
),
)
}
#[must_use]
pub fn offered_pairing(header: Option<&str>) -> Option<&str> {
let raw = header?.trim();
let (scheme, token) = raw.split_once(' ')?;
scheme
.eq_ignore_ascii_case("Bearer")
.then(|| token.trim())
.filter(|token| !token.is_empty())
}
#[cfg(test)]
#[allow(clippy::panic, reason = "test code")]
mod tests {
use super::*;
use kernel::B3Hash;
fn exposed() -> BindFace {
BindFace::Exposed {
token: B3Hash::digest(b"pairing-code"),
}
}
fn unpaired() -> BindFace {
BindFace::Loopback { token: None }
}
#[test]
fn the_acting_doors_refuse_a_stranger_and_the_editor_door_carries_the_bit() {
for door in [Door::Transcribe, Door::Enroll, Door::Drop] {
let Admission::Refuse(err) = decide_admission(door, None, &exposed()) else {
panic!("{door:?} acts on a request, so it refuses an unpaired one");
};
assert_eq!(*err.code(), AxCode::GateDenied);
}
assert!(matches!(
decide_admission(Door::Acp, None, &exposed()),
Admission::Admit(Pairing::Absent)
));
assert!(matches!(
decide_admission(Door::Transcribe, Some("pairing-code"), &exposed()),
Admission::Admit(Pairing::Held)
));
}
#[test]
fn a_city_with_no_token_configured_admits_every_door() {
for door in [Door::Transcribe, Door::Enroll, Door::Acp, Door::Drop] {
assert!(
matches!(
decide_admission(door, None, &unpaired()),
Admission::Admit(Pairing::Held)
),
"{door:?} on a loopback-only city"
);
}
}
#[test]
fn only_a_bearer_header_offers_a_token() {
assert_eq!(offered_pairing(Some("Bearer abc")), Some("abc"));
assert_eq!(offered_pairing(Some("bearer abc ")), Some("abc"));
assert_eq!(offered_pairing(Some("Basic abc")), None);
assert_eq!(offered_pairing(Some("abc")), None);
assert_eq!(offered_pairing(Some("Bearer ")), None);
assert_eq!(offered_pairing(None), None);
}
}