Skip to main content

cloud_sdk/transport/endpoint/
pair.rs

1use super::policy::validate_region;
2use super::{EndpointIdentity, EndpointPolicyError, EndpointScheme};
3
4/// Maximum regional API/token pairs in one provider policy.
5pub const MAX_REGIONAL_ENDPOINT_PAIRS: usize = 32;
6
7/// Regional endpoint-pair construction or matching failure.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum EndpointPairPolicyError {
10    /// A regional pair policy must contain at least one pair.
11    Empty,
12    /// The pair set exceeds [`MAX_REGIONAL_ENDPOINT_PAIRS`].
13    TooManyPairs,
14    /// A region identifier is invalid.
15    InvalidRegion,
16    /// API and token endpoints must both use HTTPS.
17    InsecureEndpoint,
18    /// Regions and complete pairs must be unique.
19    DuplicatePair,
20    /// The supplied region, API endpoint, and token endpoint are not one pair.
21    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/// One exact provider-owned regional API and token-authority pair.
34#[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    /// Validates one exact HTTPS API/token endpoint pair.
43    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    /// Returns the canonical provider region.
56    #[must_use]
57    pub const fn region(self) -> &'a str {
58        self.region
59    }
60
61    /// Returns the exact API endpoint identity.
62    #[must_use]
63    pub const fn api(self) -> EndpointIdentity<'a> {
64        self.api
65    }
66
67    /// Returns the exact token endpoint identity.
68    #[must_use]
69    pub const fn token(self) -> EndpointIdentity<'a> {
70        self.token
71    }
72}
73
74/// Allocation-free finite policy for geographic API/token authority pairs.
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct EndpointPairPolicy<'a> {
77    pairs: &'a [RegionalEndpointPair<'a>],
78}
79
80impl<'a> EndpointPairPolicy<'a> {
81    /// Validates a finite, unique provider-owned pair set.
82    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    /// Returns whether one exact regional API/token combination is admitted.
103    #[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    /// Fails closed unless all three values identify one reviewed pair.
116    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}