use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc;
use chrono::{DateTime, Utc};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PrincipalType {
User,
AssumedRole,
FederatedUser,
Root,
Unknown,
}
impl PrincipalType {
pub fn as_str(self) -> &'static str {
match self {
PrincipalType::User => "user",
PrincipalType::AssumedRole => "assumed-role",
PrincipalType::FederatedUser => "federated-user",
PrincipalType::Root => "root",
PrincipalType::Unknown => "unknown",
}
}
pub fn from_arn(arn: &str) -> Self {
if arn.ends_with(":root") {
PrincipalType::Root
} else if arn.contains(":user/") {
PrincipalType::User
} else if arn.contains(":assumed-role/") {
PrincipalType::AssumedRole
} else if arn.contains(":federated-user/") {
PrincipalType::FederatedUser
} else {
PrincipalType::Unknown
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Principal {
pub arn: String,
pub user_id: String,
pub account_id: String,
pub principal_type: PrincipalType,
pub source_identity: Option<String>,
pub tags: Option<HashMap<String, String>>,
}
impl Principal {
pub fn is_root(&self) -> bool {
matches!(self.principal_type, PrincipalType::Root) || self.arn.ends_with(":root")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedCredential {
pub secret_access_key: String,
pub session_token: Option<String>,
pub principal: Principal,
pub session_policies: Vec<String>,
pub mfa_present: bool,
pub token_issued_at: Option<DateTime<Utc>>,
pub federated_provider: Option<String>,
}
impl ResolvedCredential {
pub fn principal_arn(&self) -> &str {
&self.principal.arn
}
pub fn user_id(&self) -> &str {
&self.principal.user_id
}
pub fn account_id(&self) -> &str {
&self.principal.account_id
}
}
pub trait CredentialResolver: Send + Sync {
fn resolve(&self, access_key_id: &str) -> Option<ResolvedCredential>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IamAction {
pub service: &'static str,
pub action: &'static str,
pub resource: String,
}
impl IamAction {
pub fn action_string(&self) -> String {
format!("{}:{}", self.service, self.action)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IamDecision {
Allow,
ImplicitDeny,
ExplicitDeny,
}
impl IamDecision {
pub fn is_allow(self) -> bool {
matches!(self, IamDecision::Allow)
}
}
#[derive(Debug, Clone, Default)]
pub struct ConditionContext {
pub aws_username: Option<String>,
pub aws_userid: Option<String>,
pub aws_principal_arn: Option<String>,
pub aws_principal_account: Option<String>,
pub aws_principal_type: Option<String>,
pub aws_source_ip: Option<IpAddr>,
pub aws_current_time: Option<DateTime<Utc>>,
pub aws_epoch_time: Option<i64>,
pub aws_secure_transport: Option<bool>,
pub aws_requested_region: Option<String>,
pub aws_mfa_present: Option<bool>,
pub aws_mfa_age_seconds: Option<i64>,
pub aws_called_via: Vec<String>,
pub aws_source_vpce: Option<String>,
pub aws_source_vpc: Option<String>,
pub aws_vpc_source_ip: Option<IpAddr>,
pub aws_federated_provider: Option<String>,
pub aws_token_issue_time: Option<DateTime<Utc>>,
pub service_keys: BTreeMap<String, Vec<String>>,
pub resource_tags: Option<HashMap<String, String>>,
pub request_tags: Option<HashMap<String, String>>,
pub principal_tags: Option<HashMap<String, String>>,
}
impl ConditionContext {
pub fn lookup(&self, key: &str) -> Option<Vec<String>> {
let lower = key.to_ascii_lowercase();
let one = |s: &str| Some(vec![s.to_string()]);
if lower.starts_with("aws:resourcetag/") {
let tag_key = &key[16..]; return self
.resource_tags
.as_ref()
.and_then(|tags| tags.get(tag_key))
.map(|v| vec![v.clone()]);
}
if lower.starts_with("aws:requesttag/") {
let tag_key = &key[15..];
return self
.request_tags
.as_ref()
.and_then(|tags| tags.get(tag_key))
.map(|v| vec![v.clone()]);
}
if lower.starts_with("aws:principaltag/") {
let tag_key = &key[17..];
return self
.principal_tags
.as_ref()
.and_then(|tags| tags.get(tag_key))
.map(|v| vec![v.clone()]);
}
if lower == "aws:tagkeys" {
return self
.request_tags
.as_ref()
.map(|tags| tags.keys().cloned().collect());
}
match lower.as_str() {
"aws:username" => self.aws_username.as_deref().and_then(one),
"aws:userid" => self.aws_userid.as_deref().and_then(one),
"aws:principalarn" => self.aws_principal_arn.as_deref().and_then(one),
"aws:principalaccount" => self.aws_principal_account.as_deref().and_then(one),
"aws:principaltype" => self.aws_principal_type.as_deref().and_then(one),
"aws:sourceip" => self.aws_source_ip.map(|ip| vec![ip.to_string()]),
"aws:currenttime" => self
.aws_current_time
.map(|t| vec![t.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)]),
"aws:epochtime" => self.aws_epoch_time.map(|e| vec![e.to_string()]),
"aws:securetransport" => self.aws_secure_transport.map(|b| vec![b.to_string()]),
"aws:requestedregion" => self.aws_requested_region.as_deref().and_then(one),
"aws:multifactorauthpresent" => self.aws_mfa_present.map(|b| vec![b.to_string()]),
"aws:multifactorauthage" => self.aws_mfa_age_seconds.map(|s| vec![s.to_string()]),
"aws:calledvia" => {
if self.aws_called_via.is_empty() {
None
} else {
Some(self.aws_called_via.clone())
}
}
"aws:sourcevpce" => self.aws_source_vpce.as_deref().and_then(one),
"aws:sourcevpc" => self.aws_source_vpc.as_deref().and_then(one),
"aws:vpcsourceip" => self.aws_vpc_source_ip.map(|ip| vec![ip.to_string()]),
"aws:federatedprovider" => self.aws_federated_provider.as_deref().and_then(one),
"aws:tokenissuetime" => self
.aws_token_issue_time
.map(|t| vec![t.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)]),
_ => {
if let Some(vs) = self.service_keys.get(&lower) {
if vs.is_empty() {
None
} else {
Some(vs.clone())
}
} else {
self.service_keys
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, vs)| vs.clone())
}
}
}
}
}
pub trait IamPolicyEvaluator: Send + Sync {
fn evaluate(
&self,
principal: &Principal,
action: &IamAction,
context: &ConditionContext,
session_policies: &[String],
scps: Option<&[String]>,
) -> IamDecision;
#[allow(clippy::too_many_arguments)]
fn evaluate_with_resource_policy(
&self,
principal: &Principal,
action: &IamAction,
context: &ConditionContext,
resource_policy_json: Option<&str>,
resource_account_id: &str,
session_policies: &[String],
scps: Option<&[String]>,
) -> IamDecision;
}
pub trait ScpResolver: Send + Sync {
fn scps_for(&self, principal: &Principal) -> Option<Vec<String>>;
}
pub trait ResourcePolicyProvider: Send + Sync {
fn resource_policy(&self, service: &str, resource_arn: &str) -> Option<String>;
fn resource_owner_account(&self, _service: &str, _resource_arn: &str) -> Option<String> {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PassRoleError {
RoleNotFound(String),
TrustPolicyDenies {
role_arn: String,
service_principal: String,
},
InvalidTrustPolicy(String),
}
impl std::fmt::Display for PassRoleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RoleNotFound(arn) => write!(f, "role not found: {arn}"),
Self::TrustPolicyDenies {
role_arn,
service_principal,
} => write!(
f,
"Role's trust policy does not allow {service_principal} to assume the role: {role_arn}"
),
Self::InvalidTrustPolicy(arn) => {
write!(f, "invalid trust policy on role {arn}")
}
}
}
}
impl std::error::Error for PassRoleError {}
pub trait RoleTrustValidator: Send + Sync {
fn validate(
&self,
account_id: &str,
role_arn: &str,
service_principal: &str,
) -> Result<(), PassRoleError>;
}
pub struct MultiResourcePolicyProvider {
providers: Vec<Arc<dyn ResourcePolicyProvider>>,
}
impl MultiResourcePolicyProvider {
pub fn new(providers: Vec<Arc<dyn ResourcePolicyProvider>>) -> Self {
Self { providers }
}
pub fn shared(
providers: Vec<Arc<dyn ResourcePolicyProvider>>,
) -> Arc<dyn ResourcePolicyProvider> {
Arc::new(Self::new(providers))
}
pub fn len(&self) -> usize {
self.providers.len()
}
pub fn is_empty(&self) -> bool {
self.providers.is_empty()
}
}
impl ResourcePolicyProvider for MultiResourcePolicyProvider {
fn resource_policy(&self, service: &str, resource_arn: &str) -> Option<String> {
self.providers
.iter()
.find_map(|p| p.resource_policy(service, resource_arn))
}
fn resource_owner_account(&self, service: &str, resource_arn: &str) -> Option<String> {
self.providers
.iter()
.find_map(|p| p.resource_owner_account(service, resource_arn))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum IamMode {
#[default]
Off,
Soft,
Strict,
}
impl IamMode {
pub fn is_enabled(self) -> bool {
!matches!(self, IamMode::Off)
}
pub fn is_strict(self) -> bool {
matches!(self, IamMode::Strict)
}
pub fn as_str(self) -> &'static str {
match self {
IamMode::Off => "off",
IamMode::Soft => "soft",
IamMode::Strict => "strict",
}
}
}
impl fmt::Display for IamMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
pub struct ParseIamModeError(String);
impl fmt::Display for ParseIamModeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid IAM mode `{}`; expected one of: off, soft, strict",
self.0
)
}
}
impl std::error::Error for ParseIamModeError {}
impl FromStr for IamMode {
type Err = ParseIamModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"off" | "none" | "disabled" => Ok(IamMode::Off),
"soft" | "audit" | "warn" => Ok(IamMode::Soft),
"strict" | "enforce" | "deny" => Ok(IamMode::Strict),
other => Err(ParseIamModeError(other.to_string())),
}
}
}
pub fn is_root_bypass(access_key_id: &str) -> bool {
access_key_id
.trim()
.get(..4)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("test"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iam_mode_default_is_off() {
assert_eq!(IamMode::default(), IamMode::Off);
assert!(!IamMode::default().is_enabled());
}
#[test]
fn iam_mode_from_str_accepts_primary_values() {
assert_eq!(IamMode::from_str("off").unwrap(), IamMode::Off);
assert_eq!(IamMode::from_str("soft").unwrap(), IamMode::Soft);
assert_eq!(IamMode::from_str("strict").unwrap(), IamMode::Strict);
}
#[test]
fn iam_mode_from_str_is_case_insensitive_and_trimmed() {
assert_eq!(IamMode::from_str(" OFF ").unwrap(), IamMode::Off);
assert_eq!(IamMode::from_str("Soft").unwrap(), IamMode::Soft);
assert_eq!(IamMode::from_str("STRICT").unwrap(), IamMode::Strict);
}
#[test]
fn iam_mode_from_str_accepts_aliases() {
assert_eq!(IamMode::from_str("disabled").unwrap(), IamMode::Off);
assert_eq!(IamMode::from_str("audit").unwrap(), IamMode::Soft);
assert_eq!(IamMode::from_str("enforce").unwrap(), IamMode::Strict);
}
#[test]
fn iam_mode_from_str_rejects_garbage() {
assert!(IamMode::from_str("").is_err());
assert!(IamMode::from_str("allow").is_err());
assert!(IamMode::from_str("yes").is_err());
}
#[test]
fn iam_mode_display_roundtrips() {
for mode in [IamMode::Off, IamMode::Soft, IamMode::Strict] {
assert_eq!(IamMode::from_str(&mode.to_string()).unwrap(), mode);
}
}
#[test]
fn iam_mode_flags() {
assert!(!IamMode::Off.is_enabled());
assert!(!IamMode::Off.is_strict());
assert!(IamMode::Soft.is_enabled());
assert!(!IamMode::Soft.is_strict());
assert!(IamMode::Strict.is_enabled());
assert!(IamMode::Strict.is_strict());
}
#[test]
fn root_bypass_matches_test_prefix() {
assert!(is_root_bypass("test"));
assert!(is_root_bypass("TEST"));
assert!(is_root_bypass("Test"));
assert!(is_root_bypass("testAccessKey"));
assert!(is_root_bypass("TESTAKIAIOSFODNN7EXAMPLE"));
}
#[test]
fn root_bypass_does_not_panic_on_multibyte_input() {
assert!(!is_root_bypass("té"));
assert!(!is_root_bypass("日本語キー"));
assert!(!is_root_bypass("🔑🔑"));
}
#[test]
fn principal_type_from_arn_classifies_known_shapes() {
assert_eq!(
PrincipalType::from_arn("arn:aws:iam::123456789012:user/alice"),
PrincipalType::User
);
assert_eq!(
PrincipalType::from_arn("arn:aws:sts::123456789012:assumed-role/R/s"),
PrincipalType::AssumedRole
);
assert_eq!(
PrincipalType::from_arn("arn:aws:sts::123456789012:federated-user/bob"),
PrincipalType::FederatedUser
);
assert_eq!(
PrincipalType::from_arn("arn:aws:iam::123456789012:root"),
PrincipalType::Root
);
}
#[test]
fn principal_type_unparseable_is_unknown_not_root() {
assert_eq!(
PrincipalType::from_arn("not-an-arn"),
PrincipalType::Unknown
);
assert_eq!(PrincipalType::from_arn(""), PrincipalType::Unknown);
assert_eq!(
PrincipalType::from_arn("arn:aws:iam::123456789012:something-weird"),
PrincipalType::Unknown
);
let p = Principal {
arn: "garbage".to_string(),
user_id: "x".to_string(),
account_id: "123456789012".to_string(),
principal_type: PrincipalType::Unknown,
source_identity: None,
tags: None,
};
assert!(!p.is_root());
}
#[test]
fn principal_is_root_covers_root_type_and_arn_suffix() {
let p = Principal {
arn: "arn:aws:iam::123456789012:root".to_string(),
user_id: "AIDAROOT".to_string(),
account_id: "123456789012".to_string(),
principal_type: PrincipalType::Root,
source_identity: None,
tags: None,
};
assert!(p.is_root());
let user = Principal {
arn: "arn:aws:iam::123456789012:user/alice".to_string(),
user_id: "AIDAALICE".to_string(),
account_id: "123456789012".to_string(),
principal_type: PrincipalType::User,
source_identity: None,
tags: None,
};
assert!(!user.is_root());
}
#[test]
fn resolved_credential_accessors_forward_to_principal() {
let rc = ResolvedCredential {
secret_access_key: "s".into(),
session_token: None,
principal: Principal {
arn: "arn:aws:iam::123456789012:user/alice".into(),
user_id: "AIDAALICE".into(),
account_id: "123456789012".into(),
principal_type: PrincipalType::User,
source_identity: None,
tags: None,
},
session_policies: Vec::new(),
mfa_present: false,
token_issued_at: None,
federated_provider: None,
};
assert_eq!(rc.principal_arn(), "arn:aws:iam::123456789012:user/alice");
assert_eq!(rc.user_id(), "AIDAALICE");
assert_eq!(rc.account_id(), "123456789012");
}
#[test]
fn root_bypass_rejects_non_test_keys() {
assert!(!is_root_bypass(""));
assert!(!is_root_bypass(" "));
assert!(!is_root_bypass("AKIAIOSFODNN7EXAMPLE"));
assert!(!is_root_bypass("FKIA123456"));
assert!(!is_root_bypass("tes"));
assert!(!is_root_bypass("tst"));
}
struct FakeProvider {
service: &'static str,
arn: &'static str,
policy: &'static str,
}
impl ResourcePolicyProvider for FakeProvider {
fn resource_policy(&self, service: &str, resource_arn: &str) -> Option<String> {
if service.eq_ignore_ascii_case(self.service) && resource_arn == self.arn {
Some(self.policy.to_string())
} else {
None
}
}
}
fn fake(
service: &'static str,
arn: &'static str,
policy: &'static str,
) -> Arc<dyn ResourcePolicyProvider> {
Arc::new(FakeProvider {
service,
arn,
policy,
})
}
#[test]
fn multi_provider_empty_always_returns_none() {
let m = MultiResourcePolicyProvider::new(vec![]);
assert!(m.is_empty());
assert_eq!(m.len(), 0);
assert_eq!(m.resource_policy("s3", "arn:aws:s3:::x"), None);
}
#[test]
fn multi_provider_delegates_to_single_child() {
let m = MultiResourcePolicyProvider::new(vec![fake("s3", "arn:aws:s3:::b", r#"{"v":1}"#)]);
assert_eq!(m.len(), 1);
assert_eq!(
m.resource_policy("s3", "arn:aws:s3:::b").as_deref(),
Some(r#"{"v":1}"#)
);
assert_eq!(m.resource_policy("s3", "arn:aws:s3:::missing"), None);
assert_eq!(m.resource_policy("sns", "arn:aws:s3:::b"), None);
}
#[test]
fn multi_provider_hits_first_matching_child() {
let m = MultiResourcePolicyProvider::new(vec![
fake("s3", "arn:aws:s3:::b", r#"{"v":"s3"}"#),
fake("sns", "arn:aws:sns:us-east-1:123:t", r#"{"v":"sns"}"#),
]);
assert_eq!(
m.resource_policy("s3", "arn:aws:s3:::b").as_deref(),
Some(r#"{"v":"s3"}"#)
);
assert_eq!(
m.resource_policy("sns", "arn:aws:sns:us-east-1:123:t")
.as_deref(),
Some(r#"{"v":"sns"}"#)
);
}
#[test]
fn multi_provider_is_order_independent_when_services_differ() {
let children: Vec<Arc<dyn ResourcePolicyProvider>> = vec![
fake("s3", "arn:aws:s3:::b", "s3-doc"),
fake("sns", "arn:aws:sns:us-east-1:123:t", "sns-doc"),
fake(
"lambda",
"arn:aws:lambda:us-east-1:123:function:f",
"lam-doc",
),
];
let forward = MultiResourcePolicyProvider::new(children.clone());
let reversed = MultiResourcePolicyProvider::new({
let mut v = children.clone();
v.reverse();
v
});
for (svc, arn) in [
("s3", "arn:aws:s3:::b"),
("sns", "arn:aws:sns:us-east-1:123:t"),
("lambda", "arn:aws:lambda:us-east-1:123:function:f"),
] {
assert_eq!(
forward.resource_policy(svc, arn),
reversed.resource_policy(svc, arn),
"service {svc}"
);
}
}
#[test]
fn multi_provider_returns_none_for_unhandled_service() {
let m = MultiResourcePolicyProvider::new(vec![fake("s3", "arn:aws:s3:::b", "doc")]);
assert_eq!(
m.resource_policy("kms", "arn:aws:kms:us-east-1:123:key/k"),
None
);
assert_eq!(m.resource_policy("iam", "arn:aws:iam::123:role/r"), None);
}
#[test]
fn multi_provider_shared_wraps_in_arc() {
let arc = MultiResourcePolicyProvider::shared(vec![fake("s3", "arn:aws:s3:::b", "doc")]);
assert_eq!(
arc.resource_policy("s3", "arn:aws:s3:::b").as_deref(),
Some("doc")
);
}
#[test]
fn lookup_mfa_present_emits_bool_string() {
let ctx = ConditionContext {
aws_mfa_present: Some(true),
..Default::default()
};
assert_eq!(
ctx.lookup("aws:MultiFactorAuthPresent"),
Some(vec!["true".to_string()])
);
let ctx = ConditionContext {
aws_mfa_present: Some(false),
..Default::default()
};
assert_eq!(
ctx.lookup("aws:multifactorauthpresent"),
Some(vec!["false".to_string()])
);
}
#[test]
fn lookup_mfa_age_emits_seconds() {
let ctx = ConditionContext {
aws_mfa_age_seconds: Some(900),
..Default::default()
};
assert_eq!(
ctx.lookup("aws:MultiFactorAuthAge"),
Some(vec!["900".to_string()])
);
}
#[test]
fn lookup_called_via_returns_full_chain() {
let ctx = ConditionContext {
aws_called_via: vec![
"cloudformation.amazonaws.com".to_string(),
"lambda.amazonaws.com".to_string(),
],
..Default::default()
};
assert_eq!(
ctx.lookup("aws:CalledVia"),
Some(vec![
"cloudformation.amazonaws.com".to_string(),
"lambda.amazonaws.com".to_string(),
])
);
}
#[test]
fn lookup_called_via_empty_returns_none() {
let ctx = ConditionContext::default();
assert_eq!(ctx.lookup("aws:CalledVia"), None);
}
#[test]
fn lookup_source_vpc_keys() {
let ctx = ConditionContext {
aws_source_vpc: Some("vpc-123".to_string()),
aws_source_vpce: Some("vpce-456".to_string()),
aws_vpc_source_ip: Some("10.0.1.5".parse::<IpAddr>().unwrap()),
..Default::default()
};
assert_eq!(
ctx.lookup("aws:SourceVpc"),
Some(vec!["vpc-123".to_string()])
);
assert_eq!(
ctx.lookup("aws:SourceVpce"),
Some(vec!["vpce-456".to_string()])
);
assert_eq!(
ctx.lookup("aws:VpcSourceIp"),
Some(vec!["10.0.1.5".to_string()])
);
}
#[test]
fn lookup_federated_provider_and_token_issue_time() {
use chrono::TimeZone;
let ctx = ConditionContext {
aws_federated_provider: Some("cognito-identity.amazonaws.com".to_string()),
aws_token_issue_time: Some(
chrono::Utc.with_ymd_and_hms(2026, 4, 30, 12, 0, 0).unwrap(),
),
..Default::default()
};
assert_eq!(
ctx.lookup("aws:FederatedProvider"),
Some(vec!["cognito-identity.amazonaws.com".to_string()])
);
assert_eq!(
ctx.lookup("aws:TokenIssueTime"),
Some(vec!["2026-04-30T12:00:00Z".to_string()])
);
}
fn abac_context() -> ConditionContext {
ConditionContext {
resource_tags: Some(
[("Environment", "prod"), ("CostCenter", "42")]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
),
request_tags: Some(
[("Project", "web"), ("Team", "platform")]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
),
principal_tags: Some(
[("Department", "eng"), ("Role", "developer")]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
),
..Default::default()
}
}
#[test]
fn lookup_resource_tag_case_sensitive_key() {
let ctx = abac_context();
assert_eq!(
ctx.lookup("aws:ResourceTag/Environment"),
Some(vec!["prod".to_string()])
);
assert_eq!(ctx.lookup("aws:ResourceTag/environment"), None);
}
#[test]
fn lookup_resource_tag_prefix_case_insensitive() {
let ctx = abac_context();
assert_eq!(
ctx.lookup("AWS:resourcetag/Environment"),
Some(vec!["prod".to_string()])
);
assert_eq!(
ctx.lookup("Aws:RESOURCETAG/CostCenter"),
Some(vec!["42".to_string()])
);
}
#[test]
fn lookup_request_tag() {
let ctx = abac_context();
assert_eq!(
ctx.lookup("aws:RequestTag/Project"),
Some(vec!["web".to_string()])
);
assert_eq!(ctx.lookup("aws:RequestTag/project"), None);
}
#[test]
fn lookup_principal_tag() {
let ctx = abac_context();
assert_eq!(
ctx.lookup("aws:PrincipalTag/Department"),
Some(vec!["eng".to_string()])
);
assert_eq!(ctx.lookup("aws:PrincipalTag/department"), None);
}
#[test]
fn lookup_tag_keys_returns_all_request_tag_keys() {
let ctx = abac_context();
let mut keys = ctx.lookup("aws:TagKeys").unwrap();
keys.sort();
assert_eq!(keys, vec!["Project", "Team"]);
}
#[test]
fn lookup_tag_keys_case_insensitive() {
let ctx = abac_context();
assert!(ctx.lookup("AWS:TAGKEYS").is_some());
assert!(ctx.lookup("aws:tagkeys").is_some());
}
#[test]
fn lookup_tag_none_when_field_not_set() {
let ctx = ConditionContext::default();
assert_eq!(ctx.lookup("aws:ResourceTag/Foo"), None);
assert_eq!(ctx.lookup("aws:RequestTag/Foo"), None);
assert_eq!(ctx.lookup("aws:PrincipalTag/Foo"), None);
assert_eq!(ctx.lookup("aws:TagKeys"), None);
}
#[test]
fn lookup_tag_missing_key_returns_none() {
let ctx = abac_context();
assert_eq!(ctx.lookup("aws:ResourceTag/NonExistent"), None);
assert_eq!(ctx.lookup("aws:RequestTag/NonExistent"), None);
assert_eq!(ctx.lookup("aws:PrincipalTag/NonExistent"), None);
}
}