cloud_sdk/transport/endpoint/
pair.rs1use super::policy::validate_region;
2use super::{EndpointIdentity, EndpointPolicyError, EndpointScheme};
3
4pub const MAX_REGIONAL_ENDPOINT_PAIRS: usize = 32;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum EndpointPairPolicyError {
10 Empty,
12 TooManyPairs,
14 InvalidRegion,
16 InsecureEndpoint,
18 DuplicatePair,
20 PairMismatch,
22}
23
24impl_static_error!(EndpointPairPolicyError,
25 Self::Empty => "regional endpoint pair set is empty",
26 Self::TooManyPairs => "regional endpoint pair set exceeds the length limit",
27 Self::InvalidRegion => "regional endpoint pair has an invalid region",
28 Self::InsecureEndpoint => "regional endpoint pair is not entirely HTTPS",
29 Self::DuplicatePair => "regional endpoint pair set contains a duplicate",
30 Self::PairMismatch => "API and token endpoints do not match one regional pair",
31);
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub struct RegionalEndpointPair<'a> {
36 region: &'a str,
37 api: EndpointIdentity<'a>,
38 token: EndpointIdentity<'a>,
39}
40
41impl<'a> RegionalEndpointPair<'a> {
42 pub fn new(
44 region: &'a str,
45 api: EndpointIdentity<'a>,
46 token: EndpointIdentity<'a>,
47 ) -> Result<Self, EndpointPairPolicyError> {
48 validate_region(region).map_err(map_region_error)?;
49 if api.scheme() != EndpointScheme::Https || token.scheme() != EndpointScheme::Https {
50 return Err(EndpointPairPolicyError::InsecureEndpoint);
51 }
52 Ok(Self { region, api, token })
53 }
54
55 #[must_use]
57 pub const fn region(self) -> &'a str {
58 self.region
59 }
60
61 #[must_use]
63 pub const fn api(self) -> EndpointIdentity<'a> {
64 self.api
65 }
66
67 #[must_use]
69 pub const fn token(self) -> EndpointIdentity<'a> {
70 self.token
71 }
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct EndpointPairPolicy<'a> {
77 pairs: &'a [RegionalEndpointPair<'a>],
78}
79
80impl<'a> EndpointPairPolicy<'a> {
81 pub fn new(pairs: &'a [RegionalEndpointPair<'a>]) -> Result<Self, EndpointPairPolicyError> {
83 if pairs.is_empty() {
84 return Err(EndpointPairPolicyError::Empty);
85 }
86 if pairs.len() > MAX_REGIONAL_ENDPOINT_PAIRS {
87 return Err(EndpointPairPolicyError::TooManyPairs);
88 }
89 for (index, pair) in pairs.iter().enumerate() {
90 let Some(tail) = pairs.get(index.saturating_add(1)..) else {
91 return Err(EndpointPairPolicyError::DuplicatePair);
92 };
93 if tail.iter().any(|other| {
94 pair.region == other.region || pair.api == other.api || pair.token == other.token
95 }) {
96 return Err(EndpointPairPolicyError::DuplicatePair);
97 }
98 }
99 Ok(Self { pairs })
100 }
101
102 #[must_use]
104 pub fn admits(
105 self,
106 region: &str,
107 api: EndpointIdentity<'_>,
108 token: EndpointIdentity<'_>,
109 ) -> bool {
110 self.pairs
111 .iter()
112 .any(|pair| pair.region == region && pair.api == api && pair.token == token)
113 }
114
115 pub fn verify(
117 self,
118 region: &str,
119 api: EndpointIdentity<'_>,
120 token: EndpointIdentity<'_>,
121 ) -> Result<(), EndpointPairPolicyError> {
122 validate_region(region).map_err(map_region_error)?;
123 if self.admits(region, api, token) {
124 Ok(())
125 } else {
126 Err(EndpointPairPolicyError::PairMismatch)
127 }
128 }
129}
130
131const fn map_region_error(_error: EndpointPolicyError) -> EndpointPairPolicyError {
132 EndpointPairPolicyError::InvalidRegion
133}
134
135#[cfg(test)]
136mod tests {
137 use super::{EndpointPairPolicy, EndpointPairPolicyError, RegionalEndpointPair};
138 use crate::transport::{EndpointIdentity, EndpointScheme};
139
140 fn endpoint(host: &'static str, path: &'static str) -> EndpointIdentity<'static> {
141 EndpointIdentity::new(EndpointScheme::Https, host, 443, path)
142 .unwrap_or_else(|_| unreachable!())
143 }
144
145 #[test]
146 fn exact_region_pairs_reject_cross_region_and_alias_combinations() {
147 let eu_api = endpoint("eu.api.example", "/v2");
148 let eu_token = endpoint("www.example", "/auth/oauth2/token");
149 let ca_api = endpoint("ca.api.example", "/v2");
150 let ca_token = endpoint("ca.example", "/auth/oauth2/token");
151 let pairs = [
152 RegionalEndpointPair::new("eu", eu_api, eu_token).unwrap_or_else(|_| unreachable!()),
153 RegionalEndpointPair::new("ca", ca_api, ca_token).unwrap_or_else(|_| unreachable!()),
154 ];
155 let policy = EndpointPairPolicy::new(&pairs).unwrap_or_else(|_| unreachable!());
156 assert!(policy.verify("eu", eu_api, eu_token).is_ok());
157 assert!(policy.verify("ca", ca_api, ca_token).is_ok());
158 assert_eq!(
159 policy.verify("eu", eu_api, ca_token),
160 Err(EndpointPairPolicyError::PairMismatch)
161 );
162 assert_eq!(
163 policy.verify("eu", endpoint("api.eu.example", "/v2"), eu_token,),
164 Err(EndpointPairPolicyError::PairMismatch)
165 );
166 }
167
168 #[test]
169 fn pair_sets_are_nonempty_bounded_unique_and_https_only() {
170 assert_eq!(
171 EndpointPairPolicy::new(&[]),
172 Err(EndpointPairPolicyError::Empty)
173 );
174 let api = endpoint("eu.api.example", "/v2");
175 let token = endpoint("www.example", "/auth/oauth2/token");
176 let pair = RegionalEndpointPair::new("eu", api, token).unwrap_or_else(|_| unreachable!());
177 assert_eq!(
178 EndpointPairPolicy::new(&[pair, pair]),
179 Err(EndpointPairPolicyError::DuplicatePair)
180 );
181 let http = EndpointIdentity::new(EndpointScheme::Http, "www.example", 80, "/token")
182 .unwrap_or_else(|_| unreachable!());
183 assert_eq!(
184 RegionalEndpointPair::new("eu", api, http),
185 Err(EndpointPairPolicyError::InsecureEndpoint)
186 );
187 }
188}