iam-rs 0.7.0

Complete Rust library for parsing, validating, and evaluating IAM policies. Provider-agnostic authorization engine with full AWS IAM compatibility.
Documentation

iam-rs

USAGE NOTICE: This library is still in final phase of verification and may have breaking changes. Pin exact version. It is functional and ready to use, but it has not been externally verified. Small differences may exist compared to AWS's own internal implementation. Please report any discrepancies.

Crates.io Documentation License: MIT

A comprehensive Rust library for parsing, validating, and evaluating AWS IAM (Identity and Access Management) policies. Provider-agnostic and designed for building flexible authorization systems with full AWS IAM compatibility.

๐Ÿš€ Key Features

  • ๐Ÿ”’ Complete IAM Policy Support: Full implementation of AWS IAM policy language including conditions, principals, actions, and resources
  • โš–๏ธ Policy Evaluation Engine: Production-ready authorization engine with proper AWS IAM precedence rules
  • ๐Ÿท๏ธ Advanced ARN Support: Comprehensive ARN parsing, validation, and wildcard pattern matching
  • ๐ŸŽฏ Rich Condition Engine: Support for all AWS condition operators (String, Numeric, Date, Boolean, IP, ARN, Binary, Null)
  • ๏ฟฝ Variable Interpolation: Dynamic policy variables with default fallback values (e.g., ${aws:username, 'anonymous'})
  • ๐Ÿ“ฆ Type-Safe APIs: Strong typing with comprehensive enums, builder patterns, and Serde integration
  • โšก High Performance: Zero-copy parsing, efficient evaluation, and minimal dependencies
  • ๐Ÿงช Production Ready: Extensive test suite with 100+ tests covering real-world scenarios

๐Ÿ“ฆ Installation

cargo add iam-rs

๐Ÿƒ Quick Start

Simple Authorization Check

use iam_rs::{evaluate_policy, Arn, IAMRequest, IAMPolicy, IAMStatement, Effect, Action, IAMResource, Decision, Principal, PrincipalId};

// Create a policy allowing S3 read access
let policy = IAMPolicy::new()
    .add_statement(
        IAMStatement::new(IAMEffect::Allow)
            .with_action(IAMAction::Single("s3:GetObject".to_string()))
            .with_resource(IAMResource::Single("arn:aws:s3:::my-bucket/*".to_string()))
    );

// Create an authorization request
let request = IAMRequest::new(
    Principal::Aws(PrincipalId::String("arn:aws:iam::123456789012:user/alice".to_string()))
    "s3:GetObject",
    Arn::parse("arn:aws:s3:::my-bucket/file.txt").unwrap()
);

// Evaluate the request
match evaluate_policy(&policy, &request)? {
    Decision::Allow => println!("โœ“ Access granted"),
    Decision::Deny => println!("โœ— Access denied"),
    Decision::NotApplicable => println!("? No applicable policy (implicit deny)"),
}

Policy with Conditions

use iam_rs::{IAMPolicy, IAMStatement, IAMEffect, IAMAction, IAMResource, IAMOperator, Context, ContextValue};
use serde_json::json;

// Create context for condition evaluation
let mut context = Context::new();
context.insert("aws:username".to_string(), ContextValue::String("alice".to_string()));
context.insert("aws:CurrentTime".to_string(), ContextValue::String("2024-06-01T12:00:00Z".to_string()));
context.insert("s3:prefix".to_string(), ContextValue::String("uploads/".to_string()));

// Policy with string and date conditions
let policy = IAMPolicy::new()
    .with_id("ConditionalPolicy")
    .add_statement(
        IAMStatement::new(IAMEffect::Allow)
            .with_sid("AllowUploadToUserFolder")
            .with_action(IAMAction::Single("s3:PutObject".to_string()))
            .with_resource(IAMResource::Single("arn:aws:s3:::my-bucket/${aws:username}/*".to_string()))
            .with_condition(
                IAMOperator::StringEquals,
                "s3:prefix".to_string(),
                json!("uploads/")
            )
            .with_condition(
                IAMOperator::DateGreaterThan,
                "aws:CurrentTime".to_string(),
                json!("2024-01-01T00:00:00Z")
            )
    );

let request = IAMRequest::new_with_context(
    Principal::Aws(PrincipalId::String("arn:aws:iam::123456789012:user/alice".to_string())),
    "s3:PutObject",
    Arn::parse("arn:aws:s3:::my-bucket/alice/uploads/document.pdf").unwrap(),
    context
);

let decision = evaluate_policy(&policy, &request)?;

๐Ÿ“‹ Core Components

IAM Policy Structure

use iam_rs::{IAMPolicy, IAMStatement, Effect, Action, Resource, Principal};

let policy = IAMPolicy::new()
    .with_version(IAMVersion::V20121017)  // AWS standard version
    .with_id("MySecurityPolicy")
    .add_statement(
        IAMStatement::new(IAMEffect::Allow)
            .with_sid("AllowSpecificUsers")
            .with_principal(Principal::from_aws_users(&[
                "arn:aws:iam::123456789012:user/alice",
                "arn:aws:iam::123456789012:user/bob"
            ]))
            .with_action(IAMAction::Multiple(vec![
                "s3:GetObject".to_string(),
                "s3:PutObject".to_string()
            ]))
            .with_resource(IAMResource::Single("arn:aws:s3:::secure-bucket/*".to_string()))
    );

Advanced Pattern Matching

ARN Wildcard Patterns

use iam_rs::Arn;

let arn = Arn::parse("arn:aws:s3:::my-bucket/users/alice/documents/file.pdf")?;

// Test various wildcard patterns
let patterns = [
    "arn:aws:s3:::my-bucket/*",           // โœ“ Matches any object in bucket
    "arn:aws:s3:::my-bucket/users/*",     // โœ“ Matches any user path
    "arn:aws:s3:::my-bucket/users/alice/*", // โœ“ Matches Alice's files
    "arn:aws:s3:::*/documents/*",         // โœ“ Matches any bucket documents
    "arn:aws:s3:::my-bucket/*/file.pdf",  // โœ“ Matches file.pdf anywhere
    "arn:aws:s3:::my-bucket/users/bob/*", // โœ— Different user path
];

for pattern in patterns {
    if arn.matches(Arn::parse(pattern).unwrap()).unwrap() {
        println!("โœ“ ARN matches pattern: {}", pattern);
    }
}

Action Wildcards

// Action wildcard matching
let actions = IAMAction::Multiple(vec![
    "s3:*".to_string(),           // All S3 actions
    "s3:Get*".to_string(),        // All S3 Get actions
    "s3:Put*".to_string(),        // All S3 Put actions
    "iam:List*".to_string(),      // All IAM List actions
]);

๐Ÿ”ง Variable Interpolation

IAM-rs supports AWS policy variables with default fallback values, enabling dynamic resource paths and conditions.

Basic Variable Usage

use iam_rs::{interpolate_variables, Context, ContextValue};

// Set up context
let mut context = Context::new();
context.insert("aws:username".to_string(), ContextValue::String("alice".to_string()));
context.insert("aws:PrincipalTag/team".to_string(), ContextValue::String("red".to_string()));

// Basic variable interpolation
let resource_pattern = "arn:aws:s3:::company-bucket/${aws:username}/*";
let resolved = interpolate_variables(resource_pattern, &context)?;
// Result: "arn:aws:s3:::company-bucket/alice/*"

// Variable with default fallback
let team_pattern = "arn:aws:s3:::team-bucket-${aws:PrincipalTag/team, 'default'}/*";
let resolved = interpolate_variables(team_pattern, &context)?;
// Result: "arn:aws:s3:::team-bucket-red/*"

Variables with Default Values

// When context key is missing, use default value
let empty_context = Context::new();

let pattern = "arn:aws:s3:::bucket-${aws:PrincipalTag/department, 'general'}/*";
let resolved = interpolate_variables(pattern, &empty_context)?;
// Result: "arn:aws:s3:::bucket-general/*" (uses default)

// Common variable patterns
let patterns = [
    "${aws:username}",                          // Current user
    "${aws:userid}",                            // User ID
    "${aws:PrincipalTag/team, 'default'}",     // Principal tag with fallback
    "${aws:RequestedRegion, 'us-east-1'}",     // Region with fallback
    "${aws:CurrentTime}",                       // Current timestamp
    "${s3:prefix, 'uploads/'}",                 // S3 prefix with fallback
];

Dynamic Policy Example

// Policy that grants access to user-specific paths with team fallback
let policy = IAMPolicy::new()
    .add_statement(
        IAMStatement::new(IAMEffect::Allow)
            .with_action(IAMAction::Single("s3:*".to_string()))
            .with_resource(IAMResource::Multiple(vec![
                // User's personal folder
                "arn:aws:s3:::company-data/${aws:username}/*".to_string(),
                // Team shared folder (with fallback)
                "arn:aws:s3:::team-data/${aws:PrincipalTag/team, 'shared'}/*".to_string(),
                // Department folder (with fallback)
                "arn:aws:s3:::dept-data/${aws:PrincipalTag/department, 'general'}/*".to_string(),
            ]))
            .with_condition(
                IAMOperator::StringLike,
                "s3:prefix".to_string(),
                json!("${aws:username}/*")
            )
    );

๐ŸŽฏ Condition Operators

IAM-rs supports all AWS condition operators with full type safety:

String Conditions

use iam_rs::Operator;

// Basic string operations
IAMOperator::StringEquals         // Exact match
IAMOperator::StringNotEquals      // Not equal
IAMOperator::StringEqualsIgnoreCase // Case-insensitive match
IAMOperator::StringLike           // Wildcard matching (*, ?)
IAMOperator::StringNotLike        // Inverse wildcard matching

// Set-based string operations
IAMOperator::ForAnyValueStringEquals     // At least one value matches
IAMOperator::ForAllValuesStringEquals    // All values match

Numeric and Date Conditions

// Numeric comparisons
IAMOperator::NumericEquals
IAMOperator::NumericNotEquals
IAMOperator::NumericLessThan
IAMOperator::NumericLessThanEquals
IAMOperator::NumericGreaterThan
IAMOperator::NumericGreaterThanEquals

// Date/time comparisons
IAMOperator::DateEquals
IAMOperator::DateNotEquals
IAMOperator::DateLessThan
IAMOperator::DateGreaterThan
IAMOperator::DateLessThanEquals
IAMOperator::DateGreaterThanEquals

Specialized Conditions

// Boolean conditions
IAMOperator::Bool

// IP address conditions
IAMOperator::IpAddress            // IP within CIDR range
IAMOperator::NotIpAddress         // IP not in CIDR range

// ARN conditions
IAMOperator::ArnEquals            // Exact ARN match
IAMOperator::ArnLike              // ARN wildcard matching
IAMOperator::ArnNotEquals
IAMOperator::ArnNotLike

// Null checks
IAMOperator::Null                 // Key exists/doesn't exist

// Binary data
IAMOperator::BinaryEquals         // Base64 binary comparison

Complex Condition Example

let statement = IAMStatement::new(IAMEffect::Allow)
    .with_action(IAMAction::Single("s3:GetObject".to_string()))
    .with_resource(IAMResource::Single("arn:aws:s3:::secure-bucket/*".to_string()))
    // Must be from trusted IP range
    .with_condition(
        IAMOperator::IpAddress,
        "aws:SourceIp".to_string(),
        json!(["203.0.113.0/24", "198.51.100.0/24"])
    )
    // Must have MFA
    .with_condition(
        IAMOperator::Bool,
        "aws:MultiFactorAuthPresent".to_string(),
        json!(true)
    )
    // Must be during business hours
    .with_condition(
        IAMOperator::DateGreaterThan,
        "aws:CurrentTime".to_string(),
        json!("08:00:00Z")
    )
    .with_condition(
        IAMOperator::DateLessThan,
        "aws:CurrentTime".to_string(),
        json!("18:00:00Z")
    )
    // User must have required tag
    .with_condition(
        IAMOperator::StringEquals,
        "aws:PrincipalTag/clearance".to_string(),
        json!("high")
    );

โš–๏ธ Policy Evaluation Engine

Advanced Evaluation Options

use iam_rs::{PolicyEvaluator, EvaluationOptions};

let evaluator = PolicyEvaluator::with_policies(vec![policy1, policy2, policy3])
    .with_options(EvaluationOptions {
        stop_on_explicit_deny: true,        // Stop at first explicit deny
        collect_match_details: true,        // Collect debug information
        max_statements: 1000,               // Safety limit
        ignore_resource_constraints: false, // Ignore Resource/NotResource constraints
    });

let result = evaluator.evaluate(&request)?;

println!("Decision: {:?}", result.decision);
println!("Evaluated {} statements", result.statement_details.len());

// Examine detailed results
for statement_match in result.statement_details {
    println!("Statement '{}': {} - {}",
        statement_match.sid.unwrap_or_default(),
        if statement_match.conditions_satisfied { "MATCHED" } else { "NO MATCH" },
        statement_match.reason
    );
}

IAM Precedence Rules

The evaluation engine implements proper AWS IAM logic:

  1. Explicit Deny: Always takes precedence over Allow
  2. Explicit Allow: Required for access (no implicit allow)
  3. Implicit Deny: Default when no Allow statements match
  4. Conditions: Must be satisfied for statement to apply
  5. Multiple Policies: Combined with proper precedence
// Example demonstrating precedence
let allow_policy = IAMPolicy::new()
    .add_statement(
        IAMStatement::new(IAMEffect::Allow)
            .with_action(IAMAction::Single("s3:*".to_string()))
            .with_resource(IAMResource::Single("*".to_string()))
    );

let deny_policy = IAMPolicy::new()
    .add_statement(
        IAMStatement::new(IAMEffect::Deny)  // This will override the Allow
            .with_action(IAMAction::Single("s3:DeleteObject".to_string()))
            .with_resource(IAMResource::Single("arn:aws:s3:::protected-bucket/*".to_string()))
    );

let policies = vec![allow_policy, deny_policy];
let result = evaluate_policies(&policies, &delete_request)?;
// Result: Decision::Deny (Explicit deny wins)

๐Ÿ“ JSON Policy Support

Parsing from JSON

let json_policy = r#"
{
  "Version": "2012-10-17",
  "Id": "S3BucketPolicy",
  "Statement": [
    {
      "Sid": "AllowUserAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:user/alice"
      },
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-bucket/${aws:username}/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "AES256"
        },
        "NumericLessThan": {
          "s3:max-keys": "10"
        }
      }
    }
  ]
}
"#;

let policy = IAMPolicy::from_json(json_policy)?;
println!("Loaded policy with {} statements", policy.statement.len());

Generating JSON

// Create policy programmatically
let policy = IAMPolicy::new()
    .with_id("GeneratedPolicy")
    .add_statement(
        IAMStatement::new(IAMEffect::Allow)
            .with_sid("S3Access")
            .with_action(IAMAction::Single("s3:GetObject".to_string()))
            .with_resource(IAMResource::Single("arn:aws:s3:::my-bucket/*".to_string()))
    );

// Export to JSON
let json_output = policy.to_json()?;
println!("{}", json_output);

๐Ÿงช Examples

Run the comprehensive examples to see all features in action:

# ARN parsing and wildcard matching
cargo run --example arn_demo

# Policy validation and structure
cargo run --example validation_demo

# Complete evaluation engine with conditions
cargo run --example evaluation_demo

Example Scenarios Covered

  • โœ… Basic Allow/Deny policies with simple action/resource matching
  • โœ… Wildcard patterns for actions, resources, and principals
  • โœ… Complex conditions with String, Numeric, Date, Boolean, IP, and ARN operators
  • โœ… Variable interpolation with fallback values for dynamic policies
  • โœ… Multi-policy evaluation with proper precedence handling
  • โœ… Real-world scenarios like user folder access, time-based restrictions
  • โœ… Resource-based policies for S3 buckets, Lambda functions, etc.
  • โœ… Cross-account access with proper principal validation

๐Ÿค Contributing

Contributions are welcome! This library aims to be the definitive Rust implementation of AWS IAM policy evaluation.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Add tests for new functionality
  4. Run the test suite (cargo test)
  5. Check code quality (cargo clippy)
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.