use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use awsim_iam_policy::{
AuthzRequest, ContextValue, Decision, EvalContext, PolicyDocument, evaluate,
};
use crate::error::AwsError;
use crate::router::RequestContext;
#[derive(Clone)]
pub struct ResolvedPrincipal {
pub arn: String,
pub account: String,
pub identity_policies: Vec<PolicyDocument>,
pub permissions_boundary: Option<PolicyDocument>,
pub is_root: bool,
pub tags: HashMap<String, String>,
pub session_policy: Option<PolicyDocument>,
}
pub trait PrincipalLookup: Send + Sync {
fn resolve_access_key(&self, access_key: &str) -> Option<ResolvedPrincipal>;
fn resolve_arn(&self, _arn: &str) -> Option<ResolvedPrincipal> {
None
}
fn resolve_secret(&self, _access_key: &str) -> Option<String> {
None
}
fn record_access_key_used(&self, _access_key: &str, _service: &str, _region: &str) {}
}
pub trait ResourcePolicyLookup: Send + Sync {
fn lookup(&self, resource_arn: &str) -> Option<PolicyDocument>;
}
pub trait GrantLookup: Send + Sync {
fn allows(&self, principal_arn: &str, action: &str, resource_arn: &str) -> bool;
}
pub trait ScpLookup: Send + Sync {
fn lookup(&self, principal_arn: &str) -> Vec<PolicyDocument>;
}
pub trait KmsKeyLookup: Send + Sync {
fn resolve_key(&self, key_ref: &str, account: &str, region: &str) -> Option<String>;
}
pub trait SecretLookup: Send + Sync {
fn secret_exists(&self, secret_ref: &str, account: &str, region: &str) -> bool;
}
pub trait ParameterLookup: Send + Sync {
fn parameter_exists(&self, parameter_ref: &str, account: &str, region: &str) -> bool;
}
pub trait CloudMapRegistrar: Send + Sync {
fn register_instance(
&self,
registry_arn: &str,
instance_id: &str,
attributes: &std::collections::HashMap<String, String>,
account: &str,
region: &str,
) -> bool;
fn deregister_instance(
&self,
registry_arn: &str,
instance_id: &str,
account: &str,
region: &str,
);
}
pub trait LambdaInvoker: Send + Sync {
fn invoke(
&self,
function_name: &str,
payload: &serde_json::Value,
account: &str,
region: &str,
) -> Result<serde_json::Value, AwsError>;
}
pub struct NoopPrincipalLookup;
impl PrincipalLookup for NoopPrincipalLookup {
fn resolve_access_key(&self, _access_key: &str) -> Option<ResolvedPrincipal> {
None
}
}
pub struct AuthzEngine {
pub principal_lookup: Arc<dyn PrincipalLookup>,
pub resource_policy_lookups: HashMap<String, Arc<dyn ResourcePolicyLookup>>,
pub grant_lookups: HashMap<String, Arc<dyn GrantLookup>>,
pub scp_lookup: Option<Arc<dyn ScpLookup>>,
enforced: AtomicBool,
pub admin_access_key: Option<String>,
}
impl AuthzEngine {
pub fn new(enabled: bool) -> Self {
Self {
principal_lookup: Arc::new(NoopPrincipalLookup),
resource_policy_lookups: HashMap::new(),
grant_lookups: HashMap::new(),
scp_lookup: None,
enforced: AtomicBool::new(enabled),
admin_access_key: None,
}
}
pub fn from_env() -> Self {
let enabled = std::env::var("AWSIM_IAM_ENFORCE").ok().as_deref() == Some("true");
let mut engine = Self::new(enabled);
engine.admin_access_key = std::env::var("AWSIM_ADMIN_ACCESS_KEY")
.ok()
.filter(|s| !s.is_empty());
engine
}
pub fn set_enabled(&self, enabled: bool) {
self.enforced.store(enabled, Ordering::Relaxed);
}
pub fn enabled(&self) -> bool {
self.enforced.load(Ordering::Relaxed)
}
fn is_admin_key(&self, key: &str) -> bool {
self.admin_access_key.as_deref() == Some(key)
}
pub fn is_admin_access_key(&self, key: &str) -> bool {
self.is_admin_key(key)
}
pub fn check(
&self,
ctx: &RequestContext,
action: &str,
resource: &str,
) -> Result<(), AwsError> {
if !self.enabled() {
return Ok(());
}
let access_key = match ctx.access_key.as_deref() {
Some(k) if !k.is_empty() => k,
_ => {
return Err(AwsError::access_denied_for(action, "anonymous", resource));
}
};
if self.is_admin_key(access_key) {
return Ok(());
}
let principal = match self.principal_lookup.resolve_access_key(access_key) {
Some(p) => p,
None => {
return Err(AwsError::access_denied_for(
action,
&format!("AccessKey:{access_key}"),
resource,
));
}
};
if principal.is_root {
return Ok(());
}
let resource_policy = self
.resource_policy_lookups
.get(&ctx.service)
.and_then(|lookup| lookup.lookup(resource));
let context = build_request_context(ctx, &principal);
let req = AuthzRequest {
principal_arn: &principal.arn,
principal_account: &principal.account,
action,
resource_arn: resource,
context: &context,
};
let scps: Vec<PolicyDocument> = self
.scp_lookup
.as_ref()
.map(|l| l.lookup(&principal.arn))
.unwrap_or_default();
let eval_ctx = EvalContext {
identity_policies: &principal.identity_policies,
permissions_boundary: principal.permissions_boundary.as_ref(),
resource_policy: resource_policy.as_ref(),
scps: &scps,
session_policy: principal.session_policy.as_ref(),
};
match evaluate(&req, &eval_ctx) {
Decision::Allow => Ok(()),
Decision::ImplicitDeny => {
if let Some(lookup) = self.grant_lookups.get(&ctx.service)
&& lookup.allows(&principal.arn, action, resource)
{
return Ok(());
}
Err(AwsError::access_denied_for(
action,
&principal.arn,
resource,
))
}
Decision::ExplicitDeny => Err(AwsError::access_denied_for(
action,
&principal.arn,
resource,
)),
}
}
}
impl Default for AuthzEngine {
fn default() -> Self {
Self::new(false)
}
}
impl AuthzEngine {
pub fn check_pass_role(
&self,
ctx: &RequestContext,
role_arn: &str,
target_service: &str,
) -> Result<(), AwsError> {
let _ = target_service;
self.check(ctx, "iam:PassRole", role_arn)
}
}
fn build_request_context(
ctx: &RequestContext,
principal: &ResolvedPrincipal,
) -> HashMap<String, ContextValue> {
let mut context = HashMap::new();
let now = chrono::Utc::now();
context.insert("aws:CurrentTime".to_string(), ContextValue::Date(now));
context.insert(
"aws:EpochTime".to_string(),
ContextValue::Number(now.timestamp() as f64),
);
context.insert(
"aws:SecureTransport".to_string(),
ContextValue::Bool(ctx.is_secure),
);
if let Some(ref ip) = ctx.source_ip {
context.insert("aws:SourceIp".to_string(), ContextValue::Ip(ip.clone()));
}
context.insert(
"aws:PrincipalArn".to_string(),
ContextValue::String(principal.arn.clone()),
);
context.insert(
"aws:PrincipalAccount".to_string(),
ContextValue::String(principal.account.clone()),
);
for (k, v) in &principal.tags {
context.insert(
format!("aws:PrincipalTag/{k}"),
ContextValue::String(v.clone()),
);
}
context
}