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 = 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}