- Specify production-ready implementation details for sal-git package. - Add a detailed code review and quality assurance process section. - Include comprehensive success metrics and validation checklists for production readiness. - Improve security considerations and risk mitigation strategies. - Add stricter code review criteria based on sal-git's conversion. - Update README with security configurations and environment variables.
421 lines
14 KiB
Rust
421 lines
14 KiB
Rust
use redis::Cmd;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::error::Error;
|
|
use std::fmt;
|
|
use std::process::{Command, Output};
|
|
|
|
// Simple redis client functionality with configurable connection
|
|
fn execute_redis_command(cmd: &mut redis::Cmd) -> redis::RedisResult<String> {
|
|
// Get Redis URL from environment variables with fallback
|
|
let redis_url = get_redis_url();
|
|
log::debug!("Connecting to Redis at: {}", mask_redis_url(&redis_url));
|
|
|
|
let client = redis::Client::open(redis_url)?;
|
|
let mut con = client.get_connection()?;
|
|
cmd.query(&mut con)
|
|
}
|
|
|
|
/// Get Redis URL from environment variables with secure fallbacks
|
|
fn get_redis_url() -> String {
|
|
std::env::var("REDIS_URL")
|
|
.or_else(|_| std::env::var("SAL_REDIS_URL"))
|
|
.unwrap_or_else(|_| "redis://127.0.0.1/".to_string())
|
|
}
|
|
|
|
/// Mask sensitive information in Redis URL for logging
|
|
fn mask_redis_url(url: &str) -> String {
|
|
if let Ok(parsed) = url::Url::parse(url) {
|
|
if parsed.password().is_some() {
|
|
format!(
|
|
"{}://{}:***@{}:{}/{}",
|
|
parsed.scheme(),
|
|
parsed.username(),
|
|
parsed.host_str().unwrap_or("unknown"),
|
|
parsed.port().unwrap_or(6379),
|
|
parsed.path().trim_start_matches('/')
|
|
)
|
|
} else {
|
|
url.to_string()
|
|
}
|
|
} else {
|
|
"redis://***masked***".to_string()
|
|
}
|
|
}
|
|
|
|
// Define a custom error type for GitExecutor operations
|
|
#[derive(Debug)]
|
|
pub enum GitExecutorError {
|
|
GitCommandFailed(String),
|
|
CommandExecutionError(std::io::Error),
|
|
RedisError(redis::RedisError),
|
|
JsonError(serde_json::Error),
|
|
AuthenticationError(String),
|
|
SshAgentNotLoaded,
|
|
InvalidAuthConfig(String),
|
|
}
|
|
|
|
// Implement Display for GitExecutorError
|
|
impl fmt::Display for GitExecutorError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
GitExecutorError::GitCommandFailed(e) => write!(f, "Git command failed: {}", e),
|
|
GitExecutorError::CommandExecutionError(e) => {
|
|
write!(f, "Command execution error: {}", e)
|
|
}
|
|
GitExecutorError::RedisError(e) => write!(f, "Redis error: {}", e),
|
|
GitExecutorError::JsonError(e) => write!(f, "JSON error: {}", e),
|
|
GitExecutorError::AuthenticationError(e) => write!(f, "Authentication error: {}", e),
|
|
GitExecutorError::SshAgentNotLoaded => write!(f, "SSH agent is not loaded"),
|
|
GitExecutorError::InvalidAuthConfig(e) => {
|
|
write!(f, "Invalid authentication configuration: {}", e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Implement Error trait for GitExecutorError
|
|
impl Error for GitExecutorError {
|
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
match self {
|
|
GitExecutorError::CommandExecutionError(e) => Some(e),
|
|
GitExecutorError::RedisError(e) => Some(e),
|
|
GitExecutorError::JsonError(e) => Some(e),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
// From implementations for error conversion
|
|
impl From<redis::RedisError> for GitExecutorError {
|
|
fn from(err: redis::RedisError) -> Self {
|
|
GitExecutorError::RedisError(err)
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Error> for GitExecutorError {
|
|
fn from(err: serde_json::Error) -> Self {
|
|
GitExecutorError::JsonError(err)
|
|
}
|
|
}
|
|
|
|
impl From<std::io::Error> for GitExecutorError {
|
|
fn from(err: std::io::Error) -> Self {
|
|
GitExecutorError::CommandExecutionError(err)
|
|
}
|
|
}
|
|
|
|
// Status enum for GitConfig
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
pub enum GitConfigStatus {
|
|
#[serde(rename = "error")]
|
|
Error,
|
|
#[serde(rename = "ok")]
|
|
Ok,
|
|
}
|
|
|
|
// Auth configuration for a specific git server
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct GitServerAuth {
|
|
pub sshagent: Option<bool>,
|
|
pub key: Option<String>,
|
|
pub username: Option<String>,
|
|
pub password: Option<String>,
|
|
}
|
|
|
|
// Main configuration structure from Redis
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct GitConfig {
|
|
pub status: GitConfigStatus,
|
|
pub auth: HashMap<String, GitServerAuth>,
|
|
}
|
|
|
|
// GitExecutor struct
|
|
pub struct GitExecutor {
|
|
config: Option<GitConfig>,
|
|
}
|
|
|
|
impl GitExecutor {
|
|
// Create a new GitExecutor
|
|
pub fn new() -> Self {
|
|
GitExecutor { config: None }
|
|
}
|
|
|
|
// Initialize by loading configuration from Redis
|
|
pub fn init(&mut self) -> Result<(), GitExecutorError> {
|
|
// Try to load config from Redis
|
|
match self.load_config_from_redis() {
|
|
Ok(config) => {
|
|
self.config = Some(config);
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
// If Redis error, we'll proceed without config
|
|
// This is not a fatal error as we might use default git behavior
|
|
log::warn!("Failed to load git config from Redis: {}", e);
|
|
self.config = None;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load configuration from Redis
|
|
fn load_config_from_redis(&self) -> Result<GitConfig, GitExecutorError> {
|
|
// Create Redis command to get the herocontext:git key
|
|
let mut cmd = Cmd::new();
|
|
cmd.arg("GET").arg("herocontext:git");
|
|
|
|
// Execute the command
|
|
let result: redis::RedisResult<String> = execute_redis_command(&mut cmd);
|
|
|
|
match result {
|
|
Ok(json_str) => {
|
|
// Parse the JSON string into GitConfig
|
|
let config: GitConfig = serde_json::from_str(&json_str)?;
|
|
|
|
// Validate the config
|
|
if config.status == GitConfigStatus::Error {
|
|
return Err(GitExecutorError::InvalidAuthConfig(
|
|
"Config status is error".to_string(),
|
|
));
|
|
}
|
|
|
|
Ok(config)
|
|
}
|
|
Err(e) => Err(GitExecutorError::RedisError(e)),
|
|
}
|
|
}
|
|
|
|
// Check if SSH agent is loaded
|
|
fn is_ssh_agent_loaded(&self) -> bool {
|
|
let output = Command::new("ssh-add").arg("-l").output();
|
|
|
|
match output {
|
|
Ok(output) => output.status.success() && !output.stdout.is_empty(),
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
// Get authentication configuration for a git URL
|
|
fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
|
|
if let Some(config) = &self.config {
|
|
let (server, _, _) = crate::parse_git_url(url);
|
|
if !server.is_empty() {
|
|
return config.auth.get(&server);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
// Validate authentication configuration
|
|
fn validate_auth_config(&self, auth: &GitServerAuth) -> Result<(), GitExecutorError> {
|
|
// Rule: If sshagent is true, other fields should be empty
|
|
if let Some(true) = auth.sshagent {
|
|
if auth.key.is_some() || auth.username.is_some() || auth.password.is_some() {
|
|
return Err(GitExecutorError::InvalidAuthConfig(
|
|
"When sshagent is true, key, username, and password must be empty".to_string(),
|
|
));
|
|
}
|
|
// Check if SSH agent is actually loaded
|
|
if !self.is_ssh_agent_loaded() {
|
|
return Err(GitExecutorError::SshAgentNotLoaded);
|
|
}
|
|
}
|
|
|
|
// Rule: If key is set, other fields should be empty
|
|
if let Some(_) = &auth.key {
|
|
if auth.sshagent.unwrap_or(false) || auth.username.is_some() || auth.password.is_some()
|
|
{
|
|
return Err(GitExecutorError::InvalidAuthConfig(
|
|
"When key is set, sshagent, username, and password must be empty".to_string(),
|
|
));
|
|
}
|
|
}
|
|
|
|
// Rule: If username is set, password should be set and other fields empty
|
|
if let Some(_) = &auth.username {
|
|
if auth.sshagent.unwrap_or(false) || auth.key.is_some() {
|
|
return Err(GitExecutorError::InvalidAuthConfig(
|
|
"When username is set, sshagent and key must be empty".to_string(),
|
|
));
|
|
}
|
|
if auth.password.is_none() {
|
|
return Err(GitExecutorError::InvalidAuthConfig(
|
|
"When username is set, password must also be set".to_string(),
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Execute a git command with authentication
|
|
pub fn execute(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
|
|
// Extract the git URL if this is a command that needs authentication
|
|
let url_arg = self.extract_git_url_from_args(args);
|
|
|
|
// If we have a URL and authentication config, use it
|
|
if let Some(url) = url_arg {
|
|
if let Some(auth) = self.get_auth_for_url(&url) {
|
|
// Validate the authentication configuration
|
|
self.validate_auth_config(auth)?;
|
|
|
|
// Execute with the appropriate authentication method
|
|
return self.execute_with_auth(args, auth);
|
|
}
|
|
}
|
|
|
|
// No special authentication needed, execute normally
|
|
self.execute_git_command(args)
|
|
}
|
|
|
|
// Extract git URL from command arguments
|
|
fn extract_git_url_from_args<'a>(&self, args: &[&'a str]) -> Option<&'a str> {
|
|
// Commands that might contain a git URL
|
|
if args.contains(&"clone")
|
|
|| args.contains(&"fetch")
|
|
|| args.contains(&"pull")
|
|
|| args.contains(&"push")
|
|
{
|
|
// The URL is typically the last argument for clone, or after remote for others
|
|
for (i, &arg) in args.iter().enumerate() {
|
|
if arg == "clone" && i + 1 < args.len() {
|
|
return Some(args[i + 1]);
|
|
}
|
|
if (arg == "fetch" || arg == "pull" || arg == "push") && i + 1 < args.len() {
|
|
// For these commands, the URL might be specified as a remote name
|
|
// We'd need more complex logic to resolve remote names to URLs
|
|
// For now, we'll just return None
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
// Execute git command with authentication
|
|
fn execute_with_auth(
|
|
&self,
|
|
args: &[&str],
|
|
auth: &GitServerAuth,
|
|
) -> Result<Output, GitExecutorError> {
|
|
// Handle different authentication methods
|
|
if let Some(true) = auth.sshagent {
|
|
// Use SSH agent (already validated that it's loaded)
|
|
self.execute_git_command(args)
|
|
} else if let Some(key) = &auth.key {
|
|
// Use SSH key
|
|
self.execute_with_ssh_key(args, key)
|
|
} else if let Some(username) = &auth.username {
|
|
// Use username/password
|
|
if let Some(password) = &auth.password {
|
|
self.execute_with_credentials(args, username, password)
|
|
} else {
|
|
// This should never happen due to validation
|
|
Err(GitExecutorError::AuthenticationError(
|
|
"Password is required when username is set".to_string(),
|
|
))
|
|
}
|
|
} else {
|
|
// No authentication method specified, use default
|
|
self.execute_git_command(args)
|
|
}
|
|
}
|
|
|
|
// Execute git command with SSH key
|
|
fn execute_with_ssh_key(&self, args: &[&str], key: &str) -> Result<Output, GitExecutorError> {
|
|
// Create a command with GIT_SSH_COMMAND to specify the key
|
|
let ssh_command = format!("ssh -i {} -o IdentitiesOnly=yes", key);
|
|
|
|
let mut command = Command::new("git");
|
|
command.env("GIT_SSH_COMMAND", ssh_command);
|
|
command.args(args);
|
|
|
|
let output = command.output()?;
|
|
|
|
if output.status.success() {
|
|
Ok(output)
|
|
} else {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
Err(GitExecutorError::GitCommandFailed(error.to_string()))
|
|
}
|
|
}
|
|
|
|
// Execute git command with username/password using secure credential helper
|
|
fn execute_with_credentials(
|
|
&self,
|
|
args: &[&str],
|
|
username: &str,
|
|
password: &str,
|
|
) -> Result<Output, GitExecutorError> {
|
|
// Use git credential helper approach for security
|
|
// Create a temporary credential helper script
|
|
let temp_dir = std::env::temp_dir();
|
|
let helper_script = temp_dir.join(format!("git_helper_{}", std::process::id()));
|
|
|
|
// Create credential helper script content
|
|
let script_content = format!(
|
|
"#!/bin/bash\necho username={}\necho password={}\n",
|
|
username, password
|
|
);
|
|
|
|
// Write the helper script
|
|
std::fs::write(&helper_script, script_content)
|
|
.map_err(|e| GitExecutorError::CommandExecutionError(e))?;
|
|
|
|
// Make it executable
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mut perms = std::fs::metadata(&helper_script)
|
|
.map_err(|e| GitExecutorError::CommandExecutionError(e))?
|
|
.permissions();
|
|
perms.set_mode(0o755);
|
|
std::fs::set_permissions(&helper_script, perms)
|
|
.map_err(|e| GitExecutorError::CommandExecutionError(e))?;
|
|
}
|
|
|
|
// Execute git command with credential helper
|
|
let mut command = Command::new("git");
|
|
command.args(args);
|
|
command.env("GIT_ASKPASS", &helper_script);
|
|
command.env("GIT_TERMINAL_PROMPT", "0"); // Disable terminal prompts
|
|
|
|
log::debug!("Executing git command with credential helper");
|
|
let output = command.output()?;
|
|
|
|
// Clean up the temporary helper script
|
|
let _ = std::fs::remove_file(&helper_script);
|
|
|
|
if output.status.success() {
|
|
Ok(output)
|
|
} else {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
log::error!("Git command failed: {}", error);
|
|
Err(GitExecutorError::GitCommandFailed(error.to_string()))
|
|
}
|
|
}
|
|
|
|
// Basic git command execution
|
|
fn execute_git_command(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
|
|
let mut command = Command::new("git");
|
|
command.args(args);
|
|
|
|
let output = command.output()?;
|
|
|
|
if output.status.success() {
|
|
Ok(output)
|
|
} else {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
Err(GitExecutorError::GitCommandFailed(error.to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Implement Default for GitExecutor
|
|
impl Default for GitExecutor {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|