mithril_aggregator/services/snapshotter/ancillary_signer/
gcp_kms_resource_name.rs1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3
4use serde::{Deserialize, Deserializer, Serialize};
5
6use mithril_common::StdResult;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12pub struct GcpCryptoKeyVersionResourceName {
13 pub project: String,
15 pub location: String,
17 pub key_ring: String,
19 pub key_name: String,
21 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}