pdk_core/
client.rs

1// Copyright (c) 2025, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5//! Tools to generate and parse service names.
6
7use de::Deserialize;
8use serde::{de, Deserializer};
9use sha2::{Digest, Sha256};
10
11use classy::client::{InvalidUri, Service, Uri};
12
13use crate::policy_context::api::Metadata;
14
15const SERVICE_NAME_SUFFIX: &str = "service";
16
17/// Deserializes a valid URI into a [`Service`] using policy's metadata to generate the service name
18pub fn deserialize_service<'de, D>(deserializer: D) -> Result<Service, D::Error>
19where
20    D: Deserializer<'de>,
21{
22    use serde::de::Error;
23
24    let string: String = Deserialize::deserialize(deserializer)?;
25
26    let metadata = Metadata::new();
27    let service = new_service(string.as_str(), &metadata)
28        .map_err(|err: InvalidUri| Error::custom(error_message(string.as_str(), err)))?;
29
30    Ok(service)
31}
32
33/// Deserializes a valid URI into a [`Option<Service>`] using policy's metadata to generate the service name
34pub fn deserialize_service_opt<'de, D>(deserializer: D) -> Result<Option<Service>, D::Error>
35where
36    D: Deserializer<'de>,
37{
38    use serde::de::Error;
39
40    let string: Option<String> = Deserialize::deserialize(deserializer)?;
41
42    match string {
43        Some(string) => {
44            let metadata = Metadata::new();
45            let service = new_service(string.as_str(), &metadata)
46                .map_err(|err: InvalidUri| Error::custom(error_message(string.as_str(), err)))?;
47            Ok(Some(service))
48        }
49        None => Ok(None),
50    }
51}
52
53/// Deserializes a set of valid URIs into a [`Vec<Service>`] using policy's metadata to generate the service name
54pub fn deserialize_service_vec<'de, D>(deserializer: D) -> Result<Vec<Service>, D::Error>
55where
56    D: Deserializer<'de>,
57{
58    use serde::de::Error;
59
60    let strings: Vec<String> = Deserialize::deserialize(deserializer)?;
61
62    let metadata = Metadata::new();
63
64    let services: Vec<Service> = strings
65        .iter()
66        .map(|string| {
67            let service = new_service(string.as_str(), &metadata)
68                .map_err(|err: InvalidUri| Error::custom(error_message(string.as_str(), err)))?;
69
70            Ok(service)
71        })
72        .collect::<Result<Vec<_>, _>>()?;
73
74    Ok(services)
75}
76
77/// Deserializes a set of valid URIs into a [`Option<Vec<Service>>`] using policy's metadata to generate the service name
78pub fn deserialize_service_opt_vec<'de, D>(
79    deserializer: D,
80) -> Result<Option<Vec<Service>>, D::Error>
81where
82    D: Deserializer<'de>,
83{
84    use serde::de::Error;
85
86    let strings: Option<Vec<String>> = Deserialize::deserialize(deserializer)?;
87
88    match strings {
89        Some(strings) => {
90            let metadata = Metadata::new();
91
92            let services: Vec<Service> = strings
93                .iter()
94                .map(|string| {
95                    let service =
96                        new_service(string.as_str(), &metadata).map_err(|err: InvalidUri| {
97                            Error::custom(error_message(string.as_str(), err))
98                        })?;
99
100                    Ok(service)
101                })
102                .collect::<Result<Vec<_>, _>>()?;
103
104            Ok(Some(services))
105        }
106        None => Ok(None),
107    }
108}
109
110/// Generates a compliant [`Service`] name using the policy name and the authority part of an URL
111pub fn service_name(policy_name: &str, authority: &str) -> String {
112    let full_name = format!("{policy_name}-{authority}");
113
114    let digest = &Sha256::digest(full_name.as_str());
115    let hex = &format!("{digest:x}")[..7];
116
117    let compliant_full_name: String = full_name
118        .chars()
119        .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
120        .collect();
121
122    let capped_name = &compliant_full_name[..std::cmp::min(47, compliant_full_name.len())];
123
124    format!("{}-{}-{}", capped_name.to_owned(), hex, SERVICE_NAME_SUFFIX)
125}
126
127fn new_service(string: &str, metadata: &Metadata) -> Result<Service, InvalidUri> {
128    let uri: Uri = string.parse()?;
129    let service_namespace = metadata.policy_metadata.policy_namespace.as_str();
130    let service_name = service_name(
131        metadata.policy_metadata.policy_name.as_str(),
132        uri.authority(),
133    );
134    Ok(Service::from(service_name.as_str(), service_namespace, uri))
135}
136
137fn error_message(string: &str, err: InvalidUri) -> String {
138    format!("Error parsing {string} as Uri: {err}")
139}
140
141#[cfg(test)]
142mod test {
143    use std::vec::IntoIter;
144
145    use serde::de::value::{Error as ValueError, SeqDeserializer, StrDeserializer};
146    use serde::de::{Error, IntoDeserializer};
147    use serde_derive::Deserialize;
148    use serde_json::json;
149
150    use classy::hl::Service;
151
152    use crate::client::{
153        deserialize_service, deserialize_service_opt, deserialize_service_opt_vec,
154        deserialize_service_vec, service_name,
155    };
156
157    #[test]
158    fn service_name_test() {
159        let name = service_name("policy", "authority");
160
161        assert_eq!(name, "policy-authority-15aeecc-service");
162    }
163
164    #[test]
165    fn service_name_is_idempotent() {
166        let name = service_name("policy", "authority");
167        let name2 = service_name("policy", "authority");
168
169        assert_eq!(name, name2);
170    }
171
172    #[test]
173    fn service_name_long_does_not_exceed_63_chars() {
174        let authority = "a-really-long-authority-exceeding-max-large-service-name";
175        let name = service_name("policy", authority);
176
177        assert_eq!(name.len(), 63);
178        assert_eq!(
179            name,
180            "policy-a-really-long-authority-exceeding-max-la-3b21324-service"
181        );
182    }
183
184    #[test]
185    fn service_name_two_long_names_that_end_different_have_different_hashes() {
186        let authority = "a-really-long-authority-exceeding-max-large-service-name";
187        let authority_2 = "a-really-long-authority-exceeding-max-large-service-name-2";
188        let name = service_name("policy", authority);
189        let name2 = service_name("policy", authority_2);
190
191        assert_ne!(name, name2);
192    }
193
194    #[test]
195    fn service_name_two_different_names_that_have_same_sanitized_name_have_different_hashes() {
196        let authority = "authority:port";
197        let authority_2 = "authority@port";
198        let name = service_name("policy", authority);
199        let name2 = service_name("policy", authority_2);
200
201        assert_ne!(name, name2);
202    }
203
204    #[test]
205    fn service_name_invalid_chars_in_authority_are_converted() {
206        let authority = "username:password@[ipv6]:port";
207        let name = service_name("policy", authority);
208
209        assert_eq!(name, "policy-username-password--ipv6--port-c248764-service");
210    }
211
212    #[test]
213    fn deserialize_service_successful_parsing() {
214        let uri_string = "https://localhost:8080/api";
215
216        let result = deserialize_service(deserializer(uri_string));
217
218        assert!(result.is_ok());
219        let service = result.unwrap();
220        assert_eq!(service.uri().to_string(), uri_string.to_string());
221        assert_eq!(
222            service.cluster_name(),
223            "NoPolicyId-localhost-8080-a5ec11e-service.NoPolicyNamespace.svc"
224        );
225    }
226
227    #[test]
228    fn deserialize_service_invalid_uri() {
229        let uri_string = "localhost:8080/api";
230
231        let result = deserialize_service(deserializer(uri_string));
232
233        assert_eq!(
234            result.unwrap_err(),
235            ValueError::custom("Error parsing localhost:8080/api as Uri: invalid format")
236        )
237    }
238
239    #[test]
240    fn deserialize_service_vec_successful_parsing() {
241        let uri_string_1 = "https://localhost:8080/api";
242        let uri_string_2 = "https://another-host:9090/api";
243        let vec = vec![uri_string_1, uri_string_2];
244
245        let result = deserialize_service_vec(vec_deserializer(vec));
246
247        assert!(result.is_ok(), "{:?}", result.err());
248        let services = result.unwrap();
249        assert_eq!(services.len(), 2);
250        let service1 = services.first().unwrap();
251        assert_eq!(service1.uri().to_string(), uri_string_1.to_string());
252        assert_eq!(
253            service1.cluster_name(),
254            "NoPolicyId-localhost-8080-a5ec11e-service.NoPolicyNamespace.svc"
255        );
256        let service2 = services.get(1).unwrap();
257        assert_eq!(service2.uri().to_string(), uri_string_2.to_string());
258        assert_eq!(
259            service2.cluster_name(),
260            "NoPolicyId-another-host-9090-f35336e-service.NoPolicyNamespace.svc"
261        );
262    }
263
264    #[test]
265    fn deserialize_service_vec_invalid_uri_first_element() {
266        let uri_string_1 = "localhost:8080/api";
267        let uri_string_2 = "https://another-host:9090/api";
268        let vec = vec![uri_string_1, uri_string_2];
269
270        let result = deserialize_service_vec(vec_deserializer(vec));
271
272        assert_eq!(
273            result.unwrap_err(),
274            ValueError::custom("Error parsing localhost:8080/api as Uri: invalid format")
275        )
276    }
277
278    #[test]
279    fn deserialize_service_vec_invalid_uri_last_element() {
280        let uri_string_1 = "https://localhost:8080/api";
281        let uri_string_2 = "another-host:9090/api";
282        let vec = vec![uri_string_1, uri_string_2];
283
284        let result = deserialize_service_vec(vec_deserializer(vec));
285
286        assert_eq!(
287            result.unwrap_err(),
288            ValueError::custom("Error parsing another-host:9090/api as Uri: invalid format")
289        )
290    }
291
292    #[test]
293    fn deserialize_opt_service_successful_parsing() {
294        let uri_string = "https://localhost:8080/api";
295
296        let test: TestOpt = serde_json::from_value(json!({ "service": uri_string })).unwrap();
297
298        let option = test.service;
299        assert!(option.is_some());
300
301        let service = option.unwrap();
302        assert_eq!(service.uri().to_string(), uri_string.to_string());
303        assert_eq!(
304            service.cluster_name(),
305            "NoPolicyId-localhost-8080-a5ec11e-service.NoPolicyNamespace.svc"
306        );
307    }
308
309    #[test]
310    fn deserialize_opt_service_invalid_uri() {
311        let result: Result<TestOpt, serde_json::Error> =
312            serde_json::from_value(json!({ "service": "localhost:8080/api" }));
313
314        assert_eq!(
315            result.unwrap_err().to_string(),
316            "Error parsing localhost:8080/api as Uri: invalid format"
317        )
318    }
319
320    #[test]
321    fn deserialize_opt_missing_field() {
322        let test: TestOpt = serde_json::from_value(json!({})).unwrap();
323
324        assert!(test.service.is_none());
325    }
326
327    #[test]
328    fn deserialize_service_opt_vec_successful_parsing() {
329        let uri_string_1 = "https://localhost:8080/api";
330        let uri_string_2 = "https://another-host:9090/api";
331
332        let test: TestOptVec =
333            serde_json::from_value(json!({ "services": [uri_string_1, uri_string_2] })).unwrap();
334
335        let option = test.services;
336        assert!(option.is_some());
337
338        let services = option.unwrap();
339        assert_eq!(services.len(), 2);
340        let service1 = services.first().unwrap();
341        assert_eq!(service1.uri().to_string(), uri_string_1.to_string());
342        assert_eq!(
343            service1.cluster_name(),
344            "NoPolicyId-localhost-8080-a5ec11e-service.NoPolicyNamespace.svc"
345        );
346        let service2 = services.get(1).unwrap();
347        assert_eq!(service2.uri().to_string(), uri_string_2.to_string());
348        assert_eq!(
349            service2.cluster_name(),
350            "NoPolicyId-another-host-9090-f35336e-service.NoPolicyNamespace.svc"
351        );
352    }
353
354    #[test]
355    fn deserialize_service_opt_vec_invalid_first_uri() {
356        let result: Result<TestOptVec, serde_json::Error> = serde_json::from_value(
357            json!({ "services": ["localhost:8080/api", "https://another-host:9090/api"] }),
358        );
359
360        assert_eq!(
361            result.unwrap_err().to_string(),
362            "Error parsing localhost:8080/api as Uri: invalid format"
363        )
364    }
365
366    #[test]
367    fn deserialize_service_opt_vec_invalid_last_uri() {
368        let result: Result<TestOptVec, serde_json::Error> = serde_json::from_value(
369            json!({ "services": ["https://localhost:8080/api", "another-host:9090/api"] }),
370        );
371
372        assert_eq!(
373            result.unwrap_err().to_string(),
374            "Error parsing another-host:9090/api as Uri: invalid format"
375        )
376    }
377
378    #[test]
379    fn deserialize_service_opt_vec_missing_field() {
380        let test: TestOptVec = serde_json::from_value(json!({})).unwrap();
381
382        assert!(test.services.is_none());
383    }
384
385    #[derive(Debug, Deserialize)]
386    struct TestOpt {
387        #[serde(default)]
388        #[serde(deserialize_with = "deserialize_service_opt")]
389        pub service: Option<Service>,
390    }
391
392    #[derive(Debug, Deserialize)]
393    struct TestOptVec {
394        #[serde(default)]
395        #[serde(deserialize_with = "deserialize_service_opt_vec")]
396        pub services: Option<Vec<Service>>,
397    }
398
399    fn deserializer(uri_string: &str) -> StrDeserializer<serde::de::value::Error> {
400        let deserializer: StrDeserializer<ValueError> = uri_string.into_deserializer();
401        deserializer
402    }
403
404    fn vec_deserializer(vec: Vec<&str>) -> SeqDeserializer<IntoIter<&str>, ValueError> {
405        let deserializer = vec.into_deserializer();
406        deserializer
407    }
408}