use std::{sync::Mutex, time::Duration};
use azure_core::auth::{Secret, AccessToken};
use super::{
azure_named_key_credential::AzureNamedKeyCredential,
shared_access_signature::{SasSignatureError, SharedAccessSignature},
AzureSasCredential,
};
#[derive(Debug)]
pub struct SharedAccessCredential {
source_key_credential: Option<AzureNamedKeyCredential>,
source_sas_credential: Option<AzureSasCredential>,
shared_access_signature: Mutex<SharedAccessSignature>,
}
impl From<SharedAccessSignature> for SharedAccessCredential {
fn from(shared_access_signature: SharedAccessSignature) -> Self {
Self {
source_key_credential: None,
source_sas_credential: None,
shared_access_signature: Mutex::new(shared_access_signature),
}
}
}
impl SharedAccessCredential {
const SIGNATURE_REFRESH_BUFFER: Duration = Duration::from_secs(10 * 60);
const SIGNATURE_EXTENSION_DURATION: Duration = Duration::from_secs(30 * 60);
pub(crate) fn from_signature(shared_access_signature: SharedAccessSignature) -> Self {
Self {
source_key_credential: None,
source_sas_credential: None,
shared_access_signature: Mutex::new(shared_access_signature),
}
}
pub fn try_from_sas_credential(
source_sas_credential: AzureSasCredential,
) -> Result<Self, SasSignatureError> {
let shared_access_signature =
SharedAccessSignature::try_from_signature(source_sas_credential.signature())?;
Ok(Self {
source_key_credential: None,
source_sas_credential: Some(source_sas_credential),
shared_access_signature: Mutex::new(shared_access_signature),
})
}
pub fn try_from_named_key_credential(
source_credential: AzureNamedKeyCredential,
signature_resource: impl Into<String>,
) -> Result<Self, SasSignatureError> {
let (name, key) = (source_credential.name(), source_credential.key());
let shared_access_signature =
SharedAccessSignature::try_from_parts(signature_resource, name, key, None)?;
Ok(Self {
source_key_credential: Some(source_credential),
source_sas_credential: None,
shared_access_signature: Mutex::new(shared_access_signature),
})
}
}
impl SharedAccessCredential {
pub(crate) async fn get_token(&self, _scopes: &[&str]) -> azure_core::Result<AccessToken> {
let mut signature = self.shared_access_signature.lock().map_err(|error| {
azure_core::Error::new(azure_core::error::ErrorKind::Other, error.to_string())
})?;
if signature.shared_access_key().is_empty() {
match &self.source_sas_credential {
Some(sas_credential) if sas_credential.signature() != signature.value() => {
*signature =
SharedAccessSignature::try_from_signature(sas_credential.signature())?;
}
_ => {}
}
return Ok(AccessToken {
token: Secret::new(signature.value().to_string()),
expires_on: *signature.signature_expiration(),
});
}
if let Some(key_credential) = &self.source_key_credential {
let (name, key) = (key_credential.name(), key_credential.key());
if signature.shared_access_key_name() != name || signature.shared_access_key() != key {
*signature =
SharedAccessSignature::try_from_parts(signature.resource(), name, key, None)?;
}
}
if *signature.signature_expiration()
<= crate::util::time::now_utc() + Self::SIGNATURE_REFRESH_BUFFER
{
signature.update_with_new_expiration(Self::SIGNATURE_EXTENSION_DURATION)?;
}
Ok(AccessToken {
token: Secret::new(signature.value().to_string()),
expires_on: *signature.signature_expiration(),
})
}
}
#[cfg(test)]
mod tests {
use time::Duration as TimeSpan;
use crate::authorization::shared_access_signature::SharedAccessSignature;
use super::{AzureNamedKeyCredential, SharedAccessCredential};
#[tokio::test]
async fn get_token_returns_signature_value_with_key_constsructor_initializes_properties() {
let signature =
SharedAccessSignature::try_from_parts("hub-name", "keyName", "key", None).unwrap();
let source_credential = AzureNamedKeyCredential::new(
signature.shared_access_key_name(),
signature.shared_access_key(),
);
let credential = SharedAccessCredential::try_from_named_key_credential(
source_credential,
signature.resource(),
)
.unwrap();
let token = credential.get_token(&[""]).await.unwrap();
assert_eq!(token.token.secret(), signature.value());
}
#[tokio::test]
async fn get_token_returns_signature_value() {
let signature =
SharedAccessSignature::try_from_parts("hub-name", "keyName", "key", None).unwrap();
let credential = SharedAccessCredential::from_signature(signature.clone());
let token = credential.get_token(&[""]).await.unwrap();
assert_eq!(token.token.secret(), signature.value());
}
#[tokio::test]
async fn get_token_extends_an_expired_token_when_created_with_shared_key() {
let expires_on = crate::util::time::now_utc() - TimeSpan::hours(2);
let signature =
SharedAccessSignature::try_new("hub-name", "keyName", "key", expires_on).unwrap();
let credential = SharedAccessCredential::from_signature(signature);
let expected_expiration =
crate::util::time::now_utc() + SharedAccessCredential::SIGNATURE_EXTENSION_DURATION;
let token = credential.get_token(&[""]).await.unwrap();
assert!(token.expires_on - expected_expiration < TimeSpan::seconds(1));
}
#[tokio::test]
async fn get_token_extends_a_token_close_to_expiring_when_created_with_shared_key() {
let expires_on =
crate::util::time::now_utc() + SharedAccessCredential::SIGNATURE_REFRESH_BUFFER / 2;
let signature =
SharedAccessSignature::try_new("hub-name", "keyName", "key", expires_on).unwrap();
let credential = SharedAccessCredential::from_signature(signature);
let expected_expiration =
crate::util::time::now_utc() + SharedAccessCredential::SIGNATURE_EXTENSION_DURATION;
let token = credential.get_token(&[""]).await.unwrap();
assert!(token.expires_on - expected_expiration < TimeSpan::seconds(1));
}
#[tokio::test]
async fn get_token_does_not_extend_an_expired_token_when_created_without_the_key() {
let expires_on = crate::util::time::now_utc() - TimeSpan::hours(2);
let value = format!("SharedAccessSignature sr=https%3A%2F%2Ffake-test.servicebus.windows.net%2F&sig=nNBNavJfBiHuXUzWOLhSvI3bVgqbQUzA7Po8%2F4wQQng%3D&se={}&skn=fakeKey", expires_on.unix_timestamp());
let source_signature = SharedAccessSignature::try_from_signature(&value).unwrap();
let signature =
SharedAccessSignature::try_from_signature(source_signature.value()).unwrap();
let credential = SharedAccessCredential::from_signature(signature);
let expected_expiration = expires_on;
let token = credential.get_token(&[""]).await.unwrap();
assert!(token.expires_on - expected_expiration < TimeSpan::seconds(1));
}
#[tokio::test]
async fn get_token_does_not_extend_a_token_close_to_expiring_when_created_without_the_key() {
let expires_on =
crate::util::time::now_utc() + SharedAccessCredential::SIGNATURE_REFRESH_BUFFER / 2;
let value = format!("SharedAccessSignature sr=https%3A%2F%2Ffake-test.servicebus.windows.net%2F&sig=nNBNavJfBiHuXUzWOLhSvI3bVgqbQUzA7Po8%2F4wQQng%3D&se={}&skn=fakeKey", expires_on.unix_timestamp());
let source_signature = SharedAccessSignature::try_from_signature(&value).unwrap();
let signature =
SharedAccessSignature::try_from_signature(source_signature.value()).unwrap();
let credential = SharedAccessCredential::from_signature(signature);
let expected_expiration = expires_on;
let token = credential.get_token(&[""]).await.unwrap();
assert!(token.expires_on - expected_expiration < TimeSpan::seconds(1));
}
}