use crate::error::Result;
use async_trait::async_trait;
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration;
pub mod aws_sts;
pub mod azure_token;
pub mod cloudflare;
pub mod command;
pub mod gcp_iam;
pub mod github_app;
pub mod vault;
#[derive(Debug, Clone)]
pub struct Lease {
pub credentials: IndexMap<String, String>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub lease_id: String,
}
#[async_trait]
pub trait LeaseBackend: Send + Sync {
async fn create_lease(&self, duration: Duration, label: &str) -> Result<Lease>;
async fn revoke_lease(
&self,
_lease_id: &str,
_credentials: Option<&IndexMap<String, String>>,
) -> Result<()> {
Ok(())
}
fn max_lease_duration(&self) -> Duration;
}
fn default_gcp_scopes() -> Vec<String> {
vec!["https://www.googleapis.com/auth/cloud-platform".to_string()]
}
fn default_command_timeout() -> String {
"30s".to_string()
}
fn default_gcp_env_var() -> String {
"CLOUDSDK_AUTH_ACCESS_TOKEN".to_string()
}
fn default_vault_method() -> String {
"get".to_string()
}
fn default_azure_env_var() -> String {
"AZURE_ACCESS_TOKEN".to_string()
}
fn default_cloudflare_env_var() -> String {
"CLOUDFLARE_API_TOKEN".to_string()
}
fn default_github_env_var() -> String {
"GITHUB_TOKEN".to_string()
}
pub fn generate_lease_id(prefix: &str) -> String {
let suffix: u64 = rand::random();
format!("{prefix}-{suffix:016x}")
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum LeaseBackendConfig {
AwsSts {
region: String,
#[serde(skip_serializing_if = "Option::is_none")]
profile: Option<String>,
role_arn: String,
#[serde(skip_serializing_if = "Option::is_none")]
endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<String>,
},
GcpIam {
service_account_email: String,
#[serde(default = "default_gcp_scopes")]
scopes: Vec<String>,
#[serde(default = "default_gcp_env_var")]
env_var: String,
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<String>,
},
Vault {
#[serde(skip_serializing_if = "Option::is_none")]
address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<String>,
secret_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
namespace: Option<String>,
env_map: IndexMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<String>,
#[serde(default = "default_vault_method")]
method: String,
},
AzureToken {
scope: String,
#[serde(default = "default_azure_env_var")]
env_var: String,
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<String>,
},
Cloudflare {
#[serde(default)]
token_type: cloudflare::CloudflareTokenType,
#[serde(skip_serializing_if = "Option::is_none")]
account_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
policies: Option<Vec<cloudflare::CloudflarePolicy>>,
#[serde(default = "default_cloudflare_env_var")]
env_var: String,
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<String>,
},
GithubApp {
app_id: String,
installation_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
private_key_file: Option<String>,
#[serde(default = "default_github_env_var")]
env_var: String,
#[serde(skip_serializing_if = "Option::is_none")]
permissions: Option<IndexMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
repositories: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
api_base: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<String>,
},
Command {
create_command: String,
#[serde(skip_serializing_if = "Option::is_none")]
revoke_command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<String>,
#[serde(default = "default_command_timeout")]
timeout: String,
},
}
impl LeaseBackendConfig {
pub fn check_prerequisites(&self) -> Option<String> {
match self {
LeaseBackendConfig::AwsSts { profile, .. } => aws_sts::check_prerequisites(profile),
LeaseBackendConfig::GcpIam { .. } => gcp_iam::check_prerequisites(),
LeaseBackendConfig::Vault { address, token, .. } => {
vault::check_prerequisites(address, token)
}
LeaseBackendConfig::AzureToken { .. } => azure_token::check_prerequisites(),
LeaseBackendConfig::Cloudflare { .. } => cloudflare::check_prerequisites(),
LeaseBackendConfig::GithubApp {
private_key_file, ..
} => github_app::check_prerequisites(private_key_file),
LeaseBackendConfig::Command { .. } => command::check_prerequisites(),
}
}
pub fn required_env_vars(&self) -> Vec<(&'static str, &'static str)> {
match self {
LeaseBackendConfig::AwsSts { .. } => aws_sts::required_env_vars(),
LeaseBackendConfig::GcpIam { .. } => gcp_iam::required_env_vars(),
LeaseBackendConfig::Vault { address, token, .. } => {
vault::required_env_vars(address, token)
}
LeaseBackendConfig::AzureToken { .. } => azure_token::required_env_vars(),
LeaseBackendConfig::Cloudflare { .. } => cloudflare::required_env_vars(),
LeaseBackendConfig::GithubApp { .. } => github_app::required_env_vars(),
LeaseBackendConfig::Command { .. } => command::required_env_vars(),
}
}
pub fn produces_env_var(&self, key: &str) -> bool {
match self {
LeaseBackendConfig::AwsSts { .. } => aws_sts::PRODUCED_ENV_VARS.contains(&key),
LeaseBackendConfig::GcpIam { env_var, .. } => env_var == key,
LeaseBackendConfig::Vault { env_map, .. } => env_map.values().any(|v| v == key),
LeaseBackendConfig::AzureToken { env_var, .. } => env_var == key,
LeaseBackendConfig::Command { .. } => false,
LeaseBackendConfig::Cloudflare { env_var, .. } => env_var == key,
LeaseBackendConfig::GithubApp { env_var, .. } => env_var == key,
}
}
pub fn consumed_env_vars(&self) -> &'static [&'static str] {
match self {
LeaseBackendConfig::AwsSts { .. } => aws_sts::CONSUMED_ENV_VARS,
LeaseBackendConfig::GcpIam { .. } => gcp_iam::CONSUMED_ENV_VARS,
LeaseBackendConfig::Vault { .. } => vault::CONSUMED_ENV_VARS,
LeaseBackendConfig::AzureToken { .. } => azure_token::CONSUMED_ENV_VARS,
LeaseBackendConfig::Command { .. } => command::CONSUMED_ENV_VARS,
LeaseBackendConfig::Cloudflare { .. } => cloudflare::CONSUMED_ENV_VARS,
LeaseBackendConfig::GithubApp { .. } => github_app::CONSUMED_ENV_VARS,
}
}
pub fn create_backend(&self) -> Result<Box<dyn LeaseBackend>> {
match self {
LeaseBackendConfig::AwsSts {
region,
profile,
role_arn,
endpoint,
..
} => Ok(Box::new(aws_sts::AwsStsBackend::new(
region.clone(),
profile.clone(),
role_arn.clone(),
endpoint.clone(),
))),
LeaseBackendConfig::GcpIam {
service_account_email,
scopes,
env_var,
..
} => Ok(Box::new(gcp_iam::GcpIamBackend::new(
service_account_email.clone(),
scopes.clone(),
env_var.clone(),
))),
LeaseBackendConfig::Vault {
address,
token,
secret_path,
namespace,
env_map,
method,
..
} => Ok(Box::new(vault::VaultBackend::new(
address.clone(),
token.clone(),
secret_path.clone(),
namespace.clone(),
env_map.clone(),
method.clone(),
)?)),
LeaseBackendConfig::AzureToken { scope, env_var, .. } => Ok(Box::new(
azure_token::AzureTokenBackend::new(scope.clone(), env_var.clone()),
)),
LeaseBackendConfig::Cloudflare {
token_type,
account_id,
policies,
env_var,
..
} => Ok(Box::new(cloudflare::CloudflareBackend::new(
token_type.clone(),
account_id.clone(),
policies.clone(),
env_var.clone(),
)?)),
LeaseBackendConfig::GithubApp {
app_id,
installation_id,
private_key_file,
env_var,
permissions,
repositories,
api_base,
..
} => Ok(Box::new(github_app::GitHubAppBackend::new(
app_id.clone(),
installation_id.clone(),
private_key_file.clone(),
env_var.clone(),
permissions.clone(),
repositories.clone(),
api_base.clone(),
))),
LeaseBackendConfig::Command {
create_command,
revoke_command,
timeout,
..
} => {
let timeout = crate::lease::parse_duration(timeout)?;
Ok(Box::new(command::CommandBackend::new(
create_command.clone(),
revoke_command.clone(),
timeout,
)))
}
}
}
pub fn config_hash(&self) -> String {
let mut serialized =
serde_json::to_value(self).expect("LeaseBackendConfig serialization should never fail");
if let Some(obj) = serialized.as_object_mut() {
obj.remove("duration");
obj.remove("timeout");
}
let json = serde_json::to_string(&serialized)
.expect("LeaseBackendConfig serialization should never fail");
let hash = blake3::hash(json.as_bytes());
hash.to_hex()[..16].to_string()
}
pub fn duration(&self) -> Option<&str> {
match self {
LeaseBackendConfig::AwsSts { duration, .. }
| LeaseBackendConfig::GcpIam { duration, .. }
| LeaseBackendConfig::Vault { duration, .. }
| LeaseBackendConfig::AzureToken { duration, .. }
| LeaseBackendConfig::Cloudflare { duration, .. }
| LeaseBackendConfig::GithubApp { duration, .. }
| LeaseBackendConfig::Command { duration, .. } => duration.as_deref(),
}
}
}