use core::fmt;
use std::string::String;
use cloud_sdk::authentication::{AuthenticationScope, ScopeValue, ScopeValueError};
use cloud_sdk::{ProviderId, ServiceId};
use super::{BearerToken, HttpsEndpoint};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BearerCredentialScopeError {
ValueRejected(ScopeValueError),
AllocationFailed,
EndpointIdentityRejected,
}
impl fmt::Display for BearerCredentialScopeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::ValueRejected(_) => "bearer credential scope value was rejected",
Self::AllocationFailed => "bearer credential scope allocation failed",
Self::EndpointIdentityRejected => "bearer credential endpoint identity was rejected",
})
}
}
impl core::error::Error for BearerCredentialScopeError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::ValueRejected(error) => Some(error),
Self::AllocationFailed => None,
Self::EndpointIdentityRejected => None,
}
}
}
pub struct BearerCredentialScope {
provider: ProviderId,
service: ServiceId,
endpoint: HttpsEndpoint,
audience: Option<String>,
account: Option<String>,
tenant: Option<String>,
}
impl BearerCredentialScope {
#[must_use]
pub const fn new(provider: ProviderId, service: ServiceId, endpoint: HttpsEndpoint) -> Self {
Self {
provider,
service,
endpoint,
audience: None,
account: None,
tenant: None,
}
}
pub fn try_with_audience(mut self, value: &str) -> Result<Self, BearerCredentialScopeError> {
self.audience = Some(copy_scope_value(value)?);
Ok(self)
}
pub fn try_with_account(mut self, value: &str) -> Result<Self, BearerCredentialScopeError> {
self.account = Some(copy_scope_value(value)?);
Ok(self)
}
pub fn try_with_tenant(mut self, value: &str) -> Result<Self, BearerCredentialScopeError> {
self.tenant = Some(copy_scope_value(value)?);
Ok(self)
}
pub(crate) fn borrowed(&self) -> Result<AuthenticationScope<'_>, BearerCredentialScopeError> {
self.borrowed_with_endpoint(self.endpoint_identity()?)
}
pub(crate) fn borrowed_with_endpoint<'a>(
&'a self,
endpoint: cloud_sdk::transport::EndpointIdentity<'a>,
) -> Result<AuthenticationScope<'a>, BearerCredentialScopeError> {
let mut scope = AuthenticationScope::unscoped()
.with_provider(self.provider)
.with_service(self.service)
.with_endpoint(endpoint);
if let Some(value) = self.audience.as_deref() {
scope = scope.with_audience(
ScopeValue::new(value).map_err(BearerCredentialScopeError::ValueRejected)?,
);
}
if let Some(value) = self.account.as_deref() {
scope = scope.with_account(
ScopeValue::new(value).map_err(BearerCredentialScopeError::ValueRejected)?,
);
}
if let Some(value) = self.tenant.as_deref() {
scope = scope.with_tenant(
ScopeValue::new(value).map_err(BearerCredentialScopeError::ValueRejected)?,
);
}
Ok(scope)
}
pub(crate) const fn provider(&self) -> ProviderId {
self.provider
}
pub(crate) const fn service(&self) -> ServiceId {
self.service
}
pub(crate) fn endpoint_identity(
&self,
) -> Result<cloud_sdk::transport::EndpointIdentity<'_>, BearerCredentialScopeError> {
self.endpoint
.identity()
.map_err(|_| BearerCredentialScopeError::EndpointIdentityRejected)
}
pub(crate) fn matches_endpoint(&self, endpoint: &HttpsEndpoint) -> bool {
self.endpoint_identity()
.ok()
.zip(endpoint.identity().ok())
.is_some_and(|(credential, configured)| credential == configured)
}
}
impl fmt::Debug for BearerCredentialScope {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BearerCredentialScope")
.field("provider", &self.provider)
.field("service", &self.service)
.field("endpoint", &"[redacted]")
.field("audience", &self.audience.as_ref().map(|_| "[redacted]"))
.field("account", &self.account.as_ref().map(|_| "[redacted]"))
.field("tenant", &self.tenant.as_ref().map(|_| "[redacted]"))
.finish()
}
}
pub struct BearerCredential {
pub(crate) token: BearerToken,
pub(crate) scope: BearerCredentialScope,
}
impl BearerCredential {
#[must_use]
pub const fn new(token: BearerToken, scope: BearerCredentialScope) -> Self {
Self { token, scope }
}
}
impl fmt::Debug for BearerCredential {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("BearerCredential([redacted])")
}
}
fn copy_scope_value(value: &str) -> Result<String, BearerCredentialScopeError> {
ScopeValue::new(value).map_err(BearerCredentialScopeError::ValueRejected)?;
let mut owned = String::new();
owned
.try_reserve_exact(value.len())
.map_err(|_| BearerCredentialScopeError::AllocationFailed)?;
owned.push_str(value);
Ok(owned)
}
#[cfg(test)]
mod tests {
use cloud_sdk::transport::CustomEndpointAcknowledgement;
use cloud_sdk::{ProviderId, ServiceId};
use super::{BearerCredentialScope, BearerCredentialScopeError, ScopeValueError};
use crate::shared::HttpsEndpoint;
#[test]
fn owned_scope_validates_values_and_redacts_all_provider_fields() {
let provider = ProviderId::new("example").unwrap_or_else(|_| unreachable!());
let service = ServiceId::new("compute").unwrap_or_else(|_| unreachable!());
let endpoint = HttpsEndpoint::new_custom(
"https://api.example.test/v1",
CustomEndpointAcknowledgement::trusted_operator_configuration(),
)
.unwrap_or_else(|_| unreachable!());
let scope = BearerCredentialScope::new(provider, service, endpoint)
.try_with_audience("secret-audience")
.and_then(|scope| scope.try_with_account("secret-account"))
.and_then(|scope| scope.try_with_tenant("secret-tenant"));
assert!(scope.is_ok());
if let Ok(scope) = scope {
let debug = std::format!("{scope:?}");
assert!(debug.contains("[redacted]"));
assert!(!debug.contains("secret-audience"));
assert!(!debug.contains("secret-account"));
assert!(!debug.contains("secret-tenant"));
assert!(scope.borrowed().is_ok());
}
}
#[test]
fn invalid_scope_value_does_not_create_partial_owned_scope() {
let provider = ProviderId::new("example").unwrap_or_else(|_| unreachable!());
let service = ServiceId::new("compute").unwrap_or_else(|_| unreachable!());
let endpoint = HttpsEndpoint::new_custom(
"https://api.example.test/v1",
CustomEndpointAcknowledgement::trusted_operator_configuration(),
)
.unwrap_or_else(|_| unreachable!());
let result = BearerCredentialScope::new(provider, service, endpoint)
.try_with_audience("contains space");
assert!(matches!(
result,
Err(BearerCredentialScopeError::ValueRejected(
ScopeValueError::InvalidByte
))
));
}
}