use std::io::Read;
use boatramp_core::access::{BasicAuth, RateLimit};
use clap::Subcommand;
use crate::client;
use crate::config::ProjectConfig;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("empty password")]
EmptyPassword,
#[error("rps must be > 0")]
RpsZero,
#[error(transparent)]
Client(#[from] crate::client::ClientError),
#[error(transparent)]
Io(#[from] std::io::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, clap::Args)]
pub struct AccessArgs {
#[arg(long, env = "BOATRAMP_SERVER", global = true)]
server: Option<String>,
#[arg(long, env = "BOATRAMP_SITE", global = true)]
site: Option<String>,
#[command(subcommand)]
command: AccessCommand,
}
#[derive(Debug, Subcommand)]
enum AccessCommand {
Show,
BasicAuth {
#[command(subcommand)]
command: BasicAuthCommand,
},
Ip {
#[command(subcommand)]
command: IpCommand,
},
RateLimit {
#[command(subcommand)]
command: RateLimitCommand,
},
TrustedProxy {
#[command(subcommand)]
command: TrustedProxyCommand,
},
}
#[derive(Debug, Subcommand)]
enum BasicAuthCommand {
Add {
user: String,
#[arg(long)]
password: Option<String>,
#[arg(long)]
realm: Option<String>,
},
Rm {
user: String,
},
Clear,
}
#[derive(Debug, Subcommand)]
enum IpCommand {
Allow {
cidr: String,
},
Deny {
cidr: String,
},
Clear,
}
#[derive(Debug, Subcommand)]
enum RateLimitCommand {
Set {
rps: u32,
#[arg(long)]
burst: Option<u32>,
},
Off,
}
#[derive(Debug, Subcommand)]
enum TrustedProxyCommand {
Add {
cidr: String,
},
Clear,
}
pub async fn run(args: AccessArgs, config: &ProjectConfig) -> Result<()> {
let (server, site) = client::resolve_target(args.server, args.site, config)?;
let cp = client::ControlPlane::new(
server,
client::http_client(client::token(config).as_deref()),
client::resolve_project(config),
);
let mut site_config = cp.fetch_site_config(&site).await?;
let access = &mut site_config.access;
match args.command {
AccessCommand::Show => {
print_access(&site_config.access);
return Ok(());
}
AccessCommand::BasicAuth { command } => match command {
BasicAuthCommand::Add {
user,
password,
realm,
} => {
let password = match password {
Some(p) => p,
None => read_stdin_line()?,
};
if password.is_empty() {
return Err(Error::EmptyPassword);
}
let hash = boatramp_core::access::hash_password(&password);
let basic = access.basic_auth.get_or_insert_with(|| BasicAuth {
realm: "Restricted".to_string(),
users: Default::default(),
});
if let Some(realm) = realm {
basic.realm = realm;
}
basic.users.insert(user.clone(), hash);
println!("added basic-auth user {user} to {site}");
}
BasicAuthCommand::Rm { user } => {
if let Some(basic) = &mut access.basic_auth {
basic.users.remove(&user);
if basic.users.is_empty() {
access.basic_auth = None;
}
}
println!("removed basic-auth user {user} from {site}");
}
BasicAuthCommand::Clear => {
access.basic_auth = None;
println!("disabled basic auth for {site}");
}
},
AccessCommand::Ip { command } => match command {
IpCommand::Allow { cidr } => {
push_unique(&mut access.ip.allow, &cidr);
println!("allow {cidr} on {site}");
}
IpCommand::Deny { cidr } => {
push_unique(&mut access.ip.deny, &cidr);
println!("deny {cidr} on {site}");
}
IpCommand::Clear => {
access.ip.allow.clear();
access.ip.deny.clear();
println!("cleared IP rules for {site}");
}
},
AccessCommand::RateLimit { command } => match command {
RateLimitCommand::Set { rps, burst } => {
if rps == 0 {
return Err(Error::RpsZero);
}
access.rate_limit = Some(RateLimit {
rps,
burst: burst.unwrap_or(0),
});
println!(
"rate limit {rps} req/s (burst {}) on {site}",
burst.unwrap_or(rps)
);
}
RateLimitCommand::Off => {
access.rate_limit = None;
println!("disabled rate limiting for {site}");
}
},
AccessCommand::TrustedProxy { command } => match command {
TrustedProxyCommand::Add { cidr } => {
push_unique(&mut access.trusted_proxies, &cidr);
println!("trust proxy {cidr} on {site}");
}
TrustedProxyCommand::Clear => {
access.trusted_proxies.clear();
println!("cleared trusted proxies for {site}");
}
},
}
cp.put_site_config(&site, &site_config).await?;
Ok(())
}
fn push_unique(list: &mut Vec<String>, value: &str) {
if !list.iter().any(|existing| existing == value) {
list.push(value.to_string());
}
}
fn read_stdin_line() -> Result<String> {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
Ok(buf.trim().to_string())
}
fn print_access(access: &boatramp_core::access::AccessConfig) {
if !access.is_enforced() && access.trusted_proxies.is_empty() {
println!("no access control configured");
return;
}
if let Some(basic) = &access.basic_auth {
let users: Vec<&str> = basic.users.keys().map(String::as_str).collect();
println!(
"basic-auth realm \"{}\", users: {}",
basic.realm,
users.join(", ")
);
}
if !access.ip.allow.is_empty() {
println!("ip allow {}", access.ip.allow.join(", "));
}
if !access.ip.deny.is_empty() {
println!("ip deny {}", access.ip.deny.join(", "));
}
if let Some(rl) = &access.rate_limit {
println!(
"rate limit {} req/s, burst {}",
rl.rps,
rl.burst_capacity()
);
}
if !access.trusted_proxies.is_empty() {
println!("trusted px {}", access.trusted_proxies.join(", "));
}
}