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 GrantLookup: Send + Sync {
33 fn allows(&self, principal_arn: &str, action: &str, resource_arn: &str) -> bool;
34}
35
36pub trait ScpLookup: Send + Sync {
37 fn lookup(&self, principal_arn: &str) -> Vec<PolicyDocument>;
38}
39
40pub struct NoopPrincipalLookup;
41
42impl PrincipalLookup for NoopPrincipalLookup {
43 fn resolve_access_key(&self, _access_key: &str) -> Option<ResolvedPrincipal> {
44 None
45 }
46}
47
48pub struct AuthzEngine {
49 pub principal_lookup: Arc<dyn PrincipalLookup>,
50 pub resource_policy_lookups: HashMap<String, Arc<dyn ResourcePolicyLookup>>,
51 pub grant_lookups: HashMap<String, Arc<dyn GrantLookup>>,
52 pub scp_lookup: Option<Arc<dyn ScpLookup>>,
53 pub enabled: bool,
54}
55
56impl AuthzEngine {
57 pub fn new(enabled: bool) -> Self {
58 Self {
59 principal_lookup: Arc::new(NoopPrincipalLookup),
60 resource_policy_lookups: HashMap::new(),
61 grant_lookups: HashMap::new(),
62 scp_lookup: None,
63 enabled,
64 }
65 }
66
67 pub fn from_env() -> Self {
68 let enabled = std::env::var("AWSIM_IAM_ENFORCE").ok().as_deref() == Some("true");
69 Self::new(enabled)
70 }
71
72 pub fn check(
73 &self,
74 ctx: &RequestContext,
75 action: &str,
76 resource: &str,
77 ) -> Result<(), AwsError> {
78 if !self.enabled {
79 return Ok(());
80 }
81
82 let access_key = match ctx.access_key.as_deref() {
83 Some(k) if !k.is_empty() => k,
84 _ => {
85 return Err(AwsError::access_denied_for(action, "anonymous", resource));
86 }
87 };
88
89 let principal = match self.principal_lookup.resolve_access_key(access_key) {
90 Some(p) => p,
91 None => {
92 return Err(AwsError::access_denied_for(
93 action,
94 &format!("AccessKey:{access_key}"),
95 resource,
96 ));
97 }
98 };
99
100 if principal.is_root {
101 return Ok(());
102 }
103
104 let resource_policy = self
105 .resource_policy_lookups
106 .get(&ctx.service)
107 .and_then(|lookup| lookup.lookup(resource));
108
109 let context: HashMap<String, ContextValue> = HashMap::new();
110
111 let req = AuthzRequest {
112 principal_arn: &principal.arn,
113 principal_account: &principal.account,
114 action,
115 resource_arn: resource,
116 context: &context,
117 };
118
119 let scps: Vec<PolicyDocument> = self
120 .scp_lookup
121 .as_ref()
122 .map(|l| l.lookup(&principal.arn))
123 .unwrap_or_default();
124
125 let eval_ctx = EvalContext {
126 identity_policies: &principal.identity_policies,
127 permissions_boundary: principal.permissions_boundary.as_ref(),
128 resource_policy: resource_policy.as_ref(),
129 scps: &scps,
130 session_policy: None,
131 };
132
133 match evaluate(&req, &eval_ctx) {
134 Decision::Allow => Ok(()),
135 Decision::ImplicitDeny => {
140 if let Some(lookup) = self.grant_lookups.get(&ctx.service)
141 && lookup.allows(&principal.arn, action, resource)
142 {
143 return Ok(());
144 }
145 Err(AwsError::access_denied_for(
146 action,
147 &principal.arn,
148 resource,
149 ))
150 }
151 Decision::ExplicitDeny => Err(AwsError::access_denied_for(
152 action,
153 &principal.arn,
154 resource,
155 )),
156 }
157 }
158}
159
160impl Default for AuthzEngine {
161 fn default() -> Self {
162 Self::new(false)
163 }
164}