use reqsign_core::SigningCredential;
use reqsign_core::time::Timestamp;
use std::fmt::{Debug, Formatter};
use std::time::Duration;
const REDACTED: &str = "REDACTED";
#[derive(Clone)]
pub enum Credential {
SharedKey {
account_name: String,
account_key: String,
},
SasToken {
token: String,
expires_at: Option<Timestamp>,
},
BearerToken {
token: String,
expires_in: Option<Timestamp>,
},
}
impl Debug for Credential {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Credential::SharedKey { .. } => f
.debug_struct("Credential::SharedKey")
.field("account_name", &REDACTED)
.field("account_key", &REDACTED)
.finish(),
Credential::SasToken { expires_at, .. } => f
.debug_struct("Credential::SasToken")
.field("token", &REDACTED)
.field("expires_at", expires_at)
.finish(),
Credential::BearerToken { expires_in, .. } => f
.debug_struct("Credential::BearerToken")
.field("token", &REDACTED)
.field("expires_in", expires_in)
.finish(),
}
}
}
impl SigningCredential for Credential {
fn is_valid(&self) -> bool {
self.is_valid_at(Timestamp::now() + Duration::from_secs(20))
}
fn is_valid_at(&self, timestamp: Timestamp) -> bool {
match self {
Credential::SharedKey {
account_name,
account_key,
} => !account_name.is_empty() && !account_key.is_empty(),
Credential::SasToken { token, expires_at } => {
!token.is_empty() && expires_at.is_none_or(|expires| expires > timestamp)
}
Credential::BearerToken { token, expires_in } => {
if token.is_empty() {
return false;
}
expires_in.is_none_or(|expires| expires > timestamp)
}
}
}
}
impl Credential {
pub fn with_shared_key(account_name: &str, account_key: &str) -> Self {
Self::SharedKey {
account_name: account_name.to_string(),
account_key: account_key.to_string(),
}
}
pub fn with_sas_token(sas_token: &str) -> Self {
Self::SasToken {
token: sas_token.to_string(),
expires_at: None,
}
}
pub fn with_sas_token_expires_at(sas_token: &str, expires_at: Timestamp) -> Self {
Self::SasToken {
token: sas_token.to_string(),
expires_at: Some(expires_at),
}
}
pub fn with_bearer_token(bearer_token: &str, expires_in: Option<Timestamp>) -> Self {
Self::BearerToken {
token: bearer_token.to_string(),
expires_in,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn separates_bearer_cache_freshness_from_exact_validity() {
let now = Timestamp::now();
let credential =
Credential::with_bearer_token("token", Some(now + Duration::from_secs(10)));
assert!(!credential.is_valid());
assert!(credential.is_valid_at(now + Duration::from_secs(5)));
assert!(!credential.is_valid_at(now + Duration::from_secs(10)));
}
#[test]
fn separates_sas_cache_freshness_from_exact_validity() {
let now = Timestamp::now();
let credential = Credential::with_sas_token_expires_at(
"sv=2020-12-06&sig=secret",
now + Duration::from_secs(10),
);
assert!(!credential.is_valid());
assert!(credential.is_valid_at(now + Duration::from_secs(5)));
assert!(!credential.is_valid_at(now + Duration::from_secs(10)));
}
#[test]
fn debug_fully_redacts_credential_material() {
let debug = [
format!(
"{:?}",
Credential::with_shared_key(
"accountprefixmiddlesuffixaccount",
"keyprefix-middle-keysuffix",
)
),
format!(
"{:?}",
Credential::with_sas_token("sv=2020-12-06&sig=sasprefix-middle-sassignaturesuffix",)
),
format!(
"{:?}",
Credential::with_bearer_token("bearerprefix-middle-bearersuffix", None,)
),
]
.join("\n");
assert!(debug.contains("Credential::SharedKey"));
assert!(debug.contains("Credential::SasToken"));
assert!(debug.contains("Credential::BearerToken"));
assert!(debug.contains(REDACTED));
for fragment in [
"accountprefix",
"suffixaccount",
"keyprefix",
"keysuffix",
"sasprefix",
"sassignaturesuffix",
"bearerprefix",
"bearersuffix",
] {
assert!(!debug.contains(fragment));
}
}
}