use aws_smithy_types::config_bag::Layer;
use aws_smithy_types::date_time::Format;
use aws_smithy_types::type_erasure::TypeErasedBox;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use zeroize::Zeroizing;
use aws_smithy_runtime_api::client::identity::Identity;
use crate::attributes::AccountId;
use crate::credential_feature::AwsCredentialFeature;
pub struct Credentials(Arc<Inner>, HashMap<TypeId, TypeErasedBox>);
impl Clone for Credentials {
fn clone(&self) -> Self {
let mut new_map = HashMap::with_capacity(self.1.len());
for (k, v) in &self.1 {
new_map.insert(
*k,
v.try_clone()
.expect("values are guaranteed to implement `Clone` via `set_property`"),
);
}
Self(self.0.clone(), new_map)
}
}
impl PartialEq for Credentials {
#[inline] fn eq(&self, other: &Credentials) -> bool {
self.0 == other.0
}
}
impl Eq for Credentials {}
#[derive(Clone, Eq, PartialEq)]
struct Inner {
access_key_id: Zeroizing<String>,
secret_access_key: Zeroizing<String>,
session_token: Zeroizing<Option<String>>,
expires_after: Option<SystemTime>,
account_id: Option<AccountId>,
provider_name: &'static str,
}
impl Debug for Credentials {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut creds = f.debug_struct("Credentials");
creds
.field("provider_name", &self.0.provider_name)
.field("access_key_id", &self.0.access_key_id.as_str())
.field("secret_access_key", &"** redacted **");
if let Some(expiry) = self.expiry() {
if let Some(formatted) = expiry.duration_since(UNIX_EPOCH).ok().and_then(|dur| {
aws_smithy_types::DateTime::from_secs(dur.as_secs() as _)
.fmt(Format::DateTime)
.ok()
}) {
creds.field("expires_after", &formatted);
} else {
creds.field("expires_after", &expiry);
}
} else {
creds.field("expires_after", &"never");
}
if let Some(account_id) = &self.0.account_id {
creds.field("account_id", &account_id.as_str());
}
for (i, prop) in self.1.values().enumerate() {
creds.field(&format!("property_{i}"), prop);
}
creds.finish()
}
}
#[cfg(feature = "hardcoded-credentials")]
const STATIC_CREDENTIALS: &str = "Static";
impl Credentials {
pub fn builder() -> CredentialsBuilder {
CredentialsBuilder::default()
}
pub fn new(
access_key_id: impl Into<String>,
secret_access_key: impl Into<String>,
session_token: Option<String>,
expires_after: Option<SystemTime>,
provider_name: &'static str,
) -> Self {
Credentials(
Arc::new(Inner {
access_key_id: Zeroizing::new(access_key_id.into()),
secret_access_key: Zeroizing::new(secret_access_key.into()),
session_token: Zeroizing::new(session_token),
expires_after,
account_id: None,
provider_name,
}),
HashMap::new(),
)
}
#[cfg(feature = "hardcoded-credentials")]
pub fn from_keys(
access_key_id: impl Into<String>,
secret_access_key: impl Into<String>,
session_token: Option<String>,
) -> Self {
Self::new(
access_key_id,
secret_access_key,
session_token,
None,
STATIC_CREDENTIALS,
)
}
pub fn access_key_id(&self) -> &str {
&self.0.access_key_id
}
pub fn secret_access_key(&self) -> &str {
&self.0.secret_access_key
}
pub fn expiry(&self) -> Option<SystemTime> {
self.0.expires_after
}
pub fn expiry_mut(&mut self) -> &mut Option<SystemTime> {
&mut Arc::make_mut(&mut self.0).expires_after
}
pub fn account_id(&self) -> Option<&AccountId> {
self.0.account_id.as_ref()
}
pub fn session_token(&self) -> Option<&str> {
self.0.session_token.as_deref()
}
#[doc(hidden)]
pub fn set_property<T: Any + Clone + Debug + Send + Sync + 'static>(&mut self, prop: T) {
self.1
.insert(TypeId::of::<T>(), TypeErasedBox::new_with_clone(prop));
}
#[doc(hidden)]
pub fn get_property<T: Any + Debug + Send + Sync + 'static>(&self) -> Option<&T> {
self.1
.get(&TypeId::of::<T>())
.and_then(|b| b.downcast_ref())
}
#[doc(hidden)]
pub fn get_property_mut<T: Any + Debug + Send + Sync + 'static>(&mut self) -> Option<&mut T> {
self.1
.get_mut(&TypeId::of::<T>())
.and_then(|b| b.downcast_mut())
}
#[doc(hidden)]
pub fn get_property_mut_or_default<T: Any + Clone + Debug + Default + Send + Sync + 'static>(
&mut self,
) -> &mut T {
self.1
.entry(TypeId::of::<T>())
.or_insert_with(|| TypeErasedBox::new_with_clone(T::default()))
.downcast_mut()
.expect("typechecked")
}
}
#[derive(Default, Clone)]
#[allow(missing_debug_implementations)] pub struct CredentialsBuilder {
access_key_id: Option<Zeroizing<String>>,
secret_access_key: Option<Zeroizing<String>>,
session_token: Zeroizing<Option<String>>,
expires_after: Option<SystemTime>,
account_id: Option<AccountId>,
provider_name: Option<&'static str>,
}
impl CredentialsBuilder {
pub fn access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
self.access_key_id = Some(Zeroizing::new(access_key_id.into()));
self
}
pub fn secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
self.secret_access_key = Some(Zeroizing::new(secret_access_key.into()));
self
}
pub fn session_token(mut self, session_token: impl Into<String>) -> Self {
self.set_session_token(Some(session_token.into()));
self
}
pub fn set_session_token(&mut self, session_token: Option<String>) {
self.session_token = Zeroizing::new(session_token);
}
pub fn expiry(mut self, expiry: SystemTime) -> Self {
self.set_expiry(Some(expiry));
self
}
pub fn set_expiry(&mut self, expiry: Option<SystemTime>) {
self.expires_after = expiry;
}
pub fn account_id(mut self, account_id: impl Into<AccountId>) -> Self {
self.set_account_id(Some(account_id.into()));
self
}
pub fn set_account_id(&mut self, account_id: Option<AccountId>) {
self.account_id = account_id;
}
pub fn provider_name(mut self, provider_name: &'static str) -> Self {
self.provider_name = Some(provider_name);
self
}
pub fn build(self) -> Credentials {
Credentials(
Arc::new(Inner {
access_key_id: self
.access_key_id
.expect("required field `access_key_id` missing"),
secret_access_key: self
.secret_access_key
.expect("required field `secret_access_key` missing"),
session_token: self.session_token,
expires_after: self.expires_after,
account_id: self.account_id,
provider_name: self
.provider_name
.expect("required field `provider_name` missing"),
}),
HashMap::new(),
)
}
}
#[cfg(feature = "test-util")]
impl Credentials {
pub fn for_tests() -> Self {
Self::new(
"ANOTREAL",
"notrealrnrELgWzOk3IfjzDKtFBhDby",
None,
None,
"test",
)
}
pub fn for_tests_with_session_token() -> Self {
Self::new(
"ANOTREAL",
"notrealrnrELgWzOk3IfjzDKtFBhDby",
Some("notarealsessiontoken".to_string()),
None,
"test",
)
}
}
#[cfg(feature = "test-util")]
impl CredentialsBuilder {
pub fn for_tests() -> Self {
CredentialsBuilder::default()
.access_key_id("ANOTREAL")
.secret_access_key("notrealrnrELgWzOk3IfjzDKtFBhDby")
.provider_name("test")
}
}
impl From<Credentials> for Identity {
fn from(val: Credentials) -> Self {
let expiry = val.expiry();
let mut builder = if let Some(account_id) = val.account_id() {
Identity::builder().property(account_id.clone())
} else {
Identity::builder()
};
builder.set_expiration(expiry);
let features = val.get_property::<Vec<AwsCredentialFeature>>().cloned();
let has_account_id = val.account_id().is_some();
if features.is_some() || has_account_id {
let mut layer = Layer::new("IdentityResolutionFeatureIdTracking");
if let Some(features) = features {
for feat in features {
layer.store_append(feat);
}
}
if has_account_id {
layer.store_append(AwsCredentialFeature::ResolvedAccountId);
}
builder.set_property(layer.freeze());
}
builder.data(val).build().expect("set required fields")
}
}
#[cfg(test)]
mod test {
use crate::Credentials;
use std::time::{Duration, UNIX_EPOCH};
#[test]
fn debug_impl() {
let creds = Credentials::new(
"akid",
"secret",
Some("token".into()),
Some(UNIX_EPOCH + Duration::from_secs(1234567890)),
"debug tester",
);
assert_eq!(
format!("{:?}", creds),
r#"Credentials { provider_name: "debug tester", access_key_id: "akid", secret_access_key: "** redacted **", expires_after: "2009-02-13T23:31:30Z" }"#
);
let creds = Credentials::builder()
.access_key_id("akid")
.secret_access_key("secret")
.session_token("token")
.expiry(UNIX_EPOCH + Duration::from_secs(1234567890))
.account_id("012345678901")
.provider_name("debug tester")
.build();
assert_eq!(
format!("{:?}", creds),
r#"Credentials { provider_name: "debug tester", access_key_id: "akid", secret_access_key: "** redacted **", expires_after: "2009-02-13T23:31:30Z", account_id: "012345678901" }"#
);
}
#[cfg(feature = "test-util")]
#[test]
fn equality_ignores_properties() {
#[derive(Clone, Debug)]
struct Foo;
let mut creds1 = Credentials::for_tests_with_session_token();
creds1.set_property(crate::credential_feature::AwsCredentialFeature::CredentialsCode);
let mut creds2 = Credentials::for_tests_with_session_token();
creds2.set_property(Foo);
assert_eq!(creds1, creds2)
}
#[cfg(feature = "test-util")]
#[test]
fn identity_inherits_feature_properties() {
use crate::credential_feature::AwsCredentialFeature;
use aws_smithy_runtime_api::client::identity::Identity;
use aws_smithy_types::config_bag::FrozenLayer;
let mut creds = Credentials::for_tests_with_session_token();
let mut feature_props = vec![
AwsCredentialFeature::CredentialsCode,
AwsCredentialFeature::CredentialsStsSessionToken,
];
creds.set_property(feature_props.clone());
let identity = Identity::from(creds);
let maybe_props = identity
.property::<FrozenLayer>()
.unwrap()
.load::<AwsCredentialFeature>()
.cloned()
.collect::<Vec<AwsCredentialFeature>>();
feature_props.reverse();
assert_eq!(maybe_props, feature_props)
}
#[cfg(feature = "test-util")]
#[test]
fn from_credentials_adds_resolved_account_id_feature() {
use crate::credential_feature::AwsCredentialFeature;
use aws_smithy_runtime_api::client::identity::Identity;
use aws_smithy_types::config_bag::FrozenLayer;
let creds = Credentials::builder()
.access_key_id("test")
.secret_access_key("test")
.account_id("123456789012")
.provider_name("test")
.build();
let identity = Identity::from(creds);
let layer = identity.property::<FrozenLayer>().unwrap();
let features = layer
.load::<AwsCredentialFeature>()
.cloned()
.collect::<Vec<_>>();
assert!(features.contains(&AwsCredentialFeature::ResolvedAccountId));
}
}