mithril_aggregator/services/snapshotter/ancillary_signer/
gcp_kms_resource_name.rs

1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3
4use serde::{Deserialize, Deserializer, Serialize};
5
6use mithril_common::StdResult;
7
8/// Name of a CryptoKeyVersion that represents an individual cryptographic key and the associated key material.
9///
10/// see name property in <https://cloud.google.com/kms/docs/reference/rpc/google.cloud.kms.v1#google.cloud.kms.v1.CryptoKeyVersion>
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12pub struct GcpCryptoKeyVersionResourceName {
13    /// The project ID of the Google Cloud project.
14    pub project: String,
15    /// The location of the key ring.
16    pub location: String,
17    /// The key ring that contains the key.
18    pub key_ring: String,
19    /// The name of the key.
20    pub key_name: String,
21    /// The version of the key.
22    pub version: String,
23}
24
25const PROJECT_KEY: &str = "projects";
26const LOCATION_KEY: &str = "locations";
27const KEY_RING_KEY: &str = "keyRings";
28const KEY_NAME_KEY: &str = "cryptoKeys";
29const VERSION_KEY: &str = "cryptoKeyVersions";
30
31impl Display for GcpCryptoKeyVersionResourceName {
32    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
33        write!(
34            f,
35            "{PROJECT_KEY}/{}/{LOCATION_KEY}/{}/{KEY_RING_KEY}/{}/{KEY_NAME_KEY}/{}/{VERSION_KEY}/{}",
36            self.project, self.location, self.key_ring, self.key_name, self.version
37        )
38    }
39}
40
41impl FromStr for GcpCryptoKeyVersionResourceName {
42    type Err = anyhow::Error;
43
44    fn from_str(s: &str) -> StdResult<Self> {
45        let error = format!(
46            "Invalid resource name: '{s}' does not match pattern '{PROJECT_KEY}/../{LOCATION_KEY}/../{KEY_RING_KEY}/../{KEY_NAME_KEY}/../{VERSION_KEY}/..'"
47        );
48        let parts: Vec<&str> = s.split('/').collect();
49
50        if parts.len() != 10 {
51            anyhow::bail!(error);
52        }
53
54        if parts[0] != PROJECT_KEY
55            || parts[2] != LOCATION_KEY
56            || parts[4] != KEY_RING_KEY
57            || parts[6] != KEY_NAME_KEY
58            || parts[8] != VERSION_KEY
59        {
60            anyhow::bail!(error);
61        }
62
63        if parts.iter().any(|part| part.is_empty()) {
64            anyhow::bail!(error);
65        }
66
67        Ok(Self {
68            project: parts[1].to_string(),
69            location: parts[3].to_string(),
70            key_ring: parts[5].to_string(),
71            key_name: parts[7].to_string(),
72            version: parts[9].to_string(),
73        })
74    }
75}
76
77impl<'de: 'a, 'a> Deserialize<'de> for GcpCryptoKeyVersionResourceName {
78    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
79    where
80        D: Deserializer<'de>,
81    {
82        use serde::de::Error;
83        let str: &'a str = Deserialize::deserialize(deserializer)?;
84        Self::from_str(str).map_err(Error::custom)
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn convert_crypto_key_resource_name_to_string_use_gcp_format() {
94        let gcp_key = GcpCryptoKeyVersionResourceName {
95            project: "my_project".to_string(),
96            location: "my_location".to_string(),
97            key_ring: "my_key_ring".to_string(),
98            key_name: "my_key".to_string(),
99            version: "1".to_string(),
100        };
101
102        assert_eq!(
103            gcp_key.to_string(),
104            "projects/my_project/locations/my_location/keyRings/my_key_ring/cryptoKeys/my_key/cryptoKeyVersions/1".to_string()
105        );
106    }
107
108    mod parse_from_str {
109        use super::*;
110
111        #[test]
112        fn with_correctly_formatted_str_retrieve_all_keys() {
113            let resource_name_string = format!(
114                "{PROJECT_KEY}/my_project/{LOCATION_KEY}/my_location/{KEY_RING_KEY}/my_key_ring/{KEY_NAME_KEY}/my_key/{VERSION_KEY}/1"
115            );
116
117            for parsed_resource_name in [
118                GcpCryptoKeyVersionResourceName::from_str(&resource_name_string).unwrap(),
119                serde_json::from_str(&format!(r#""{resource_name_string}""#)).unwrap(),
120            ] {
121                assert_eq!(
122                    parsed_resource_name,
123                    GcpCryptoKeyVersionResourceName {
124                        project: "my_project".to_string(),
125                        location: "my_location".to_string(),
126                        key_ring: "my_key_ring".to_string(),
127                        key_name: "my_key".to_string(),
128                        version: "1".to_string(),
129                    }
130                )
131            }
132        }
133
134        #[test]
135        fn with_missing_key_yield_error() {
136            GcpCryptoKeyVersionResourceName::from_str(&format!(
137                "/proj/{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
138            ))
139            .expect_err("Expected an error with missing key");
140        }
141
142        #[test]
143        fn with_missing_value_yield_error() {
144            GcpCryptoKeyVersionResourceName::from_str(
145                &format!(
146                    "{PROJECT_KEY}//{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
147                )
148            ).expect_err("Expected an error with missing value");
149        }
150
151        #[test]
152        fn with_invalid_key_yield_error() {
153            const INVALID_KEY: &str = "invalid_key";
154            GcpCryptoKeyVersionResourceName::from_str(
155                &format!(
156                    "{INVALID_KEY}/proj/{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
157                )
158            ).expect_err("Expected an error with invalid key for 'projects");
159            GcpCryptoKeyVersionResourceName::from_str(
160                &format!(
161                    "{PROJECT_KEY}/proj/{INVALID_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
162                )
163            )
164                .expect_err("Expected an error with invalid key for 'locations");
165            GcpCryptoKeyVersionResourceName::from_str(
166                &format!(
167                    "{PROJECT_KEY}/proj/{LOCATION_KEY}/loc/{INVALID_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
168                )
169            ).expect_err("Expected an error with invalid key for 'keyRings");
170            GcpCryptoKeyVersionResourceName::from_str(
171                &format!(
172                    "{PROJECT_KEY}/proj/{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{INVALID_KEY}/key/{VERSION_KEY}/1",
173                )
174            ).expect_err("Expected an error with invalid key for 'cryptoKeys");
175            GcpCryptoKeyVersionResourceName::from_str(
176                &format!(
177                    "{PROJECT_KEY}/proj/{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{INVALID_KEY}/1",
178                )
179            ).expect_err("Expected an error with invalid key for 'cryptoKeyVersions");
180        }
181    }
182}