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