use super::*;
impl ResourceProvisioner {
pub(super) fn get_att_secrets_manager_secret(
&self,
physical_id: &str,
attribute: &str,
) -> Option<String> {
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let secret = state.secrets.get(physical_id)?;
match attribute {
"Arn" | "Id" => Some(secret.arn.clone()),
_ => None,
}
}
pub(super) fn create_secrets_manager_secret(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let kms_key_id = props
.get("KmsKeyId")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = format!(
"arn:aws:secretsmanager:{}:{}:secret:{}",
state.region, state.account_id, name
);
if state.secrets.contains_key(&arn) {
return Err(format!("Secret {name} already exists"));
}
let now = Utc::now();
let mut versions = BTreeMap::new();
let mut current_version_id: Option<String> = None;
let initial_string: Option<String> =
if let Some(secret_string) = props.get("SecretString").and_then(|v| v.as_str()) {
Some(secret_string.to_string())
} else if let Some(gen) = props.get("GenerateSecretString") {
Some(generate_secret_string_payload(gen)?)
} else {
None
};
if let Some(secret_string) = initial_string {
let version_id = Uuid::new_v4().to_string();
versions.insert(
version_id.clone(),
SecretVersion {
version_id: version_id.clone(),
secret_string: Some(secret_string),
secret_binary: None,
stages: vec!["AWSCURRENT".to_string()],
created_at: now,
},
);
current_version_id = Some(version_id);
}
let mut tags: Vec<(String, String)> = Vec::new();
if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
for t in arr {
if let (Some(k), Some(v)) = (
t.get("Key").and_then(|x| x.as_str()),
t.get("Value").and_then(|x| x.as_str()),
) {
tags.push((k.to_string(), v.to_string()));
}
}
}
let tags_set = !tags.is_empty();
let secret = Secret {
name: name.clone(),
arn: arn.clone(),
description,
kms_key_id,
versions,
current_version_id,
tags,
tags_ever_set: tags_set,
deleted: false,
deletion_date: None,
created_at: now,
last_changed_at: now,
last_accessed_at: None,
rotation_enabled: None,
rotation_lambda_arn: None,
rotation_rules: None,
last_rotated_at: None,
resource_policy: None,
};
state.secrets.insert(arn.clone(), secret);
Ok(ProvisionResult::new(arn.clone())
.with("Id", arn.clone())
.with("Name", name))
}
pub(super) fn delete_secrets_manager_secret(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
state.secrets.remove(physical_id);
Ok(())
}
pub(super) fn create_secrets_manager_rotation_schedule(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let secret_id = props
.get("SecretId")
.and_then(|v| v.as_str())
.ok_or("SecretId is required")?
.to_string();
let rotation_lambda_arn = props
.get("RotationLambdaARN")
.and_then(|v| v.as_str())
.map(String::from);
let automatically_after_days = props
.get("RotationRules")
.and_then(|v| v.get("AutomaticallyAfterDays"))
.and_then(|v| v.as_i64());
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let secret_arn = if state.secrets.contains_key(&secret_id) {
secret_id.clone()
} else {
let candidate = format!(
"arn:aws:secretsmanager:{}:{}:secret:{}",
state.region, state.account_id, secret_id
);
if state.secrets.contains_key(&candidate) {
candidate
} else {
return Err(format!("Secret {secret_id} not yet provisioned"));
}
};
let secret = state
.secrets
.get_mut(&secret_arn)
.ok_or_else(|| format!("Secret {secret_arn} not found"))?;
secret.rotation_enabled = Some(true);
secret.rotation_lambda_arn = rotation_lambda_arn;
secret.rotation_rules = Some(RotationRules {
automatically_after_days,
});
secret.last_changed_at = Utc::now();
Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
}
pub(super) fn delete_secrets_manager_rotation_schedule(
&self,
physical_id: &str,
) -> Result<(), String> {
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(secret) = state.secrets.get_mut(physical_id) {
secret.rotation_enabled = Some(false);
secret.rotation_lambda_arn = None;
secret.rotation_rules = None;
secret.last_changed_at = Utc::now();
}
Ok(())
}
pub(super) fn create_secrets_manager_resource_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let secret_id = props
.get("SecretId")
.and_then(|v| v.as_str())
.ok_or("SecretId is required")?
.to_string();
let policy_doc = props
.get("ResourcePolicy")
.ok_or("ResourcePolicy is required")?;
let policy_str = match policy_doc {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let secret_arn = if state.secrets.contains_key(&secret_id) {
secret_id.clone()
} else {
let candidate = format!(
"arn:aws:secretsmanager:{}:{}:secret:{}",
state.region, state.account_id, secret_id
);
if state.secrets.contains_key(&candidate) {
candidate
} else {
return Err(format!("Secret {secret_id} not yet provisioned"));
}
};
let secret = state
.secrets
.get_mut(&secret_arn)
.ok_or_else(|| format!("Secret {secret_arn} not found"))?;
secret.resource_policy = Some(policy_str);
secret.last_changed_at = Utc::now();
Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
}
pub(super) fn delete_secrets_manager_resource_policy(
&self,
physical_id: &str,
) -> Result<(), String> {
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(secret) = state.secrets.get_mut(physical_id) {
secret.resource_policy = None;
secret.last_changed_at = Utc::now();
}
Ok(())
}
pub(super) fn create_secrets_manager_target_attachment(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let secret_id = props
.get("SecretId")
.and_then(|v| v.as_str())
.ok_or("SecretId is required")?
.to_string();
let target_type = props
.get("TargetType")
.and_then(|v| v.as_str())
.ok_or("TargetType is required")?;
let target_id = props
.get("TargetId")
.and_then(|v| v.as_str())
.ok_or("TargetId is required")?;
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let secret_arn = if state.secrets.contains_key(&secret_id) {
secret_id.clone()
} else {
let candidate = format!(
"arn:aws:secretsmanager:{}:{}:secret:{}",
state.region, state.account_id, secret_id
);
if state.secrets.contains_key(&candidate) {
candidate
} else {
return Err(format!("Secret {secret_id} not yet provisioned"));
}
};
let secret = state
.secrets
.get_mut(&secret_arn)
.ok_or_else(|| format!("Secret {secret_arn} not found"))?;
let now = Utc::now();
if secret.current_version_id.is_none() {
let version_id = Uuid::new_v4().to_string();
secret.versions.insert(
version_id.clone(),
SecretVersion {
version_id: version_id.clone(),
secret_string: Some("{}".to_string()),
secret_binary: None,
stages: vec!["AWSCURRENT".to_string()],
created_at: now,
},
);
secret.current_version_id = Some(version_id);
}
if let Some(version_id) = secret.current_version_id.clone() {
if let Some(version) = secret.versions.get_mut(&version_id) {
let mut existing: serde_json::Value = version
.secret_string
.as_deref()
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or_else(|| serde_json::json!({}));
if let Some(obj) = existing.as_object_mut() {
let engine = match target_type {
"AWS::RDS::DBInstance" | "AWS::RDS::DBCluster" => "postgres",
_ => "unknown",
};
obj.entry("engine".to_string())
.or_insert(serde_json::json!(engine));
obj.insert("host".to_string(), serde_json::json!(target_id));
obj.entry("dbInstanceIdentifier".to_string())
.or_insert(serde_json::json!(target_id));
}
version.secret_string = Some(existing.to_string());
}
}
secret.last_changed_at = now;
Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
}
}