use std::{collections::BTreeMap, time::UNIX_EPOCH};
use scion_protobuf::proto::control_plane::v1 as buffa_cp;
use sciparse::{rpc::FromRpcError, segment::SignedPathSegment};
use crate::{
pg_wap::{
crpc::model,
sni::{CustomerDomain, SniFormatError},
},
proto::anapaya::wap::v1 as rpc,
};
#[derive(thiserror::Error, Debug)]
pub enum ConvertError {
#[error("the request does not name a target")]
NoTargets,
#[error("the target {target:?} is not a valid domain: {source}")]
InvalidTarget {
target: String,
source: SniFormatError,
},
#[error("a {kind} segment granted for the target {target:?} is unusable: {source}")]
InvalidSegment {
target: String,
kind: &'static str,
source: FromRpcError,
},
}
impl rpc::AuthorizeTargetsRequest {
pub fn try_into_model(self) -> Result<model::AuthorizeTargetsRequest, ConvertError> {
if self.targets.is_empty() {
return Err(ConvertError::NoTargets);
}
let mut targets = BTreeMap::new();
for (target, segments) in self.targets {
let segments = segments.try_into_model(&target)?;
let domain = CustomerDomain::new(target.clone())
.map_err(|source| ConvertError::InvalidTarget { target, source })?;
targets.insert(domain, segments);
}
Ok(model::AuthorizeTargetsRequest { targets })
}
}
impl rpc::AuthSegments {
fn try_into_model(self, target: &str) -> Result<model::AuthSegments, ConvertError> {
let Self {
up_segments,
down_segments,
core_segments,
..
} = self;
Ok(model::AuthSegments {
up_segments: parse_segments(target, "up", up_segments)?,
down_segments: parse_segments(target, "down", down_segments)?,
core_segments: parse_segments(target, "core", core_segments)?,
})
}
}
fn parse_segments(
target: &str,
kind: &'static str,
segments: Vec<buffa_cp::PathSegment>,
) -> Result<Vec<SignedPathSegment>, ConvertError> {
segments
.into_iter()
.map(|segment| {
SignedPathSegment::try_from_rpc(segment).map_err(|source| {
ConvertError::InvalidSegment {
target: target.to_owned(),
kind,
source,
}
})
})
.collect()
}
impl From<model::AuthorizeTargetsResponse> for rpc::AuthorizeTargetsResponse {
fn from(response: model::AuthorizeTargetsResponse) -> Self {
Self {
client_ip: response.client_ip.to_string(),
wap_id: response.wap_id,
data_plane_port: u32::from(response.data_plane_port),
expiry_time: response
.expiry_time
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
}
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use super::*;
use crate::pg_wap::test_util::{client_ip, core_segment, down_segment, sni, up_segment};
fn request(segments: rpc::AuthSegments) -> rpc::AuthorizeTargetsRequest {
rpc::AuthorizeTargetsRequest {
targets: [(sni().customer_domain().as_str().to_owned(), segments)]
.into_iter()
.collect(),
}
}
fn fingerprints(segments: &[SignedPathSegment]) -> Vec<sciparse::segment::SegmentFp> {
segments
.iter()
.map(SignedPathSegment::fingerprint)
.collect()
}
#[test]
fn a_target_is_keyed_by_its_customer_domain_and_keeps_its_segments_grouped() {
let (up, down, core) = (up_segment(0), down_segment(0), core_segment(0));
let converted = request(rpc::AuthSegments {
up_segments: vec![up.clone().into_rpc()],
down_segments: vec![down.clone().into_rpc()],
core_segments: vec![core.clone().into_rpc()],
})
.try_into_model()
.expect("the request is convertible");
assert_eq!(
converted.targets.keys().collect::<Vec<_>>(),
vec![&CustomerDomain::from(sni().customer_domain())]
);
let granted = converted
.targets
.get(sni().customer_domain().as_str())
.expect("the target is in the request");
assert_eq!(fingerprints(&granted.up_segments), fingerprints(&[up]));
assert_eq!(fingerprints(&granted.down_segments), fingerprints(&[down]));
assert_eq!(fingerprints(&granted.core_segments), fingerprints(&[core]));
}
#[test]
fn a_request_without_targets_is_not_convertible() {
let err = rpc::AuthorizeTargetsRequest::default()
.try_into_model()
.expect_err("a request without targets is rejected");
assert!(matches!(err, ConvertError::NoTargets), "got {err}");
}
#[test]
fn a_target_that_is_not_a_domain_is_not_convertible() {
let err = rpc::AuthorizeTargetsRequest {
targets: [("not a domain".to_owned(), rpc::AuthSegments::default())]
.into_iter()
.collect(),
}
.try_into_model()
.expect_err("a target that is not a domain is rejected");
assert!(
matches!(err, ConvertError::InvalidTarget { .. }),
"got {err}"
);
}
#[test]
fn a_segment_that_cannot_be_parsed_is_not_convertible() {
let err = request(rpc::AuthSegments {
up_segments: vec![buffa_cp::PathSegment {
segment_info: vec![0xff, 0xff],
as_entries: Vec::new(),
}],
..rpc::AuthSegments::default()
})
.try_into_model()
.expect_err("an unparseable segment is rejected");
assert!(
matches!(err, ConvertError::InvalidSegment { kind: "up", .. }),
"got {err}"
);
}
#[test]
fn a_response_carries_the_expiry_as_seconds_since_the_epoch() {
let response = rpc::AuthorizeTargetsResponse::from(model::AuthorizeTargetsResponse {
client_ip: client_ip(),
wap_id: "wap-1".to_owned(),
data_plane_port: 8443,
expiry_time: UNIX_EPOCH + Duration::from_secs(1_800),
});
assert_eq!(
response,
rpc::AuthorizeTargetsResponse {
client_ip: client_ip().to_string(),
wap_id: "wap-1".to_owned(),
data_plane_port: 8443,
expiry_time: 1_800,
}
);
}
#[test]
fn an_expiry_before_the_epoch_becomes_zero() {
let response = rpc::AuthorizeTargetsResponse::from(model::AuthorizeTargetsResponse {
client_ip: client_ip(),
wap_id: "wap-1".to_owned(),
data_plane_port: 8443,
expiry_time: SystemTime::UNIX_EPOCH - Duration::from_secs(1),
});
assert_eq!(response.expiry_time, 0);
}
}