# AshIAM

AWS IAM-style policy evaluation for Ash Framework.

This extension provides IAM-style authorization for Ash resources using AWS IAM-like policy documents. It supports wildcard matching, deny precedence, configurable policy sources, multiple policy documents, and both CRUD and generic actions.

## Features

- **AWS IAM-compatible policy evaluation** - Uses the same logic as AWS IAM
- **High-performance authorization** - Sub-microsecond evaluation with regex caching
- **Multiple policy documents** - Support for both single and multiple policy documents
- **Deny precedence** - Explicit deny statements override allow statements
- **Wildcard matching** - Support for wildcard patterns in resources and actions
- **Configurable policy sources** - Get policies from actor attributes or custom fetchers
- **Complete Ash integration** - Supports both CRUD actions (with filters) and generic actions (with simple checks)
- **Flexible action mapping** - Map Ash actions to custom IAM verbs for cleaner policies

## Performance

AshIam is optimized for production use with much Claude README enthusiasm!

- **~2μs average evaluation time** for simple policies
- **100x+ performance improvement** over basic implementations
- **Regex pattern caching** to avoid recompilation
- **Early termination** on explicit deny statements
- **ETS-based caching** for compiled patterns

See [PERFORMANCE.md](PERFORMANCE.md) for detailed benchmarks and optimization guide.

## Installation

The package can be installed by adding `ash_iam` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:ash_iam, "~> 1.1.0"}
  ]
end
```

## Quick Start

1. Add the extension to your resource:

```elixir
defmodule MyApp.User do
  use Ash.Resource,
    domain: MyApp.Domain,
    data_layer: Ash.DataLayer.Ets,
    authorizers: [Ash.Policy.Authorizer],
    extensions: [AshIam]

  # ... your resource definition

  iam do
    permission_base "myapp:user"
  end
end
```

2. Provide IAM policy documents in your actor:

```elixir
actor = %{
  iam_policy: %{
    "Statement" => [
      %{"Effect" => "Allow", "Action" => ["*"], "Resource" => ["myapp:user:*"]},
      %{"Effect" => "Deny", "Action" => ["destroy"], "Resource" => ["myapp:user:5"]}
    ]
  }
}

# Policies are automatically evaluated by Ash
MyApp.User |> Ash.read(actor: actor)
```

## Policy Format

Policies follow AWS IAM JSON format:

```elixir
%{
  "Statement" => [
    %{
      "Effect" => "Allow" | "Deny",
      "Action" => ["action1", "action2", "*"],
      "Resource" => ["resource:pattern:*", "*"]
    }
  ]
}
```

Multiple policy documents are also supported:

```elixir
[
  %{"Statement" => [...]},
  %{"Statement" => [...]}
]
```

## Configuration

### Resource Configuration

- `permission_base` - The base resource identifier (required)
- `action_to_iam_mapping` - Maps Ash actions to IAM verbs
- `policy_key` - Actor attribute containing the policy (default: `:iam_policy`)
- `policy_fetcher` - Custom function to fetch policies

### Application Configuration

```elixir
config :ash_iam, iam_stem: "production"
```

This adds a prefix to all permission bases during evaluation.

## Generic Actions

AshIAM fully supports Ash generic actions alongside traditional CRUD operations. Generic actions use simple authorization checks (not filters) since they don't operate on record sets.

### Basic Generic Action Example

```elixir
defmodule MyApp.ReportResource do
  use Ash.Resource,
    domain: MyApp.Domain,
    data_layer: Ash.DataLayer.Ets,
    authorizers: [Ash.Policy.Authorizer],
    extensions: [AshIam]

  actions do
    # Regular CRUD actions work as before
    defaults [:create, :read, :update, :destroy]

    # Generic actions are now supported
    action :export_to_xlsx, :string do
      argument :format, :string, allow_nil?: false
      argument :include_headers, :boolean, default: true

      run fn input, _context ->
        format = input.arguments.format
        headers = input.arguments.include_headers
        result = "Exported data in #{format} format (headers: #{headers})"
        {:ok, result}
      end
    end

    action :send_notification do
      argument :message, :string, allow_nil?: false
      argument :recipient, :string, allow_nil?: false

      run fn input, _context ->
        # Send notification logic here
        :ok
      end
    end
  end

  iam do
    permission_base "myapp:report"
    action_to_iam_mapping create: :create,
                          read: :read,
                          update: :update,
                          delete: :delete,
                          export_to_xlsx: :export,
                          send_notification: :notify
  end
end
```

### Using Generic Actions with IAM Policies

```elixir
# Actor with permission to export but not send notifications
actor = %{
  iam_policy: %{
    "Statement" => [
      %{"Effect" => "Allow", "Action" => ["export"], "Resource" => ["myapp:report:*"]},
      %{"Effect" => "Deny", "Action" => ["notify"], "Resource" => ["myapp:report:*"]}
    ]
  }
}

# This will work
{:ok, result} = 
  MyApp.ReportResource
  |> Ash.ActionInput.for_action(:export_to_xlsx, %{format: "xlsx"})
  |> Ash.run_action(actor: actor)

# This will be denied with Ash.Error.Forbidden
try do
  MyApp.ReportResource
  |> Ash.ActionInput.for_action(:send_notification, %{message: "Hi", recipient: "user@example.com"})
  |> Ash.run_action!(actor: actor)
rescue
  Ash.Error.Forbidden -> # Authorization failed
end
```

### How It Works

- **CRUD actions** (create, read, update, destroy) use **filter checks** for query-level authorization
- **Generic actions** use **simple checks** for straightforward allow/deny decisions
- **Same IAM policies** work for both action types using your `action_to_iam_mapping`
- **Automatic detection** - AshIAM automatically detects action types and applies the correct check

Documentation can be found at [https://hexdocs.pm/ash_iam](https://hexdocs.pm/ash_iam).
