cloud_sdk/transport/endpoint/
policy.rs1use super::EndpointIdentity;
4
5pub const MAX_OFFICIAL_ENDPOINTS: usize = 32;
7
8pub const MAX_ENDPOINT_REGION_BYTES: usize = 63;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum EndpointPolicyError {
14 EmptyOfficialSet,
16 TooManyOfficialEndpoints,
18 InvalidRegion,
20 DestinationMismatch,
22}
23
24impl_static_error!(EndpointPolicyError,
25 Self::EmptyOfficialSet => "official endpoint set is empty",
26 Self::TooManyOfficialEndpoints => "official endpoint set exceeds the length limit",
27 Self::InvalidRegion => "endpoint region identifier is invalid",
28 Self::DestinationMismatch => "endpoint destination is not admitted by policy",
29);
30
31#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub enum EndpointPolicyKind {
34 Fixed,
36 OfficialSet,
38 RegionDerived,
40 AcknowledgedCustom,
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub struct CustomEndpointAcknowledgement {
58 private: (),
59}
60
61impl CustomEndpointAcknowledgement {
62 #[must_use]
64 pub const fn trusted_operator_configuration() -> Self {
65 Self { private: () }
66 }
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct AcknowledgedCustomEndpoint<'a> {
72 identity: EndpointIdentity<'a>,
73 acknowledgement: CustomEndpointAcknowledgement,
74}
75
76impl<'a> AcknowledgedCustomEndpoint<'a> {
77 #[must_use]
79 pub const fn new(
80 identity: EndpointIdentity<'a>,
81 acknowledgement: CustomEndpointAcknowledgement,
82 ) -> Self {
83 Self {
84 identity,
85 acknowledgement,
86 }
87 }
88
89 #[must_use]
91 pub const fn identity(self) -> EndpointIdentity<'a> {
92 self.identity
93 }
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub struct RegionEndpoint<'a> {
99 region: &'a str,
100 identity: EndpointIdentity<'a>,
101}
102
103impl<'a> RegionEndpoint<'a> {
104 pub fn new(
106 region: &'a str,
107 identity: EndpointIdentity<'a>,
108 ) -> Result<Self, EndpointPolicyError> {
109 validate_region(region)?;
110 Ok(Self { region, identity })
111 }
112
113 #[must_use]
115 pub const fn region(self) -> &'a str {
116 self.region
117 }
118
119 #[must_use]
121 pub const fn identity(self) -> EndpointIdentity<'a> {
122 self.identity
123 }
124}
125
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub enum EndpointPolicy<'a> {
129 Fixed(EndpointIdentity<'a>),
131 OfficialSet(&'a [EndpointIdentity<'a>]),
133 RegionDerived(RegionEndpoint<'a>),
135 AcknowledgedCustom(AcknowledgedCustomEndpoint<'a>),
137}
138
139impl<'a> EndpointPolicy<'a> {
140 #[must_use]
142 pub const fn fixed(identity: EndpointIdentity<'a>) -> Self {
143 Self::Fixed(identity)
144 }
145
146 pub fn official_set(
148 identities: &'a [EndpointIdentity<'a>],
149 ) -> Result<Self, EndpointPolicyError> {
150 if identities.is_empty() {
151 return Err(EndpointPolicyError::EmptyOfficialSet);
152 }
153 if identities.len() > MAX_OFFICIAL_ENDPOINTS {
154 return Err(EndpointPolicyError::TooManyOfficialEndpoints);
155 }
156 Ok(Self::OfficialSet(identities))
157 }
158
159 #[must_use]
161 pub const fn region_derived(endpoint: RegionEndpoint<'a>) -> Self {
162 Self::RegionDerived(endpoint)
163 }
164
165 #[must_use]
167 pub const fn acknowledged_custom(endpoint: AcknowledgedCustomEndpoint<'a>) -> Self {
168 Self::AcknowledgedCustom(endpoint)
169 }
170
171 #[must_use]
173 pub const fn kind(self) -> EndpointPolicyKind {
174 match self {
175 Self::Fixed(_) => EndpointPolicyKind::Fixed,
176 Self::OfficialSet(_) => EndpointPolicyKind::OfficialSet,
177 Self::RegionDerived(_) => EndpointPolicyKind::RegionDerived,
178 Self::AcknowledgedCustom(_) => EndpointPolicyKind::AcknowledgedCustom,
179 }
180 }
181
182 #[must_use]
184 pub fn admits(self, candidate: EndpointIdentity<'_>) -> bool {
185 match self {
186 Self::Fixed(identity) => identity == candidate,
187 Self::OfficialSet(identities) => identities.contains(&candidate),
188 Self::RegionDerived(endpoint) => endpoint.identity() == candidate,
189 Self::AcknowledgedCustom(endpoint) => endpoint.identity() == candidate,
190 }
191 }
192
193 pub fn verify(self, candidate: EndpointIdentity<'_>) -> Result<(), EndpointPolicyError> {
195 if self.admits(candidate) {
196 Ok(())
197 } else {
198 Err(EndpointPolicyError::DestinationMismatch)
199 }
200 }
201}
202
203fn validate_region(region: &str) -> Result<(), EndpointPolicyError> {
204 if region.is_empty()
205 || region.len() > MAX_ENDPOINT_REGION_BYTES
206 || region.starts_with('-')
207 || region.ends_with('-')
208 || !region
209 .bytes()
210 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
211 {
212 return Err(EndpointPolicyError::InvalidRegion);
213 }
214 Ok(())
215}
216
217#[cfg(test)]
218mod tests {
219 use super::{
220 AcknowledgedCustomEndpoint, CustomEndpointAcknowledgement, EndpointPolicy,
221 EndpointPolicyError, EndpointPolicyKind, RegionEndpoint,
222 };
223 use crate::transport::{EndpointIdentity, EndpointScheme};
224
225 #[test]
226 fn all_policy_classes_match_only_their_exact_destination() {
227 let a = identity("a.example");
228 let b = identity("b.example");
229 let other = identity("other.example");
230 assert!(a.is_ok() && b.is_ok() && other.is_ok());
231 let (Ok(a), Ok(b), Ok(other)) = (a, b, other) else {
232 return;
233 };
234 let official = [a, b];
235 let set = EndpointPolicy::official_set(&official);
236 let region = RegionEndpoint::new("eu-west-1", b);
237 let custom = AcknowledgedCustomEndpoint::new(
238 a,
239 CustomEndpointAcknowledgement::trusted_operator_configuration(),
240 );
241 assert!(set.is_ok() && region.is_ok());
242 if let (Ok(set), Ok(region)) = (set, region) {
243 let policies = [
244 EndpointPolicy::fixed(a),
245 set,
246 EndpointPolicy::region_derived(region),
247 EndpointPolicy::acknowledged_custom(custom),
248 ];
249 let mut policies = policies.into_iter();
250 assert!(policies.next().is_some_and(|policy| policy.kind()
251 == EndpointPolicyKind::Fixed
252 && policy.admits(a)));
253 assert!(
254 policies
255 .next()
256 .is_some_and(|policy| policy.admits(a) && policy.admits(b))
257 );
258 assert!(policies.next().is_some_and(|policy| policy.admits(b)));
259 assert!(policies.next().is_some_and(|policy| policy.admits(a)));
260 for policy in [
261 EndpointPolicy::fixed(a),
262 set,
263 EndpointPolicy::region_derived(region),
264 EndpointPolicy::acknowledged_custom(custom),
265 ] {
266 assert_eq!(
267 policy.verify(other),
268 Err(EndpointPolicyError::DestinationMismatch)
269 );
270 }
271 }
272 }
273
274 #[test]
275 fn official_sets_and_regions_are_bounded_and_canonical() {
276 assert_eq!(
277 EndpointPolicy::official_set(&[]),
278 Err(EndpointPolicyError::EmptyOfficialSet)
279 );
280 let endpoint = identity("region.example");
281 assert!(endpoint.is_ok());
282 let Ok(endpoint) = endpoint else { return };
283 for region in ["", "EU-west-1", "-eu", "eu-", "eu_west"] {
284 assert_eq!(
285 RegionEndpoint::new(region, endpoint),
286 Err(EndpointPolicyError::InvalidRegion)
287 );
288 }
289 }
290
291 #[test]
292 fn policy_accepts_non_static_provider_owned_identity() {
293 let host_bytes = *b"runtime-region.example";
294 let host = core::str::from_utf8(&host_bytes);
295 assert!(host.is_ok());
296 let Ok(host) = host else { return };
297 let endpoint = identity(host);
298 assert!(endpoint.is_ok());
299 if let Ok(endpoint) = endpoint {
300 let regional = RegionEndpoint::new("runtime-1", endpoint);
301 assert!(regional.is_ok());
302 if let Ok(regional) = regional {
303 assert!(EndpointPolicy::region_derived(regional).admits(endpoint));
304 }
305 }
306 }
307
308 fn identity(
309 host: &str,
310 ) -> Result<EndpointIdentity<'_>, crate::transport::EndpointIdentityError> {
311 EndpointIdentity::new(EndpointScheme::Https, host, 443, "/v1")
312 }
313}