cts_common/auth/claims/
services.rs1use serde::de::IntoDeserializer;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use url::Url;
5
6#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum ServiceType {
10 ZeroKms,
11 Secrets,
12}
13
14impl ServiceType {
15 pub fn as_str(&self) -> &'static str {
17 match self {
18 ServiceType::ZeroKms => "zerokms",
19 ServiceType::Secrets => "secrets",
20 }
21 }
22}
23
24#[derive(Debug, Clone, Default, PartialEq, Serialize)]
32#[serde(transparent)]
33pub struct Services(BTreeMap<ServiceType, Url>);
34
35impl<'de> Deserialize<'de> for Services {
36 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
37 where
38 D: serde::Deserializer<'de>,
39 {
40 let raw: BTreeMap<String, Url> = BTreeMap::deserialize(deserializer)?;
41 let mut services = BTreeMap::new();
42 for (key, url) in raw {
43 match ServiceType::deserialize(
44 <&str as IntoDeserializer<serde::de::value::Error>>::into_deserializer(
45 key.as_str(),
46 ),
47 ) {
48 Ok(service_type) => {
49 services.insert(service_type, url);
50 }
51 Err(_) => {
52 tracing::warn!(service_type = %key, "Unknown service type in token; ignoring");
53 }
54 }
55 }
56 Ok(Services(services))
57 }
58}
59
60impl Services {
61 pub fn new() -> Self {
62 Self(BTreeMap::new())
63 }
64
65 pub fn insert(&mut self, service_type: ServiceType, url: Url) {
66 self.0.insert(service_type, url);
67 }
68
69 pub fn get(&self, service_type: ServiceType) -> Option<&Url> {
70 self.0.get(&service_type)
71 }
72
73 pub fn is_empty(&self) -> bool {
74 self.0.is_empty()
75 }
76
77 pub fn iter(&self) -> impl Iterator<Item = (&ServiceType, &Url)> {
78 self.0.iter()
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn test_services_roundtrip() {
88 let mut services = Services::new();
89 services.insert(
90 ServiceType::ZeroKms,
91 Url::parse("https://us-east-1.aws.viturhosted.net/").unwrap(),
92 );
93
94 let json = serde_json::to_string(&services).unwrap();
95 assert_eq!(
96 json,
97 r#"{"zerokms":"https://us-east-1.aws.viturhosted.net/"}"#
98 );
99
100 let deserialized: Services = serde_json::from_str(&json).unwrap();
101 assert_eq!(
102 deserialized.get(ServiceType::ZeroKms).map(|u| u.as_str()),
103 Some("https://us-east-1.aws.viturhosted.net/")
104 );
105 }
106
107 #[test]
108 fn test_empty_services_not_serialized_as_null() {
109 let services = Services::new();
110 let json = serde_json::to_string(&services).unwrap();
111 assert_eq!(json, "{}");
112 }
113
114 #[test]
115 fn test_default_is_empty() {
116 let services = Services::default();
117 assert!(services.is_empty());
118 }
119
120 #[test]
121 fn test_missing_key_returns_none() {
122 let services = Services::new();
123 assert_eq!(services.get(ServiceType::ZeroKms), None);
124 }
125
126 #[test]
127 fn test_service_type_as_str() {
128 assert_eq!(ServiceType::ZeroKms.as_str(), "zerokms");
129 assert_eq!(ServiceType::Secrets.as_str(), "secrets");
130 }
131
132 #[test]
133 fn test_services_roundtrip_with_multiple_services() {
134 let mut services = Services::new();
135 services.insert(
136 ServiceType::Secrets,
137 Url::parse("https://dashboard.cipherstash.com/").unwrap(),
138 );
139 services.insert(
140 ServiceType::ZeroKms,
141 Url::parse("https://us-east-1.aws.viturhosted.net/").unwrap(),
142 );
143
144 let json = serde_json::to_string(&services).unwrap();
145 let deserialized: Services = serde_json::from_str(&json).unwrap();
146
147 assert_eq!(
148 deserialized.get(ServiceType::Secrets).map(|u| u.as_str()),
149 Some("https://dashboard.cipherstash.com/"),
150 "secrets endpoint should roundtrip through serde"
151 );
152 assert_eq!(
153 deserialized.get(ServiceType::ZeroKms).map(|u| u.as_str()),
154 Some("https://us-east-1.aws.viturhosted.net/"),
155 "zerokms endpoint should roundtrip through serde"
156 );
157 }
158
159 #[test]
160 fn test_iter_populated_services() {
161 let mut services = Services::new();
162 let url = Url::parse("https://zerokms.example.com/").unwrap();
163 services.insert(ServiceType::ZeroKms, url.clone());
164
165 let entries: Vec<_> = services.iter().collect();
166 assert_eq!(entries.len(), 1);
167 assert_eq!(entries[0], (&ServiceType::ZeroKms, &url));
168 }
169
170 #[test]
171 fn test_iter_empty_services() {
172 let services = Services::new();
173 assert_eq!(services.iter().count(), 0);
174 }
175
176 #[test]
177 fn test_unknown_service_type_is_skipped() {
178 let json = r#"{"zerokms":"https://us-east-1.aws.viturhosted.net/","newservice":"https://new.example.com/"}"#;
179 let services: Services = serde_json::from_str(json).unwrap();
180
181 assert_eq!(
182 services.get(ServiceType::ZeroKms).map(|u| u.as_str()),
183 Some("https://us-east-1.aws.viturhosted.net/"),
184 "known service should be preserved"
185 );
186 assert!(
187 services.iter().all(|(k, _)| *k != ServiceType::Secrets),
188 "unknown service should not appear as a known variant"
189 );
190 assert_eq!(
191 services.iter().count(),
192 1,
193 "only the known service should be present"
194 );
195 }
196
197 #[test]
198 fn test_all_unknown_services_produces_empty() {
199 let json = r#"{"foo":"https://foo.example.com/"}"#;
200 let services: Services = serde_json::from_str(json).unwrap();
201 assert!(services.is_empty());
202 }
203}