Skip to main content

alien_core/resources/
key.rs

1use crate::{
2    error::{ErrorData, Result},
3    resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef, ResourceType},
4};
5use alien_error::AlienError;
6use bon::Builder;
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10/// A customer-managed encryption key.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "camelCase", deny_unknown_fields)]
14#[builder(start_fn = new)]
15pub struct Key {
16    /// Identifier for the key resource.
17    #[builder(start_fn)]
18    pub id: String,
19}
20
21impl Key {
22    pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("key");
23
24    pub fn id(&self) -> &str {
25        &self.id
26    }
27}
28
29/// Stable identity of a provider key family.
30///
31/// Provider-native version rotation does not change this value. Replacing the
32/// provider key does.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
35#[serde(tag = "provider", rename_all = "lowercase", deny_unknown_fields)]
36pub enum KeyFingerprint {
37    /// An immutable AWS KMS key ARN. Alias ARNs are not accepted.
38    Aws { key_arn: String },
39    /// A full GCP CryptoKey resource name, without a CryptoKeyVersion.
40    Gcp { crypto_key_name: String },
41    /// An Azure Key Vault key family pinned to its original lineage.
42    Azure {
43        vault_resource_id: String,
44        key_name: String,
45        lineage_version_id: String,
46    },
47}
48
49/// Outputs generated by a successfully provisioned Key.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
52#[serde(rename_all = "camelCase")]
53pub struct KeyOutputs {
54    /// Latest key availability observation. Absent on older imported states.
55    /// A false observation suspends dependent reconciliation without replacing resources.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub available: Option<bool>,
58    /// Stable provider key-family identity.
59    pub fingerprint: KeyFingerprint,
60    /// Exact provider key version used for new wrapping operations.
61    pub wrapping_key_id: String,
62}
63
64impl ResourceDefinition for Key {
65    fn get_resource_type(&self) -> ResourceType {
66        Self::RESOURCE_TYPE
67    }
68
69    fn id(&self) -> &str {
70        &self.id
71    }
72
73    fn replace_after_failed_create_is_safe(&self) -> bool {
74        // Data encrypted with the key cannot be decrypted once it is gone.
75        false
76    }
77
78    fn get_dependencies(&self) -> Vec<ResourceRef> {
79        Vec::new()
80    }
81
82    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
83        let Some(new_key) = new_config.as_any().downcast_ref::<Self>() else {
84            return Err(AlienError::new(ErrorData::UnexpectedResourceType {
85                resource_id: self.id.clone(),
86                expected: Self::RESOURCE_TYPE,
87                actual: new_config.get_resource_type(),
88            }));
89        };
90
91        if self.id != new_key.id {
92            return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
93                resource_id: self.id.clone(),
94                reason: "the 'id' field is immutable".to_string(),
95            }));
96        }
97
98        Ok(())
99    }
100
101    fn as_any(&self) -> &dyn Any {
102        self
103    }
104
105    fn as_any_mut(&mut self) -> &mut dyn Any {
106        self
107    }
108
109    fn box_clone(&self) -> Box<dyn ResourceDefinition> {
110        Box::new(self.clone())
111    }
112
113    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
114        other.as_any().downcast_ref::<Self>() == Some(self)
115    }
116
117    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
118        serde_json::to_value(self)
119    }
120}
121
122impl ResourceOutputsDefinition for KeyOutputs {
123    fn is_available(&self) -> bool {
124        self.available.unwrap_or(true)
125    }
126
127    fn get_resource_type(&self) -> ResourceType {
128        Key::RESOURCE_TYPE
129    }
130
131    fn as_any(&self) -> &dyn Any {
132        self
133    }
134
135    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
136        Box::new(self.clone())
137    }
138
139    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
140        other.as_any().downcast_ref::<Self>() == Some(self)
141    }
142
143    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
144        serde_json::to_value(self)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn provider_rotation_preserves_the_key_family_fingerprint() {
154        let fingerprint = KeyFingerprint::Gcp {
155            crypto_key_name: "projects/example/locations/us/keyRings/data/cryptoKeys/customer"
156                .to_string(),
157        };
158        let before = KeyOutputs {
159            available: None,
160            fingerprint: fingerprint.clone(),
161            wrapping_key_id: "projects/example/locations/us/keyRings/data/cryptoKeys/customer/cryptoKeyVersions/1".to_string(),
162        };
163        let after = KeyOutputs {
164            available: None,
165            fingerprint,
166            wrapping_key_id: "projects/example/locations/us/keyRings/data/cryptoKeys/customer/cryptoKeyVersions/2".to_string(),
167        };
168
169        assert_eq!(before.fingerprint, after.fingerprint);
170        assert_ne!(before.wrapping_key_id, after.wrapping_key_id);
171    }
172}