1use std::collections::HashSet;
42
43use fakecloud_core::auth::{Principal, PrincipalType};
44use serde_json::Value;
45
46use crate::condition::{CompiledCondition, ConditionContext};
47use crate::state::IamState;
48
49pub type RequestContext = ConditionContext;
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Decision {
64 Allow,
65 ImplicitDeny,
66 ExplicitDeny,
67}
68
69impl Decision {
70 pub fn is_allow(self) -> bool {
72 matches!(self, Decision::Allow)
73 }
74}
75
76#[derive(Debug, Clone)]
86pub struct EvalRequest<'a> {
87 pub principal: &'a Principal,
88 pub action: String,
89 pub resource: String,
90 pub context: RequestContext,
91}
92
93#[derive(Debug, Clone)]
95pub(crate) struct ParsedStatement {
96 pub effect: Effect,
97 pub action: ActionMatch,
98 pub resource: ResourceMatch,
99 pub condition: Option<CompiledCondition>,
103 pub principal: PrincipalPattern,
107}
108
109#[derive(Debug, Clone)]
116pub(crate) enum PrincipalPattern {
117 None,
123 Principal(Vec<PrincipalRef>),
126 NotPrincipal(Vec<PrincipalRef>),
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
142pub(crate) enum PrincipalRef {
143 AnyAws,
147 AwsAccountRoot(String),
150 AwsArn(String),
154 Service(String),
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub(crate) enum Effect {
164 Allow,
165 Deny,
166}
167
168#[derive(Debug, Clone)]
171pub(crate) enum ActionMatch {
172 Action(Vec<String>),
173 NotAction(Vec<String>),
174}
175
176#[derive(Debug, Clone)]
178pub(crate) enum ResourceMatch {
179 Resource(Vec<String>),
180 NotResource(Vec<String>),
181 Implicit,
189}
190
191#[derive(Debug, Clone, Default)]
197pub struct PolicyDocument {
198 pub(crate) statements: Vec<ParsedStatement>,
199}
200
201impl PolicyDocument {
202 pub fn parse(json: &str) -> Self {
206 let value: Value = match serde_json::from_str(json) {
207 Ok(v) => v,
208 Err(e) => {
209 tracing::warn!(error = %e, "failed to parse policy document JSON; ignoring");
210 return Self::default();
211 }
212 };
213 Self::from_value(&value)
214 }
215
216 pub fn from_value(value: &Value) -> Self {
220 let statements = match value.get("Statement") {
221 Some(Value::Array(arr)) => arr.iter().filter_map(parse_statement).collect::<Vec<_>>(),
222 Some(obj @ Value::Object(_)) => parse_statement(obj).into_iter().collect(),
223 _ => Vec::new(),
224 };
225 Self { statements }
226 }
227
228 pub fn statement_count(&self) -> usize {
232 self.statements.len()
233 }
234}
235
236fn parse_statement(value: &Value) -> Option<ParsedStatement> {
237 let obj = value.as_object()?;
238 let effect = match obj.get("Effect")?.as_str()? {
239 "Allow" => Effect::Allow,
240 "Deny" => Effect::Deny,
241 other => {
242 tracing::warn!(effect = other, "unknown Effect; ignoring statement");
243 return None;
244 }
245 };
246 let action = if let Some(a) = obj.get("Action") {
247 ActionMatch::Action(coerce_string_list(a))
248 } else if let Some(na) = obj.get("NotAction") {
249 ActionMatch::NotAction(coerce_string_list(na))
250 } else {
251 tracing::warn!("statement has no Action or NotAction; ignoring");
252 return None;
253 };
254 let resource = if let Some(r) = obj.get("Resource") {
255 ResourceMatch::Resource(coerce_string_list(r))
256 } else if let Some(nr) = obj.get("NotResource") {
257 ResourceMatch::NotResource(coerce_string_list(nr))
258 } else {
259 ResourceMatch::Implicit
260 };
261 let condition = obj.get("Condition").map(CompiledCondition::parse);
262 let principal = if let Some(np) = obj.get("NotPrincipal") {
263 PrincipalPattern::NotPrincipal(parse_principal(np))
264 } else if let Some(p) = obj.get("Principal") {
265 PrincipalPattern::Principal(parse_principal(p))
266 } else {
267 PrincipalPattern::None
268 };
269 Some(ParsedStatement {
270 effect,
271 action,
272 resource,
273 condition,
274 principal,
275 })
276}
277
278fn parse_principal(value: &Value) -> Vec<PrincipalRef> {
291 let mut out = Vec::new();
292 match value {
293 Value::String(s) if s == "*" => out.push(PrincipalRef::AnyAws),
294 Value::String(other) => {
295 tracing::debug!(
296 target: "fakecloud::iam::audit",
297 principal = %other,
298 "Principal string other than \"*\" is not a recognized shape; skipping"
299 );
300 }
301 Value::Object(map) => {
302 for (key, v) in map {
303 match key.as_str() {
304 "AWS" => {
305 for s in coerce_string_list(v) {
306 out.push(classify_aws_principal(&s));
307 }
308 }
309 "Service" => {
310 for s in coerce_string_list(v) {
311 out.push(PrincipalRef::Service(s));
312 }
313 }
314 other => {
315 tracing::debug!(
316 target: "fakecloud::iam::audit",
317 principal_type = %other,
318 "Principal type not implemented in this rollout; skipping entry"
319 );
320 }
321 }
322 }
323 }
324 _ => {
325 tracing::debug!(
326 target: "fakecloud::iam::audit",
327 "Principal has an unexpected JSON shape; skipping"
328 );
329 }
330 }
331 out
332}
333
334fn classify_aws_principal(s: &str) -> PrincipalRef {
335 if s == "*" {
336 return PrincipalRef::AnyAws;
337 }
338 if let Some(rest) = s.strip_prefix("arn:aws:iam::") {
340 if let Some((account, tail)) = rest.split_once(':') {
341 if tail == "root" && !account.is_empty() {
342 return PrincipalRef::AwsAccountRoot(account.to_string());
343 }
344 }
345 }
346 if s.len() == 12 && s.chars().all(|c| c.is_ascii_digit()) {
348 return PrincipalRef::AwsAccountRoot(s.to_string());
349 }
350 PrincipalRef::AwsArn(s.to_string())
351}
352
353fn coerce_string_list(value: &Value) -> Vec<String> {
357 match value {
358 Value::String(s) => vec![s.clone()],
359 Value::Array(arr) => arr
360 .iter()
361 .filter_map(|v| v.as_str().map(|s| s.to_string()))
362 .collect(),
363 _ => Vec::new(),
364 }
365}
366
367pub fn evaluate(policies: &[PolicyDocument], request: &EvalRequest<'_>) -> Decision {
385 evaluate_with_gates(policies, None, None, request)
386}
387
388pub fn evaluate_with_gates(
410 identity: &[PolicyDocument],
411 boundary: Option<&[PolicyDocument]>,
412 session: Option<&[PolicyDocument]>,
413 request: &EvalRequest<'_>,
414) -> Decision {
415 evaluate_with_gates_and_scps(identity, boundary, session, None, request)
416}
417
418pub fn evaluate_with_gates_and_scps(
424 identity: &[PolicyDocument],
425 boundary: Option<&[PolicyDocument]>,
426 session: Option<&[PolicyDocument]>,
427 scps: Option<&[PolicyDocument]>,
428 request: &EvalRequest<'_>,
429) -> Decision {
430 let identity_decision = evaluate_inner(identity, request, false);
431 intersect_layers(identity_decision, boundary, session, scps, request)
432}
433
434fn intersect_layers(
439 identity_decision: Decision,
440 boundary: Option<&[PolicyDocument]>,
441 session: Option<&[PolicyDocument]>,
442 scps: Option<&[PolicyDocument]>,
443 request: &EvalRequest<'_>,
444) -> Decision {
445 if matches!(identity_decision, Decision::ExplicitDeny) {
446 return Decision::ExplicitDeny;
447 }
448 let scp_decision = scps.map(|docs| evaluate_scp_chain(docs, request));
453 if matches!(scp_decision, Some(Decision::ExplicitDeny)) {
454 if let Some(scps_slice) = scps {
455 tracing::debug!(
456 target: "fakecloud::iam::audit",
457 action = %request.action,
458 principal_arn = %request.principal.arn,
459 scp_count = scps_slice.len(),
460 "SCP ceiling produced ExplicitDeny"
461 );
462 }
463 return Decision::ExplicitDeny;
464 }
465 let boundary_decision = boundary.map(|policies| evaluate_inner(policies, request, false));
466 if matches!(boundary_decision, Some(Decision::ExplicitDeny)) {
467 return Decision::ExplicitDeny;
468 }
469 let session_decision = session.map(|policies| evaluate_inner(policies, request, false));
470 if matches!(session_decision, Some(Decision::ExplicitDeny)) {
471 return Decision::ExplicitDeny;
472 }
473 let identity_allows = matches!(identity_decision, Decision::Allow);
475 let boundary_allows = boundary_decision
476 .map(|d| matches!(d, Decision::Allow))
477 .unwrap_or(true);
478 let session_allows = session_decision
479 .map(|d| matches!(d, Decision::Allow))
480 .unwrap_or(true);
481 let scp_allows = scp_decision
482 .map(|d| matches!(d, Decision::Allow))
483 .unwrap_or(true);
484 if identity_allows && boundary_allows && session_allows && scp_allows {
485 Decision::Allow
486 } else {
487 if scps.is_some() && !scp_allows {
488 tracing::debug!(
489 target: "fakecloud::iam::audit",
490 action = %request.action,
491 principal_arn = %request.principal.arn,
492 "SCP ceiling did not allow action; capped to ImplicitDeny"
493 );
494 }
495 Decision::ImplicitDeny
496 }
497}
498
499fn evaluate_scp_chain(scps: &[PolicyDocument], request: &EvalRequest<'_>) -> Decision {
504 if scps.is_empty() {
505 return Decision::ImplicitDeny;
509 }
510 let mut all_allow = true;
511 for doc in scps {
512 match evaluate_inner(std::slice::from_ref(doc), request, false) {
513 Decision::ExplicitDeny => return Decision::ExplicitDeny,
514 Decision::Allow => {}
515 Decision::ImplicitDeny => all_allow = false,
516 }
517 }
518 if all_allow {
519 Decision::Allow
520 } else {
521 Decision::ImplicitDeny
522 }
523}
524
525pub fn evaluate_with_resource_policy(
541 identity_policies: &[PolicyDocument],
542 resource_policy: Option<&PolicyDocument>,
543 request: &EvalRequest<'_>,
544 resource_account_id: &str,
545) -> Decision {
546 evaluate_with_resource_policy_and_gates(
547 identity_policies,
548 None,
549 None,
550 resource_policy,
551 request,
552 resource_account_id,
553 )
554}
555
556pub fn evaluate_with_resource_policy_and_gates(
575 identity_policies: &[PolicyDocument],
576 boundary: Option<&[PolicyDocument]>,
577 session: Option<&[PolicyDocument]>,
578 resource_policy: Option<&PolicyDocument>,
579 request: &EvalRequest<'_>,
580 resource_account_id: &str,
581) -> Decision {
582 evaluate_with_resource_policy_and_gates_and_scps(
583 identity_policies,
584 boundary,
585 session,
586 None,
587 resource_policy,
588 request,
589 resource_account_id,
590 )
591}
592
593pub fn evaluate_with_resource_policy_and_gates_and_scps(
599 identity_policies: &[PolicyDocument],
600 boundary: Option<&[PolicyDocument]>,
601 session: Option<&[PolicyDocument]>,
602 scps: Option<&[PolicyDocument]>,
603 resource_policy: Option<&PolicyDocument>,
604 request: &EvalRequest<'_>,
605 resource_account_id: &str,
606) -> Decision {
607 let identity_raw = evaluate_inner(identity_policies, request, false);
608 if matches!(identity_raw, Decision::ExplicitDeny) {
609 return Decision::ExplicitDeny;
610 }
611 let identity_gated = intersect_layers(identity_raw, boundary, session, scps, request);
616 if matches!(identity_gated, Decision::ExplicitDeny) {
617 return Decision::ExplicitDeny;
618 }
619
620 let same_account = request.principal.account_id == resource_account_id;
621 if resource_policy.is_none() && same_account {
624 return identity_gated;
625 }
626 let resource = match resource_policy {
627 Some(policy) => evaluate_inner(std::slice::from_ref(policy), request, true),
628 None => Decision::ImplicitDeny,
629 };
630 if matches!(resource, Decision::ExplicitDeny) {
631 return Decision::ExplicitDeny;
632 }
633 let identity_allows = matches!(identity_gated, Decision::Allow);
634 let resource_allows = matches!(resource, Decision::Allow);
635 let allowed = if same_account {
636 identity_allows || resource_allows
637 } else {
638 identity_allows && resource_allows
639 };
640 if allowed {
641 Decision::Allow
642 } else {
643 Decision::ImplicitDeny
644 }
645}
646
647fn evaluate_inner(
648 policies: &[PolicyDocument],
649 request: &EvalRequest<'_>,
650 is_resource_policy: bool,
651) -> Decision {
652 let mut allowed = false;
653 for policy in policies {
654 for statement in &policy.statements {
655 match &statement.principal {
659 PrincipalPattern::None => {
660 if is_resource_policy {
661 tracing::debug!(
666 target: "fakecloud::iam::audit",
667 action = %request.action,
668 "resource policy statement has no Principal; skipping"
669 );
670 continue;
671 }
672 }
673 PrincipalPattern::Principal(refs) => {
674 if !principal_matches(refs, request.principal) {
675 continue;
676 }
677 }
678 PrincipalPattern::NotPrincipal(refs) => {
679 if refs.is_empty() {
680 tracing::debug!(
681 target: "fakecloud::iam::audit",
682 action = %request.action,
683 "NotPrincipal has no recognized principal types; statement does not apply"
684 );
685 continue;
686 }
687 if principal_matches(refs, request.principal) {
689 continue;
690 }
691 }
692 }
693 if !action_matches(&statement.action, &request.action) {
694 continue;
695 }
696 if !resource_matches(&statement.resource, &request.resource) {
697 continue;
698 }
699 if let Some(condition) = &statement.condition {
700 if !condition.matches(&request.context) {
701 tracing::debug!(
702 target: "fakecloud::iam::audit",
703 action = %request.action,
704 "condition did not match; statement does not apply"
705 );
706 continue;
707 }
708 }
709 match statement.effect {
710 Effect::Deny => return Decision::ExplicitDeny,
711 Effect::Allow => allowed = true,
712 }
713 }
714 }
715 if allowed {
716 Decision::Allow
717 } else {
718 Decision::ImplicitDeny
719 }
720}
721
722fn principal_matches(refs: &[PrincipalRef], principal: &Principal) -> bool {
727 refs.iter().any(|r| match r {
728 PrincipalRef::AnyAws => true,
729 PrincipalRef::AwsAccountRoot(account) => &principal.account_id == account,
730 PrincipalRef::AwsArn(arn) => &principal.arn == arn,
731 PrincipalRef::Service(service) => principal_is_service(principal, service),
732 })
733}
734
735fn principal_is_service(principal: &Principal, service: &str) -> bool {
744 matches!(principal.principal_type, PrincipalType::AssumedRole)
745 && principal.arn.contains(service)
746}
747
748fn action_matches(action: &ActionMatch, request_action: &str) -> bool {
749 match action {
750 ActionMatch::Action(patterns) => patterns
751 .iter()
752 .any(|p| iam_glob_match(p, request_action, true)),
753 ActionMatch::NotAction(patterns) => patterns
754 .iter()
755 .all(|p| !iam_glob_match(p, request_action, true)),
756 }
757}
758
759fn resource_matches(resource: &ResourceMatch, request_resource: &str) -> bool {
760 match resource {
761 ResourceMatch::Resource(patterns) => patterns
762 .iter()
763 .any(|p| iam_glob_match(p, request_resource, false)),
764 ResourceMatch::NotResource(patterns) => patterns
765 .iter()
766 .all(|p| !iam_glob_match(p, request_resource, false)),
767 ResourceMatch::Implicit => true,
768 }
769}
770
771fn iam_glob_match(pattern: &str, value: &str, case_insensitive_service_prefix: bool) -> bool {
777 if case_insensitive_service_prefix {
778 if let (Some((p_svc, p_act)), Some((v_svc, v_act))) =
779 (pattern.split_once(':'), value.split_once(':'))
780 {
781 if !glob_match(&p_svc.to_ascii_lowercase(), &v_svc.to_ascii_lowercase()) {
782 return false;
783 }
784 return glob_match(p_act, v_act);
785 }
786 }
787 glob_match(pattern, value)
788}
789
790fn glob_match(pattern: &str, value: &str) -> bool {
794 let p: Vec<char> = pattern.chars().collect();
795 let v: Vec<char> = value.chars().collect();
796 let mut pi = 0usize;
797 let mut vi = 0usize;
798 let mut star: Option<usize> = None;
799 let mut star_v: usize = 0;
800 while vi < v.len() {
801 if pi < p.len() && (p[pi] == '?' || p[pi] == v[vi]) {
802 pi += 1;
803 vi += 1;
804 } else if pi < p.len() && p[pi] == '*' {
805 star = Some(pi);
806 star_v = vi;
807 pi += 1;
808 } else if let Some(s) = star {
809 pi = s + 1;
810 star_v += 1;
811 vi = star_v;
812 } else {
813 return false;
814 }
815 }
816 while pi < p.len() && p[pi] == '*' {
817 pi += 1;
818 }
819 pi == p.len()
820}
821
822pub fn collect_identity_policies(state: &IamState, principal: &Principal) -> Vec<PolicyDocument> {
834 let mut docs = Vec::new();
835 let mut seen_managed: HashSet<String> = HashSet::new();
836 match principal.principal_type {
837 PrincipalType::User => {
838 if let Some(user_name) = user_name_from_arn(&principal.arn) {
839 collect_user_policies(state, user_name, &mut docs, &mut seen_managed);
840 }
841 }
842 PrincipalType::AssumedRole => {
843 if let Some(role_name) = role_name_from_assumed_role_arn(&principal.arn) {
844 collect_role_policies(state, role_name, &mut docs, &mut seen_managed);
845 }
846 }
847 PrincipalType::Root => {
848 }
853 PrincipalType::FederatedUser | PrincipalType::Unknown => {
854 }
856 }
857 docs
858}
859
860fn collect_user_policies(
861 state: &IamState,
862 user_name: &str,
863 docs: &mut Vec<PolicyDocument>,
864 seen_managed: &mut HashSet<String>,
865) {
866 if let Some(inline) = state.user_inline_policies.get(user_name) {
867 for doc in inline.values() {
868 docs.push(PolicyDocument::parse(doc));
869 }
870 }
871 if let Some(arns) = state.user_policies.get(user_name) {
872 for arn in arns {
873 if !seen_managed.insert(arn.clone()) {
874 continue;
875 }
876 if let Some(doc) = managed_policy_default_document(state, arn) {
877 docs.push(PolicyDocument::parse(&doc));
878 }
879 }
880 }
881 for (group_name, group) in &state.groups {
883 if !group.members.iter().any(|m| m == user_name) {
884 continue;
885 }
886 for doc in group.inline_policies.values() {
887 docs.push(PolicyDocument::parse(doc));
888 }
889 for arn in &group.attached_policies {
890 if !seen_managed.insert(arn.clone()) {
891 continue;
892 }
893 if let Some(doc) = managed_policy_default_document(state, arn) {
894 docs.push(PolicyDocument::parse(&doc));
895 }
896 }
897 let _ = group_name;
898 }
899}
900
901fn collect_role_policies(
902 state: &IamState,
903 role_name: &str,
904 docs: &mut Vec<PolicyDocument>,
905 seen_managed: &mut HashSet<String>,
906) {
907 if let Some(inline) = state.role_inline_policies.get(role_name) {
908 for doc in inline.values() {
909 docs.push(PolicyDocument::parse(doc));
910 }
911 }
912 if let Some(arns) = state.role_policies.get(role_name) {
913 for arn in arns {
914 if !seen_managed.insert(arn.clone()) {
915 continue;
916 }
917 if let Some(doc) = managed_policy_default_document(state, arn) {
918 docs.push(PolicyDocument::parse(&doc));
919 }
920 }
921 }
922}
923
924pub fn collect_boundary_policies(
946 state: &IamState,
947 principal: &Principal,
948) -> Option<Vec<PolicyDocument>> {
949 if principal.is_root() {
950 return None;
951 }
952 let boundary_arn = match principal.principal_type {
953 PrincipalType::User => {
954 let user_name = user_name_from_arn(&principal.arn)?;
955 let user = state.users.get(user_name)?;
956 user.permissions_boundary.clone()?
957 }
958 PrincipalType::AssumedRole => {
959 let role_name = role_name_from_assumed_role_arn(&principal.arn)?;
960 if role_name.starts_with("AWSServiceRoleFor") {
961 return None;
966 }
967 let role = state.roles.get(role_name)?;
968 role.permissions_boundary.clone()?
969 }
970 _ => return None,
972 };
973 match managed_policy_default_document(state, &boundary_arn) {
974 Some(doc) => Some(vec![PolicyDocument::parse(&doc)]),
975 None => {
976 tracing::debug!(
977 target: "fakecloud::iam::audit",
978 principal_arn = %principal.arn,
979 boundary_arn = %boundary_arn,
980 "permission boundary ARN does not resolve to a known managed policy; denying all actions"
981 );
982 Some(Vec::new())
983 }
984 }
985}
986
987fn managed_policy_default_document(state: &IamState, arn: &str) -> Option<String> {
988 let policy = state.policies.get(arn)?;
989 policy
990 .versions
991 .iter()
992 .find(|v| v.is_default)
993 .or_else(|| policy.versions.first())
994 .map(|v| v.document.clone())
995}
996
997fn user_name_from_arn(arn: &str) -> Option<&str> {
1008 let after = arn.rsplit_once(":user/").map(|(_, name)| name)?;
1009 Some(after.rsplit('/').next().unwrap_or(after))
1011}
1012
1013fn role_name_from_assumed_role_arn(arn: &str) -> Option<&str> {
1014 let after = arn.rsplit_once(":assumed-role/")?.1;
1016 Some(after.split('/').next().unwrap_or(after))
1017}
1018
1019#[cfg(test)]
1020#[allow(clippy::cloned_ref_to_slice_refs)]
1021mod tests {
1022 use super::*;
1023 use serde_json::json;
1024
1025 fn principal_user(arn: &str) -> Principal {
1026 Principal {
1027 arn: arn.to_string(),
1028 user_id: "AIDA".into(),
1029 account_id: "123456789012".into(),
1030 principal_type: PrincipalType::User,
1031 source_identity: None,
1032 tags: None,
1033 }
1034 }
1035
1036 fn req<'a>(principal: &'a Principal, action: &str, resource: &str) -> EvalRequest<'a> {
1037 EvalRequest {
1038 principal,
1039 action: action.to_string(),
1040 resource: resource.to_string(),
1041 context: RequestContext::default(),
1042 }
1043 }
1044
1045 fn doc(json: serde_json::Value) -> PolicyDocument {
1046 PolicyDocument::from_value(&json)
1047 }
1048
1049 #[test]
1052 fn glob_literal_match() {
1053 assert!(glob_match("foo", "foo"));
1054 assert!(!glob_match("foo", "bar"));
1055 }
1056
1057 #[test]
1058 fn glob_star_matches_any() {
1059 assert!(glob_match("*", "foo"));
1060 assert!(glob_match("*", ""));
1061 assert!(glob_match("foo*", "foobar"));
1062 assert!(glob_match("*bar", "foobar"));
1063 assert!(glob_match("f*r", "foobar"));
1064 assert!(!glob_match("foo*", "fo"));
1065 }
1066
1067 #[test]
1068 fn glob_question_mark_matches_one() {
1069 assert!(glob_match("f?o", "foo"));
1070 assert!(!glob_match("f?o", "fo"));
1071 assert!(!glob_match("f?o", "foo!"));
1072 }
1073
1074 #[test]
1075 fn glob_no_backtracking_explosion() {
1076 assert!(!glob_match("a*a*a*a*a*b", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
1078 }
1079
1080 #[test]
1083 fn iam_action_service_prefix_is_case_insensitive() {
1084 assert!(iam_glob_match("S3:GetObject", "s3:GetObject", true));
1085 assert!(iam_glob_match("s3:GetObject", "S3:GetObject", true));
1086 }
1087
1088 #[test]
1089 fn iam_action_name_is_case_sensitive() {
1090 assert!(!iam_glob_match("s3:getobject", "s3:GetObject", true));
1092 assert!(iam_glob_match("s3:GetObject", "s3:GetObject", true));
1093 }
1094
1095 #[test]
1096 fn iam_action_supports_wildcards() {
1097 assert!(iam_glob_match("s3:Get*", "s3:GetObject", true));
1098 assert!(iam_glob_match("s3:*", "s3:DeleteObject", true));
1099 assert!(iam_glob_match("*", "s3:GetObject", true));
1100 assert!(!iam_glob_match("s3:Get*", "s3:PutObject", true));
1101 }
1102
1103 #[test]
1106 fn empty_policy_set_is_implicit_deny() {
1107 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1108 assert_eq!(
1109 evaluate(&[], &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")),
1110 Decision::ImplicitDeny
1111 );
1112 }
1113
1114 #[test]
1115 fn allow_with_matching_action_and_resource() {
1116 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1117 let policy = doc(json!({
1118 "Version": "2012-10-17",
1119 "Statement": [{
1120 "Effect": "Allow",
1121 "Action": "s3:GetObject",
1122 "Resource": "arn:aws:s3:::bucket/key"
1123 }]
1124 }));
1125 assert_eq!(
1126 evaluate(
1127 &[policy],
1128 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
1129 ),
1130 Decision::Allow
1131 );
1132 }
1133
1134 #[test]
1135 fn deny_takes_precedence_over_allow() {
1136 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1137 let allow = doc(json!({
1138 "Statement": [{
1139 "Effect": "Allow",
1140 "Action": "*",
1141 "Resource": "*"
1142 }]
1143 }));
1144 let deny = doc(json!({
1145 "Statement": [{
1146 "Effect": "Deny",
1147 "Action": "s3:DeleteObject",
1148 "Resource": "*"
1149 }]
1150 }));
1151 assert_eq!(
1152 evaluate(
1153 &[allow.clone(), deny.clone()],
1154 &req(&p, "s3:DeleteObject", "arn:aws:s3:::bucket/key")
1155 ),
1156 Decision::ExplicitDeny
1157 );
1158 assert_eq!(
1160 evaluate(
1161 &[deny, allow],
1162 &req(&p, "s3:DeleteObject", "arn:aws:s3:::bucket/key")
1163 ),
1164 Decision::ExplicitDeny
1165 );
1166 }
1167
1168 #[test]
1169 fn allow_with_wrong_action_is_implicit_deny() {
1170 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1171 let policy = doc(json!({
1172 "Statement": [{
1173 "Effect": "Allow",
1174 "Action": "s3:GetObject",
1175 "Resource": "*"
1176 }]
1177 }));
1178 assert_eq!(
1179 evaluate(
1180 &[policy],
1181 &req(&p, "s3:DeleteObject", "arn:aws:s3:::bucket/key")
1182 ),
1183 Decision::ImplicitDeny
1184 );
1185 }
1186
1187 #[test]
1188 fn allow_with_wrong_resource_is_implicit_deny() {
1189 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1190 let policy = doc(json!({
1191 "Statement": [{
1192 "Effect": "Allow",
1193 "Action": "s3:GetObject",
1194 "Resource": "arn:aws:s3:::other-bucket/*"
1195 }]
1196 }));
1197 assert_eq!(
1198 evaluate(
1199 &[policy],
1200 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
1201 ),
1202 Decision::ImplicitDeny
1203 );
1204 }
1205
1206 #[test]
1207 fn resource_wildcard_matches_arn_path() {
1208 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1209 let policy = doc(json!({
1210 "Statement": [{
1211 "Effect": "Allow",
1212 "Action": "s3:GetObject",
1213 "Resource": "arn:aws:s3:::bucket/*"
1214 }]
1215 }));
1216 assert_eq!(
1217 evaluate(
1218 &[policy],
1219 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/path/to/key")
1220 ),
1221 Decision::Allow
1222 );
1223 }
1224
1225 #[test]
1226 fn not_action_excludes_listed_actions() {
1227 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1228 let policy = doc(json!({
1229 "Statement": [{
1230 "Effect": "Allow",
1231 "NotAction": "s3:DeleteObject",
1232 "Resource": "*"
1233 }]
1234 }));
1235 assert_eq!(
1237 evaluate(
1238 &[policy.clone()],
1239 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
1240 ),
1241 Decision::Allow
1242 );
1243 assert_eq!(
1245 evaluate(
1246 &[policy],
1247 &req(&p, "s3:DeleteObject", "arn:aws:s3:::bucket/key")
1248 ),
1249 Decision::ImplicitDeny
1250 );
1251 }
1252
1253 #[test]
1254 fn not_resource_excludes_listed_resources() {
1255 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1256 let policy = doc(json!({
1257 "Statement": [{
1258 "Effect": "Allow",
1259 "Action": "s3:GetObject",
1260 "NotResource": "arn:aws:s3:::secret-bucket/*"
1261 }]
1262 }));
1263 assert_eq!(
1264 evaluate(
1265 &[policy.clone()],
1266 &req(&p, "s3:GetObject", "arn:aws:s3:::public-bucket/key")
1267 ),
1268 Decision::Allow
1269 );
1270 assert_eq!(
1271 evaluate(
1272 &[policy],
1273 &req(&p, "s3:GetObject", "arn:aws:s3:::secret-bucket/key")
1274 ),
1275 Decision::ImplicitDeny
1276 );
1277 }
1278
1279 fn req_with_ctx<'a>(
1280 principal: &'a Principal,
1281 action: &str,
1282 resource: &str,
1283 context: RequestContext,
1284 ) -> EvalRequest<'a> {
1285 EvalRequest {
1286 principal,
1287 action: action.to_string(),
1288 resource: resource.to_string(),
1289 context,
1290 }
1291 }
1292
1293 fn ctx_alice() -> RequestContext {
1294 RequestContext {
1295 aws_username: Some("alice".into()),
1296 aws_principal_arn: Some("arn:aws:iam::123456789012:user/alice".into()),
1297 aws_principal_account: Some("123456789012".into()),
1298 aws_principal_type: Some("User".into()),
1299 aws_userid: Some("AIDA".into()),
1300 ..Default::default()
1301 }
1302 }
1303
1304 #[test]
1305 fn condition_string_equals_username_allows_match() {
1306 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1307 let policy = doc(json!({
1308 "Statement": [{
1309 "Effect": "Allow",
1310 "Action": "*",
1311 "Resource": "*",
1312 "Condition": { "StringEquals": { "aws:username": "alice" } }
1313 }]
1314 }));
1315 assert_eq!(
1316 evaluate(
1317 &[policy],
1318 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
1319 ),
1320 Decision::Allow
1321 );
1322 }
1323
1324 #[test]
1325 fn condition_string_equals_username_denies_mismatch() {
1326 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1327 let policy = doc(json!({
1328 "Statement": [{
1329 "Effect": "Allow",
1330 "Action": "*",
1331 "Resource": "*",
1332 "Condition": { "StringEquals": { "aws:username": "bob" } }
1333 }]
1334 }));
1335 assert_eq!(
1336 evaluate(
1337 &[policy],
1338 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
1339 ),
1340 Decision::ImplicitDeny
1341 );
1342 }
1343
1344 #[test]
1345 fn deny_with_condition_fires_when_condition_matches() {
1346 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1347 let policy = doc(json!({
1351 "Statement": [
1352 {
1353 "Effect": "Deny",
1354 "Action": "*",
1355 "Resource": "*",
1356 "Condition": { "Bool": { "aws:SecureTransport": "false" } }
1357 },
1358 {
1359 "Effect": "Allow",
1360 "Action": "s3:GetObject",
1361 "Resource": "*"
1362 }
1363 ]
1364 }));
1365 let mut ctx = ctx_alice();
1366 ctx.aws_secure_transport = Some(false);
1367 assert_eq!(
1368 evaluate(
1369 &[policy.clone()],
1370 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx)
1371 ),
1372 Decision::ExplicitDeny
1373 );
1374 let mut ctx_secure = ctx_alice();
1377 ctx_secure.aws_secure_transport = Some(true);
1378 assert_eq!(
1379 evaluate(
1380 &[policy],
1381 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_secure)
1382 ),
1383 Decision::Allow
1384 );
1385 }
1386
1387 #[test]
1388 fn condition_ip_address_allows_within_cidr() {
1389 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1390 let policy = doc(json!({
1391 "Statement": [{
1392 "Effect": "Allow",
1393 "Action": "s3:GetObject",
1394 "Resource": "*",
1395 "Condition": { "IpAddress": { "aws:SourceIp": "10.0.0.0/24" } }
1396 }]
1397 }));
1398 let mut ctx = ctx_alice();
1399 ctx.aws_source_ip = Some("10.0.0.17".parse().unwrap());
1400 assert_eq!(
1401 evaluate(
1402 &[policy.clone()],
1403 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx)
1404 ),
1405 Decision::Allow
1406 );
1407 let mut wrong = ctx_alice();
1408 wrong.aws_source_ip = Some("192.168.1.1".parse().unwrap());
1409 assert_eq!(
1410 evaluate(
1411 &[policy],
1412 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", wrong)
1413 ),
1414 Decision::ImplicitDeny
1415 );
1416 }
1417
1418 #[test]
1419 fn condition_date_less_than_blocks_expired() {
1420 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1421 let policy = doc(json!({
1422 "Statement": [{
1423 "Effect": "Allow",
1424 "Action": "s3:GetObject",
1425 "Resource": "*",
1426 "Condition": {
1427 "DateLessThan": { "aws:CurrentTime": "2020-01-01T00:00:00Z" }
1428 }
1429 }]
1430 }));
1431 let mut ctx = ctx_alice();
1432 ctx.aws_current_time = Some(
1433 chrono::DateTime::parse_from_rfc3339("2024-06-15T12:00:00Z")
1434 .unwrap()
1435 .with_timezone(&chrono::Utc),
1436 );
1437 assert_eq!(
1438 evaluate(
1439 &[policy],
1440 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx)
1441 ),
1442 Decision::ImplicitDeny
1443 );
1444 }
1445
1446 #[test]
1447 fn condition_missing_key_without_if_exists_denies() {
1448 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1451 let policy = doc(json!({
1452 "Statement": [{
1453 "Effect": "Allow",
1454 "Action": "*",
1455 "Resource": "*",
1456 "Condition": { "IpAddress": { "aws:SourceIp": "10.0.0.0/8" } }
1457 }]
1458 }));
1459 assert_eq!(
1460 evaluate(
1461 &[policy],
1462 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
1463 ),
1464 Decision::ImplicitDeny
1465 );
1466 }
1467
1468 #[test]
1469 fn condition_if_exists_passes_on_missing_key() {
1470 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1471 let policy = doc(json!({
1472 "Statement": [{
1473 "Effect": "Allow",
1474 "Action": "*",
1475 "Resource": "*",
1476 "Condition": {
1477 "IpAddressIfExists": { "aws:SourceIp": "10.0.0.0/8" }
1478 }
1479 }]
1480 }));
1481 assert_eq!(
1483 evaluate(
1484 &[policy],
1485 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
1486 ),
1487 Decision::Allow
1488 );
1489 }
1490
1491 #[test]
1492 fn condition_multiple_operators_all_must_match() {
1493 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1494 let policy = doc(json!({
1495 "Statement": [{
1496 "Effect": "Allow",
1497 "Action": "*",
1498 "Resource": "*",
1499 "Condition": {
1500 "StringEquals": { "aws:username": "alice" },
1501 "IpAddress": { "aws:SourceIp": "10.0.0.0/24" }
1502 }
1503 }]
1504 }));
1505 let mut ctx = ctx_alice();
1506 ctx.aws_source_ip = Some("10.0.0.1".parse().unwrap());
1507 assert_eq!(
1508 evaluate(
1509 &[policy.clone()],
1510 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx)
1511 ),
1512 Decision::Allow
1513 );
1514 let mut wrong_ip = ctx_alice();
1515 wrong_ip.aws_source_ip = Some("192.168.1.1".parse().unwrap());
1516 assert_eq!(
1517 evaluate(
1518 &[policy],
1519 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", wrong_ip)
1520 ),
1521 Decision::ImplicitDeny
1522 );
1523 }
1524
1525 #[test]
1526 fn condition_unknown_operator_fails_closed() {
1527 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1528 let policy = doc(json!({
1529 "Statement": [{
1530 "Effect": "Allow",
1531 "Action": "*",
1532 "Resource": "*",
1533 "Condition": { "NotARealOperator": { "aws:username": "alice" } }
1534 }]
1535 }));
1536 assert_eq!(
1537 evaluate(
1538 &[policy],
1539 &req_with_ctx(&p, "s3:GetObject", "arn:aws:s3:::bucket/key", ctx_alice())
1540 ),
1541 Decision::ImplicitDeny
1542 );
1543 }
1544
1545 #[test]
1546 fn array_action_matches_any_entry() {
1547 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1548 let policy = doc(json!({
1549 "Statement": [{
1550 "Effect": "Allow",
1551 "Action": ["s3:GetObject", "s3:PutObject"],
1552 "Resource": "*"
1553 }]
1554 }));
1555 assert_eq!(
1556 evaluate(
1557 &[policy.clone()],
1558 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
1559 ),
1560 Decision::Allow
1561 );
1562 assert_eq!(
1563 evaluate(
1564 &[policy],
1565 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
1566 ),
1567 Decision::Allow
1568 );
1569 }
1570
1571 #[test]
1572 fn statement_without_effect_is_dropped() {
1573 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1574 let policy = doc(json!({
1575 "Statement": [
1576 { "Action": "s3:GetObject", "Resource": "*" },
1577 { "Effect": "Allow", "Action": "s3:GetObject", "Resource": "*" }
1578 ]
1579 }));
1580 assert_eq!(policy.statement_count(), 1);
1583 assert_eq!(
1584 evaluate(
1585 &[policy],
1586 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
1587 ),
1588 Decision::Allow
1589 );
1590 }
1591
1592 #[test]
1593 fn statement_without_action_is_dropped() {
1594 let policy = doc(json!({
1595 "Statement": [{ "Effect": "Allow", "Resource": "*" }]
1596 }));
1597 assert_eq!(policy.statement_count(), 0);
1598 }
1599
1600 #[test]
1601 fn implicit_resource_acts_like_wildcard() {
1602 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1603 let policy = doc(json!({
1604 "Statement": [{ "Effect": "Allow", "Action": "s3:GetObject" }]
1605 }));
1606 assert_eq!(
1607 evaluate(
1608 &[policy],
1609 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
1610 ),
1611 Decision::Allow
1612 );
1613 }
1614
1615 #[test]
1616 fn malformed_policy_json_is_implicit_deny() {
1617 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1618 let policy = PolicyDocument::parse("{ this is not valid json");
1619 assert_eq!(policy.statement_count(), 0);
1620 assert_eq!(
1621 evaluate(
1622 &[policy],
1623 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
1624 ),
1625 Decision::ImplicitDeny
1626 );
1627 }
1628
1629 #[test]
1630 fn deny_short_circuits_after_match() {
1631 let p = principal_user("arn:aws:iam::123456789012:user/alice");
1632 let policy = doc(json!({
1633 "Statement": [
1634 { "Effect": "Deny", "Action": "*", "Resource": "*" },
1635 { "Effect": "Allow", "Action": "s3:GetObject", "Resource": "*" }
1636 ]
1637 }));
1638 assert_eq!(
1639 evaluate(
1640 &[policy],
1641 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
1642 ),
1643 Decision::ExplicitDeny
1644 );
1645 }
1646
1647 #[test]
1648 fn user_name_from_arn_strips_iam_path() {
1649 assert_eq!(
1651 user_name_from_arn("arn:aws:iam::123456789012:user/alice"),
1652 Some("alice")
1653 );
1654 assert_eq!(
1659 user_name_from_arn("arn:aws:iam::123456789012:user/engineering/alice"),
1660 Some("alice")
1661 );
1662 assert_eq!(
1663 user_name_from_arn("arn:aws:iam::123456789012:user/path/to/alice"),
1664 Some("alice")
1665 );
1666 assert_eq!(user_name_from_arn("arn:aws:iam::123456789012:role/r"), None);
1667 }
1668
1669 #[test]
1670 fn collect_identity_policies_resolves_pathed_user() {
1671 use crate::state::IamUser;
1675 use chrono::Utc;
1676 let mut state = IamState::new("123456789012");
1677 state.users.insert(
1678 "alice".to_string(),
1679 IamUser {
1680 user_name: "alice".into(),
1681 user_id: "AIDAALICE".into(),
1682 arn: "arn:aws:iam::123456789012:user/engineering/alice".into(),
1683 path: "/engineering/".into(),
1684 created_at: Utc::now(),
1685 tags: Vec::new(),
1686 permissions_boundary: None,
1687 },
1688 );
1689 let mut inline = std::collections::HashMap::new();
1690 inline.insert(
1691 "AllowGet".to_string(),
1692 r#"{"Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#
1693 .to_string(),
1694 );
1695 state
1696 .user_inline_policies
1697 .insert("alice".to_string(), inline);
1698
1699 let principal = Principal {
1700 arn: "arn:aws:iam::123456789012:user/engineering/alice".to_string(),
1701 user_id: "AIDAALICE".to_string(),
1702 account_id: "123456789012".to_string(),
1703 principal_type: PrincipalType::User,
1704 source_identity: None,
1705 tags: None,
1706 };
1707 let docs = collect_identity_policies(&state, &principal);
1708 assert_eq!(docs.len(), 1, "pathed user's inline policy was missed");
1709 assert_eq!(
1710 evaluate(
1711 &docs,
1712 &req(&principal, "s3:GetObject", "arn:aws:s3:::bucket/key")
1713 ),
1714 Decision::Allow
1715 );
1716 }
1717
1718 #[test]
1719 fn role_name_from_assumed_role_arn_strips_session() {
1720 assert_eq!(
1721 role_name_from_assumed_role_arn("arn:aws:sts::123456789012:assumed-role/ops/session-1"),
1722 Some("ops")
1723 );
1724 }
1725
1726 #[test]
1729 fn collect_identity_policies_picks_up_user_inline() {
1730 use crate::state::IamUser;
1731 use chrono::Utc;
1732 let mut state = IamState::new("123456789012");
1733 state.users.insert(
1734 "alice".to_string(),
1735 IamUser {
1736 user_name: "alice".into(),
1737 user_id: "AIDAALICE".into(),
1738 arn: "arn:aws:iam::123456789012:user/alice".into(),
1739 path: "/".into(),
1740 created_at: Utc::now(),
1741 tags: Vec::new(),
1742 permissions_boundary: None,
1743 },
1744 );
1745 let mut inline = std::collections::HashMap::new();
1746 inline.insert(
1747 "AllowGet".to_string(),
1748 r#"{"Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#
1749 .to_string(),
1750 );
1751 state
1752 .user_inline_policies
1753 .insert("alice".to_string(), inline);
1754
1755 let principal = principal_user("arn:aws:iam::123456789012:user/alice");
1756 let docs = collect_identity_policies(&state, &principal);
1757 assert_eq!(docs.len(), 1);
1758 assert_eq!(
1759 evaluate(
1760 &docs,
1761 &req(&principal, "s3:GetObject", "arn:aws:s3:::bucket/key")
1762 ),
1763 Decision::Allow
1764 );
1765 }
1766
1767 #[test]
1768 fn collect_identity_policies_picks_up_managed_via_groups() {
1769 use crate::state::{IamGroup, IamPolicy, IamUser, PolicyVersion};
1770 use chrono::Utc;
1771 let mut state = IamState::new("123456789012");
1772 state.users.insert(
1773 "alice".to_string(),
1774 IamUser {
1775 user_name: "alice".into(),
1776 user_id: "AIDAALICE".into(),
1777 arn: "arn:aws:iam::123456789012:user/alice".into(),
1778 path: "/".into(),
1779 created_at: Utc::now(),
1780 tags: Vec::new(),
1781 permissions_boundary: None,
1782 },
1783 );
1784 let policy_arn = "arn:aws:iam::123456789012:policy/AllowGet".to_string();
1785 state.policies.insert(
1786 policy_arn.clone(),
1787 IamPolicy {
1788 policy_name: "AllowGet".into(),
1789 policy_id: "ANPA1".into(),
1790 arn: policy_arn.clone(),
1791 path: "/".into(),
1792 description: "".into(),
1793 created_at: Utc::now(),
1794 tags: Vec::new(),
1795 default_version_id: "v1".into(),
1796 versions: vec![PolicyVersion {
1797 version_id: "v1".into(),
1798 document: r#"{"Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#.into(),
1799 is_default: true,
1800 created_at: Utc::now(),
1801 }],
1802 next_version_num: 2,
1803 attachment_count: 1,
1804 },
1805 );
1806 state.groups.insert(
1807 "readers".to_string(),
1808 IamGroup {
1809 group_name: "readers".into(),
1810 group_id: "AGPA1".into(),
1811 arn: "arn:aws:iam::123456789012:group/readers".into(),
1812 path: "/".into(),
1813 created_at: Utc::now(),
1814 members: vec!["alice".into()],
1815 inline_policies: std::collections::HashMap::new(),
1816 attached_policies: vec![policy_arn],
1817 },
1818 );
1819 let principal = principal_user("arn:aws:iam::123456789012:user/alice");
1820 let docs = collect_identity_policies(&state, &principal);
1821 assert_eq!(docs.len(), 1);
1822 assert_eq!(
1823 evaluate(
1824 &docs,
1825 &req(&principal, "s3:GetObject", "arn:aws:s3:::bucket/key")
1826 ),
1827 Decision::Allow
1828 );
1829 }
1830
1831 #[test]
1832 fn collect_identity_policies_for_root_returns_empty() {
1833 let state = IamState::new("123456789012");
1834 let principal = Principal {
1835 arn: "arn:aws:iam::123456789012:root".into(),
1836 user_id: "ROOT".into(),
1837 account_id: "123456789012".into(),
1838 principal_type: PrincipalType::Root,
1839 source_identity: None,
1840 tags: None,
1841 };
1842 assert!(collect_identity_policies(&state, &principal).is_empty());
1846 }
1847
1848 const ACCT_A: &str = "111111111111";
1851 const ACCT_B: &str = "222222222222";
1852
1853 fn principal_in(account: &str, user: &str) -> Principal {
1854 Principal {
1855 arn: format!("arn:aws:iam::{account}:user/{user}"),
1856 user_id: format!("AIDA{user}"),
1857 account_id: account.into(),
1858 principal_type: PrincipalType::User,
1859 source_identity: None,
1860 tags: None,
1861 }
1862 }
1863
1864 fn assumed_role_principal(account: &str, role_arn_tail: &str) -> Principal {
1865 Principal {
1866 arn: format!("arn:aws:sts::{account}:assumed-role/{role_arn_tail}"),
1867 user_id: "AROAEXAMPLE".into(),
1868 account_id: account.into(),
1869 principal_type: PrincipalType::AssumedRole,
1870 source_identity: None,
1871 tags: None,
1872 }
1873 }
1874
1875 fn eval_cross(
1876 identity: Option<serde_json::Value>,
1877 resource: Option<serde_json::Value>,
1878 principal: &Principal,
1879 resource_account_id: &str,
1880 ) -> Decision {
1881 let identity_docs: Vec<PolicyDocument> = identity.into_iter().map(doc).collect();
1882 let resource_doc = resource.map(doc);
1883 let request = req(principal, "s3:GetObject", "arn:aws:s3:::bucket/key");
1884 evaluate_with_resource_policy(
1885 &identity_docs,
1886 resource_doc.as_ref(),
1887 &request,
1888 resource_account_id,
1889 )
1890 }
1891
1892 fn allow_get_wildcard() -> serde_json::Value {
1893 json!({"Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]})
1894 }
1895
1896 fn deny_get_wildcard() -> serde_json::Value {
1897 json!({"Statement":[{"Effect":"Deny","Action":"s3:GetObject","Resource":"*"}]})
1898 }
1899
1900 fn resource_allow_for(principal_arn: &str) -> serde_json::Value {
1901 json!({
1902 "Statement": [{
1903 "Effect": "Allow",
1904 "Principal": {"AWS": principal_arn},
1905 "Action": "s3:GetObject",
1906 "Resource": "arn:aws:s3:::bucket/key"
1907 }]
1908 })
1909 }
1910
1911 #[test]
1912 fn same_account_identity_only_allow() {
1913 let p = principal_in(ACCT_A, "alice");
1914 assert_eq!(
1915 eval_cross(Some(allow_get_wildcard()), None, &p, ACCT_A),
1916 Decision::Allow
1917 );
1918 }
1919
1920 #[test]
1921 fn same_account_resource_only_allow_via_user_arn() {
1922 let p = principal_in(ACCT_A, "alice");
1923 let resource = resource_allow_for(&p.arn);
1924 assert_eq!(
1925 eval_cross(None, Some(resource), &p, ACCT_A),
1926 Decision::Allow
1927 );
1928 }
1929
1930 #[test]
1931 fn same_account_both_allow() {
1932 let p = principal_in(ACCT_A, "alice");
1933 assert_eq!(
1934 eval_cross(
1935 Some(allow_get_wildcard()),
1936 Some(resource_allow_for(&p.arn)),
1937 &p,
1938 ACCT_A,
1939 ),
1940 Decision::Allow
1941 );
1942 }
1943
1944 #[test]
1945 fn same_account_neither_allows_is_implicit_deny() {
1946 let p = principal_in(ACCT_A, "alice");
1947 assert_eq!(eval_cross(None, None, &p, ACCT_A), Decision::ImplicitDeny);
1948 }
1949
1950 #[test]
1951 fn identity_deny_blocks_resource_allow() {
1952 let p = principal_in(ACCT_A, "alice");
1953 let resource = resource_allow_for(&p.arn);
1954 assert_eq!(
1955 eval_cross(Some(deny_get_wildcard()), Some(resource), &p, ACCT_A),
1956 Decision::ExplicitDeny
1957 );
1958 }
1959
1960 #[test]
1961 fn resource_deny_blocks_identity_allow() {
1962 let p = principal_in(ACCT_A, "alice");
1963 let resource_deny = json!({
1964 "Statement": [{
1965 "Effect": "Deny",
1966 "Principal": "*",
1967 "Action": "s3:GetObject",
1968 "Resource": "*"
1969 }]
1970 });
1971 assert_eq!(
1972 eval_cross(Some(allow_get_wildcard()), Some(resource_deny), &p, ACCT_A,),
1973 Decision::ExplicitDeny
1974 );
1975 }
1976
1977 #[test]
1978 fn cross_account_identity_only_is_implicit_deny() {
1979 let p = principal_in(ACCT_A, "alice");
1982 assert_eq!(
1983 eval_cross(Some(allow_get_wildcard()), None, &p, ACCT_B),
1984 Decision::ImplicitDeny
1985 );
1986 }
1987
1988 #[test]
1989 fn cross_account_resource_only_is_implicit_deny() {
1990 let p = principal_in(ACCT_A, "alice");
1993 let resource = resource_allow_for(&p.arn);
1994 assert_eq!(
1995 eval_cross(None, Some(resource), &p, ACCT_B),
1996 Decision::ImplicitDeny
1997 );
1998 }
1999
2000 #[test]
2001 fn cross_account_both_allow_succeeds() {
2002 let p = principal_in(ACCT_A, "alice");
2003 let resource = resource_allow_for(&p.arn);
2004 assert_eq!(
2005 eval_cross(Some(allow_get_wildcard()), Some(resource), &p, ACCT_B),
2006 Decision::Allow
2007 );
2008 }
2009
2010 #[test]
2011 fn principal_wildcard_star_matches_any_principal() {
2012 let p = principal_in(ACCT_A, "alice");
2013 let resource = json!({
2014 "Statement": [{
2015 "Effect": "Allow",
2016 "Principal": "*",
2017 "Action": "s3:GetObject",
2018 "Resource": "*"
2019 }]
2020 });
2021 assert_eq!(
2022 eval_cross(None, Some(resource), &p, ACCT_A),
2023 Decision::Allow
2024 );
2025 }
2026
2027 #[test]
2028 fn principal_aws_star_matches_any_principal() {
2029 let p = principal_in(ACCT_A, "alice");
2030 let resource = json!({
2031 "Statement": [{
2032 "Effect": "Allow",
2033 "Principal": {"AWS": "*"},
2034 "Action": "s3:GetObject",
2035 "Resource": "*"
2036 }]
2037 });
2038 assert_eq!(
2039 eval_cross(None, Some(resource), &p, ACCT_A),
2040 Decision::Allow
2041 );
2042 }
2043
2044 #[test]
2045 fn principal_account_root_matches_any_user_in_account() {
2046 let p = principal_in(ACCT_A, "alice");
2047 let resource = resource_allow_for("arn:aws:iam::111111111111:root");
2048 assert_eq!(
2049 eval_cross(None, Some(resource), &p, ACCT_A),
2050 Decision::Allow
2051 );
2052 }
2053
2054 #[test]
2055 fn principal_account_root_does_not_match_other_account() {
2056 let p = principal_in(ACCT_A, "alice");
2057 let resource = resource_allow_for("arn:aws:iam::222222222222:root");
2058 assert_eq!(
2059 eval_cross(None, Some(resource), &p, ACCT_A),
2060 Decision::ImplicitDeny
2061 );
2062 }
2063
2064 #[test]
2065 fn principal_user_arn_exact_match() {
2066 let p = principal_in(ACCT_A, "alice");
2067 let resource = resource_allow_for("arn:aws:iam::111111111111:user/alice");
2068 assert_eq!(
2069 eval_cross(None, Some(resource), &p, ACCT_A),
2070 Decision::Allow
2071 );
2072 }
2073
2074 #[test]
2075 fn principal_user_arn_mismatch_is_deny() {
2076 let p = principal_in(ACCT_A, "alice");
2077 let resource = resource_allow_for("arn:aws:iam::111111111111:user/bob");
2078 assert_eq!(
2079 eval_cross(None, Some(resource), &p, ACCT_A),
2080 Decision::ImplicitDeny
2081 );
2082 }
2083
2084 #[test]
2085 fn principal_service_matches_assumed_role_containing_service_host() {
2086 let p = assumed_role_principal(
2087 ACCT_A,
2088 "AWSServiceRoleForLambda.lambda.amazonaws.com/session",
2089 );
2090 let resource = json!({
2091 "Statement": [{
2092 "Effect": "Allow",
2093 "Principal": {"Service": "lambda.amazonaws.com"},
2094 "Action": "s3:GetObject",
2095 "Resource": "*"
2096 }]
2097 });
2098 assert_eq!(
2099 eval_cross(None, Some(resource), &p, ACCT_A),
2100 Decision::Allow
2101 );
2102 }
2103
2104 #[test]
2105 fn principal_service_does_not_match_unrelated_user() {
2106 let p = principal_in(ACCT_A, "alice");
2107 let resource = json!({
2108 "Statement": [{
2109 "Effect": "Allow",
2110 "Principal": {"Service": "lambda.amazonaws.com"},
2111 "Action": "s3:GetObject",
2112 "Resource": "*"
2113 }]
2114 });
2115 assert_eq!(
2116 eval_cross(None, Some(resource), &p, ACCT_A),
2117 Decision::ImplicitDeny
2118 );
2119 }
2120
2121 #[test]
2122 fn not_principal_deny_excludes_named_user() {
2123 let alice = principal_in(ACCT_A, "alice");
2126 let resource = json!({
2127 "Statement": [
2128 {
2129 "Effect": "Allow",
2130 "Principal": "*",
2131 "Action": "s3:GetObject",
2132 "Resource": "*"
2133 },
2134 {
2135 "Effect": "Deny",
2136 "NotPrincipal": {"AWS": format!("arn:aws:iam::{ACCT_A}:user/bob")},
2137 "Action": "s3:GetObject",
2138 "Resource": "*"
2139 }
2140 ]
2141 });
2142 assert_eq!(
2143 eval_cross(None, Some(resource.clone()), &alice, ACCT_A),
2144 Decision::ExplicitDeny
2145 );
2146
2147 let bob = principal_in(ACCT_A, "bob");
2149 assert_eq!(
2150 eval_cross(None, Some(resource), &bob, ACCT_A),
2151 Decision::Allow
2152 );
2153 }
2154
2155 #[test]
2156 fn not_principal_allow_excludes_named_user() {
2157 let alice = principal_in(ACCT_A, "alice");
2160 let resource = json!({
2161 "Statement": [{
2162 "Effect": "Allow",
2163 "NotPrincipal": {"AWS": format!("arn:aws:iam::{ACCT_A}:user/bob")},
2164 "Action": "s3:GetObject",
2165 "Resource": "*"
2166 }]
2167 });
2168 assert_eq!(
2169 eval_cross(None, Some(resource.clone()), &alice, ACCT_A),
2170 Decision::Allow
2171 );
2172
2173 let bob = principal_in(ACCT_A, "bob");
2175 assert_eq!(
2176 eval_cross(None, Some(resource), &bob, ACCT_A),
2177 Decision::ImplicitDeny
2178 );
2179 }
2180
2181 #[test]
2182 fn not_principal_with_star_never_applies() {
2183 let alice = principal_in(ACCT_A, "alice");
2185 let resource = json!({
2186 "Statement": [{
2187 "Effect": "Allow",
2188 "NotPrincipal": "*",
2189 "Action": "s3:GetObject",
2190 "Resource": "*"
2191 }]
2192 });
2193 assert_eq!(
2194 eval_cross(None, Some(resource), &alice, ACCT_A),
2195 Decision::ImplicitDeny
2196 );
2197 }
2198
2199 #[test]
2200 fn not_principal_with_account_root() {
2201 let alice = principal_in(ACCT_A, "alice");
2205 let resource = json!({
2206 "Statement": [{
2207 "Effect": "Allow",
2208 "NotPrincipal": {"AWS": format!("arn:aws:iam::{ACCT_A}:root")},
2209 "Action": "s3:GetObject",
2210 "Resource": "*"
2211 }]
2212 });
2213 assert_eq!(
2214 eval_cross(None, Some(resource.clone()), &alice, ACCT_A),
2215 Decision::ImplicitDeny
2216 );
2217
2218 let eve = principal_in(ACCT_B, "eve");
2222 let resource_deny = json!({
2223 "Statement": [
2224 {
2225 "Effect": "Allow",
2226 "Principal": "*",
2227 "Action": "s3:GetObject",
2228 "Resource": "*"
2229 },
2230 {
2231 "Effect": "Deny",
2232 "NotPrincipal": {"AWS": format!("arn:aws:iam::{ACCT_A}:root")},
2233 "Action": "s3:GetObject",
2234 "Resource": "*"
2235 }
2236 ]
2237 });
2238 assert_eq!(
2240 eval_cross(None, Some(resource_deny.clone()), &eve, ACCT_A),
2241 Decision::ExplicitDeny
2242 );
2243 assert_eq!(
2245 eval_cross(None, Some(resource_deny), &alice, ACCT_A),
2246 Decision::Allow
2247 );
2248 }
2249
2250 #[test]
2251 fn not_principal_with_unrecognized_type_safe_skips() {
2252 let alice = principal_in(ACCT_A, "alice");
2255 let resource = json!({
2256 "Statement": [{
2257 "Effect": "Allow",
2258 "NotPrincipal": {"Federated": "cognito-identity.amazonaws.com"},
2259 "Action": "s3:GetObject",
2260 "Resource": "*"
2261 }]
2262 });
2263 assert_eq!(
2264 eval_cross(None, Some(resource), &alice, ACCT_A),
2265 Decision::ImplicitDeny
2266 );
2267 }
2268
2269 #[test]
2270 fn not_principal_with_multiple_entries() {
2271 let alice = principal_in(ACCT_A, "alice");
2274 let bob = principal_in(ACCT_A, "bob");
2275 let charlie = principal_in(ACCT_A, "charlie");
2276 let resource = json!({
2277 "Statement": [{
2278 "Effect": "Deny",
2279 "NotPrincipal": {"AWS": [
2280 format!("arn:aws:iam::{ACCT_A}:user/alice"),
2281 format!("arn:aws:iam::{ACCT_A}:user/bob")
2282 ]},
2283 "Action": "s3:GetObject",
2284 "Resource": "*"
2285 }]
2286 });
2287 assert_eq!(
2289 eval_cross(None, Some(resource.clone()), &alice, ACCT_A),
2290 Decision::ImplicitDeny
2291 );
2292 assert_eq!(
2293 eval_cross(None, Some(resource.clone()), &bob, ACCT_A),
2294 Decision::ImplicitDeny
2295 );
2296 assert_eq!(
2298 eval_cross(None, Some(resource), &charlie, ACCT_A),
2299 Decision::ExplicitDeny
2300 );
2301 }
2302
2303 #[test]
2304 fn resource_policy_statement_without_principal_is_skipped() {
2305 let p = principal_in(ACCT_A, "alice");
2308 let resource = json!({
2309 "Statement": [{
2310 "Effect": "Allow",
2311 "Action": "s3:GetObject",
2312 "Resource": "*"
2313 }]
2314 });
2315 assert_eq!(
2316 eval_cross(None, Some(resource), &p, ACCT_A),
2317 Decision::ImplicitDeny
2318 );
2319 }
2320
2321 #[test]
2322 fn resource_policy_condition_block_gates_access() {
2323 use crate::condition::ConditionContext;
2326 use std::net::IpAddr;
2327
2328 let p = principal_in(ACCT_A, "alice");
2329 let resource = json!({
2330 "Statement": [{
2331 "Effect": "Allow",
2332 "Principal": "*",
2333 "Action": "s3:GetObject",
2334 "Resource": "*",
2335 "Condition": {
2336 "IpAddress": {"aws:SourceIp": "10.0.0.0/8"}
2337 }
2338 }]
2339 });
2340 let resource_doc = doc(resource);
2341
2342 let ctx_ok = ConditionContext {
2343 aws_source_ip: Some("10.1.2.3".parse::<IpAddr>().unwrap()),
2344 ..ConditionContext::default()
2345 };
2346 let req_ok = EvalRequest {
2347 principal: &p,
2348 action: "s3:GetObject".to_string(),
2349 resource: "arn:aws:s3:::bucket/key".to_string(),
2350 context: ctx_ok,
2351 };
2352 assert_eq!(
2353 evaluate_with_resource_policy(&[], Some(&resource_doc), &req_ok, ACCT_A),
2354 Decision::Allow
2355 );
2356
2357 let ctx_bad = ConditionContext {
2358 aws_source_ip: Some("8.8.8.8".parse::<IpAddr>().unwrap()),
2359 ..ConditionContext::default()
2360 };
2361 let req_bad = EvalRequest {
2362 principal: &p,
2363 action: "s3:GetObject".to_string(),
2364 resource: "arn:aws:s3:::bucket/key".to_string(),
2365 context: ctx_bad,
2366 };
2367 assert_eq!(
2368 evaluate_with_resource_policy(&[], Some(&resource_doc), &req_bad, ACCT_A),
2369 Decision::ImplicitDeny
2370 );
2371 }
2372
2373 #[test]
2374 fn classify_aws_principal_recognizes_bare_account_id() {
2375 assert_eq!(
2376 classify_aws_principal("111111111111"),
2377 PrincipalRef::AwsAccountRoot("111111111111".to_string())
2378 );
2379 }
2380
2381 #[test]
2382 fn classify_aws_principal_recognizes_root_arn() {
2383 assert_eq!(
2384 classify_aws_principal("arn:aws:iam::111111111111:root"),
2385 PrincipalRef::AwsAccountRoot("111111111111".to_string())
2386 );
2387 }
2388
2389 #[test]
2390 fn classify_aws_principal_keeps_user_arn_as_arn() {
2391 assert_eq!(
2392 classify_aws_principal("arn:aws:iam::111111111111:user/alice"),
2393 PrincipalRef::AwsArn("arn:aws:iam::111111111111:user/alice".to_string())
2394 );
2395 }
2396
2397 fn allow_all() -> PolicyDocument {
2400 doc(json!({
2401 "Statement": [{
2402 "Effect": "Allow",
2403 "Action": "*",
2404 "Resource": "*"
2405 }]
2406 }))
2407 }
2408
2409 fn allow_get_object() -> PolicyDocument {
2410 doc(json!({
2411 "Statement": [{
2412 "Effect": "Allow",
2413 "Action": "s3:GetObject",
2414 "Resource": "*"
2415 }]
2416 }))
2417 }
2418
2419 fn deny_put_object() -> PolicyDocument {
2420 doc(json!({
2421 "Statement": [{
2422 "Effect": "Deny",
2423 "Action": "s3:PutObject",
2424 "Resource": "*"
2425 }]
2426 }))
2427 }
2428
2429 #[test]
2430 fn gates_absent_behaves_like_phase2_allow() {
2431 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2432 let identity = [allow_all()];
2433 assert_eq!(
2434 evaluate_with_gates(
2435 &identity,
2436 None,
2437 None,
2438 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
2439 ),
2440 Decision::Allow
2441 );
2442 }
2443
2444 #[test]
2445 fn gates_absent_behaves_like_phase2_implicit_deny() {
2446 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2447 assert_eq!(
2448 evaluate_with_gates(
2449 &[],
2450 None,
2451 None,
2452 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
2453 ),
2454 Decision::ImplicitDeny
2455 );
2456 }
2457
2458 #[test]
2459 fn boundary_caps_identity_allow() {
2460 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2461 let identity = [allow_all()];
2462 let boundary = [allow_get_object()];
2463 assert_eq!(
2465 evaluate_with_gates(
2466 &identity,
2467 Some(&boundary),
2468 None,
2469 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
2470 ),
2471 Decision::Allow
2472 );
2473 assert_eq!(
2475 evaluate_with_gates(
2476 &identity,
2477 Some(&boundary),
2478 None,
2479 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
2480 ),
2481 Decision::ImplicitDeny
2482 );
2483 }
2484
2485 #[test]
2486 fn empty_boundary_denies_everything() {
2487 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2488 let identity = [allow_all()];
2489 let boundary: [PolicyDocument; 0] = [];
2490 assert_eq!(
2493 evaluate_with_gates(
2494 &identity,
2495 Some(&boundary),
2496 None,
2497 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
2498 ),
2499 Decision::ImplicitDeny
2500 );
2501 }
2502
2503 #[test]
2504 fn explicit_deny_in_boundary_wins() {
2505 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2506 let identity = [allow_all()];
2507 let boundary = [deny_put_object()];
2508 assert_eq!(
2509 evaluate_with_gates(
2510 &identity,
2511 Some(&boundary),
2512 None,
2513 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
2514 ),
2515 Decision::ExplicitDeny
2516 );
2517 }
2518
2519 #[test]
2520 fn identity_implicit_with_boundary_allow_is_implicit_deny() {
2521 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2524 let boundary = [allow_all()];
2525 assert_eq!(
2526 evaluate_with_gates(
2527 &[],
2528 Some(&boundary),
2529 None,
2530 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
2531 ),
2532 Decision::ImplicitDeny
2533 );
2534 }
2535
2536 #[test]
2537 fn session_policy_caps_identity_allow() {
2538 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2539 let identity = [allow_all()];
2540 let session = [allow_get_object()];
2541 assert_eq!(
2542 evaluate_with_gates(
2543 &identity,
2544 None,
2545 Some(&session),
2546 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
2547 ),
2548 Decision::ImplicitDeny
2549 );
2550 assert_eq!(
2551 evaluate_with_gates(
2552 &identity,
2553 None,
2554 Some(&session),
2555 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
2556 ),
2557 Decision::Allow
2558 );
2559 }
2560
2561 #[test]
2562 fn session_policy_explicit_deny_wins() {
2563 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2564 let identity = [allow_all()];
2565 let session = [deny_put_object()];
2566 assert_eq!(
2567 evaluate_with_gates(
2568 &identity,
2569 None,
2570 Some(&session),
2571 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
2572 ),
2573 Decision::ExplicitDeny
2574 );
2575 }
2576
2577 #[test]
2578 fn boundary_and_session_must_both_allow() {
2579 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2580 let identity = [allow_all()];
2581 let boundary = [allow_all()];
2582 let session = [allow_get_object()];
2583 assert_eq!(
2585 evaluate_with_gates(
2586 &identity,
2587 Some(&boundary),
2588 Some(&session),
2589 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key")
2590 ),
2591 Decision::ImplicitDeny
2592 );
2593 assert_eq!(
2594 evaluate_with_gates(
2595 &identity,
2596 Some(&boundary),
2597 Some(&session),
2598 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key")
2599 ),
2600 Decision::Allow
2601 );
2602 }
2603
2604 #[test]
2607 fn resource_policy_gated_same_account_resource_bypasses_boundary() {
2608 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2612 let identity: [PolicyDocument; 0] = [];
2613 let boundary: [PolicyDocument; 0] = []; let resource = doc(json!({
2615 "Statement": [{
2616 "Effect": "Allow",
2617 "Principal": {"AWS": "arn:aws:iam::123456789012:user/alice"},
2618 "Action": "s3:GetObject",
2619 "Resource": "arn:aws:s3:::bucket/key"
2620 }]
2621 }));
2622 assert_eq!(
2623 evaluate_with_resource_policy_and_gates(
2624 &identity,
2625 Some(&boundary),
2626 None,
2627 Some(&resource),
2628 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
2629 "123456789012"
2630 ),
2631 Decision::Allow
2632 );
2633 }
2634
2635 #[test]
2636 fn resource_policy_gated_cross_account_identity_must_allow() {
2637 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2641 let identity: [PolicyDocument; 0] = [];
2642 let resource = doc(json!({
2643 "Statement": [{
2644 "Effect": "Allow",
2645 "Principal": "*",
2646 "Action": "s3:GetObject",
2647 "Resource": "arn:aws:s3:::bucket/key"
2648 }]
2649 }));
2650 assert_eq!(
2651 evaluate_with_resource_policy_and_gates(
2652 &identity,
2653 None,
2654 None,
2655 Some(&resource),
2656 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
2657 "999999999999"
2658 ),
2659 Decision::ImplicitDeny
2660 );
2661 }
2662
2663 #[test]
2664 fn resource_policy_gated_cross_account_boundary_caps_identity_side() {
2665 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2669 let identity = [allow_all()];
2670 let boundary: [PolicyDocument; 0] = [];
2671 let resource = doc(json!({
2672 "Statement": [{
2673 "Effect": "Allow",
2674 "Principal": "*",
2675 "Action": "s3:GetObject",
2676 "Resource": "arn:aws:s3:::bucket/key"
2677 }]
2678 }));
2679 assert_eq!(
2680 evaluate_with_resource_policy_and_gates(
2681 &identity,
2682 Some(&boundary),
2683 None,
2684 Some(&resource),
2685 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
2686 "999999999999"
2687 ),
2688 Decision::ImplicitDeny
2689 );
2690 }
2691
2692 #[test]
2693 fn resource_policy_gated_explicit_deny_in_session_wins() {
2694 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2695 let identity = [allow_all()];
2696 let session = [deny_put_object()];
2697 let resource = doc(json!({
2698 "Statement": [{
2699 "Effect": "Allow",
2700 "Principal": "*",
2701 "Action": "s3:PutObject",
2702 "Resource": "arn:aws:s3:::bucket/*"
2703 }]
2704 }));
2705 assert_eq!(
2706 evaluate_with_resource_policy_and_gates(
2707 &identity,
2708 None,
2709 Some(&session),
2710 Some(&resource),
2711 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
2712 "123456789012"
2713 ),
2714 Decision::ExplicitDeny
2715 );
2716 }
2717
2718 #[test]
2721 fn scp_caps_identity_allow_all() {
2722 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2723 let identity = [allow_all()];
2724 let scps = [allow_get_object()];
2725 assert_eq!(
2726 evaluate_with_gates_and_scps(
2727 &identity,
2728 None,
2729 None,
2730 Some(&scps),
2731 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
2732 ),
2733 Decision::Allow
2734 );
2735 assert_eq!(
2736 evaluate_with_gates_and_scps(
2737 &identity,
2738 None,
2739 None,
2740 Some(&scps),
2741 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
2742 ),
2743 Decision::ImplicitDeny
2744 );
2745 }
2746
2747 #[test]
2748 fn scp_explicit_deny_wins() {
2749 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2750 let identity = [allow_all()];
2751 let scps = [deny_put_object()];
2752 assert_eq!(
2753 evaluate_with_gates_and_scps(
2754 &identity,
2755 None,
2756 None,
2757 Some(&scps),
2758 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
2759 ),
2760 Decision::ExplicitDeny
2761 );
2762 }
2763
2764 #[test]
2765 fn scp_empty_chain_denies_everything() {
2766 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2767 let identity = [allow_all()];
2768 let scps: [PolicyDocument; 0] = [];
2769 assert_eq!(
2773 evaluate_with_gates_and_scps(
2774 &identity,
2775 None,
2776 None,
2777 Some(&scps),
2778 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
2779 ),
2780 Decision::ImplicitDeny
2781 );
2782 }
2783
2784 #[test]
2785 fn scp_none_preserves_identity_only_decision() {
2786 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2790 let identity = [allow_all()];
2791 let with_scps = evaluate_with_gates_and_scps(
2792 &identity,
2793 None,
2794 None,
2795 None,
2796 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
2797 );
2798 let without = evaluate_with_gates(
2799 &identity,
2800 None,
2801 None,
2802 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
2803 );
2804 assert_eq!(with_scps, without);
2805 assert_eq!(with_scps, Decision::Allow);
2806 }
2807
2808 #[test]
2809 fn scp_chain_intersects_across_ancestors() {
2810 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2813 let identity = [allow_all()];
2814 let scps = [allow_all(), allow_get_object()];
2815 assert_eq!(
2816 evaluate_with_gates_and_scps(
2817 &identity,
2818 None,
2819 None,
2820 Some(&scps),
2821 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
2822 ),
2823 Decision::Allow
2824 );
2825 assert_eq!(
2826 evaluate_with_gates_and_scps(
2827 &identity,
2828 None,
2829 None,
2830 Some(&scps),
2831 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
2832 ),
2833 Decision::ImplicitDeny
2834 );
2835 }
2836
2837 #[test]
2838 fn scp_intersects_with_boundary_and_session() {
2839 let p = principal_user("arn:aws:iam::123456789012:user/alice");
2840 let identity = [allow_all()];
2841 let boundary = [allow_all()];
2842 let session = [allow_all()];
2843 let scps = [allow_get_object()];
2844 assert_eq!(
2845 evaluate_with_gates_and_scps(
2846 &identity,
2847 Some(&boundary),
2848 Some(&session),
2849 Some(&scps),
2850 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
2851 ),
2852 Decision::ImplicitDeny
2853 );
2854 assert_eq!(
2855 evaluate_with_gates_and_scps(
2856 &identity,
2857 Some(&boundary),
2858 Some(&session),
2859 Some(&scps),
2860 &req(&p, "s3:GetObject", "arn:aws:s3:::bucket/key"),
2861 ),
2862 Decision::Allow
2863 );
2864 }
2865
2866 #[test]
2867 fn scp_caps_identity_side_of_resource_policy() {
2868 let p = principal_user("arn:aws:iam::111111111111:user/alice");
2872 let identity = [allow_all()];
2873 let resource = doc(serde_json::json!({
2874 "Statement": [{
2875 "Effect": "Allow",
2876 "Principal": "*",
2877 "Action": "s3:PutObject",
2878 "Resource": "arn:aws:s3:::bucket/*"
2879 }]
2880 }));
2881 let scps = [allow_get_object()];
2882 assert_eq!(
2883 evaluate_with_resource_policy_and_gates_and_scps(
2884 &identity,
2885 None,
2886 None,
2887 Some(&scps),
2888 Some(&resource),
2889 &req(&p, "s3:PutObject", "arn:aws:s3:::bucket/key"),
2890 "222222222222",
2891 ),
2892 Decision::ImplicitDeny
2893 );
2894 }
2895}