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", deny_unknown_fields)]
53pub struct KeyOutputs {
54    /// Stable provider key-family identity.
55    pub fingerprint: KeyFingerprint,
56    /// Exact provider key version used for new wrapping operations.
57    pub wrapping_key_id: String,
58}
59
60impl ResourceDefinition for Key {
61    fn get_resource_type(&self) -> ResourceType {
62        Self::RESOURCE_TYPE
63    }
64
65    fn id(&self) -> &str {
66        &self.id
67    }
68
69    fn replace_after_failed_create_is_safe(&self) -> bool {
70        // Data encrypted with the key cannot be decrypted once it is gone.
71        false
72    }
73
74    fn get_dependencies(&self) -> Vec<ResourceRef> {
75        Vec::new()
76    }
77
78    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
79        let Some(new_key) = new_config.as_any().downcast_ref::<Self>() else {
80            return Err(AlienError::new(ErrorData::UnexpectedResourceType {
81                resource_id: self.id.clone(),
82                expected: Self::RESOURCE_TYPE,
83                actual: new_config.get_resource_type(),
84            }));
85        };
86
87        if self.id != new_key.id {
88            return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
89                resource_id: self.id.clone(),
90                reason: "the 'id' field is immutable".to_string(),
91            }));
92        }
93
94        Ok(())
95    }
96
97    fn as_any(&self) -> &dyn Any {
98        self
99    }
100
101    fn as_any_mut(&mut self) -> &mut dyn Any {
102        self
103    }
104
105    fn box_clone(&self) -> Box<dyn ResourceDefinition> {
106        Box::new(self.clone())
107    }
108
109    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
110        other.as_any().downcast_ref::<Self>() == Some(self)
111    }
112
113    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
114        serde_json::to_value(self)
115    }
116}
117
118impl ResourceOutputsDefinition for KeyOutputs {
119    fn get_resource_type(&self) -> ResourceType {
120        Key::RESOURCE_TYPE
121    }
122
123    fn as_any(&self) -> &dyn Any {
124        self
125    }
126
127    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
128        Box::new(self.clone())
129    }
130
131    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
132        other.as_any().downcast_ref::<Self>() == Some(self)
133    }
134
135    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
136        serde_json::to_value(self)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn provider_rotation_preserves_the_key_family_fingerprint() {
146        let fingerprint = KeyFingerprint::Gcp {
147            crypto_key_name: "projects/example/locations/us/keyRings/data/cryptoKeys/customer"
148                .to_string(),
149        };
150        let before = KeyOutputs {
151            fingerprint: fingerprint.clone(),
152            wrapping_key_id: "projects/example/locations/us/keyRings/data/cryptoKeys/customer/cryptoKeyVersions/1".to_string(),
153        };
154        let after = KeyOutputs {
155            fingerprint,
156            wrapping_key_id: "projects/example/locations/us/keyRings/data/cryptoKeys/customer/cryptoKeyVersions/2".to_string(),
157        };
158
159        assert_eq!(before.fingerprint, after.fingerprint);
160        assert_ne!(before.wrapping_key_id, after.wrapping_key_id);
161    }
162}