use crate::app_config;
use crate::error::cli::{CliError, MultiCliError};
use crab_vault::auth::{HttpMethod, Jwt, JwtDecoder, JwtEncoder, Permission};
use chrono::Duration;
use clap::error::ErrorKind;
use clap::{Args, Subcommand};
use jsonwebtoken::Header;
use rand::random_range;
use std::io::{self, Read};
#[derive(Args)]
pub struct JwtCommandAndArgs {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Clone)]
pub enum Command {
#[command(name = "generate")]
Generate(GenerateArgs),
#[command(name = "verify")]
Verify,
}
#[derive(Args, Clone)]
pub struct GenerateArgs {
#[arg(long, default_value_t = 0)]
pub nbf_offset: i64,
#[arg(long, default_value_t = 3600)]
pub exp_offset: i64,
#[arg(long)]
pub issue_as: Option<String>,
#[arg(long, value_delimiter = ',')]
pub audiences: Option<Vec<String>>,
#[arg(long, value_delimiter = ',', default_value = "all")]
pub operations: Vec<HttpMethod>,
#[arg(long, default_value = "*")]
pub resource_pattern: String,
#[arg(long)]
pub max_size: Option<usize>,
#[arg(long, value_delimiter = ',', default_value = "*")]
pub allowed_content_type: Vec<String>,
}
pub fn exec(cmd: Command) {
match cmd {
Command::Generate(args) => generate_jwt(args),
Command::Verify => verify_jwt(),
}
.map_err(|e| e.exit_now())
.unwrap()
}
fn generate_jwt(args: GenerateArgs) -> Result<(), CliError> {
let jwt_encoder_config = app_config::server().auth().encoder();
let jwt_encoder: JwtEncoder = jwt_encoder_config
.clone()
.try_into()
.map_err(MultiCliError::exit_now)
.unwrap();
let iss = if args.issue_as.is_some() {
args.issue_as.unwrap()
} else {
jwt_encoder_config.issue_as().to_string()
};
let aud = if args.audiences.is_some() {
args.audiences.unwrap()
} else {
jwt_encoder_config.audience().to_vec()
};
let payload = Permission::new_minimum()
.permit_method(args.operations)
.permit_resource_pattern(args.resource_pattern)
.restrict_maximum_size_option(args.max_size)
.permit_content_type(args.allowed_content_type);
let claims = Jwt::new(iss, &aud, payload)
.expires_in(Duration::seconds(args.exp_offset))
.not_valid_in(Duration::seconds(args.nbf_offset));
let mut header = {
let algorithms = jwt_encoder_config.algorithms();
let random_idx = random_range(0..algorithms.len());
Header::new(algorithms[random_idx])
};
header.kid = Some({
let kids = jwt_encoder_config.kids();
let random_idx = random_range(0..kids.len());
kids[random_idx].clone()
});
let token = jwt_encoder
.encode(&header, &claims, header.kid.as_ref().unwrap())
.map_err(|e| CliError::new(ErrorKind::Io, format!("JWT encoding failed: {e}"), None))?;
println!("{}", token);
Ok(())
}
fn verify_jwt() -> Result<(), CliError> {
let mut token = String::new();
io::stdin().read_to_string(&mut token).map_err(|e| {
CliError::new(
ErrorKind::Io,
format!("Nothing to read from standard input as token input: {e}"),
None,
)
})?;
let token = token.trim();
if token.is_empty() {
return Err(CliError::new(
ErrorKind::Io,
"No token received from standard input.".to_string(),
None,
));
}
let jwt_decoder: JwtDecoder = app_config::server()
.auth()
.decoder()
.clone()
.try_into()
.map_err(MultiCliError::exit_now)
.unwrap();
let decoded = JwtDecoder::decode_unchecked(token).map_err(CliError::from)?;
let pretty_json = serde_json::to_string_pretty(&decoded).map_err(CliError::from)?;
match jwt_decoder.decode::<Permission>(token) {
Ok(_) => eprintln!("Token verified successfully. Payload (Claims):\n"),
Err(e) => eprintln!("Token invalid because of {e}. Payload (Claims):\n"),
}
println!("{}", pretty_json);
Ok(())
}