use std::fmt;
use soaprs_core::{SoapError, SoapResult};
use crate::AuthorizationName;
#[derive(Clone, PartialEq, Eq)]
pub struct SecretString(String);
impl SecretString {
pub fn new(value: impl Into<String>) -> SoapResult<Self> {
let value = value.into();
if value.is_empty() {
return Err(SoapError::validation("credential secret cannot be empty"));
}
Ok(Self(value))
}
pub fn expose_secret(&self) -> &str {
&self.0
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SecretString([REDACTED])")
}
}
impl fmt::Display for SecretString {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("[REDACTED]")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CredentialKind {
Bearer,
Password,
ApiKey,
Session,
Custom(AuthorizationName),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Credential {
strategy: AuthorizationName,
kind: CredentialKind,
identifier: Option<String>,
secret: SecretString,
}
impl Credential {
pub fn bearer(strategy: impl Into<String>, token: impl Into<String>) -> SoapResult<Self> {
Self::new(strategy, CredentialKind::Bearer, None, token)
}
pub fn api_key(
strategy: impl Into<String>,
identifier: Option<String>,
key: impl Into<String>,
) -> SoapResult<Self> {
Self::new(strategy, CredentialKind::ApiKey, identifier, key)
}
pub fn password(
strategy: impl Into<String>,
username: impl Into<String>,
password: impl Into<String>,
) -> SoapResult<Self> {
Self::new(
strategy,
CredentialKind::Password,
Some(username.into()),
password,
)
}
pub fn session(strategy: impl Into<String>, session_id: impl Into<String>) -> SoapResult<Self> {
Self::new(strategy, CredentialKind::Session, None, session_id)
}
pub fn custom(
strategy: impl Into<String>,
kind: impl Into<String>,
identifier: Option<String>,
secret: impl Into<String>,
) -> SoapResult<Self> {
Self::new(
strategy,
CredentialKind::Custom(AuthorizationName::new(kind)?),
identifier,
secret,
)
}
fn new(
strategy: impl Into<String>,
kind: CredentialKind,
identifier: Option<String>,
secret: impl Into<String>,
) -> SoapResult<Self> {
if identifier.as_ref().is_some_and(|identifier| {
identifier.is_empty() || identifier.chars().any(char::is_control)
}) {
return Err(SoapError::validation("invalid credential identifier"));
}
Ok(Self {
strategy: AuthorizationName::new(strategy)?,
kind,
identifier,
secret: SecretString::new(secret)?,
})
}
pub fn strategy(&self) -> &AuthorizationName {
&self.strategy
}
pub const fn kind(&self) -> &CredentialKind {
&self.kind
}
pub fn identifier(&self) -> Option<&str> {
self.identifier.as_deref()
}
pub const fn secret(&self) -> &SecretString {
&self.secret
}
}
#[cfg(test)]
mod tests {
use super::Credential;
#[test]
fn credential_diagnostics_never_expose_secrets() {
let Some(credential) = Credential::bearer("jwt", "secret-token").ok() else {
panic!("valid credential");
};
assert!(!format!("{credential:?}").contains("secret-token"));
assert_eq!(credential.secret().to_string(), "[REDACTED]");
assert_eq!(credential.secret().expose_secret(), "secret-token");
}
}