1use std::collections::HashMap;
2use std::sync::Arc;
3
4use awsim_iam_policy::{
5 AuthzRequest, ContextValue, Decision, EvalContext, PolicyDocument, evaluate,
6};
7
8use crate::error::AwsError;
9use crate::router::RequestContext;
10
11pub struct ResolvedPrincipal {
12 pub arn: String,
13 pub account: String,
14 pub identity_policies: Vec<PolicyDocument>,
15 pub permissions_boundary: Option<PolicyDocument>,
16 pub is_root: bool,
17}
18
19pub trait PrincipalLookup: Send + Sync {
20 fn resolve_access_key(&self, access_key: &str) -> Option<ResolvedPrincipal>;
21}
22
23pub trait ResourcePolicyLookup: Send + Sync {
24 fn lookup(&self, resource_arn: &str) -> Option<PolicyDocument>;
25}
26
27pub trait ScpLookup: Send + Sync {
28 fn lookup(&self, principal_arn: &str) -> Vec<PolicyDocument>;
29}
30
31pub struct NoopPrincipalLookup;
32
33impl PrincipalLookup for NoopPrincipalLookup {
34 fn resolve_access_key(&self, _access_key: &str) -> Option<ResolvedPrincipal> {
35 None
36 }
37}
38
39pub struct AuthzEngine {
40 pub principal_lookup: Arc<dyn PrincipalLookup>,
41 pub resource_policy_lookups: HashMap<String, Arc<dyn ResourcePolicyLookup>>,
42 pub scp_lookup: Option<Arc<dyn ScpLookup>>,
43 pub enabled: bool,
44}
45
46impl AuthzEngine {
47 pub fn new(enabled: bool) -> Self {
48 Self {
49 principal_lookup: Arc::new(NoopPrincipalLookup),
50 resource_policy_lookups: HashMap::new(),
51 scp_lookup: None,
52 enabled,
53 }
54 }
55
56 pub fn from_env() -> Self {
57 let enabled = std::env::var("AWSIM_IAM_ENFORCE")
58 .ok()
59 .as_deref()
60 == Some("true");
61 Self::new(enabled)
62 }
63
64 pub fn check(
65 &self,
66 ctx: &RequestContext,
67 action: &str,
68 resource: &str,
69 ) -> Result<(), AwsError> {
70 if !self.enabled {
71 return Ok(());
72 }
73
74 let access_key = match ctx.access_key.as_deref() {
75 Some(k) if !k.is_empty() => k,
76 _ => {
77 return Err(AwsError::access_denied_for(
78 action,
79 "anonymous",
80 resource,
81 ));
82 }
83 };
84
85 let principal = match self.principal_lookup.resolve_access_key(access_key) {
86 Some(p) => p,
87 None => {
88 return Err(AwsError::access_denied_for(
89 action,
90 &format!("AccessKey:{access_key}"),
91 resource,
92 ));
93 }
94 };
95
96 if principal.is_root {
97 return Ok(());
98 }
99
100 let resource_policy = self
101 .resource_policy_lookups
102 .get(&ctx.service)
103 .and_then(|lookup| lookup.lookup(resource));
104
105 let context: HashMap<String, ContextValue> = HashMap::new();
106
107 let req = AuthzRequest {
108 principal_arn: &principal.arn,
109 principal_account: &principal.account,
110 action,
111 resource_arn: resource,
112 context: &context,
113 };
114
115 let scps: Vec<PolicyDocument> = self
116 .scp_lookup
117 .as_ref()
118 .map(|l| l.lookup(&principal.arn))
119 .unwrap_or_default();
120
121 let eval_ctx = EvalContext {
122 identity_policies: &principal.identity_policies,
123 permissions_boundary: principal.permissions_boundary.as_ref(),
124 resource_policy: resource_policy.as_ref(),
125 scps: &scps,
126 session_policy: None,
127 };
128
129 match evaluate(&req, &eval_ctx) {
130 Decision::Allow => Ok(()),
131 Decision::ExplicitDeny | Decision::ImplicitDeny => Err(
132 AwsError::access_denied_for(action, &principal.arn, resource),
133 ),
134 }
135 }
136}
137
138impl Default for AuthzEngine {
139 fn default() -> Self {
140 Self::new(false)
141 }
142}