1use crate::common::{format_iso_timestamp, translate_column, ColumnId, UTC_TIMESTAMP_WIDTH};
2use crate::ui::table::Column as TableColumn;
3use ratatui::prelude::*;
4use std::collections::HashMap;
5
6pub fn init(i18n: &mut HashMap<String, String>) {
7 for col in Column::all() {
8 i18n.entry(col.id().to_string())
9 .or_insert_with(|| col.default_name().to_string());
10 }
11}
12
13#[derive(Debug, Clone)]
14pub struct Key {
15 pub key_id: String,
16 pub key_arn: String,
17 pub alias: String,
18 pub description: String,
19 pub key_state: String,
20 pub key_usage: String,
21 pub key_spec: String,
22 pub key_manager: String,
23 pub creation_date: String,
24 pub expiration_date: String,
25 pub deletion_date: String,
26 pub custom_key_store_id: String,
27 pub origin: String,
28 pub enabled: bool,
29 pub multi_region: bool,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum Column {
40 Alias,
41 KeyId,
42 KeyState,
43 KeyType,
44 KeySpec,
45 KeyUsage,
46 CreationDate,
47 ExpirationDate,
48 DeletionDate,
49 Origin,
50 Regionality,
51 CustomKeyStoreId,
52}
53
54impl Column {
55 const ID_ALIAS: &'static str = "column.kms.key.alias";
56 const ID_KEY_ID: &'static str = "column.kms.key.key_id";
57 const ID_KEY_STATE: &'static str = "column.kms.key.key_state";
58 const ID_KEY_TYPE: &'static str = "column.kms.key.key_type";
59 const ID_KEY_SPEC: &'static str = "column.kms.key.key_spec";
60 const ID_KEY_USAGE: &'static str = "column.kms.key.key_usage";
61 const ID_CREATION_DATE: &'static str = "column.kms.key.creation_date";
62 const ID_EXPIRATION_DATE: &'static str = "column.kms.key.expiration_date";
63 const ID_DELETION_DATE: &'static str = "column.kms.key.deletion_date";
64 const ID_ORIGIN: &'static str = "column.kms.key.origin";
65 const ID_REGIONALITY: &'static str = "column.kms.key.regionality";
66 const ID_CUSTOM_KEY_STORE_ID: &'static str = "column.kms.key.custom_key_store_id";
67
68 pub const fn id(&self) -> &'static str {
69 match self {
70 Column::Alias => Self::ID_ALIAS,
71 Column::KeyId => Self::ID_KEY_ID,
72 Column::KeyState => Self::ID_KEY_STATE,
73 Column::KeyType => Self::ID_KEY_TYPE,
74 Column::KeySpec => Self::ID_KEY_SPEC,
75 Column::KeyUsage => Self::ID_KEY_USAGE,
76 Column::CreationDate => Self::ID_CREATION_DATE,
77 Column::ExpirationDate => Self::ID_EXPIRATION_DATE,
78 Column::DeletionDate => Self::ID_DELETION_DATE,
79 Column::Origin => Self::ID_ORIGIN,
80 Column::Regionality => Self::ID_REGIONALITY,
81 Column::CustomKeyStoreId => Self::ID_CUSTOM_KEY_STORE_ID,
82 }
83 }
84
85 pub const fn default_name(&self) -> &'static str {
86 match self {
87 Column::Alias => "Alias",
88 Column::KeyId => "Key ID",
89 Column::KeyState => "Status",
90 Column::KeyType => "Key type",
91 Column::KeySpec => "Key spec",
92 Column::KeyUsage => "Key usage",
93 Column::CreationDate => "Creation date",
94 Column::ExpirationDate => "Expiration date",
95 Column::DeletionDate => "Schedule deletion on",
96 Column::Origin => "Origin",
97 Column::Regionality => "Regionality",
98 Column::CustomKeyStoreId => "Custom key store ID",
99 }
100 }
101
102 pub const fn all() -> [Column; 12] {
104 [
105 Column::Alias,
106 Column::KeyId,
107 Column::KeyState,
108 Column::KeyType,
109 Column::KeySpec,
110 Column::KeyUsage,
111 Column::CreationDate,
112 Column::ExpirationDate,
113 Column::DeletionDate,
114 Column::Origin,
115 Column::Regionality,
116 Column::CustomKeyStoreId,
117 ]
118 }
119
120 pub const fn aws_managed_columns() -> [Column; 8] {
122 [
123 Column::Alias,
124 Column::KeyId,
125 Column::KeyState,
126 Column::CreationDate,
127 Column::ExpirationDate,
128 Column::DeletionDate,
129 Column::Origin,
130 Column::CustomKeyStoreId,
131 ]
132 }
133
134 pub const fn customer_managed_columns() -> [Column; 12] {
136 [
137 Column::Alias,
138 Column::KeyId,
139 Column::KeyState,
140 Column::KeyType,
141 Column::KeySpec,
142 Column::KeyUsage,
143 Column::CreationDate,
144 Column::ExpirationDate,
145 Column::DeletionDate,
146 Column::Origin,
147 Column::Regionality,
148 Column::CustomKeyStoreId,
149 ]
150 }
151
152 pub fn ids() -> Vec<ColumnId> {
153 Self::all().iter().map(|c| c.id()).collect()
154 }
155
156 pub fn aws_managed_default_visible() -> Vec<ColumnId> {
158 vec![Self::ID_ALIAS, Self::ID_KEY_ID, Self::ID_KEY_STATE]
159 }
160
161 pub fn customer_managed_default_visible() -> Vec<ColumnId> {
163 vec![
164 Self::ID_ALIAS,
165 Self::ID_KEY_ID,
166 Self::ID_KEY_STATE,
167 Self::ID_KEY_SPEC,
168 Self::ID_KEY_USAGE,
169 ]
170 }
171
172 pub fn from_id(id: &str) -> Option<Self> {
173 match id {
174 Self::ID_ALIAS => Some(Column::Alias),
175 Self::ID_KEY_ID => Some(Column::KeyId),
176 Self::ID_KEY_STATE => Some(Column::KeyState),
177 Self::ID_KEY_TYPE => Some(Column::KeyType),
178 Self::ID_KEY_SPEC => Some(Column::KeySpec),
179 Self::ID_KEY_USAGE => Some(Column::KeyUsage),
180 Self::ID_CREATION_DATE => Some(Column::CreationDate),
181 Self::ID_EXPIRATION_DATE => Some(Column::ExpirationDate),
182 Self::ID_DELETION_DATE => Some(Column::DeletionDate),
183 Self::ID_ORIGIN => Some(Column::Origin),
184 Self::ID_REGIONALITY => Some(Column::Regionality),
185 Self::ID_CUSTOM_KEY_STORE_ID => Some(Column::CustomKeyStoreId),
186 _ => None,
187 }
188 }
189
190 pub fn name(&self) -> String {
191 translate_column(self.id(), self.default_name())
192 }
193}
194
195impl TableColumn<Key> for Column {
196 fn name(&self) -> &str {
197 Box::leak(translate_column(self.id(), self.default_name()).into_boxed_str())
198 }
199
200 fn width(&self) -> u16 {
201 let translated = translate_column(self.id(), self.default_name());
202 translated.len().max(match self {
203 Column::Alias => 30,
204 Column::KeyId => 36,
205 Column::KeyState => 20,
206 Column::KeyType => 18,
207 Column::KeySpec => 20,
208 Column::KeyUsage => 20,
209 Column::CreationDate => UTC_TIMESTAMP_WIDTH as usize,
210 Column::ExpirationDate => UTC_TIMESTAMP_WIDTH as usize,
211 Column::DeletionDate => UTC_TIMESTAMP_WIDTH as usize,
212 Column::Origin => 14,
213 Column::Regionality => 14,
214 Column::CustomKeyStoreId => 24,
215 }) as u16
216 }
217
218 fn render(&self, item: &Key) -> (String, Style) {
219 match self {
220 Column::Alias => (item.alias.clone(), Style::default()),
221 Column::KeyId => (item.key_id.clone(), Style::default()),
222 Column::KeyState => {
223 let (text, color) = format_key_state(&item.key_state);
224 (text.to_string(), Style::default().fg(color))
225 }
226 Column::KeyType => (
227 format_key_type(&item.key_spec, &item.key_usage),
228 Style::default(),
229 ),
230 Column::KeySpec => (format_key_spec(&item.key_spec), Style::default()),
231 Column::KeyUsage => (format_key_usage(&item.key_usage), Style::default()),
232 Column::CreationDate => (format_iso_timestamp(&item.creation_date), Style::default()),
233 Column::ExpirationDate => (
234 format_iso_timestamp(&item.expiration_date),
235 Style::default(),
236 ),
237 Column::DeletionDate => (format_iso_timestamp(&item.deletion_date), Style::default()),
238 Column::Origin => (format_origin(&item.origin), Style::default()),
239 Column::Regionality => (format_regionality(item.multi_region), Style::default()),
240 Column::CustomKeyStoreId => (item.custom_key_store_id.clone(), Style::default()),
241 }
242 }
243}
244
245fn format_key_state(state: &str) -> (&'static str, Color) {
246 match state {
247 "Enabled" => ("✅ Enabled", Color::Green),
248 "Disabled" => ("⏸ Disabled", Color::Yellow),
249 "PendingDeletion" => ("🗑 Pending deletion", Color::Red),
250 "PendingImport" => ("⏳ Pending import", Color::Yellow),
251 "PendingReplicaDeletion" => ("🗑 Pending replica deletion", Color::Red),
252 "Unavailable" => ("⚠ Unavailable", Color::Red),
253 _ => ("Unknown", Color::White),
254 }
255}
256
257fn format_key_type(spec: &str, usage: &str) -> String {
258 match spec {
260 "SymmetricDefault" => "Symmetric".to_string(),
261 "Hmac224" | "Hmac256" | "Hmac384" | "Hmac512" => "HMAC".to_string(),
262 _ => {
263 if spec.starts_with("Rsa") || spec.starts_with("Ecc") || spec.starts_with("Sm2") {
265 format!("Asymmetric ({})", format_key_usage(usage))
266 } else {
267 "Asymmetric".to_string()
268 }
269 }
270 }
271}
272
273fn format_key_spec(spec: &str) -> String {
274 match spec {
275 "SymmetricDefault" => "SYMMETRIC_DEFAULT".to_string(),
276 "Hmac224" => "HMAC_224".to_string(),
277 "Hmac256" => "HMAC_256".to_string(),
278 "Hmac384" => "HMAC_384".to_string(),
279 "Hmac512" => "HMAC_512".to_string(),
280 "Rsa2048" => "RSA_2048".to_string(),
281 "Rsa3072" => "RSA_3072".to_string(),
282 "Rsa4096" => "RSA_4096".to_string(),
283 "EccNistP256" => "ECC_NIST_P256".to_string(),
284 "EccNistP384" => "ECC_NIST_P384".to_string(),
285 "EccNistP521" => "ECC_NIST_P521".to_string(),
286 "EccSecgP256k1" => "ECC_SECG_P256K1".to_string(),
287 "Sm2" => "SM2".to_string(),
288 s => s.to_string(),
289 }
290}
291
292fn format_key_usage(usage: &str) -> String {
293 match usage {
294 "EncryptDecrypt" => "ENCRYPT_DECRYPT".to_string(),
295 "SignVerify" => "SIGN_VERIFY".to_string(),
296 "GenerateVerifyMac" => "GENERATE_VERIFY_MAC".to_string(),
297 "KeyAgreement" => "KEY_AGREEMENT".to_string(),
298 s => s.to_string(),
299 }
300}
301
302fn format_origin(origin: &str) -> String {
303 match origin {
304 "AwsKms" => "AWS_KMS".to_string(),
305 "External" => "EXTERNAL".to_string(),
306 "AwsCloudhsm" => "AWS_CLOUDHSM".to_string(),
307 "ExternalKeyStore" => "EXTERNAL_KEY_STORE".to_string(),
308 s => s.to_string(),
309 }
310}
311
312fn format_regionality(multi_region: bool) -> String {
313 if multi_region {
314 "Multi-Region".to_string()
315 } else {
316 "Single-Region".to_string()
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_column_ids_have_correct_prefix() {
326 for col in Column::all() {
327 assert!(
328 col.id().starts_with("column.kms.key."),
329 "Column ID '{}' should start with 'column.kms.key.'",
330 col.id()
331 );
332 }
333 }
334
335 #[test]
336 fn test_all_columns_have_default_name() {
337 for col in Column::all() {
338 assert!(!col.default_name().is_empty());
339 }
340 }
341
342 #[test]
343 fn test_from_id_roundtrip() {
344 for col in Column::all() {
345 assert_eq!(Column::from_id(col.id()), Some(col));
346 }
347 }
348
349 #[test]
350 fn test_alias_is_first_in_both_tab_columns() {
351 assert_eq!(Column::aws_managed_columns()[0], Column::Alias);
352 assert_eq!(Column::customer_managed_columns()[0], Column::Alias);
353 }
354
355 #[test]
356 fn test_aws_managed_defaults() {
357 let ids = Column::aws_managed_default_visible();
358 assert!(ids.contains(&Column::Alias.id()));
359 assert!(ids.contains(&Column::KeyId.id()));
360 assert!(ids.contains(&Column::KeyState.id()));
361 assert!(
362 !ids.contains(&Column::KeySpec.id()),
363 "KeySpec not default for AWS managed"
364 );
365 assert!(
366 !ids.contains(&Column::ExpirationDate.id()),
367 "ExpirationDate not default"
368 );
369 }
370
371 #[test]
372 fn test_customer_managed_defaults() {
373 let ids = Column::customer_managed_default_visible();
374 assert!(ids.contains(&Column::Alias.id()));
375 assert!(ids.contains(&Column::KeyId.id()));
376 assert!(ids.contains(&Column::KeyState.id()));
377 assert!(ids.contains(&Column::KeySpec.id()));
378 assert!(ids.contains(&Column::KeyUsage.id()));
379 assert!(!ids.contains(&Column::ExpirationDate.id()));
380 assert!(!ids.contains(&Column::Regionality.id()));
381 }
382
383 #[test]
384 fn test_format_regionality() {
385 assert_eq!(format_regionality(true), "Multi-Region");
386 assert_eq!(format_regionality(false), "Single-Region");
387 }
388
389 #[test]
390 fn test_format_key_type_symmetric() {
391 assert_eq!(
392 format_key_type("SymmetricDefault", "EncryptDecrypt"),
393 "Symmetric"
394 );
395 }
396}