use crate::operator_cli::OperatorCommand;
use bucketwarden_auth::OperatorRole;
use bucketwarden_policy::PolicySimulationRequest;
use bucketwarden_server::{BucketWarden, RuntimeConfig};
use serde_json::json;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolicyToolArgs {
pub principal: String,
pub action: String,
pub resource: String,
pub dry_run: bool,
}
pub fn parse_policy_command(args: &[String]) -> Result<OperatorCommand, String> {
match args.first().map(String::as_str) {
Some("explain") => Ok(OperatorCommand::PolicyExplain(parse_policy_tool_args(
&args[1..],
false,
false,
)?)),
Some("simulate") => Ok(OperatorCommand::PolicySimulate(parse_policy_tool_args(
&args[1..],
true,
true,
)?)),
Some("analyze") => Ok(OperatorCommand::PolicyAnalyze),
Some(other) => Err(format!(
"unknown policy command `{other}`; expected explain, simulate, or analyze"
)),
None => Err("missing policy command; expected explain, simulate, or analyze".to_string()),
}
}
pub fn parse_auth_command(args: &[String]) -> Result<OperatorCommand, String> {
match args {
[family, kind] if family == "report" && kind == "identity-providers" => {
Ok(OperatorCommand::AuthReportIdentityProviders)
}
[family, kind] if family == "report" && kind == "credentials" => {
Ok(OperatorCommand::AuthReportCredentials)
}
[family, kind] if family == "report" && kind == "temporary-credentials" => {
Ok(OperatorCommand::AuthReportTemporaryCredentials)
}
[family, command, rest @ ..] if family == "role" && command == "assign" => {
let values = parse_flag_values(
rest,
&["--principal", "--role", "--scope"],
"auth role assign",
)?;
let principal = take_required(&values, "--principal")?;
let role = parse_role(&take_required(&values, "--role")?)?;
let scope = take_required(&values, "--scope")?;
Ok(OperatorCommand::AuthRoleAssign {
principal,
role,
scope,
})
}
[family, command, rest @ ..] if family == "role" && command == "list" => {
let values = parse_flag_values(rest, &["--principal"], "auth role list")?;
Ok(OperatorCommand::AuthRoleList {
principal: take_required(&values, "--principal")?,
})
}
[family, command, rest @ ..] if family == "key" && command == "rotate" => {
let values = parse_flag_values(
rest,
&["--principal", "--old", "--new", "--secret"],
"auth key rotate",
)?;
Ok(OperatorCommand::AuthKeyRotate {
principal: take_required(&values, "--principal")?,
old_access_key_id: take_required(&values, "--old")?,
new_access_key_id: take_required(&values, "--new")?,
secret_access_key: take_required(&values, "--secret")?,
})
}
[family, command, rest @ ..] if family == "key" && command == "revoke" => {
let values = parse_flag_values(rest, &["--principal", "--key"], "auth key revoke")?;
Ok(OperatorCommand::AuthKeyRevoke {
principal: take_required(&values, "--principal")?,
access_key_id: take_required(&values, "--key")?,
})
}
[family, command, rest @ ..] if family == "key" && command == "report-leaked" => {
let values =
parse_flag_values(rest, &["--principal", "--key"], "auth key report-leaked")?;
Ok(OperatorCommand::AuthKeyReportLeaked {
principal: take_required(&values, "--principal")?,
access_key_id: take_required(&values, "--key")?,
})
}
[family, command, ..] => Err(format!(
"unknown auth command `{family} {command}`; expected report identity-providers/credentials/temporary-credentials, role assign/list, or key rotate/revoke/report-leaked"
)),
_ => Err("missing auth command; expected report identity-providers/credentials/temporary-credentials, role assign/list, or key rotate/revoke/report-leaked".to_string()),
}
}
pub fn policy_explain_json(args: &PolicyToolArgs) -> anyhow::Result<String> {
let runtime = seeded_policy_runtime()?;
Ok(serde_json::to_string(&runtime.policy_explanation(
&args.principal,
&args.action,
&args.resource,
))?)
}
pub fn policy_simulate_json(args: &PolicyToolArgs) -> anyhow::Result<String> {
let runtime = seeded_policy_runtime()?;
Ok(serde_json::to_string(&runtime.simulate_policy(
PolicySimulationRequest {
principal: args.principal.clone(),
action: args.action.clone(),
resource: args.resource.clone(),
dry_run: args.dry_run,
..PolicySimulationRequest::default()
},
))?)
}
pub fn policy_analyze_json() -> anyhow::Result<String> {
let mut runtime = BucketWarden::new(RuntimeConfig::development())?;
runtime.allow("*", "s3:*", "*");
Ok(serde_json::to_string(&runtime.analyze_policy())?)
}
pub fn auth_role_assign_json(
principal: &str,
role: OperatorRole,
scope: &str,
) -> anyhow::Result<String> {
let mut runtime = BucketWarden::new(RuntimeConfig::development())?;
runtime.create_local_user("operator");
runtime.create_local_user(principal);
let assignment = runtime.assign_operator_role("operator", principal, role, scope)?;
Ok(serde_json::to_string(&assignment)?)
}
pub fn auth_report_identity_providers_json() -> anyhow::Result<String> {
let runtime = BucketWarden::new(RuntimeConfig::development())?;
Ok(serde_json::to_string(
&runtime.identity_provider_support_report(),
)?)
}
pub fn auth_report_credentials_json() -> anyhow::Result<String> {
let runtime = BucketWarden::new(RuntimeConfig::development())?;
Ok(serde_json::to_string(&runtime.credential_support_report())?)
}
pub fn auth_report_temporary_credentials_json() -> anyhow::Result<String> {
let runtime = BucketWarden::new(RuntimeConfig::development())?;
Ok(serde_json::to_string(
&runtime.temporary_credential_support_report(),
)?)
}
pub fn auth_role_list_json(principal: &str) -> anyhow::Result<String> {
let mut runtime = BucketWarden::new(RuntimeConfig::development())?;
runtime.create_local_user("operator");
runtime.create_local_user(principal);
runtime.assign_operator_role("operator", principal, OperatorRole::ReadOnlyOperator, "*")?;
Ok(serde_json::to_string(
&runtime.operator_role_assignments(principal),
)?)
}
pub fn auth_key_rotate_json(
principal: &str,
old_access_key_id: &str,
new_access_key_id: &str,
secret_access_key: &str,
) -> anyhow::Result<String> {
let mut runtime = BucketWarden::new(RuntimeConfig::development())?;
runtime.create_local_user(principal);
runtime.create_access_key(principal, old_access_key_id, "old-secret")?;
let rotation = runtime.rotate_access_key(
"operator",
old_access_key_id,
new_access_key_id,
secret_access_key,
)?;
Ok(serde_json::to_string(&rotation)?)
}
pub fn auth_key_revoke_json(principal: &str, access_key_id: &str) -> anyhow::Result<String> {
let mut runtime = BucketWarden::new(RuntimeConfig::development())?;
runtime.create_local_user(principal);
runtime.create_access_key(principal, access_key_id, "secret")?;
runtime.revoke_credential(access_key_id)?;
Ok(serde_json::to_string(&json!({
"access_key_id": access_key_id,
"principal_id": principal,
"revoked": true
}))?)
}
pub fn auth_key_report_leaked_json(principal: &str, access_key_id: &str) -> anyhow::Result<String> {
let mut runtime = BucketWarden::new(RuntimeConfig::development())?;
runtime.create_local_user(principal);
runtime.create_access_key(principal, access_key_id, "secret")?;
let response = runtime.report_leaked_access_key("operator", access_key_id)?;
Ok(serde_json::to_string(&response)?)
}
fn seeded_policy_runtime() -> anyhow::Result<BucketWarden> {
let mut runtime = BucketWarden::new(RuntimeConfig::development())?;
runtime.allow("alice", "s3:GetObject", "archive/*");
runtime.deny("alice", "s3:GetObject", "archive/private/*");
runtime.allow("operator", "s3:*", "*");
Ok(runtime)
}
fn parse_policy_tool_args(
args: &[String],
default_dry_run: bool,
allow_dry_run: bool,
) -> Result<PolicyToolArgs, String> {
let mut principal = None;
let mut action = None;
let mut resource = None;
let mut dry_run = default_dry_run;
let mut index = 0;
while index < args.len() {
match args[index].as_str() {
"--principal" => {
index += 1;
principal = Some(required_value(args, index, "--principal")?);
}
"--action" => {
index += 1;
action = Some(required_value(args, index, "--action")?);
}
"--resource" => {
index += 1;
resource = Some(required_value(args, index, "--resource")?);
}
"--dry-run" if allow_dry_run => {
dry_run = true;
}
"--dry-run" => return Err("unknown policy explain flag `--dry-run`".to_string()),
other => return Err(format!("unknown policy flag `{other}`")),
}
index += 1;
}
Ok(PolicyToolArgs {
principal: principal.ok_or_else(|| "missing required `--principal <value>`".to_string())?,
action: action.ok_or_else(|| "missing required `--action <value>`".to_string())?,
resource: resource.ok_or_else(|| "missing required `--resource <value>`".to_string())?,
dry_run,
})
}
fn parse_flag_values<'a>(
args: &[String],
allowed_flags: &'a [&'a str],
command_name: &str,
) -> Result<Vec<(&'a str, String)>, String> {
let mut values = Vec::new();
let mut index = 0;
while index < args.len() {
let flag = args[index].as_str();
let Some(&known_flag) = allowed_flags.iter().find(|known| **known == flag) else {
return Err(format!("unknown {command_name} flag `{flag}`"));
};
index += 1;
values.push((known_flag, required_value(args, index, known_flag)?));
index += 1;
}
Ok(values)
}
fn take_required(values: &[(&str, String)], flag: &str) -> Result<String, String> {
values
.iter()
.find(|(known_flag, _)| *known_flag == flag)
.map(|(_, value)| value.clone())
.ok_or_else(|| format!("missing required `{flag} <value>`"))
}
fn required_value(args: &[String], index: usize, flag: &str) -> Result<String, String> {
let Some(value) = args.get(index) else {
return Err(format!("missing value for `{flag}`"));
};
if value.starts_with("--") {
return Err(format!("missing value for `{flag}`"));
}
Ok(value.clone())
}
fn parse_role(value: &str) -> Result<OperatorRole, String> {
match value {
"cluster-admin" => Ok(OperatorRole::ClusterAdmin),
"tenant-admin" => Ok(OperatorRole::TenantAdmin),
"bucket-admin" => Ok(OperatorRole::BucketAdmin),
"auditor" => Ok(OperatorRole::Auditor),
"read-only-operator" => Ok(OperatorRole::ReadOnlyOperator),
"security-officer" => Ok(OperatorRole::SecurityOfficer),
other => Err(format!(
"unknown operator role `{other}`; expected cluster-admin, tenant-admin, bucket-admin, auditor, read-only-operator, or security-officer"
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_report_json_surfaces_existing_support_contracts() {
let identity = auth_report_identity_providers_json().expect("identity providers");
let credentials = auth_report_credentials_json().expect("credentials");
let temporary = auth_report_temporary_credentials_json().expect("temporary credentials");
assert!(identity.contains("oidc"));
assert!(identity.contains("assume_role_with_web_identity"));
assert!(credentials.contains("rotate_access_key"));
assert!(credentials.contains("temporary-credentials"));
assert!(temporary.contains("session scope is explicit and non-expanding"));
assert!(temporary.contains("assume_role_with_web_identity"));
}
}