Skip to main content

rusticity_term/kms/
key.rs

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/// All possible columns across both tabs.
33/// AWS managed keys: Alias(locked), KeyId, KeyState, CreationDate,
34///   ExpirationDate, DeletionDate, Origin, CustomKeyStoreId
35/// Customer managed keys: Alias(locked), KeyId, KeyState, KeyType, KeySpec,
36///   KeyUsage, CreationDate, ExpirationDate, DeletionDate, Origin,
37///   Regionality, CustomKeyStoreId
38#[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    /// All columns (union of both tabs).
103    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    /// Columns shown in the AWS managed keys tab column selector.
121    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    /// Columns shown in the Customer managed keys tab column selector.
135    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    /// Default visible IDs for AWS managed tab: Alias + Key ID + Status
157    pub fn aws_managed_default_visible() -> Vec<ColumnId> {
158        vec![Self::ID_ALIAS, Self::ID_KEY_ID, Self::ID_KEY_STATE]
159    }
160
161    /// Default visible IDs for Customer managed tab: Alias + Key ID + Status + Key spec + Key usage
162    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    // Symmetric vs Asymmetric derived from spec
259    match spec {
260        "SymmetricDefault" => "Symmetric".to_string(),
261        "Hmac224" | "Hmac256" | "Hmac384" | "Hmac512" => "HMAC".to_string(),
262        _ => {
263            // Asymmetric: RSA, ECC, SM2
264            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}