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

Performance

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

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"}
  ]
end

Quick Start

  1. 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
  1. 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

Application Configuration

config :ash_iam,
  iam_stem: "production",
  debug: false  # Enable debug logging

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: :debug

When 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:

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
end

Using 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
end

How It Works

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
end

Using 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&#39;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

Documentation can be found at https://hexdocs.pm/ash_iam.