# 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).