diff --git a/crates/auth/src/policy/evaluator.rs b/crates/auth/src/policy/evaluator.rs index df321dedf..cf17858e3 100644 --- a/crates/auth/src/policy/evaluator.rs +++ b/crates/auth/src/policy/evaluator.rs @@ -77,6 +77,35 @@ pub fn evaluate_policies_arc( ) } +/// Returns true if an explicit Deny statement in any identity policy, +/// permissions boundary, or session policy matches the action, resource and +/// context. The server uses this only to choose the AccessDenied message form; +/// it does not change the authorization decision, which `evaluate_policies_arc` +/// already made. +#[must_use] +pub fn explicit_deny_matches_arc( + identity_policies: &[std::sync::Arc], + permissions_boundary: Option<&PolicyDocument>, + session_policy: Option<&PolicyDocument>, + action: &str, + resource_arn: &str, + context: &impl ConditionContext, +) -> bool { + identity_policies + .iter() + .map(std::sync::Arc::as_ref) + .chain(permissions_boundary) + .chain(session_policy) + .any(|policy| { + policy.statements.iter().any(|stmt| { + stmt.effect == Effect::Deny + && action_matches(stmt, action) + && resource_matches(stmt, resource_arn) + && deny_conditions_apply(stmt, context) + }) + }) +} + fn evaluate_with<'a, I>( identity_policies: I, permissions_boundary: Option<&'a PolicyDocument>, diff --git a/crates/auth/src/policy/matcher.rs b/crates/auth/src/policy/matcher.rs index 18e89a6e9..a63a21fda 100755 --- a/crates/auth/src/policy/matcher.rs +++ b/crates/auth/src/policy/matcher.rs @@ -4,8 +4,10 @@ //! Wildcard and ARN matching for IAM policy evaluation. //! //! `wildcard_match` supports `*` (zero or more characters) and `?` (exactly one -//! character). `arn_match` splits on `:` and matches each segment independently, -//! with `*` in a segment matching any value for that segment. +//! character). `arn_match` splits an ARN into its first five fields plus the +//! resource field (everything after the fifth `:`) as one field, and matches +//! each field with `wildcard_match`, so `*` and `?` in the resource field span +//! `:` and `/`. /// Match a pattern against a value. Supports `*` and `?` wildcards. /// Comparison is case-sensitive. @@ -87,11 +89,16 @@ fn wildcard_match_impl(p: &[u8], v: &[u8], ignore_case: bool) -> bool { pi == plen } -/// Match an ARN pattern against an ARN value. +/// Match an ARN pattern against an ARN value, the way IAM does. /// -/// ARN matching is segment-aware: each colon-separated segment is matched -/// independently using `wildcard_match`. Both pattern and value must have -/// the same number of segments (6 for standard ARNs). +/// The ARN is split into its first five fields (`arn`, partition, service, +/// region, account) plus the resource field (everything after the fifth `:`) +/// as a single field. Each field is matched with `wildcard_match`, so `*` and +/// `?` in the resource field span `:` and `/`. This matters for stream ARNs, +/// whose label contains `:` (for example +/// `.../stream/2026-04-08T08:40:22.123`), and for sub-resources separated by +/// `/` such as `.../index/` and `.../stream/