use std::sync::Arc;
use serde_json::Value;
use fakecloud_core::auth::{PassRoleError, RoleTrustValidator};
use crate::state::SharedIamState;
pub struct IamRoleTrustValidator {
state: SharedIamState,
}
impl IamRoleTrustValidator {
pub fn new(state: SharedIamState) -> Self {
Self { state }
}
pub fn shared(state: SharedIamState) -> Arc<dyn RoleTrustValidator> {
Arc::new(Self::new(state))
}
}
fn role_name_from_arn(role_arn: &str) -> Option<&str> {
let role_part = role_arn.strip_prefix("arn:aws:iam::")?;
let (_account, rest) = role_part.split_once(':')?;
let role_path = rest.strip_prefix("role/")?;
role_path.rsplit('/').next()
}
impl RoleTrustValidator for IamRoleTrustValidator {
fn validate(
&self,
account_id: &str,
role_arn: &str,
service_principal: &str,
) -> Result<(), PassRoleError> {
let Some(name) = role_name_from_arn(role_arn) else {
return Ok(());
};
let mas = self.state.read();
let Some(state) = mas.get(account_id) else {
return Ok(());
};
let Some(role) = state.roles.get(name) else {
return Ok(());
};
let parsed: Value = serde_json::from_str(&role.assume_role_policy_document)
.map_err(|_| PassRoleError::InvalidTrustPolicy(role_arn.to_string()))?;
if trust_policy_allows(&parsed, service_principal) {
Ok(())
} else {
Err(PassRoleError::TrustPolicyDenies {
role_arn: role_arn.to_string(),
service_principal: service_principal.to_string(),
})
}
}
}
fn trust_policy_allows(policy: &Value, service_principal: &str) -> bool {
let statements = match policy.get("Statement") {
Some(Value::Array(arr)) => arr.clone(),
Some(stmt) => vec![stmt.clone()],
None => return false,
};
for stmt in &statements {
let effect = stmt
.get("Effect")
.and_then(Value::as_str)
.unwrap_or("Allow"); if !effect.eq_ignore_ascii_case("Allow") {
continue;
}
if !action_includes(stmt.get("Action"), "sts:AssumeRole") {
continue;
}
let Some(principal) = stmt.get("Principal") else {
continue;
};
if principal_service_includes(principal, service_principal) {
return true;
}
}
false
}
fn action_includes(action: Option<&Value>, target: &str) -> bool {
match action {
Some(Value::String(s)) => s == target || s == "*",
Some(Value::Array(arr)) => arr.iter().any(|v| match v {
Value::String(s) => s == target || s == "*",
_ => false,
}),
_ => false,
}
}
fn principal_service_includes(principal: &Value, service_principal: &str) -> bool {
if let Value::String(s) = principal {
return s == "*";
}
if let Some(aws) = principal.get("AWS") {
match aws {
Value::String(s) if s == "*" => return true,
Value::Array(arr) if arr.iter().any(|v| v.as_str() == Some("*")) => return true,
_ => {}
}
}
let Some(svc) = principal.get("Service") else {
return false;
};
match svc {
Value::String(s) => s == "*" || s == service_principal,
Value::Array(arr) => arr.iter().any(|v| match v.as_str() {
Some("*") => true,
Some(p) => p == service_principal,
None => false,
}),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn allow(service: &str) -> Value {
serde_json::json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": {"Service": service}
}]
})
}
#[test]
fn parses_role_name_from_arn() {
assert_eq!(
role_name_from_arn("arn:aws:iam::000000000000:role/MyRole"),
Some("MyRole")
);
assert_eq!(
role_name_from_arn("arn:aws:iam::000000000000:role/service-role/MyRole"),
Some("MyRole")
);
assert_eq!(role_name_from_arn("not-an-arn"), None);
}
#[test]
fn allows_matching_service_principal() {
assert!(trust_policy_allows(
&allow("lambda.amazonaws.com"),
"lambda.amazonaws.com"
));
}
#[test]
fn rejects_other_service_principal() {
assert!(!trust_policy_allows(
&allow("ec2.amazonaws.com"),
"lambda.amazonaws.com"
));
}
#[test]
fn allows_array_principal() {
let policy = serde_json::json!({
"Statement": [{
"Effect": "Allow",
"Action": ["sts:AssumeRole"],
"Principal": {"Service": ["lambda.amazonaws.com", "ec2.amazonaws.com"]}
}]
});
assert!(trust_policy_allows(&policy, "ec2.amazonaws.com"));
}
#[test]
fn allows_wildcard_principal_string() {
let policy = serde_json::json!({
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": "*"
}]
});
assert!(trust_policy_allows(&policy, "lambda.amazonaws.com"));
}
#[test]
fn allows_wildcard_principal_aws() {
let policy = serde_json::json!({
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": {"AWS": "*"}
}]
});
assert!(trust_policy_allows(&policy, "ecs-tasks.amazonaws.com"));
}
#[test]
fn allows_wildcard_service() {
let policy = serde_json::json!({
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": {"Service": "*"}
}]
});
assert!(trust_policy_allows(&policy, "ec2.amazonaws.com"));
}
#[test]
fn rejects_deny_statement() {
let policy = serde_json::json!({
"Statement": [{
"Effect": "Deny",
"Action": "sts:AssumeRole",
"Principal": {"Service": "lambda.amazonaws.com"}
}]
});
assert!(!trust_policy_allows(&policy, "lambda.amazonaws.com"));
}
}