1use crate::{AspenError, Context, Decision, Policy};
2
3#[derive(Clone, Debug, Eq, PartialEq, Hash)]
5pub enum PolicySource {
6 EntityInline {
8 entity_arn: String,
10
11 entity_id: String,
13
14 policy_name: String,
16 },
17
18 EntityAttachedPolicy {
20 policy_arn: String,
22
23 policy_id: String,
25
26 version: String,
28 },
29
30 GroupInline {
32 group_arn: String,
34
35 group_id: String,
37
38 policy_name: String,
40 },
41
42 GroupAttachedPolicy {
44 group_arn: String,
46
47 group_id: String,
49
50 policy_arn: String,
52
53 policy_id: String,
55
56 version: String,
58 },
59
60 Resource {
62 resource_arn: String,
64
65 policy_name: Option<String>,
67 },
68
69 PermissionBoundary {
71 policy_arn: String,
73
74 policy_id: String,
76
77 version: String,
79 },
80
81 OrgServiceControl {
83 policy_arn: String,
85
86 policy_name: String,
88
89 applied_arn: String,
91 },
92
93 Session,
95}
96
97impl PolicySource {
98 #[inline]
104 pub fn is_boundary(&self) -> bool {
105 matches!(
106 self,
107 PolicySource::PermissionBoundary { .. } | PolicySource::OrgServiceControl { .. } | PolicySource::Session
108 )
109 }
110
111 pub fn new_entity_inline<S1, S2, S3>(entity_arn: S1, entity_id: S2, policy_name: S3) -> Self
113 where
114 S1: Into<String>,
115 S2: Into<String>,
116 S3: Into<String>,
117 {
118 Self::EntityInline {
119 entity_arn: entity_arn.into(),
120 entity_id: entity_id.into(),
121 policy_name: policy_name.into(),
122 }
123 }
124
125 pub fn new_entity_attached_policy<S1, S2, S3>(policy_arn: S1, policy_id: S2, version: S3) -> Self
127 where
128 S1: Into<String>,
129 S2: Into<String>,
130 S3: Into<String>,
131 {
132 Self::EntityAttachedPolicy {
133 policy_arn: policy_arn.into(),
134 policy_id: policy_id.into(),
135 version: version.into(),
136 }
137 }
138
139 pub fn new_group_inline<S1, S2, S3>(group_arn: S1, group_id: S2, policy_name: S3) -> Self
141 where
142 S1: Into<String>,
143 S2: Into<String>,
144 S3: Into<String>,
145 {
146 Self::GroupInline {
147 group_arn: group_arn.into(),
148 group_id: group_id.into(),
149 policy_name: policy_name.into(),
150 }
151 }
152
153 pub fn new_group_attached_policy<S1, S2, S3, S4, S5>(
155 group_arn: S1,
156 group_id: S2,
157 policy_arn: S3,
158 policy_id: S4,
159 version: S5,
160 ) -> Self
161 where
162 S1: Into<String>,
163 S2: Into<String>,
164 S3: Into<String>,
165 S4: Into<String>,
166 S5: Into<String>,
167 {
168 Self::GroupAttachedPolicy {
169 group_arn: group_arn.into(),
170 group_id: group_id.into(),
171 policy_arn: policy_arn.into(),
172 policy_id: policy_id.into(),
173 version: version.into(),
174 }
175 }
176
177 pub fn new_resource<S1, S2>(resource_arn: S1, policy_name: Option<S2>) -> Self
179 where
180 S1: Into<String>,
181 S2: Into<String>,
182 {
183 Self::Resource {
184 resource_arn: resource_arn.into(),
185 policy_name: policy_name.map(|s| s.into()),
186 }
187 }
188
189 pub fn new_permission_boundary<S1, S2, S3>(policy_arn: S1, policy_id: S2, version: S3) -> Self
191 where
192 S1: Into<String>,
193 S2: Into<String>,
194 S3: Into<String>,
195 {
196 Self::PermissionBoundary {
197 policy_arn: policy_arn.into(),
198 policy_id: policy_id.into(),
199 version: version.into(),
200 }
201 }
202
203 pub fn new_org_service_control<S1, S2, S3>(policy_arn: S1, policy_name: S2, applied_arn: S3) -> Self
205 where
206 S1: Into<String>,
207 S2: Into<String>,
208 S3: Into<String>,
209 {
210 Self::OrgServiceControl {
211 policy_arn: policy_arn.into(),
212 policy_name: policy_name.into(),
213 applied_arn: applied_arn.into(),
214 }
215 }
216
217 pub fn new_session() -> Self {
219 Self::Session
220 }
221}
222
223#[derive(Clone, Debug, Eq, PartialEq)]
225pub struct PolicySet {
226 policies: Vec<(PolicySource, Policy)>,
227}
228
229impl PolicySet {
230 pub fn new() -> Self {
232 Self {
233 policies: vec![],
234 }
235 }
236
237 pub fn add_policy(&mut self, source: PolicySource, policy: Policy) {
252 self.policies.push((source, policy));
253 }
254
255 pub fn policies(&self) -> &Vec<(PolicySource, Policy)> {
257 &self.policies
258 }
259
260 pub fn evaluate<'a>(&'a self, context: &'_ Context) -> Result<(Decision, Vec<&'a PolicySource>), AspenError> {
264 self.evaluate_core(context, false)
265 }
266
267 pub fn evaluate_all<'a>(&'a self, context: &'_ Context) -> Result<(Decision, Vec<&'a PolicySource>), AspenError> {
271 self.evaluate_core(context, true)
272 }
273
274 fn evaluate_core<'a>(
275 &'a self,
276 context: &'_ Context,
277 eval_all: bool,
278 ) -> Result<(Decision, Vec<&'a PolicySource>), AspenError> {
279 let mut allowed_sources = Vec::with_capacity(self.policies.len());
280 let denied_len = if eval_all {
281 self.policies.len()
282 } else {
283 1
284 };
285 let mut denied_sources = Vec::with_capacity(denied_len);
286
287 for (source, policy) in &self.policies {
288 match policy.evaluate(context)? {
289 Decision::Allow => {
290 if !source.is_boundary() {
291 allowed_sources.push(source)
292 }
293 }
294 Decision::Deny => {
295 denied_sources.push(source);
296 if !eval_all {
297 return Ok((Decision::Deny, denied_sources));
298 }
299 }
300 Decision::DefaultDeny => {
301 if source.is_boundary() {
302 denied_sources.push(source);
303 if !eval_all {
304 return Ok((Decision::Deny, denied_sources));
305 }
306 }
307 }
308 }
309 }
310
311 if !denied_sources.is_empty() {
312 Ok((Decision::Deny, denied_sources))
313 } else if !allowed_sources.is_empty() {
314 Ok((Decision::Allow, allowed_sources))
315 } else {
316 Ok((Decision::DefaultDeny, allowed_sources))
317 }
318 }
319}
320
321impl From<Vec<(PolicySource, Policy)>> for PolicySet {
322 fn from(policies: Vec<(PolicySource, Policy)>) -> Self {
323 Self {
324 policies,
325 }
326 }
327}
328
329impl Default for PolicySet {
330 fn default() -> Self {
331 Self::new()
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use {
338 crate::{Context, Decision, Policy, PolicySet, PolicySource},
339 indoc::indoc,
340 pretty_assertions::{assert_eq, assert_ne},
341 scratchstack_arn::Arn,
342 scratchstack_aws_principal::{Principal, PrincipalIdentity, SessionData, SessionValue, User},
343 std::{
344 collections::hash_map::DefaultHasher,
345 hash::{Hash, Hasher},
346 str::FromStr,
347 },
348 };
349
350 #[test_log::test]
351 fn test_policy_source_derived() {
352 let policy_sources = vec![
353 PolicySource::new_entity_inline(
354 "arn:aws:iam::123456789012:user/MyUser",
355 "AIDAIXEXAMPLEID000000",
356 "MyPolicy",
357 ),
358 PolicySource::new_entity_attached_policy(
359 "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess",
360 "ANPAIXEXAMPLEID000000",
361 "v1",
362 ),
363 PolicySource::new_group_inline(
364 "arn:aws:iam::123456789012:group/MyGroup",
365 "AGPAIXEXAMPLEID000000",
366 "MyPolicy",
367 ),
368 PolicySource::new_group_attached_policy(
369 "arn:aws:iam::123456789012:group/MyGroup",
370 "AGPAIXEXAMPLEID000000",
371 "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess",
372 "AGPAIXEXAMPLEID000000",
373 "v1",
374 ),
375 PolicySource::new_resource("arn:aws:dynamodb:us-west-2:123456789012:table/MyTable", Some("MyTable")),
376 PolicySource::new_permission_boundary(
377 "arn:aws:iam::123456789012:policy/MyPermissionBoundary",
378 "APBAIXEXAMPLEID000000",
379 "v1",
380 ),
381 PolicySource::new_org_service_control(
382 "arn:aws:iam::123456789012:policy/MyOrgPolicy",
383 "ANPAIXEXAMPLEID000000",
384 "v1",
385 ),
386 PolicySource::new_session(),
387 ];
388
389 for i in 0..policy_sources.len() {
390 let mut h1 = DefaultHasher::new();
391 policy_sources[i].hash(&mut h1);
392 let h1 = h1.finish();
393
394 for j in 0..policy_sources.len() {
395 let mut h2 = DefaultHasher::new();
396 policy_sources[j].hash(&mut h2);
397 let h2 = h2.finish();
398
399 if i == j {
400 assert_eq!(policy_sources[i], policy_sources[j]);
401 assert_eq!(h1, h2);
402 assert_eq!(format!("{:?}", policy_sources[i]), format!("{:?}", policy_sources[j]));
403 } else {
404 assert_ne!(policy_sources[i], policy_sources[j]);
405 assert_ne!(h1, h2);
406 assert_ne!(format!("{:?}", policy_sources[i]), format!("{:?}", policy_sources[j]));
407 }
408 }
409 }
410 }
411
412 #[test_log::test]
413 #[allow(clippy::redundant_clone)]
414 fn test_eval() {
415 let mut ps = PolicySet::default();
416
417 let entity_inline_policy_source = PolicySource::new_entity_inline(
418 "arn:aws:iam::123456789012:user/MyUser",
419 "AIDAIXEXAMPLEID000000",
420 "MyPolicy",
421 );
422 let entity_inline_policy = Policy::from_str(indoc! {r#"
423 {
424 "Version": "2012-10-17",
425 "Statement": [
426 {
427 "Effect": "Allow",
428 "Action": "*",
429 "Resource": "arn:aws:s3:::mybucket",
430 "Condition": {
431 "Bool": {
432 "AllowBucketAccess": ["true"]
433 }
434 }
435 }
436 ]
437 }"#})
438 .unwrap();
439 ps.add_policy(entity_inline_policy_source.clone(), entity_inline_policy.clone());
440
441 let entity_attached_policy_source = PolicySource::new_entity_attached_policy(
442 "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess",
443 "ANPAIXEXAMPLEID000000",
444 "v1",
445 );
446 let entity_attached_policy = Policy::from_str(indoc! {r#"
447 {
448 "Version": "2012-10-17",
449 "Statement": [
450 {
451 "Effect": "Allow",
452 "Action": [
453 "s3:GetObject",
454 "s3:ListBucket",
455 "s3:ListAllMyBuckets"
456 ],
457 "Resource": "arn:aws:s3:::*"
458 }
459 ]
460 }"#})
461 .unwrap();
462 ps.add_policy(entity_attached_policy_source.clone(), entity_attached_policy.clone());
463
464 let group_inline_policy_source = PolicySource::new_group_inline(
465 "arn:aws:iam::123456789012:group/MyGroup",
466 "AGPAIXEXAMPLEID000000",
467 "MyPolicy",
468 );
469 let group_inline_policy = Policy::from_str(indoc! {r#"
470 {
471 "Version": "2012-10-17",
472 "Statement": [
473 {
474 "Effect": "Deny",
475 "Action": "ec2:RunInstances",
476 "Resource": "*"
477 },
478 {
479 "Effect": "Deny",
480 "Action": "ec2:RunInstances",
481 "NotResource": "arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0",
482 "Principal": {
483 "CanonicalUser": "9da4bcba2132ad952bba3c8ecb37e668d99b310ce313da30c98aba4cdf009a7d"
484 }
485 }
486 ]
487 }"#})
488 .unwrap();
489 ps.add_policy(group_inline_policy_source.clone(), group_inline_policy.clone());
490
491 let group_attached_policy_source = PolicySource::new_group_attached_policy(
492 "arn:aws:iam::123456789012:group/MyGroup",
493 "AGPAIXEXAMPLEID000000",
494 "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess",
495 "AGPAIXEXAMPLEID000000",
496 "v1",
497 );
498 let group_attached_policy = Policy::from_str(indoc! {r#"
499 {
500 "Version": "2012-10-17",
501 "Statement": [
502 {
503 "Effect": "Allow",
504 "Action": [
505 "ec2:Describe*"
506 ],
507 "Resource": "*",
508 "Principal": "*"
509 }
510 ]
511 }"#})
512 .unwrap();
513 ps.add_policy(group_attached_policy_source.clone(), group_attached_policy.clone());
514
515 let resource_policy_source =
516 PolicySource::new_resource("arn:aws:dynamodb:us-west-2:123456789012:table/MyTable", Some("MyTable"));
517 let resource_policy = Policy::from_str(indoc! {r#"
518 {
519 "Version": "2012-10-17",
520 "Statement": [
521 {
522 "Effect": "Allow",
523 "Action": [
524 "dynamodb:DescribeTable",
525 "dynamodb:ListTagsOfResource"
526 ],
527 "Resource": "arn:aws:dynamodb:us-west-2:123456789012:table/MyTable",
528 "Principal": {
529 "AWS": "arn:aws:iam::123456789012:user/MyUser"
530 }
531 }
532 ]
533 }
534 "#})
535 .unwrap();
536 ps.add_policy(resource_policy_source.clone(), resource_policy.clone());
537
538 let permission_boundary_policy_source = PolicySource::new_permission_boundary(
539 "arn:aws:iam::123456789012:policy/MyPermissionBoundary",
540 "APBAIXEXAMPLEID000000",
541 "v1",
542 );
543 let permission_boundary_policy = Policy::from_str(indoc! {r#"
544 {
545 "Version": "2012-10-17",
546 "Statement": [
547 {
548 "Effect": "Allow",
549 "NotAction": "iam:Create*",
550 "Resource": "*"
551 }
552 ]
553 }"#})
554 .unwrap();
555 ps.add_policy(permission_boundary_policy_source.clone(), permission_boundary_policy.clone());
556
557 let org_service_control_policy_source = PolicySource::new_org_service_control(
558 "arn:aws:iam::123456789012:policy/MyOrgPolicy",
559 "ANPAIXEXAMPLEID000000",
560 "v1",
561 );
562 let org_service_control_policy = Policy::from_str(indoc! {r#"
563 {
564 "Version": "2012-10-17",
565 "Statement": [
566 {
567 "Effect": "Deny",
568 "Action": "iam:Delete*",
569 "Resource": "*"
570 },
571 {
572 "Effect": "Allow",
573 "Action": "*",
574 "Resource": "*"
575 }
576 ]
577 }"#})
578 .unwrap();
579 ps.add_policy(org_service_control_policy_source.clone(), org_service_control_policy.clone());
580
581 let session_source = PolicySource::new_session();
582 let session_policy = Policy::from_str(indoc! {r#"
583 {
584 "Version": "2012-10-17",
585 "Statement": [
586 {
587 "Effect": "Allow",
588 "Action": "*",
589 "Resource": "*"
590 }
591 ]
592 }
593 "#})
594 .unwrap();
595 ps.add_policy(session_source.clone(), session_policy.clone());
596
597 assert_eq!(ps.policies().len(), 8);
598 let actor =
599 Principal::from(vec![PrincipalIdentity::from(User::new("aws", "123456789012", "/", "MyUser").unwrap())]);
600 let mut sd = SessionData::new();
601 sd.insert("aws:username", SessionValue::from("MyUser"));
602 let mut context_builder = Context::builder();
603 context_builder.api("DescribeSecurityGroups").actor(actor).session_data(sd).service("ec2");
604 let context = context_builder.build().unwrap();
605 let (decision, sources) = ps.evaluate_all(&context).unwrap();
606 assert_eq!(decision, Decision::Allow);
607 assert_eq!(sources.len(), 1);
608 assert_eq!(sources[0], &group_attached_policy_source);
609 assert_eq!(ps.evaluate(&context).unwrap().0, Decision::Allow);
610
611 context_builder.api("RunInstances");
612 let context = context_builder.build().unwrap();
613 let (decision, sources) = ps.evaluate_all(&context).unwrap();
614 assert_eq!(decision, Decision::Deny);
615 assert_eq!(sources.len(), 1);
616 assert_eq!(sources[0], &group_inline_policy_source);
617 assert_eq!(ps.evaluate(&context).unwrap().0, Decision::Deny);
618
619 context_builder
620 .api("DescribeTable")
621 .service("dynamodb")
622 .resources(vec![Arn::from_str("arn:aws:dynamodb:us-west-2:123456789012:table/MyTable").unwrap()]);
623 let context = context_builder.build().unwrap();
624 let (decision, sources) = ps.evaluate_all(&context).unwrap();
625 assert_eq!(decision, Decision::Allow);
626 assert_eq!(sources, vec![&resource_policy_source,]);
627 assert_eq!(ps.evaluate(&context).unwrap().0, Decision::Allow);
628
629 context_builder
630 .service("iam")
631 .api("CreateUser")
632 .resources(vec![Arn::from_str("arn:aws:iam::123456789012:user/MyUser").unwrap()]);
633 let context = context_builder.build().unwrap();
634 let (decision, sources) = ps.evaluate_all(&context).unwrap();
635 assert_eq!(decision, Decision::Deny);
636 assert_eq!(sources.len(), 1);
637 assert_eq!(sources[0], &permission_boundary_policy_source);
638 assert_eq!(ps.evaluate(&context).unwrap().0, Decision::Deny);
639
640 context_builder
641 .service("s3")
642 .api("DeleteBucket")
643 .resources(vec![Arn::from_str("arn:aws:s3:::notmybucket").unwrap()]);
644 let context = context_builder.build().unwrap();
645 let (decision, sources) = ps.evaluate_all(&context).unwrap();
646 assert_eq!(decision, Decision::DefaultDeny);
647 assert!(sources.is_empty());
648 assert_eq!(ps.evaluate(&context).unwrap().0, Decision::DefaultDeny);
649
650 let ps2 = PolicySet::from(vec![
651 (entity_inline_policy_source.clone(), entity_inline_policy.clone()),
652 (entity_attached_policy_source.clone(), entity_attached_policy.clone()),
653 (group_inline_policy_source.clone(), group_inline_policy.clone()),
654 (group_attached_policy_source.clone(), group_attached_policy.clone()),
655 (resource_policy_source.clone(), resource_policy.clone()),
656 (permission_boundary_policy_source.clone(), permission_boundary_policy.clone()),
657 (org_service_control_policy_source.clone(), org_service_control_policy.clone()),
658 (session_source.clone(), session_policy.clone()),
659 ]);
660
661 assert_eq!(ps, ps2);
662 assert_eq!(ps.clone(), ps);
663 assert_eq!(format!("{ps:?}"), format!("{ps2:?}"));
664 }
665}