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 for detailed benchmarks and optimization guide.
Installation
The package can be installed by adding ash_iam to your list of dependencies in mix.exs:
def deps do
[
{:ash_iam, "~> 1.1.0"}
]
endQuick Start
- Add the extension to your resource:
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- Provide IAM policy documents in your actor:
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:
%{
"Statement" => [
%{
"Effect" => "Allow" | "Deny",
"Action" => ["action1", "action2", "*"],
"Resource" => ["resource:pattern:*", "*"]
}
]
}Multiple policy documents are also supported:
[
%{"Statement" => [...]},
%{"Statement" => [...]}
]Configuration
Resource Configuration
permission_base- The base resource identifier (required)action_to_iam_mapping- Maps Ash actions to IAM verbspolicy_key- Actor attribute containing the policy (default::iam_policy)policy_fetcher- Custom function to fetch policies
Application Configuration
config :ash_iam,
iam_stem: "production",
debug: false # Enable debug loggingiam_stem- Adds a prefix to all permission bases during evaluationdebug- Whentrue, enables detailed policy evaluation logging (default:false)
Debug Mode
Enable debug logging to troubleshoot authorization issues:
# In config/dev.exs or config/test.exs
config :ash_iam, debug: true
config :logger, level: :debugWhen enabled, you’ll see detailed logs like:
[debug] AshIAM Policy Evaluation Started - Resource: MyApp.User, Action: read, Record: 123, Has Policy: true
[debug] AshIAM Evaluating - Candidate: myapp:user:123, Verb: read, Policy: %{"Statement" => [...]}
[debug] AshIAM Statement [Allow] - Candidate: myapp:user:123, Verb: read, Statement: %{...} => ✓ ALLOW
[debug] AshIAM Policy Result - Resource: MyApp.User, Action: read - ALLOWED ✓This is extremely helpful for:
- Understanding why authorization is failing
- Debugging policy statement matches
- Tracing permission path evaluation
- Verifying wildcard pattern behavior
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
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
endUsing Generic Actions with IAM Policies
# 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
endHow 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:
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
endUsing Nested Permissions in Policies
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
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.