alien_bindings/providers/vault/
aws_parameter_store.rs1use crate::error::{ErrorData, Result};
2use alien_aws_clients::ssm::{GetParameterRequest, PutParameterRequest, SsmApi, SsmClient};
3use alien_error::{Context, ContextError};
4use async_trait::async_trait;
5use std::sync::Arc;
6
7#[derive(Debug)]
9pub struct AwsParameterStoreVault {
10 client: Arc<SsmClient>,
11 vault_prefix: String,
12}
13
14impl AwsParameterStoreVault {
15 pub fn new(client: Arc<SsmClient>, vault_prefix: String) -> Self {
17 Self {
18 client,
19 vault_prefix,
20 }
21 }
22
23 fn full_parameter_name(&self, secret_name: &str) -> String {
25 format!("{}-{}", self.vault_prefix, secret_name)
26 }
27}
28
29#[async_trait]
30impl crate::traits::Binding for AwsParameterStoreVault {}
31
32#[async_trait]
33impl crate::traits::Vault for AwsParameterStoreVault {
34 async fn get_secret(&self, secret_name: &str) -> Result<String> {
36 let full_name = self.full_parameter_name(secret_name);
37
38 let request = GetParameterRequest::builder()
39 .name(full_name.clone())
40 .with_decryption(true)
41 .build();
42
43 let response =
44 self.client
45 .get_parameter(request)
46 .await
47 .context(ErrorData::CloudPlatformError {
48 message: format!("Failed to get parameter '{}'", full_name),
49 resource_id: None,
50 })?;
51
52 let parameter = response.parameter.ok_or_else(|| {
53 alien_error::AlienError::new(ErrorData::CloudPlatformError {
54 message: format!("Parameter '{}' missing in response", full_name),
55 resource_id: None,
56 })
57 })?;
58
59 parameter.value.ok_or_else(|| {
60 alien_error::AlienError::new(ErrorData::CloudPlatformError {
61 message: format!("Parameter '{}' has no value", full_name),
62 resource_id: None,
63 })
64 })
65 }
66
67 async fn set_secret(&self, secret_name: &str, value: &str) -> Result<()> {
69 let full_name = self.full_parameter_name(secret_name);
70
71 let request = PutParameterRequest::builder()
72 .name(full_name.clone())
73 .value(value.to_string())
74 .parameter_type("SecureString".to_string())
75 .description(format!(
76 "Secret managed by Alien vault {}",
77 self.vault_prefix
78 ))
79 .overwrite(true)
80 .build();
81
82 self.client
83 .put_parameter(request)
84 .await
85 .context(ErrorData::CloudPlatformError {
86 message: format!("Failed to put parameter '{}'", full_name),
87 resource_id: None,
88 })?;
89
90 Ok(())
91 }
92
93 async fn delete_secret(&self, secret_name: &str) -> Result<()> {
95 let full_name = self.full_parameter_name(secret_name);
96
97 match self.client.delete_parameter(&full_name).await {
98 Ok(_) => Ok(()),
99 Err(error)
100 if matches!(
101 error.error,
102 Some(alien_client_core::ErrorData::RemoteResourceNotFound { .. })
103 ) =>
104 {
105 Ok(())
106 }
107 Err(error) => Err(error.context(ErrorData::CloudPlatformError {
108 message: format!("Failed to delete parameter '{full_name}'"),
109 resource_id: None,
110 })),
111 }
112 }
113
114 async fn list_secrets(&self) -> Result<Vec<String>> {
122 Err(alien_error::AlienError::new(
123 ErrorData::OperationNotSupported {
124 operation: "vault.list_secrets".to_string(),
125 reason: format!(
126 "AWS Parameter Store names are flat ('{{vault_prefix}}-{{name}}'); a \
127 BeginsWith filter on vault prefix '{}' would also match sibling vaults \
128 whose prefix extends this one (e.g. '{}-prod'). Listing will return once \
129 the naming scheme has unambiguous separation between vault prefix and \
130 secret name.",
131 self.vault_prefix, self.vault_prefix
132 ),
133 },
134 ))
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::traits::Vault as _;
142 use alien_aws_clients::AwsCredentialProvider;
143 use alien_core::{AwsClientConfig, AwsCredentials};
144
145 async fn test_vault() -> AwsParameterStoreVault {
146 let config = AwsClientConfig {
147 account_id: "000000000000".to_string(),
148 region: "us-east-1".to_string(),
149 credentials: AwsCredentials::AccessKeys {
150 access_key_id: "test".to_string(),
151 secret_access_key: "test".to_string(),
152 session_token: None,
153 },
154 service_overrides: None,
155 };
156 let credentials = AwsCredentialProvider::from_config(config)
157 .await
158 .expect("static access-key credentials never fail to construct");
159 let client = Arc::new(SsmClient::new(reqwest::Client::new(), credentials));
160 AwsParameterStoreVault::new(client, "app".to_string())
161 }
162
163 #[tokio::test]
168 async fn list_secrets_reports_not_supported() {
169 let vault = test_vault().await;
170
171 let error = vault
172 .list_secrets()
173 .await
174 .expect_err("list_secrets must stay withheld until namespace isolation lands");
175
176 assert_eq!(error.code, "OPERATION_NOT_SUPPORTED");
177 assert!(
178 error.to_string().contains("app"),
179 "message should name the vault prefix so operators can diagnose which vault hit \
180 this, got: {error}"
181 );
182 }
183}