Skip to main content

awsim_core/
authz.rs

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
27/// Service-specific authorization side-channel. Used by KMS grants today —
28/// a grant is an out-of-band Allow that lets a principal perform listed
29/// operations on a resource even if the identity policy and key policy
30/// would otherwise deny. Returns `true` when at least one grant matches the
31/// principal + action + resource.
32pub 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            // Implicit deny is the natural outcome when neither the identity
136            // policy nor the resource policy explicitly allows. KMS grants
137            // are an out-of-band Allow path — give them a chance before we
138            // actually fail the request. Explicit deny still wins absolutely.
139            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}