AshGrant

Permission-based authorization extension for Ash Framework.

AshGrant connects three Ash-native concepts — resources, actions, and expr() scopes — through a permission string ([!]resource:instance_id:action:scope[:field_group]). Permissions resolve to native Ash filters and policy checks, with deny-wins semantics.

Authorization:

UI Integration:

Verification & Tooling:

AshGrant handles permission evaluation, not role management. Resolve roles to permission strings in your resolver.

Installation

Add ash_grant to your list of dependencies in mix.exs:

def deps do
  [
    {:ash_grant, "~> 0.13"}
  ]
end

Quick Start

1. Add the Extension to Your Resource

defmodule MyApp.Blog.Post do
  use Ash.Resource,
    domain: MyApp.Blog,
    authorizers: [Ash.Policy.Authorizer],
    extensions: [AshGrant]

  ash_grant do
    # Resolver converts actor to permission strings
    resolver fn actor, _context ->
      case actor do
        %{role: :admin} -> ["post:*:*:all"]           # Full access
        %{role: :editor} -> [
          "post:*:read:all",                          # Read all posts
          "post:*:create:all",                        # Create posts
          "post:*:update:own"                         # Update own posts only
        ]
        %{role: :viewer} -> ["post:*:read:published"] # Read published only
        _ -> []
      end
    end

    default_policies true  # Auto-generates read/write policies

    # Scopes define row-level filters (referenced by permission strings)
    scope :all, true
    scope :own, expr(author_id == ^actor(:id))
    scope :published, expr(status == :published)
  end

  # ... attributes, actions, etc.
end

How it works:

  1. Actor (%{role: :editor, id: "user_123"}) is passed to the resolver
  2. Resolver returns permission strings like "post:*:update:own"
  3. Permission post:*:update:own references scope :own
  4. Scope :own adds filter author_id == actor.id to queries

2. Use It

# Editor can read all posts
editor = %{id: "user_123", role: :editor}
Post |> Ash.read!(actor: editor)

# Editor can only update their own posts
Ash.update!(post, %{title: "New Title"}, actor: editor)
# => Succeeds if post.author_id == "user_123"
# => Fails if post.author_id != "user_123"

# Viewer can only read published posts
viewer = %{id: "user_456", role: :viewer}
Post |> Ash.read!(actor: viewer)
# => Returns only posts where status == :published

Guides

Architecture

                    Ash Policy Check                Ash Calculation
                          |                              |
            +-------------+-------------+--------+  +---v-----------+
            |                           |        |  | CanPerform    |
      +-----v-----+              +------v------+ |  | (UI booleans) |
      |  Check    |              | FilterCheck | |  +---+-----------+
      | (writes)  |              |  (reads)    | |      |
      +-----+-----+              +------+------+ |      |
            |                           |        |      |
            +-----------+---------------+-+------+------+
                        |
            +-----------v-----------+
            | PermissionResolver    |
            | (actor -> permissions)|
            +-----------+-----------+
                        |
            +-----------v-----------+
            | Evaluator             |
            | (deny-wins matching)  |
            +-----------+-----------+
                        |
            +-----------v-----------+
            | Scope DSL / Field     |
            | Groups / Resolver     |
            +-----------------------+

Disclosure

I've been a developer for about six years. I became interested in Elixir, Phoenix, and Ash a couple of years ago, but only started actually building with them about four months ago. This library was born out of my own needs, and honestly, my skills in this ecosystem aren't at the level where I'd normally attempt building something like this.

Most of AshGrant was developed through TDD with Claude Code—I described what I needed, Claude Code wrote the tests and implementation, and I reviewed the results. I treated it like any third-party library: if the tests pass and the code looks reasonable, I use it. I haven't read every line of code in detail, so I can't guarantee everything works perfectly.

I'm using this in production because I need it now, but please consider this more as a proof of concept—a proposal for how authorization could be handled in Ash. I'm sharing this publicly in hopes that it can be a starting point. If others find it useful and want to contribute, we could build something better together.

If you have suggestions or find issues, please feel free to open an issue or submit a PR—contributions are very welcome.

What made this possible is how exceptionally well-documented Elixir and Ash are. The clear abstractions—DSLs, Domains, Resources, Extensions—gave me a precise vocabulary to communicate my requirements to an LLM. These well-defined concepts provided both the courage to start and the foundation to actually ship something I use in production.

I'm deeply grateful to Zach for creating Ash Framework, the Ash Core Team, all the contributors, and the broader Elixir community. We have something special here.

License

MIT License - see LICENSE for details.