Skip to content

adding aws secrets and dba features#12

Merged
kelleyblackmore merged 5 commits into
mainfrom
aws-db
Nov 14, 2025
Merged

adding aws secrets and dba features#12
kelleyblackmore merged 5 commits into
mainfrom
aws-db

Conversation

@kelleyblackmore
Copy link
Copy Markdown
Owner

these changes are for folder structure update to include how we have targets and providers.
also adding integration of AWS secrets and postgres database for a target

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/rotation.rs
Comment on lines 114 to 118
// Write updated secret
vault
.write_secret(mount, path, new_data)
backend
.write_secret(path, new_data)
.await
.context("Failed to write rotated secret")?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid writing secret before target update

The rotated value is saved to the secret backend before any target password change is attempted. If update_password or the subsequent verification fails, the function returns an error but the secret store already contains the new value while the target system still uses the old password, leaving clients with unusable credentials. Consider updating/verifying the target first (or rolling back the secret) so that failures don’t produce a split‑brain state.

Useful? React with 👍 / 👎.

Comment thread src/backends/aws_secrets.rs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds significant new functionality to the secret rotation tool by introducing multi-backend support (AWS Secrets Manager alongside HashiCorp Vault) and target-based password rotation (PostgreSQL databases and REST APIs). The changes involve a major refactoring to support abstraction layers for both secret backends and rotation targets.

Key Changes:

  • Added AWS Secrets Manager as an alternative backend to Vault
  • Introduced target abstraction for rotating passwords in databases and APIs
  • Refactored codebase into modular backend and target systems
  • Updated CLI to support new backend and target options

Reviewed Changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/backends/mod.rs New module defining backend abstractions and exports
src/backends/secret_backend.rs Core trait for secret management backends
src/backends/vault.rs Vault backend implementation with wrapper
src/backends/aws_secrets.rs AWS Secrets Manager backend implementation
src/targets/mod.rs New module for password update targets
src/targets/target.rs Core trait for password rotation targets
src/targets/postgres.rs PostgreSQL database target implementation
src/targets/api.rs REST API target implementation
src/rotation.rs Updated rotation logic to support backends and targets
src/config.rs Extended configuration for multiple backends and targets
src/cli.rs New CLI module with command execution logic
src/main.rs Simplified main entry point delegating to CLI module
src/lib.rs Library interface with public exports
src/env_updater.rs Minor improvements to environment variable handling
Cargo.toml Added AWS SDK and PostgreSQL dependencies
README.md Comprehensive documentation updates with examples
examples/config.toml Example configuration with new options

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/targets/postgres.rs
Comment thread src/cli.rs Outdated
Comment on lines +536 to +542
// Try to find password key
secret
.data
.values()
.next()
.cloned()
.ok_or_else(|| anyhow::anyhow!("No password found in secret at {}", password_path))?
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The read_secret method in AWS backend uses .values().next() to get the first password value when password_path is provided, but doesn't specify which key it expects. This could be ambiguous if the secret contains multiple keys.

Consider documenting the expected key name (e.g., "password") or allow configuration to specify which key to use for the admin password.

Suggested change
// Try to find password key
secret
.data
.values()
.next()
.cloned()
.ok_or_else(|| anyhow::anyhow!("No password found in secret at {}", password_path))?
// Use password_key if provided, otherwise default to "password"
let password_key = config.password_key.as_deref().unwrap_or("password");
secret
.data
.get(password_key)
.cloned()
.ok_or_else(|| anyhow::anyhow!(
"No password found in secret at {} with key '{}'. \
Please ensure the secret contains a '{}' field or set password_key in config.",
password_path, password_key, password_key
))?

Copilot uses AI. Check for mistakes.
Comment thread src/targets/postgres.rs
Comment on lines +40 to +43
let _connection_handle = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("PostgreSQL connection error: {}", e);
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Connection errors are printed to stderr using eprintln! instead of being logged through the tracing framework. This is inconsistent with the rest of the codebase which uses tracing::error! and similar macros.

Change to:

if let Err(e) = connection.await {
    tracing::error!("PostgreSQL connection error: {}", e);
}

Copilot uses AI. Check for mistakes.
Comment thread src/targets/api.rs Outdated
Comment on lines +119 to +124
async fn verify_connection(&self, _username: &str, _password: &str, _database: Option<&str>) -> Result<()> {
// API targets may not support verification, or it could be done via a separate endpoint
// For now, we'll skip verification for API targets
info!("Verification not supported for API targets");
Ok(())
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API target's verify_connection method always returns Ok(()) without actually verifying anything. This creates a false sense of security since callers expect this to validate the password update succeeded.

Consider either:

  1. Implementing actual verification (e.g., calling a verification endpoint if the API supports it)
  2. Removing this from the trait if not all targets can support it
  3. Documenting clearly that verification is not supported for API targets and may fail silently

Copilot uses AI. Check for mistakes.
Comment thread src/rotation.rs
Comment on lines +138 to 146
let mut metadata = match backend.read_metadata(path).await {
Ok(existing) => existing,
Err(e) => {
warn!(
"Failed to read existing metadata for {}/{}: {}. Proceeding with defaults.",
mount, path, e
"Failed to read existing metadata for {}: {}. Proceeding with defaults.",
path, e
);
HashMap::new()
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metadata reading logic has changed from returning existing.custom_metadata.unwrap_or_default() to just existing. However, the return type remains HashMap<String, String> but read_metadata now returns this directly. This looks correct, but the comment on line 141-143 still refers to the old behavior mentioning "custom_metadata".

Update the comment to reflect the new behavior:

let mut metadata = match backend.read_metadata(path).await {
    Ok(existing) => existing,
    Err(e) => {
        warn!(
            "Failed to read existing metadata for {}: {}. Proceeding with defaults.",
            path, e
        );
        HashMap::new()
    }
};

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +40
.filter_map(|tag| {
tag.key()
.and_then(|k| tag.value().map(|v| (k.to_string(), v.to_string())))
})
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Missing error handling for the case when .tags() returns tags but some tags have missing keys or values. The .filter_map() will silently skip these, which could lead to incomplete metadata without any indication to the user.

Suggested change
.filter_map(|tag| {
tag.key()
.and_then(|k| tag.value().map(|v| (k.to_string(), v.to_string())))
})
.map(|tag| {
match (tag.key(), tag.value()) {
(Some(k), Some(v)) => Some((k.to_string(), v.to_string())),
(key, value) => {
// Log a warning if key or value is missing
tracing::warn!(
"Skipping AWS tag with missing key or value: key={:?}, value={:?}",
key,
value
);
None
}
}
})
.filter_map(|x| x)

Copilot uses AI. Check for mistakes.
Comment thread src/config.rs
Comment on lines +110 to +140
fn default_api_method() -> String {
"POST".to_string()
}

fn default_password_field() -> String {
"password".to_string()
}

fn default_api_timeout() -> u64 {
30
}

fn default_db_port() -> u16 {
5432
}

fn default_ssl_mode() -> String {
"prefer".to_string()
}

fn default_backend() -> String {
"vault".to_string()
}

fn default_mount() -> String {
"secret".to_string()
}

fn default_aws_region() -> String {
"us-east-1".to_string()
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The default functions for configuration fields are defined but not consistently used. default_mount() returns "secret" but the VaultConfig mount field doesn't use #[serde(default = "default_mount")], it's only added later. This inconsistency could lead to confusion about which defaults are actually applied.

Copilot uses AI. Check for mistakes.
Comment thread src/targets/postgres.rs
Comment on lines +133 to +134
// Close the test connection by dropping it
drop(test_connection);
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test connection is created but never properly closed. The test_connection variable is dropped without being awaited, which means the connection might not be gracefully terminated. This could lead to lingering connections or resource leaks.

Consider awaiting the connection to ensure it's properly closed:

drop(test_client);
// Ensure connection is properly closed
let _ = test_connection.await;
Suggested change
// Close the test connection by dropping it
drop(test_connection);
// Ensure the test connection is properly closed by awaiting it
let _ = test_connection.await;

Copilot uses AI. Check for mistakes.
Comment thread src/targets/postgres.rs Outdated
Comment on lines +109 to +118
async fn verify_connection(&self, username: &str, password: &str, database: Option<&str>) -> Result<()> {
info!("Verifying connection for user: {}", username);

let db_name = database.unwrap_or(&self.config.database);
let connection_string = Self::build_connection_string(
&self.config.host,
self.config.port,
username,
password,
db_name,
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The database parameter in verify_connection is marked as Option<&str> but the function always uses either the provided value or falls back to self.config.database. This suggests the parameter should be required rather than optional, or the function signature could be simplified.

Consider either:

  1. Making it non-optional if it's always expected
  2. Documenting when and why callers would pass None vs Some(db_name)
Suggested change
async fn verify_connection(&self, username: &str, password: &str, database: Option<&str>) -> Result<()> {
info!("Verifying connection for user: {}", username);
let db_name = database.unwrap_or(&self.config.database);
let connection_string = Self::build_connection_string(
&self.config.host,
self.config.port,
username,
password,
db_name,
async fn verify_connection(&self, username: &str, password: &str, database: &str) -> Result<()> {
info!("Verifying connection for user: {}", username);
let connection_string = Self::build_connection_string(
&self.config.host,
self.config.port,
username,
password,
database,

Copilot uses AI. Check for mistakes.
Comment thread src/config.rs
#[serde(default)]
pub targets: Option<TargetsConfig>,
}

Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration has a confusing mix of optional backend configs. The vault field is now Option<VaultConfig> and aws is Option<AwsConfig>, but there's no validation to ensure at least one backend is configured when backend is set to a specific value. A user could set backend = "vault" without providing vault configuration, leading to a runtime error.

Consider adding a validation method that ensures the selected backend has its configuration present.

Suggested change
impl Config {
/// Validates that the selected backend has its configuration present.
pub fn validate_backends(&self) -> Result<()> {
match self.backend.as_str() {
"vault" => {
if self.vault.is_none() {
anyhow::bail!("Backend 'vault' selected but no vault configuration provided.");
}
}
"aws" => {
if self.aws.is_none() {
anyhow::bail!("Backend 'aws' selected but no AWS configuration provided.");
}
}
_ => {
// If other backends are supported, add checks here.
}
}
Ok(())
}
}

Copilot uses AI. Check for mistakes.
@kelleyblackmore kelleyblackmore merged commit 563670c into main Nov 14, 2025
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants