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 =
114                format!("{PROJECT_KEY}/my_project/{LOCATION_KEY}/my_location/{KEY_RING_KEY}/my_key_ring/{KEY_NAME_KEY}/my_key/{VERSION_KEY}/1");
115
116            for parsed_resource_name in [
117                GcpCryptoKeyVersionResourceName::from_str(&resource_name_string).unwrap(),
118                serde_json::from_str(&format!(r#""{resource_name_string}""#)).unwrap(),
119            ] {
120                assert_eq!(
121                    parsed_resource_name,
122                    GcpCryptoKeyVersionResourceName {
123                        project: "my_project".to_string(),
124                        location: "my_location".to_string(),
125                        key_ring: "my_key_ring".to_string(),
126                        key_name: "my_key".to_string(),
127                        version: "1".to_string(),
128                    }
129                )
130            }
131        }
132
133        #[test]
134        fn with_missing_key_yield_error() {
135            GcpCryptoKeyVersionResourceName::from_str(&format!(
136                "/proj/{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
137            ))
138            .expect_err("Expected an error with missing key");
139        }
140
141        #[test]
142        fn with_missing_value_yield_error() {
143            GcpCryptoKeyVersionResourceName::from_str(
144                &format!(
145                    "{PROJECT_KEY}//{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
146                )
147            ).expect_err("Expected an error with missing value");
148        }
149
150        #[test]
151        fn with_invalid_key_yield_error() {
152            const INVALID_KEY: &str = "invalid_key";
153            GcpCryptoKeyVersionResourceName::from_str(
154                &format!(
155                    "{INVALID_KEY}/proj/{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
156                )
157            ).expect_err("Expected an error with invalid key for 'projects");
158            GcpCryptoKeyVersionResourceName::from_str(
159                &format!(
160                    "{PROJECT_KEY}/proj/{INVALID_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
161                )
162            )
163                .expect_err("Expected an error with invalid key for 'locations");
164            GcpCryptoKeyVersionResourceName::from_str(
165                &format!(
166                    "{PROJECT_KEY}/proj/{LOCATION_KEY}/loc/{INVALID_KEY}/kr/{KEY_NAME_KEY}/key/{VERSION_KEY}/1",
167                )
168            ).expect_err("Expected an error with invalid key for 'keyRings");
169            GcpCryptoKeyVersionResourceName::from_str(
170                &format!(
171                    "{PROJECT_KEY}/proj/{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{INVALID_KEY}/key/{VERSION_KEY}/1",
172                )
173            ).expect_err("Expected an error with invalid key for 'cryptoKeys");
174            GcpCryptoKeyVersionResourceName::from_str(
175                &format!(
176                    "{PROJECT_KEY}/proj/{LOCATION_KEY}/loc/{KEY_RING_KEY}/kr/{KEY_NAME_KEY}/key/{INVALID_KEY}/1",
177                )
178            ).expect_err("Expected an error with invalid key for 'cryptoKeyVersions");
179        }
180    }
181}