snap_control/pg_wap/crpc/
convert.rs1use std::{collections::BTreeMap, time::UNIX_EPOCH};
19
20use scion_protobuf::proto::control_plane::v1 as buffa_cp;
21use sciparse::{rpc::FromRpcError, segment::SignedPathSegment};
22
23use crate::{
24 pg_wap::{
25 crpc::model,
26 sni::{CustomerDomain, SniFormatError},
27 },
28 proto::anapaya::wap::v1 as rpc,
29};
30
31#[derive(thiserror::Error, Debug)]
36pub enum ConvertError {
37 #[error("the request does not name a target")]
39 NoTargets,
40 #[error("the target {target:?} is not a valid domain: {source}")]
42 InvalidTarget {
43 target: String,
45 source: SniFormatError,
47 },
48 #[error("a {kind} segment granted for the target {target:?} is unusable: {source}")]
50 InvalidSegment {
51 target: String,
53 kind: &'static str,
55 source: FromRpcError,
57 },
58}
59
60impl rpc::AuthorizeTargetsRequest {
61 pub fn try_into_model(self) -> Result<model::AuthorizeTargetsRequest, ConvertError> {
63 if self.targets.is_empty() {
64 return Err(ConvertError::NoTargets);
65 }
66
67 let mut targets = BTreeMap::new();
68 for (target, segments) in self.targets {
69 let segments = segments.try_into_model(&target)?;
70 let domain = CustomerDomain::new(target.clone())
71 .map_err(|source| ConvertError::InvalidTarget { target, source })?;
72
73 targets.insert(domain, segments);
74 }
75
76 Ok(model::AuthorizeTargetsRequest { targets })
77 }
78}
79
80impl rpc::AuthSegments {
81 fn try_into_model(self, target: &str) -> Result<model::AuthSegments, ConvertError> {
87 let Self {
88 up_segments,
89 down_segments,
90 core_segments,
91 ..
92 } = self;
93
94 Ok(model::AuthSegments {
95 up_segments: parse_segments(target, "up", up_segments)?,
96 down_segments: parse_segments(target, "down", down_segments)?,
97 core_segments: parse_segments(target, "core", core_segments)?,
98 })
99 }
100}
101
102fn parse_segments(
103 target: &str,
104 kind: &'static str,
105 segments: Vec<buffa_cp::PathSegment>,
106) -> Result<Vec<SignedPathSegment>, ConvertError> {
107 segments
108 .into_iter()
109 .map(|segment| {
110 SignedPathSegment::try_from_rpc(segment).map_err(|source| {
111 ConvertError::InvalidSegment {
112 target: target.to_owned(),
113 kind,
114 source,
115 }
116 })
117 })
118 .collect()
119}
120
121impl From<model::AuthorizeTargetsResponse> for rpc::AuthorizeTargetsResponse {
122 fn from(response: model::AuthorizeTargetsResponse) -> Self {
123 Self {
124 client_ip: response.client_ip.to_string(),
125 wap_id: response.wap_id,
126 data_plane_port: u32::from(response.data_plane_port),
127 expiry_time: response
130 .expiry_time
131 .duration_since(UNIX_EPOCH)
132 .unwrap_or_default()
133 .as_secs(),
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use std::time::{Duration, SystemTime};
141
142 use super::*;
143 use crate::pg_wap::test_util::{client_ip, core_segment, down_segment, sni, up_segment};
144
145 fn request(segments: rpc::AuthSegments) -> rpc::AuthorizeTargetsRequest {
147 rpc::AuthorizeTargetsRequest {
148 targets: [(sni().customer_domain().as_str().to_owned(), segments)]
149 .into_iter()
150 .collect(),
151 }
152 }
153
154 fn fingerprints(segments: &[SignedPathSegment]) -> Vec<sciparse::segment::SegmentFp> {
155 segments
156 .iter()
157 .map(SignedPathSegment::fingerprint)
158 .collect()
159 }
160
161 #[test]
162 fn a_target_is_keyed_by_its_customer_domain_and_keeps_its_segments_grouped() {
163 let (up, down, core) = (up_segment(0), down_segment(0), core_segment(0));
164
165 let converted = request(rpc::AuthSegments {
166 up_segments: vec![up.clone().into_rpc()],
167 down_segments: vec![down.clone().into_rpc()],
168 core_segments: vec![core.clone().into_rpc()],
169 })
170 .try_into_model()
171 .expect("the request is convertible");
172
173 assert_eq!(
174 converted.targets.keys().collect::<Vec<_>>(),
175 vec![&CustomerDomain::from(sni().customer_domain())]
176 );
177
178 let granted = converted
179 .targets
180 .get(sni().customer_domain().as_str())
181 .expect("the target is in the request");
182 assert_eq!(fingerprints(&granted.up_segments), fingerprints(&[up]));
183 assert_eq!(fingerprints(&granted.down_segments), fingerprints(&[down]));
184 assert_eq!(fingerprints(&granted.core_segments), fingerprints(&[core]));
185 }
186
187 #[test]
188 fn a_request_without_targets_is_not_convertible() {
189 let err = rpc::AuthorizeTargetsRequest::default()
190 .try_into_model()
191 .expect_err("a request without targets is rejected");
192
193 assert!(matches!(err, ConvertError::NoTargets), "got {err}");
194 }
195
196 #[test]
197 fn a_target_that_is_not_a_domain_is_not_convertible() {
198 let err = rpc::AuthorizeTargetsRequest {
199 targets: [("not a domain".to_owned(), rpc::AuthSegments::default())]
200 .into_iter()
201 .collect(),
202 }
203 .try_into_model()
204 .expect_err("a target that is not a domain is rejected");
205
206 assert!(
207 matches!(err, ConvertError::InvalidTarget { .. }),
208 "got {err}"
209 );
210 }
211
212 #[test]
213 fn a_segment_that_cannot_be_parsed_is_not_convertible() {
214 let err = request(rpc::AuthSegments {
215 up_segments: vec![buffa_cp::PathSegment {
216 segment_info: vec![0xff, 0xff],
217 as_entries: Vec::new(),
218 }],
219 ..rpc::AuthSegments::default()
220 })
221 .try_into_model()
222 .expect_err("an unparseable segment is rejected");
223
224 assert!(
225 matches!(err, ConvertError::InvalidSegment { kind: "up", .. }),
226 "got {err}"
227 );
228 }
229
230 #[test]
231 fn a_response_carries_the_expiry_as_seconds_since_the_epoch() {
232 let response = rpc::AuthorizeTargetsResponse::from(model::AuthorizeTargetsResponse {
233 client_ip: client_ip(),
234 wap_id: "wap-1".to_owned(),
235 data_plane_port: 8443,
236 expiry_time: UNIX_EPOCH + Duration::from_secs(1_800),
237 });
238
239 assert_eq!(
240 response,
241 rpc::AuthorizeTargetsResponse {
242 client_ip: client_ip().to_string(),
243 wap_id: "wap-1".to_owned(),
244 data_plane_port: 8443,
245 expiry_time: 1_800,
246 }
247 );
248 }
249
250 #[test]
251 fn an_expiry_before_the_epoch_becomes_zero() {
252 let response = rpc::AuthorizeTargetsResponse::from(model::AuthorizeTargetsResponse {
253 client_ip: client_ip(),
254 wap_id: "wap-1".to_owned(),
255 data_plane_port: 8443,
256 expiry_time: SystemTime::UNIX_EPOCH - Duration::from_secs(1),
257 });
258
259 assert_eq!(response.expiry_time, 0);
260 }
261}