use crate::Result;
use bytes::Bytes;
use clap::{Subcommand, ValueEnum};
use rand::{RngCore as _, rng};
use rsasl::mechanisms::scram::tools::hash_password;
use sha2::{Sha256, Sha512, digest::generic_array::GenericArray};
use tansu_client::{Client, ConnectionManager};
use tansu_sans_io::{
AlterUserScramCredentialsRequest, ErrorCode, ScramMechanism,
alter_user_scram_credentials_request::{ScramCredentialDeletion, ScramCredentialUpsertion},
};
use tracing::debug;
use url::Url;
use super::DEFAULT_BROKER;
#[derive(Clone, Debug, Subcommand)]
pub(super) enum Command {
Create {
#[arg(long, default_value = DEFAULT_BROKER)]
broker: Url,
name: String,
password: String,
#[arg(long, default_value = "8192")]
iterations: Option<u32>,
#[arg(long, value_enum, default_value = "scram512")]
mechanism: Mechanism,
},
Delete {
#[arg(long, default_value = DEFAULT_BROKER)]
broker: Url,
name: String,
#[arg(long, value_enum, default_value = "scram512")]
mechanism: Mechanism,
},
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, ValueEnum)]
pub(super) enum Mechanism {
Scram256,
#[default]
Scram512,
}
impl Mechanism {
fn salted_password(&self, password: &[u8], iterations: u32, salt: &[u8]) -> Bytes {
match self {
Self::Scram256 => {
let mut salted_password = GenericArray::default();
hash_password::<Sha256>(password, iterations, salt, &mut salted_password);
Bytes::copy_from_slice(salted_password.as_slice())
}
Self::Scram512 => {
let mut salted_password = GenericArray::default();
hash_password::<Sha512>(password, iterations, salt, &mut salted_password);
Bytes::copy_from_slice(salted_password.as_slice())
}
}
}
}
impl From<&Mechanism> for i8 {
fn from(value: &Mechanism) -> Self {
match value {
Mechanism::Scram256 => ScramMechanism::Scram256.into(),
Mechanism::Scram512 => ScramMechanism::Scram512.into(),
}
}
}
impl Command {
fn broker(&self) -> Url {
match self {
Command::Create { broker, .. } => broker.to_owned(),
Command::Delete { broker, .. } => broker.to_owned(),
}
}
fn upsertions(&self) -> Option<Vec<ScramCredentialUpsertion>> {
match self {
Command::Create {
name,
password,
iterations,
mechanism,
..
} => {
const DEFAULT_SALT_LEN: usize = 32;
const DEFAULT_ITERATIONS: u32 = 2u32.pow(14);
let mut salt = [0u8; DEFAULT_SALT_LEN];
rng().fill_bytes(&mut salt);
let iterations = iterations.unwrap_or(DEFAULT_ITERATIONS);
let salted_password =
mechanism.salted_password(password.as_bytes(), iterations, &salt[..]);
Some(
[ScramCredentialUpsertion::default()
.name(name.into())
.mechanism(mechanism.into())
.iterations(iterations as i32)
.salt(Bytes::copy_from_slice(&salt[..]))
.salted_password(salted_password)]
.into(),
)
}
_ => Some([].into()),
}
}
fn deletions(&self) -> Option<Vec<ScramCredentialDeletion>> {
match self {
Command::Create { .. } => Some([].into()),
Command::Delete {
name,
mechanism: mode,
..
} => Some(
[ScramCredentialDeletion::default()
.name(name.into())
.mechanism(mode.into())]
.into(),
),
}
}
pub(super) async fn main(self) -> Result<ErrorCode> {
let client = ConnectionManager::builder(self.broker())
.client_id(Some(env!("CARGO_PKG_NAME").into()))
.build()
.await
.inspect(|pool| debug!(?pool))
.map(Client::new)?;
let req = AlterUserScramCredentialsRequest::default()
.deletions(self.deletions())
.upsertions(self.upsertions());
_ = client
.call(req)
.await
.inspect(|response| debug!(?response))?;
Ok(ErrorCode::None)
}
}