scratchstack_aws_principal/
service.rs

1use {
2    crate::{utils::validate_dns, PrincipalError},
3    scratchstack_arn::utils::validate_region,
4    std::fmt::{Display, Formatter, Result as FmtResult},
5};
6
7/// Details about an AWS or AWS-like service.
8///
9/// Service structs are immutable.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct Service {
12    /// Name of the service.
13    service_name: String,
14
15    /// The region the service is running in. If None, the service is global.
16    region: Option<String>,
17
18    /// The DNS suffix of the service. This is usually amazonaws.com.
19    dns_suffix: String,
20}
21
22impl Service {
23    /// Create a [Service] object representing an AWS(-ish) service.
24    ///
25    /// # Arguments
26    ///
27    /// * `service_name`: The name of the service. This must meet the following requirements or a
28    ///     [PrincipalError::InvalidService] error will be returned:
29    ///     *   The name must contain between 1 and 32 characters.
30    ///     *   The name must be composed to ASCII alphanumeric characters or one of `, - . = @ _`.
31    /// * `region`: The region the service is running in. If `None`, the service is global.
32    /// * `dns_suffix`: The DNS suffix of the service. This is usually amazonaws.com.
33    ///
34    /// If all of the requirements are met, a [Service] object is returned.  Otherwise, a [PrincipalError] error is
35    /// returned.
36    ///
37    /// # Example
38    /// ```
39    /// # use scratchstack_aws_principal::Service;
40    /// let service = Service::new("s3", Some("us-east-1".to_string()), "amazonaws.com").unwrap();
41    /// assert_eq!(service.service_name(), "s3");
42    /// assert_eq!(service.region(), Some("us-east-1"));
43    /// assert_eq!(service.dns_suffix(), "amazonaws.com");
44    /// assert_eq!(service.regional_dns_name(), "s3.us-east-1.amazonaws.com");
45    /// assert_eq!(service.global_dns_name(), "s3.amazonaws.com");
46    /// ```
47    pub fn new(service_name: &str, region: Option<String>, dns_suffix: &str) -> Result<Self, PrincipalError> {
48        validate_dns(service_name, 32, PrincipalError::InvalidService)?;
49        validate_dns(dns_suffix, 128, PrincipalError::InvalidService)?;
50
51        let region = match region {
52            None => None,
53            Some(region) => {
54                validate_region(region.as_str())?;
55                Some(region)
56            }
57        };
58
59        Ok(Self {
60            service_name: service_name.to_string(),
61            region,
62            dns_suffix: dns_suffix.into(),
63        })
64    }
65
66    /// The name of the service.
67    #[inline]
68    pub fn service_name(&self) -> &str {
69        &self.service_name
70    }
71
72    /// The region of the service. If the service is global, this will be `None`.
73    #[inline]
74    pub fn region(&self) -> Option<&str> {
75        self.region.as_deref()
76    }
77
78    /// The DNS suffix of the service.
79    #[inline]
80    pub fn dns_suffix(&self) -> &str {
81        &self.dns_suffix
82    }
83
84    /// The regional DNS name of the service. If the service is global, this will be the same as the global DNS name.
85    pub fn regional_dns_name(&self) -> String {
86        match &self.region {
87            None => format!("{}.{}", self.service_name, self.dns_suffix),
88            Some(region) => format!("{}.{}.{}", self.service_name, region, self.dns_suffix),
89        }
90    }
91
92    /// The global DNS name of the service (omitting the regional component, if any).
93    pub fn global_dns_name(&self) -> String {
94        format!("{}.{}", self.service_name, self.dns_suffix)
95    }
96}
97
98impl Display for Service {
99    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
100        match &self.region {
101            None => write!(f, "{}.{}", self.service_name, self.dns_suffix),
102            Some(region) => write!(f, "{}.{}.{}", self.service_name, region, self.dns_suffix),
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use {
110        super::Service,
111        crate::{PrincipalIdentity, PrincipalSource},
112        std::{
113            collections::hash_map::DefaultHasher,
114            hash::{Hash, Hasher},
115        },
116    };
117
118    #[test]
119    fn check_components() {
120        let s1 = Service::new("s3", None, "amazonaws.com").unwrap();
121        let s2 = Service::new("s3", Some("us-east-1".into()), "amazonaws.com").unwrap();
122
123        assert_eq!(s1.service_name(), "s3");
124        assert_eq!(s1.region(), None);
125        assert_eq!(s1.dns_suffix(), "amazonaws.com");
126
127        assert_eq!(s2.service_name(), "s3");
128        assert_eq!(s2.region(), Some("us-east-1"));
129        assert_eq!(s2.dns_suffix(), "amazonaws.com");
130
131        let p = PrincipalIdentity::from(s1);
132        let source = p.source();
133        assert_eq!(source, PrincipalSource::Service);
134        assert_eq!(source.to_string(), "Service".to_string());
135    }
136
137    #[test]
138    fn check_derived() {
139        let s1a = Service::new("s3", None, "amazonaws.com").unwrap();
140        let s1b = Service::new("s3", None, "amazonaws.com").unwrap();
141        let s2 = Service::new("s3", None, "amazonaws.net").unwrap();
142        let s3 = Service::new("s3", Some("us-east-1".into()), "amazonaws.net").unwrap();
143        let s4 = Service::new("s3", Some("us-east-2".into()), "amazonaws.net").unwrap();
144        let s5 = Service::new("s4", None, "amazonaws.net").unwrap();
145        let s6 = Service::new("s4", Some("us-east-1".into()), "amazonaws.net").unwrap();
146
147        assert_eq!(s1a, s1b);
148        assert_ne!(s1a, s2);
149        assert_eq!(s1a, s1a);
150        assert_ne!(s1a, s3);
151        assert_ne!(s2, s3);
152        assert_ne!(s3, s4);
153        assert_ne!(s4, s5);
154        assert_ne!(s5, s6);
155
156        // Ensure we can hash a service.
157        let mut h1a = DefaultHasher::new();
158        let mut h1b = DefaultHasher::new();
159        let mut h2 = DefaultHasher::new();
160        s1a.hash(&mut h1a);
161        s1b.hash(&mut h1b);
162        s2.hash(&mut h2);
163        let hash1a = h1a.finish();
164        let hash1b = h1b.finish();
165        let hash2 = h2.finish();
166        assert_eq!(hash1a, hash1b);
167        assert_ne!(hash1a, hash2);
168
169        // Ensure ordering is logical.
170        assert!(s1a <= s1b);
171        assert!(s1a < s2);
172        assert!(s2 > s1a);
173        assert!(s1a < s3);
174        assert!(s2 < s3);
175        assert!(s1a < s4);
176        assert!(s2 < s4);
177        assert!(s3 < s4);
178        assert!(s1a < s5);
179        assert!(s2 < s5);
180        assert!(s3 < s5);
181        assert!(s4 < s5);
182        assert!(s1a < s6);
183        assert!(s2 < s6);
184        assert!(s3 < s6);
185        assert!(s4 < s6);
186        assert!(s5 < s6);
187        assert_eq!(s1a.clone().max(s2.clone()), s2);
188        assert_eq!(s1a.clone().min(s3), s1a);
189
190        // Ensure formatting is correct to the DNS name.
191        assert_eq!(s1a.to_string(), "s3.amazonaws.com");
192        assert_eq!(s6.to_string(), "s4.us-east-1.amazonaws.net");
193
194        // Ensure we can debug print a service.
195        let _ = format!("{s1a:?}");
196    }
197
198    #[test]
199    fn check_valid_services() {
200        let s1a = Service::new("service-name", None, "amazonaws.com").unwrap();
201        let s1b = Service::new("service-name", None, "amazonaws.com").unwrap();
202        let s2 = Service::new("service-name2", None, "amazonaws.com").unwrap();
203        let s3 = Service::new("service-name", Some("us-east-1".to_string()), "amazonaws.com").unwrap();
204        let s4 = Service::new("aservice-name-with-32-characters", None, "amazonaws.com").unwrap();
205
206        assert_eq!(s1a, s1b);
207        assert_ne!(s1a, s2);
208        assert_eq!(s1a, s1a.clone());
209
210        assert_eq!(s1a.to_string(), "service-name.amazonaws.com");
211        assert_eq!(s2.to_string(), "service-name2.amazonaws.com");
212        assert_eq!(s3.to_string(), "service-name.us-east-1.amazonaws.com");
213        assert_eq!(s4.to_string(), "aservice-name-with-32-characters.amazonaws.com");
214
215        assert_eq!(s1a.regional_dns_name(), "service-name.amazonaws.com");
216        assert_eq!(s1a.global_dns_name(), "service-name.amazonaws.com");
217
218        assert_eq!(s3.regional_dns_name(), "service-name.us-east-1.amazonaws.com");
219        assert_eq!(s3.global_dns_name(), "service-name.amazonaws.com");
220    }
221
222    #[test]
223    fn check_invalid_services() {
224        assert_eq!(
225            Service::new("service name", None, "amazonaws.com",).unwrap_err().to_string(),
226            r#"Invalid service name: "service name""#
227        );
228
229        assert_eq!(
230            Service::new("service name", Some("us-east-1".to_string()), "amazonaws.com",).unwrap_err().to_string(),
231            r#"Invalid service name: "service name""#
232        );
233
234        assert_eq!(
235            Service::new("service!name", None, "amazonaws.com",).unwrap_err().to_string(),
236            r#"Invalid service name: "service!name""#
237        );
238
239        assert_eq!(
240            Service::new("service!name", Some("us-east-1".to_string()), "amazonaws.com",).unwrap_err().to_string(),
241            r#"Invalid service name: "service!name""#
242        );
243
244        assert_eq!(Service::new("", None, "amazonaws.com",).unwrap_err().to_string(), r#"Invalid service name: """#);
245
246        assert_eq!(
247            Service::new("a-service-name-with-33-characters", None, "amazonaws.com",).unwrap_err().to_string(),
248            r#"Invalid service name: "a-service-name-with-33-characters""#
249        );
250
251        assert_eq!(
252            Service::new("service-name", Some("us-east-".to_string()), "amazonaws.com",).unwrap_err().to_string(),
253            r#"Invalid region: "us-east-""#
254        );
255
256        assert_eq!(
257            Service::new("service-name", Some("us-east-1".to_string()), "amazonaws..com",).unwrap_err().to_string(),
258            r#"Invalid service name: "amazonaws..com""#
259        );
260    }
261}
262// end tests -- do not delete; needed for coverage.