Permit.Absinthe

Authorization-aware GraphQL with Absinthe and Permit for Elixir.

Contact UsVisit CuriosumLicense: MIT


Permit.Absinthe provides integration between the Permit authorization library and Absinthe GraphQL for Elixir.

Hex version badgeVersion badgeActions StatusLicense badge

Installation

If you use Igniter for project setup, add permit_absinthe and igniter to your deps and run:

mix permit_absinthe.install

This patches your Absinthe schema module to add use Permit.Absinthe automatically.

Options:

OptionDescriptionDefault
--authorization-moduleAuthorization module name<MyApp>.Authorization
--schema-moduleAbsinthe schema module to patch<MyApp>Web.Schema

Example with options:

mix permit_absinthe.install \
--authorization-module MyApp.Auth \
--schema-module MyAppWeb.GraphqlSchema

Manual installation

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

def deps do
[
{:permit_absinthe, "~> 0.2.0"}
]
end

Then add use Permit.Absinthe to your Absinthe schema module:

defmodule MyAppWeb.Schema do
use Absinthe.Schema
use Permit.Absinthe, authorization_module: MyApp.Authorization
# ...
end

Permit.Absinthe depends on :permit and permit_ecto, as well as on Absinthe and Dataloader. It does not depend on Absinthe.Plug except for running tests.

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/permit_absinthe.

Features

How it works

Permit.Absinthe applies Permit rules using three pieces of information on each field resolution.

If can(subject) |> action?(resource) is allowed, the resolver continues; otherwise it returns an authorization error (or your custom handler result).

Ecto is the source of truth for matching permission conditions against data, and it constructs queries based on defined permissions to fetch records or lists of records. See Permit.Ecto docs for more on this.

Usage

Which integration should I pick?

Here's a quick table outlining which feature you should pick when plugging in Permit authorization into a specific Absinthe use case.

If you need...UseWhy
Simple query resolverresolve &load_and_authorize/2Lowest boilerplate for standard resource reads
• Query resolver + custom processing
• Mutation
Permit.Absinthe.MiddlewareKeeps full resolver control while reusing Permit loading/authorization
Association loadingresolve &authorized_dataloader/3Preserves Dataloader batching and enforces Permit rules
Authorization declared explicitly in schema directivesdirectives: [:load_and_authorize]Makes authorization behavior more visible at schema level
Non-standard authorization or loading flowVanilla Permit calls inside your resolverMaximum flexibility for advanced/custom cases

Map GraphQL types to Permit resources

In your Absinthe schema, define metadata that maps your GraphQL types to Ecto schemas:

use Permit.Absinthe, authorization_module: MyApp.Authorization
object :post do
permit schema: MyApp.Blog.Post
field :id, :id
field :title, :string
field :content, :string
end

Add field-specific actions

By default, queries map to the :read action while mutations require explicit actions. You can specify these using the permit macro (or meta likewise):

field :unpublished_posts, list_of(:post) do
permit action: :view_unpublished
resolve &PostResolver.unpublished_posts/3
end
field :create_post, :post do
permit action: :create
resolve &PostResolver.create_post/3
end

Customisation options

The permit macro supports a set of options that let you customize how Permit Absinthe loads data, scopes Ecto queries, and formats success/errors.

Important caveat about callbacks

Permit Absinthe captures callback functions passed to permit (for example permit loader: &external_notes_loader/1) as AST at compile time to avoid Absinthe boilerplate. References, function calls, and aliases are supported, but functions defined in your schema module must be public (use def, not defp).

Using authorization resolvers

Permit.Absinthe provides resolver functions to load and authorize resources automatically:

defmodule MyApp.Schema do
use Absinthe.Schema
use Permit.Absinthe, authorization_module: MyApp.Authorization
object :post do
permit schema: MyApp.Blog.Post
field :id, :id
field :title, :string
field :content, :string
end
query do
field :post, :post do
permit action: :read
arg :id, non_null(:id)
resolve &load_and_authorize/2
end
field :posts, list_of(:post) do
permit action: :read
resolve &load_and_authorize/2
end
end
# ...
end

Custom id field names and parameters can be specified:

field :post_by_slug, :post do
arg :slug, non_null(:string)
permit action: :read, id_param_name: :slug, id_struct_field_name: :slug
resolve &load_and_authorize/2
end

Load & authorize using Absinthe Middleware

In mutations, or when custom and more complex resolution logic is required, the Permit.Absinthe.Middleware can be used, preloading the resource (or list of resources) into context, which then can be consumed in a custom Absinthe resolver function.

query do
@desc "Get all articles"
field :articles, list_of(:article) do
permit action: :read
middleware Permit.Absinthe.Middleware
resolve(fn _parent, _args, %{context: context} = _resolution ->
# ...
{:ok, context.loaded_resources}
end)
# This would be equivalent:
#
# resolve &load_and_authorize/2
end
@desc "Get a specific article by ID"
field :article, :article do
permit action: :read
middleware Permit.Absinthe.Middleware
arg :id, non_null(:id)
resolve(fn _parent, _args, %{context: context} = _resolution ->
{:ok, context.loaded_resource}
end)
# This would be equivalent:
#
# resolve &load_and_authorize/2
end
end
mutation do
@desc "Update an article"
field :update_article, :article do
permit action: :update
arg(:id, non_null(:id))
arg(:name, non_null(:string))
arg(:content, non_null(:string))
middleware Permit.Absinthe.Middleware
resolve(fn _, %{name: name, content: content}, %{context: context} ->
case Blog.Content.update_article(context.loaded_resource, %{name: name, content: content}) do
{:ok, article} ->
{:ok, article}
{:error, changeset} ->
{:error, "Could not update article: #{inspect(changeset)}"}
end
end)
end
end

Dataloader integration

If you already use Absinthe + Dataloader, you usually get efficient batch loading but still need to ensure each loaded record is filtered through your authorization rules.
permit_absinthe solves that by providing authorized_dataloader/3: a resolver that keeps Dataloader batching and applies Permit authorization in the same flow.

To set it up:

  1. Keep the standard Absinthe dataloader plugin in your schema.
  2. Add permit metadata to the type/field so Permit knows what resource/action to authorize (defaulting to :read).
  3. Use resolve &authorized_dataloader/3 for associations you want to batch-load safely.

Schema setup:

def plugins do
[Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end

Field setup:

object :item do
permit schema: MyApp.Item
field :subitems, list_of(:subitem) do
permit action: :read
resolve &authorized_dataloader/3
end
end

Compared to Permit.Absinthe v0.1, this means the removal of Permit.Absinthe.Middleware.DataloaderSetup altogether. The resolver function takes care of creating and managing necessary dataloader source structures in the Absinthe resolution.

Authorizing with GraphQL directives

Permit.Absinthe provides the :load_and_authorize directive to automatically load and authorize resources in your GraphQL fields.

The most reliable way to add Permit directives to your schema is using the prototype schema:

defmodule MyAppWeb.Schema do
use Absinthe.Schema
@prototype_schema Permit.Absinthe.Schema.Prototype
# Your schema definition...
query do
field :items, list_of(:item), directives: [:load_and_authorize] do
permit(action: :read)
resolve(fn _, %{context: %{loaded_resources: items}} ->
{:ok, items}
end)
end
end
end

The :load_and_authorize directive works with both single resources and lists of resources, ensuring that only accessible items are returned to the client based on the permission rules defined in your authorization module.

Custom resolvers with vanilla Permit authorization

For more complex authorization scenarios, you can implement custom resolvers using vanilla Permit syntax:

defmodule MyApp.Resolvers.Post do
import MyApp.Authorization
def create_post(_, args, %{context: %{current_user: user}} = _resolution) do
if can(user) |> create?(MyApp.Post) do
{:ok, MyApp.Blog.create_post(args)}
else
{:error, "Unauthorized"}
end
end
end

Default behaviour (quick reference)

AreaDefault
Action selectionQueries default to :read; mutations must set permit action: ... explicitly.
Subject lookupUses resolution.context[:current_user] unless you provide fetch_subject.
Resource mappingUses the schema set via permit schema: ... on the GraphQL type.
Single-resource missReturns {:error, "Not found"} when the record does not exist.
Unauthorized accessReturns {:error, "Unauthorized"} by default (or :unauthorized_message / handle_unauthorized if configured).
Error customizationhandle_unauthorized and handle_not_found override default error tuples.

Community & support

Contributing

We welcome contributions! Please see the main Permit Contributing Guide for details.

Feel free to submit bugfix requests and feature ideas in Permit.Absinthe's GitHub Issues and create pull requests, whereas the best place to discuss development ideas and questions is the Permit channel in the Elixir Slack.

Development setup

Contact

License

This project is licensed under the MIT License - see the LICENSE file for details.