README.md

# 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

## Nested Resource Permissions

For hierarchical resource relationships, AshIAM supports nested permission paths using Ash calculations. This enables permission patterns like `author:5:posts:10:comments:123` for deeply nested resources.

### Defining Nested Resources

Use the `permission_identifier` option with an Ash calculation to build dynamic permission paths:

```elixir
defmodule MyApp.Post do
  use Ash.Resource,
    extensions: [AshIam]

  attributes do
    uuid_primary_key :id
    attribute :author_id, :uuid
    attribute :title, :string, public?: true
  end

  relationships do
    belongs_to :author, MyApp.Author
  end

  calculations do
    # Use expr for best performance - pushes to SQL
    calculate :iam_permission_path, :string,
      expr("author:" <> type(author_id, :string) <> ":posts:" <> type(id, :string))
  end

  iam do
    permission_base "post"
    permission_identifier :iam_permission_path
    action_to_iam_mapping read: :read, update: :update, delete: :delete
  end
end
```

### Using Nested Permissions in Policies

```elixir
actor = %{
  iam_policy: %{
    "Statement" => [
      # Allow reading all posts by author 5
      %{"Effect" => "Allow", "Action" => ["read"], "Resource" => ["author:5:posts:*"]},
      # Allow updating specific post
      %{"Effect" => "Allow", "Action" => ["update"], "Resource" => ["author:5:posts:10"]},
      # Deny deleting any posts
      %{"Effect" => "Deny", "Action" => ["delete"], "Resource" => ["author:*:posts:*"]}
    ]
  }
}

# Query automatically filters to author 5's posts
{:ok, posts} = MyApp.Post |> Ash.read(actor: actor)
```

### Deep Nesting Example

```elixir
defmodule MyApp.Comment do
  # ...attributes and relationships...

  calculations do
    calculate :iam_permission_path, :string,
      expr(
        "author:" <> type(author_id, :string) <>
        ":posts:" <> type(post_id, :string) <>
        ":comments:" <> type(id, :string)
      )
  end

  iam do
    permission_base "comment"
    permission_identifier :iam_permission_path
  end
end

# Policy can target specific nesting levels
actor = %{
  iam_policy: %{
    "Statement" => [
      # Allow all comments on post 10 by author 5
      %{"Effect" => "Allow", "Action" => ["read"],
        "Resource" => ["author:5:posts:10:comments:*"]}
    ]
  }
}
```

### Performance Considerations

- **Exact matches** (no wildcards) use query-level filtering for optimal performance
- **Wildcard patterns** fall back to record-level checks for accuracy
- **Calculations using `expr()`** can be pushed down to SQL for efficient filtering
- **Module-based calculations** may require loading records, but Ash optimizes with joins

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