use super::*;
use fakecloud_aws::arn::Arn;
use serde_json::json;
fn principal_user(arn: &str) -> Principal {
Principal {
arn: arn.to_string(),
user_id: "AIDA".into(),
account_id: "123456789012".into(),
principal_type: PrincipalType::User,
source_identity: None,
tags: None,
}
}
fn req<'a>(principal: &'a Principal, action: &str, resource: &str) -> EvalRequest<'a> {
EvalRequest {
principal,
action: action.to_string(),
resource: resource.to_string(),
context: RequestContext::default(),
}
}
fn doc(json: serde_json::Value) -> PolicyDocument {
PolicyDocument::from_value(&json)
}
#[test]
fn glob_literal_match() {
assert!(glob_match("foo", "foo"));
assert!(!glob_match("foo", "bar"));
}
#[test]
fn glob_star_matches_any() {
assert!(glob_match("*", "foo"));
assert!(glob_match("*", ""));
assert!(glob_match("foo*", "foobar"));
assert!(glob_match("*bar", "foobar"));
assert!(glob_match("f*r", "foobar"));
assert!(!glob_match("foo*", "fo"));
}
#[test]
fn glob_question_mark_matches_one() {
assert!(glob_match("f?o", "foo"));
assert!(!glob_match("f?o", "fo"));
assert!(!glob_match("f?o", "foo!"));
}
#[test]
fn glob_no_backtracking_explosion() {
assert!(!glob_match("a*a*a*a*a*b", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
}
#[test]
fn iam_action_service_prefix_is_case_insensitive() {
assert!(iam_glob_match("S3:GetObject", "s3:GetObject", true));
assert!(iam_glob_match("s3:GetObject", "S3:GetObject", true));
}
#[test]
fn iam_action_name_is_case_sensitive() {
assert!(!iam_glob_match("s3:getobject", "s3:GetObject", true));
assert!(iam_glob_match("s3:GetObject", "s3:GetObject", true));
}
#[test]
fn iam_action_supports_wildcards() {
assert!(iam_glob_match("s3:Get*", "s3:GetObject", true));
assert!(iam_glob_match("s3:*", "s3:DeleteObject", true));
assert!(iam_glob_match("*", "s3:GetObject", true));
assert!(!iam_glob_match("s3:Get*", "s3:PutObject", true));
}
#[test]
fn empty_policy_set_is_implicit_deny() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
assert_eq!(
evaluate(&[], &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")),
Decision::ImplicitDeny
);
}
#[test]
fn allow_with_matching_action_and_resource() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket/key"
}]
}));
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn deny_takes_precedence_over_allow() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let allow = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}]
}));
let deny = doc(json!({
"Statement": [{
"Effect": "Deny",
"Action": "s3:DeleteObject",
"Resource": "*"
}]
}));
assert_eq!(
evaluate(
&[allow.clone(), deny.clone()],
&req(&p, "s3:DeleteObject", "arn:aws:s3:::bucket/key")
),
Decision::ExplicitDeny
);
assert_eq!(
evaluate(
&[deny, allow],
&req(&p, "s3:DeleteObject", "arn:aws:s3:::bucket/key")
),
Decision::ExplicitDeny
);
}
#[test]
fn allow_with_wrong_action_is_implicit_deny() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "*"
}]
}));
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:DeleteObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
}
#[test]
fn allow_with_wrong_resource_is_implicit_deny() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::other-bucket/*"
}]
}));
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
}
#[test]
fn resource_wildcard_matches_arn_path() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket/*"
}]
}));
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/path/to/key")
),
Decision::Allow
);
}
#[test]
fn not_action_excludes_listed_actions() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"NotAction": "s3:DeleteObject",
"Resource": "*"
}]
}));
assert_eq!(
evaluate(
&[policy.clone()],
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:DeleteObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
}
#[test]
fn not_resource_excludes_listed_resources() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"NotResource": "arn:aws:s3:::secret-bucket/*"
}]
}));
assert_eq!(
evaluate(
&[policy.clone()],
&req(&p, "s3:GetObject", "arn:aws:s3:::public-bucket/key")
),
Decision::Allow
);
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:GetObject", "arn:aws:s3:::secret-bucket/key")
),
Decision::ImplicitDeny
);
}
fn req_with_ctx<'a>(
principal: &'a Principal,
action: &str,
resource: &str,
context: RequestContext,
) -> EvalRequest<'a> {
EvalRequest {
principal,
action: action.to_string(),
resource: resource.to_string(),
context,
}
}
fn ctx_alice() -> RequestContext {
RequestContext {
aws_username: Some("alice".into()),
aws_principal_arn: Some("arn:aws:iam::123456789012:user/alice".into()),
aws_principal_account: Some("123456789012".into()),
aws_principal_type: Some("User".into()),
aws_userid: Some("AIDA".into()),
..Default::default()
}
}
#[test]
fn condition_string_equals_username_allows_match() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*",
"Condition": { "StringEquals": { "aws:username": "alice" } }
}]
}));
assert_eq!(
evaluate(
&[policy],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
),
Decision::Allow
);
}
#[test]
fn condition_string_equals_username_denies_mismatch() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*",
"Condition": { "StringEquals": { "aws:username": "bob" } }
}]
}));
assert_eq!(
evaluate(
&[policy],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
),
Decision::ImplicitDeny
);
}
#[test]
fn deny_with_condition_fires_when_condition_matches() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
},
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "*"
}
]
}));
let mut ctx = ctx_alice();
ctx.aws_secure_transport = Some(false);
assert_eq!(
evaluate(
&[policy.clone()],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx)
),
Decision::ExplicitDeny
);
let mut ctx_secure = ctx_alice();
ctx_secure.aws_secure_transport = Some(true);
assert_eq!(
evaluate(
&[policy],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_secure)
),
Decision::Allow
);
}
#[test]
fn condition_ip_address_allows_within_cidr() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "*",
"Condition": { "IpAddress": { "aws:SourceIp": "10.0.0.0/24" } }
}]
}));
let mut ctx = ctx_alice();
ctx.aws_source_ip = Some("10.0.0.17".parse().unwrap());
assert_eq!(
evaluate(
&[policy.clone()],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx)
),
Decision::Allow
);
let mut wrong = ctx_alice();
wrong.aws_source_ip = Some("192.168.1.1".parse().unwrap());
assert_eq!(
evaluate(
&[policy],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", wrong)
),
Decision::ImplicitDeny
);
}
#[test]
fn condition_date_less_than_blocks_expired() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "*",
"Condition": {
"DateLessThan": { "aws:CurrentTime": "2020-01-01T00:00:00Z" }
}
}]
}));
let mut ctx = ctx_alice();
ctx.aws_current_time = Some(
chrono::DateTime::parse_from_rfc3339("2024-06-15T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc),
);
assert_eq!(
evaluate(
&[policy],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx)
),
Decision::ImplicitDeny
);
}
#[test]
fn condition_missing_key_without_if_exists_denies() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*",
"Condition": { "IpAddress": { "aws:SourceIp": "10.0.0.0/8" } }
}]
}));
assert_eq!(
evaluate(
&[policy],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
),
Decision::ImplicitDeny
);
}
#[test]
fn condition_if_exists_passes_on_missing_key() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*",
"Condition": {
"IpAddressIfExists": { "aws:SourceIp": "10.0.0.0/8" }
}
}]
}));
assert_eq!(
evaluate(
&[policy],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
),
Decision::Allow
);
}
#[test]
fn condition_multiple_operators_all_must_match() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*",
"Condition": {
"StringEquals": { "aws:username": "alice" },
"IpAddress": { "aws:SourceIp": "10.0.0.0/24" }
}
}]
}));
let mut ctx = ctx_alice();
ctx.aws_source_ip = Some("10.0.0.1".parse().unwrap());
assert_eq!(
evaluate(
&[policy.clone()],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx)
),
Decision::Allow
);
let mut wrong_ip = ctx_alice();
wrong_ip.aws_source_ip = Some("192.168.1.1".parse().unwrap());
assert_eq!(
evaluate(
&[policy],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", wrong_ip)
),
Decision::ImplicitDeny
);
}
#[test]
fn condition_unknown_operator_fails_closed() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*",
"Condition": { "NotARealOperator": { "aws:username": "alice" } }
}]
}));
assert_eq!(
evaluate(
&[policy],
&req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
),
Decision::ImplicitDeny
);
}
#[test]
fn array_action_matches_any_entry() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "*"
}]
}));
assert_eq!(
evaluate(
&[policy.clone()],
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn statement_without_effect_is_dropped() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [
{ "Action": "s3:GetObject", "Resource": "*" },
{ "Effect": "Allow", "Action": "s3:GetObject", "Resource": "*" }
]
}));
assert_eq!(policy.statement_count(), 1);
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn statement_without_action_is_dropped() {
let policy = doc(json!({
"Statement": [{ "Effect": "Allow", "Resource": "*" }]
}));
assert_eq!(policy.statement_count(), 0);
}
#[test]
fn implicit_resource_acts_like_wildcard() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [{ "Effect": "Allow", "Action": "s3:GetObject" }]
}));
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn malformed_policy_json_is_implicit_deny() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = PolicyDocument::parse("{ this is not valid json");
assert_eq!(policy.statement_count(), 0);
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
}
#[test]
fn deny_short_circuits_after_match() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let policy = doc(json!({
"Statement": [
{ "Effect": "Deny", "Action": "*", "Resource": "*" },
{ "Effect": "Allow", "Action": "s3:GetObject", "Resource": "*" }
]
}));
assert_eq!(
evaluate(
&[policy],
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::ExplicitDeny
);
}
#[test]
fn user_name_from_arn_strips_iam_path() {
assert_eq!(
user_name_from_arn("arn:aws:iam::123456789012:user/alice"),
Some("alice")
);
assert_eq!(
user_name_from_arn("arn:aws:iam::123456789012:user/engineering/alice"),
Some("alice")
);
assert_eq!(
user_name_from_arn("arn:aws:iam::123456789012:user/path/to/alice"),
Some("alice")
);
assert_eq!(user_name_from_arn("arn:aws:iam::123456789012:role/r"), None);
}
#[test]
fn collect_identity_policies_resolves_pathed_user() {
use crate::state::IamUser;
use chrono::Utc;
let mut state = IamState::new("123456789012");
state.users.insert(
"alice".to_string(),
IamUser {
user_name: "alice".into(),
user_id: "AIDAALICE".into(),
arn: "arn:aws:iam::123456789012:user/engineering/alice".into(),
path: "/engineering/".into(),
created_at: Utc::now(),
tags: Vec::new(),
permissions_boundary: None,
},
);
let mut inline = std::collections::BTreeMap::new();
inline.insert(
"AllowGet".to_string(),
r#"{"Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#.to_string(),
);
state
.user_inline_policies
.insert("alice".to_string(), inline);
let principal = Principal {
arn: "arn:aws:iam::123456789012:user/engineering/alice".to_string(),
user_id: "AIDAALICE".to_string(),
account_id: "123456789012".to_string(),
principal_type: PrincipalType::User,
source_identity: None,
tags: None,
};
let docs = collect_identity_policies(&state, &principal);
assert_eq!(docs.len(), 1, "pathed user's inline policy was missed");
assert_eq!(
evaluate(
&docs,
&req(&principal, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn role_name_from_assumed_role_arn_strips_session() {
assert_eq!(
role_name_from_assumed_role_arn("arn:aws:sts::123456789012:assumed-role/ops/session-1"),
Some("ops")
);
}
#[test]
fn collect_identity_policies_picks_up_user_inline() {
use crate::state::IamUser;
use chrono::Utc;
let mut state = IamState::new("123456789012");
state.users.insert(
"alice".to_string(),
IamUser {
user_name: "alice".into(),
user_id: "AIDAALICE".into(),
arn: "arn:aws:iam::123456789012:user/alice".into(),
path: "/".into(),
created_at: Utc::now(),
tags: Vec::new(),
permissions_boundary: None,
},
);
let mut inline = std::collections::BTreeMap::new();
inline.insert(
"AllowGet".to_string(),
r#"{"Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#.to_string(),
);
state
.user_inline_policies
.insert("alice".to_string(), inline);
let principal = principal_user("arn:aws:iam::123456789012:user/alice");
let docs = collect_identity_policies(&state, &principal);
assert_eq!(docs.len(), 1);
assert_eq!(
evaluate(
&docs,
&req(&principal, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn collect_identity_policies_picks_up_managed_via_groups() {
use crate::state::{IamGroup, IamPolicy, IamUser, PolicyVersion};
use chrono::Utc;
let mut state = IamState::new("123456789012");
state.users.insert(
"alice".to_string(),
IamUser {
user_name: "alice".into(),
user_id: "AIDAALICE".into(),
arn: "arn:aws:iam::123456789012:user/alice".into(),
path: "/".into(),
created_at: Utc::now(),
tags: Vec::new(),
permissions_boundary: None,
},
);
let policy_arn = "arn:aws:iam::123456789012:policy/AllowGet".to_string();
state.policies.insert(
policy_arn.clone(),
IamPolicy {
policy_name: "AllowGet".into(),
policy_id: "ANPA1".into(),
arn: policy_arn.clone(),
path: "/".into(),
description: "".into(),
created_at: Utc::now(),
tags: Vec::new(),
default_version_id: "v1".into(),
versions: vec![PolicyVersion {
version_id: "v1".into(),
document:
r#"{"Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#
.into(),
is_default: true,
created_at: Utc::now(),
}],
next_version_num: 2,
attachment_count: 1,
},
);
state.groups.insert(
"readers".to_string(),
IamGroup {
group_name: "readers".into(),
group_id: "AGPA1".into(),
arn: "arn:aws:iam::123456789012:group/readers".into(),
path: "/".into(),
created_at: Utc::now(),
members: vec!["alice".into()],
inline_policies: std::collections::BTreeMap::new(),
attached_policies: vec![policy_arn],
},
);
let principal = principal_user("arn:aws:iam::123456789012:user/alice");
let docs = collect_identity_policies(&state, &principal);
assert_eq!(docs.len(), 1);
assert_eq!(
evaluate(
&docs,
&req(&principal, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn collect_identity_policies_for_root_returns_empty() {
let state = IamState::new("123456789012");
let principal = Principal {
arn: "arn:aws:iam::123456789012:root".into(),
user_id: "ROOT".into(),
account_id: "123456789012".into(),
principal_type: PrincipalType::Root,
source_identity: None,
tags: None,
};
assert!(collect_identity_policies(&state, &principal).is_empty());
}
const ACCT_A: &str = "111111111111";
const ACCT_B: &str = "222222222222";
fn principal_in(account: &str, user: &str) -> Principal {
Principal {
arn: Arn::global("iam", account, &format!("user/{user}")).to_string(),
user_id: format!("AIDA{user}"),
account_id: account.into(),
principal_type: PrincipalType::User,
source_identity: None,
tags: None,
}
}
fn assumed_role_principal(account: &str, role_arn_tail: &str) -> Principal {
Principal {
arn: Arn::global("sts", account, &format!("assumed-role/{role_arn_tail}")).to_string(),
user_id: "AROAEXAMPLE".into(),
account_id: account.into(),
principal_type: PrincipalType::AssumedRole,
source_identity: None,
tags: None,
}
}
fn eval_cross(
identity: Option<serde_json::Value>,
resource: Option<serde_json::Value>,
principal: &Principal,
resource_account_id: &str,
) -> Decision {
let identity_docs: Vec<PolicyDocument> = identity.into_iter().map(doc).collect();
let resource_doc = resource.map(doc);
let request = req(principal, "s3:GetObject", "arn:aws:s3:::bucket/key");
evaluate_with_resource_policy(
&identity_docs,
resource_doc.as_ref(),
&request,
resource_account_id,
)
}
fn allow_get_wildcard() -> serde_json::Value {
json!({"Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]})
}
fn deny_get_wildcard() -> serde_json::Value {
json!({"Statement":[{"Effect":"Deny","Action":"s3:GetObject","Resource":"*"}]})
}
fn resource_allow_for(principal_arn: &str) -> serde_json::Value {
json!({
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": principal_arn},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket/key"
}]
})
}
#[test]
fn same_account_identity_only_allow() {
let p = principal_in(ACCT_A, "alice");
assert_eq!(
eval_cross(Some(allow_get_wildcard()), None, &p, ACCT_A),
Decision::Allow
);
}
#[test]
fn same_account_resource_only_allow_via_user_arn() {
let p = principal_in(ACCT_A, "alice");
let resource = resource_allow_for(&p.arn);
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::Allow
);
}
#[test]
fn same_account_both_allow() {
let p = principal_in(ACCT_A, "alice");
assert_eq!(
eval_cross(
Some(allow_get_wildcard()),
Some(resource_allow_for(&p.arn)),
&p,
ACCT_A,
),
Decision::Allow
);
}
#[test]
fn same_account_neither_allows_is_implicit_deny() {
let p = principal_in(ACCT_A, "alice");
assert_eq!(eval_cross(None, None, &p, ACCT_A), Decision::ImplicitDeny);
}
#[test]
fn identity_deny_blocks_resource_allow() {
let p = principal_in(ACCT_A, "alice");
let resource = resource_allow_for(&p.arn);
assert_eq!(
eval_cross(Some(deny_get_wildcard()), Some(resource), &p, ACCT_A),
Decision::ExplicitDeny
);
}
#[test]
fn resource_deny_blocks_identity_allow() {
let p = principal_in(ACCT_A, "alice");
let resource_deny = json!({
"Statement": [{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(Some(allow_get_wildcard()), Some(resource_deny), &p, ACCT_A,),
Decision::ExplicitDeny
);
}
#[test]
fn cross_account_identity_only_is_implicit_deny() {
let p = principal_in(ACCT_A, "alice");
assert_eq!(
eval_cross(Some(allow_get_wildcard()), None, &p, ACCT_B),
Decision::ImplicitDeny
);
}
#[test]
fn cross_account_resource_only_is_implicit_deny() {
let p = principal_in(ACCT_A, "alice");
let resource = resource_allow_for(&p.arn);
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_B),
Decision::ImplicitDeny
);
}
#[test]
fn cross_account_both_allow_succeeds() {
let p = principal_in(ACCT_A, "alice");
let resource = resource_allow_for(&p.arn);
assert_eq!(
eval_cross(Some(allow_get_wildcard()), Some(resource), &p, ACCT_B),
Decision::Allow
);
}
#[test]
fn principal_wildcard_star_matches_any_principal() {
let p = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::Allow
);
}
#[test]
fn principal_aws_star_matches_any_principal() {
let p = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::Allow
);
}
#[test]
fn principal_account_root_matches_any_user_in_account() {
let p = principal_in(ACCT_A, "alice");
let resource = resource_allow_for("arn:aws:iam::111111111111:root");
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::Allow
);
}
#[test]
fn principal_account_root_does_not_match_other_account() {
let p = principal_in(ACCT_A, "alice");
let resource = resource_allow_for("arn:aws:iam::222222222222:root");
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::ImplicitDeny
);
}
#[test]
fn principal_user_arn_exact_match() {
let p = principal_in(ACCT_A, "alice");
let resource = resource_allow_for("arn:aws:iam::111111111111:user/alice");
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::Allow
);
}
#[test]
fn principal_user_arn_mismatch_is_deny() {
let p = principal_in(ACCT_A, "alice");
let resource = resource_allow_for("arn:aws:iam::111111111111:user/bob");
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::ImplicitDeny
);
}
#[test]
fn principal_service_matches_assumed_role_containing_service_host() {
let p = assumed_role_principal(
ACCT_A,
"AWSServiceRoleForLambda.lambda.amazonaws.com/session",
);
let resource = json!({
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::Allow
);
}
#[test]
fn principal_service_does_not_match_unrelated_user() {
let p = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::ImplicitDeny
);
}
#[test]
fn not_principal_deny_excludes_named_user() {
let alice = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "*"
},
{
"Effect": "Deny",
"NotPrincipal": {"AWS": Arn::global("iam", ACCT_A, "user/bob").to_string()},
"Action": "s3:GetObject",
"Resource": "*"
}
]
});
assert_eq!(
eval_cross(None, Some(resource.clone()), &alice, ACCT_A),
Decision::ExplicitDeny
);
let bob = principal_in(ACCT_A, "bob");
assert_eq!(
eval_cross(None, Some(resource), &bob, ACCT_A),
Decision::Allow
);
}
#[test]
fn not_principal_allow_excludes_named_user() {
let alice = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"NotPrincipal": {"AWS": Arn::global("iam", ACCT_A, "user/bob").to_string()},
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource.clone()), &alice, ACCT_A),
Decision::Allow
);
let bob = principal_in(ACCT_A, "bob");
assert_eq!(
eval_cross(None, Some(resource), &bob, ACCT_A),
Decision::ImplicitDeny
);
}
#[test]
fn not_principal_with_star_never_applies() {
let alice = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"NotPrincipal": "*",
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource), &alice, ACCT_A),
Decision::ImplicitDeny
);
}
#[test]
fn not_principal_with_account_root() {
let alice = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"NotPrincipal": {"AWS": Arn::global("iam", ACCT_A, "root").to_string()},
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource.clone()), &alice, ACCT_A),
Decision::ImplicitDeny
);
let eve = principal_in(ACCT_B, "eve");
let resource_deny = json!({
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "*"
},
{
"Effect": "Deny",
"NotPrincipal": {"AWS": Arn::global("iam", ACCT_A, "root").to_string()},
"Action": "s3:GetObject",
"Resource": "*"
}
]
});
assert_eq!(
eval_cross(None, Some(resource_deny.clone()), &eve, ACCT_A),
Decision::ExplicitDeny
);
assert_eq!(
eval_cross(None, Some(resource_deny), &alice, ACCT_A),
Decision::Allow
);
}
#[test]
fn not_principal_with_unrecognized_type_safe_skips() {
let alice = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"NotPrincipal": {"CanonicalUser": "abc123"},
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource), &alice, ACCT_A),
Decision::ImplicitDeny
);
}
#[test]
fn not_principal_federated_excludes_federated_callers() {
let alice = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"NotPrincipal": {"Federated": "cognito-identity.amazonaws.com"},
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource), &alice, ACCT_A),
Decision::Allow
);
}
#[test]
fn not_principal_with_multiple_entries() {
let alice = principal_in(ACCT_A, "alice");
let bob = principal_in(ACCT_A, "bob");
let charlie = principal_in(ACCT_A, "charlie");
let resource = json!({
"Statement": [{
"Effect": "Deny",
"NotPrincipal": {"AWS": [
Arn::global("iam", ACCT_A, "user/alice").to_string(),
Arn::global("iam", ACCT_A, "user/bob").to_string()
]},
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource.clone()), &alice, ACCT_A),
Decision::ImplicitDeny
);
assert_eq!(
eval_cross(None, Some(resource.clone()), &bob, ACCT_A),
Decision::ImplicitDeny
);
assert_eq!(
eval_cross(None, Some(resource), &charlie, ACCT_A),
Decision::ExplicitDeny
);
}
#[test]
fn resource_policy_statement_without_principal_is_skipped() {
let p = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "*"
}]
});
assert_eq!(
eval_cross(None, Some(resource), &p, ACCT_A),
Decision::ImplicitDeny
);
}
#[test]
fn resource_policy_condition_block_gates_access() {
use crate::condition::ConditionContext;
use std::net::IpAddr;
let p = principal_in(ACCT_A, "alice");
let resource = json!({
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "*",
"Condition": {
"IpAddress": {"aws:SourceIp": "10.0.0.0/8"}
}
}]
});
let resource_doc = doc(resource);
let ctx_ok = ConditionContext {
aws_source_ip: Some("10.1.2.3".parse::<IpAddr>().unwrap()),
..ConditionContext::default()
};
let req_ok = EvalRequest {
principal: &p,
action: "s3:GetObject".to_string(),
resource: "arn:aws:s3:::bucket/key".to_string(),
context: ctx_ok,
};
assert_eq!(
evaluate_with_resource_policy(&[], Some(&resource_doc), &req_ok, ACCT_A),
Decision::Allow
);
let ctx_bad = ConditionContext {
aws_source_ip: Some("8.8.8.8".parse::<IpAddr>().unwrap()),
..ConditionContext::default()
};
let req_bad = EvalRequest {
principal: &p,
action: "s3:GetObject".to_string(),
resource: "arn:aws:s3:::bucket/key".to_string(),
context: ctx_bad,
};
assert_eq!(
evaluate_with_resource_policy(&[], Some(&resource_doc), &req_bad, ACCT_A),
Decision::ImplicitDeny
);
}
#[test]
fn classify_aws_principal_recognizes_bare_account_id() {
assert_eq!(
classify_aws_principal("111111111111"),
PrincipalRef::AwsAccountRoot("111111111111".to_string())
);
}
#[test]
fn classify_aws_principal_recognizes_root_arn() {
assert_eq!(
classify_aws_principal("arn:aws:iam::111111111111:root"),
PrincipalRef::AwsAccountRoot("111111111111".to_string())
);
}
#[test]
fn classify_aws_principal_keeps_user_arn_as_arn() {
assert_eq!(
classify_aws_principal("arn:aws:iam::111111111111:user/alice"),
PrincipalRef::AwsArn("arn:aws:iam::111111111111:user/alice".to_string())
);
}
fn allow_all() -> PolicyDocument {
doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}]
}))
}
fn allow_get_object() -> PolicyDocument {
doc(json!({
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "*"
}]
}))
}
fn deny_put_object() -> PolicyDocument {
doc(json!({
"Statement": [{
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "*"
}]
}))
}
#[test]
fn gates_absent_behaves_like_phase2_allow() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
assert_eq!(
evaluate_with_gates(
&identity,
None,
None,
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn gates_absent_behaves_like_phase2_implicit_deny() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
assert_eq!(
evaluate_with_gates(
&[],
None,
None,
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
}
#[test]
fn boundary_caps_identity_allow() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let boundary = [allow_get_object()];
assert_eq!(
evaluate_with_gates(
&identity,
Some(&boundary),
None,
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
assert_eq!(
evaluate_with_gates(
&identity,
Some(&boundary),
None,
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
}
#[test]
fn empty_boundary_denies_everything() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let boundary: [PolicyDocument; 0] = [];
assert_eq!(
evaluate_with_gates(
&identity,
Some(&boundary),
None,
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
}
#[test]
fn explicit_deny_in_boundary_wins() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let boundary = [deny_put_object()];
assert_eq!(
evaluate_with_gates(
&identity,
Some(&boundary),
None,
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
),
Decision::ExplicitDeny
);
}
#[test]
fn identity_implicit_with_boundary_allow_is_implicit_deny() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let boundary = [allow_all()];
assert_eq!(
evaluate_with_gates(
&[],
Some(&boundary),
None,
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
}
#[test]
fn session_policy_caps_identity_allow() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let session = [allow_get_object()];
assert_eq!(
evaluate_with_gates(
&identity,
None,
Some(&session),
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
assert_eq!(
evaluate_with_gates(
&identity,
None,
Some(&session),
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn session_policy_explicit_deny_wins() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let session = [deny_put_object()];
assert_eq!(
evaluate_with_gates(
&identity,
None,
Some(&session),
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
),
Decision::ExplicitDeny
);
}
#[test]
fn boundary_and_session_must_both_allow() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let boundary = [allow_all()];
let session = [allow_get_object()];
assert_eq!(
evaluate_with_gates(
&identity,
Some(&boundary),
Some(&session),
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
),
Decision::ImplicitDeny
);
assert_eq!(
evaluate_with_gates(
&identity,
Some(&boundary),
Some(&session),
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
),
Decision::Allow
);
}
#[test]
fn resource_policy_gated_same_account_resource_bypasses_boundary() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity: [PolicyDocument; 0] = [];
let boundary: [PolicyDocument; 0] = []; let resource = doc(json!({
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:user/alice"},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket/key"
}]
}));
assert_eq!(
evaluate_with_resource_policy_and_gates(
&identity,
Some(&boundary),
None,
Some(&resource),
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
"123456789012"
),
Decision::Allow
);
}
#[test]
fn resource_policy_gated_cross_account_identity_must_allow() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity: [PolicyDocument; 0] = [];
let resource = doc(json!({
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket/key"
}]
}));
assert_eq!(
evaluate_with_resource_policy_and_gates(
&identity,
None,
None,
Some(&resource),
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
"999999999999"
),
Decision::ImplicitDeny
);
}
#[test]
fn resource_policy_gated_cross_account_boundary_caps_identity_side() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let boundary: [PolicyDocument; 0] = [];
let resource = doc(json!({
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket/key"
}]
}));
assert_eq!(
evaluate_with_resource_policy_and_gates(
&identity,
Some(&boundary),
None,
Some(&resource),
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
"999999999999"
),
Decision::ImplicitDeny
);
}
#[test]
fn resource_policy_gated_explicit_deny_in_session_wins() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let session = [deny_put_object()];
let resource = doc(json!({
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::bucket/*"
}]
}));
assert_eq!(
evaluate_with_resource_policy_and_gates(
&identity,
None,
Some(&session),
Some(&resource),
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
"123456789012"
),
Decision::ExplicitDeny
);
}
#[test]
fn scp_caps_identity_allow_all() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let scps = [allow_get_object()];
assert_eq!(
evaluate_with_gates_and_scps(
&identity,
None,
None,
Some(&scps),
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
),
Decision::Allow
);
assert_eq!(
evaluate_with_gates_and_scps(
&identity,
None,
None,
Some(&scps),
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
),
Decision::ImplicitDeny
);
}
#[test]
fn scp_explicit_deny_wins() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let scps = [deny_put_object()];
assert_eq!(
evaluate_with_gates_and_scps(
&identity,
None,
None,
Some(&scps),
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
),
Decision::ExplicitDeny
);
}
#[test]
fn scp_empty_chain_denies_everything() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let scps: [PolicyDocument; 0] = [];
assert_eq!(
evaluate_with_gates_and_scps(
&identity,
None,
None,
Some(&scps),
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
),
Decision::ImplicitDeny
);
}
#[test]
fn scp_none_preserves_identity_only_decision() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let with_scps = evaluate_with_gates_and_scps(
&identity,
None,
None,
None,
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
);
let without = evaluate_with_gates(
&identity,
None,
None,
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
);
assert_eq!(with_scps, without);
assert_eq!(with_scps, Decision::Allow);
}
#[test]
fn scp_chain_intersects_across_ancestors() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let scps = [allow_all(), allow_get_object()];
assert_eq!(
evaluate_with_gates_and_scps(
&identity,
None,
None,
Some(&scps),
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
),
Decision::Allow
);
assert_eq!(
evaluate_with_gates_and_scps(
&identity,
None,
None,
Some(&scps),
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
),
Decision::ImplicitDeny
);
}
#[test]
fn scp_intersects_with_boundary_and_session() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let identity = [allow_all()];
let boundary = [allow_all()];
let session = [allow_all()];
let scps = [allow_get_object()];
assert_eq!(
evaluate_with_gates_and_scps(
&identity,
Some(&boundary),
Some(&session),
Some(&scps),
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
),
Decision::ImplicitDeny
);
assert_eq!(
evaluate_with_gates_and_scps(
&identity,
Some(&boundary),
Some(&session),
Some(&scps),
&req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
),
Decision::Allow
);
}
#[test]
fn scp_caps_identity_side_of_resource_policy() {
let p = principal_user("arn:aws:iam::111111111111:user/alice");
let identity = [allow_all()];
let resource = doc(serde_json::json!({
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::bucket/*"
}]
}));
let scps = [allow_get_object()];
assert_eq!(
evaluate_with_resource_policy_and_gates_and_scps(
&identity,
None,
None,
Some(&scps),
Some(&resource),
&req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
"222222222222",
),
Decision::ImplicitDeny
);
}
const KMS_ACCT: &str = "123456789012";
const KMS_KEY_ARN: &str = "arn:aws:kms:us-east-1:123456789012:key/k1";
fn kms_identity_allow() -> PolicyDocument {
doc(json!({
"Version": "2012-10-17",
"Statement": [{"Effect": "Allow", "Action": "kms:Decrypt", "Resource": "*"}]
}))
}
#[test]
fn kms_nondelegating_key_policy_blocks_identity_grant() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let key_policy = doc(json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:user/bob"},
"Action": "kms:*",
"Resource": "*"
}]
}));
let r = req(&p, "kms:Decrypt", KMS_KEY_ARN);
assert_eq!(
evaluate_with_resource_policy(&[kms_identity_allow()], Some(&key_policy), &r, KMS_ACCT),
Decision::ImplicitDeny
);
}
#[test]
fn kms_default_delegating_policy_plus_identity_allows() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let key_policy = doc(json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:root"},
"Action": "kms:*",
"Resource": "*"
}]
}));
let r = req(&p, "kms:Decrypt", KMS_KEY_ARN);
assert_eq!(
evaluate_with_resource_policy(&[kms_identity_allow()], Some(&key_policy), &r, KMS_ACCT),
Decision::Allow
);
}
#[test]
fn kms_delegating_policy_without_identity_denies() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let key_policy = doc(json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:root"},
"Action": "kms:*",
"Resource": "*"
}]
}));
let r = req(&p, "kms:Decrypt", KMS_KEY_ARN);
assert_eq!(
evaluate_with_resource_policy(&[], Some(&key_policy), &r, KMS_ACCT),
Decision::ImplicitDeny
);
}
#[test]
fn kms_direct_key_policy_grant_allows_without_identity() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let key_policy = doc(json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:user/alice"},
"Action": "kms:Decrypt",
"Resource": "*"
}]
}));
let r = req(&p, "kms:Decrypt", KMS_KEY_ARN);
assert_eq!(
evaluate_with_resource_policy(&[], Some(&key_policy), &r, KMS_ACCT),
Decision::Allow
);
}
#[test]
fn kms_explicit_deny_in_key_policy_wins() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let key_policy = doc(json!({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:root"},
"Action": "kms:*",
"Resource": "*"
},
{
"Effect": "Deny",
"Principal": {"AWS": "*"},
"Action": "kms:Decrypt",
"Resource": "*"
}
]
}));
let r = req(&p, "kms:Decrypt", KMS_KEY_ARN);
assert_eq!(
evaluate_with_resource_policy(&[kms_identity_allow()], Some(&key_policy), &r, KMS_ACCT),
Decision::ExplicitDeny
);
}
#[test]
fn kms_delegation_rule_is_case_insensitive_on_action_prefix() {
let p = principal_user("arn:aws:iam::123456789012:user/alice");
let key_policy = doc(json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:user/bob"},
"Action": "kms:*",
"Resource": "*"
}]
}));
let r = req(&p, "KMS:Decrypt", KMS_KEY_ARN);
assert_eq!(
evaluate_with_resource_policy(&[kms_identity_allow()], Some(&key_policy), &r, KMS_ACCT),
Decision::ImplicitDeny
);
}