Permit

Plain-Elixir, DSL-less, extensible authorization library for Elixir.

[![Contact Us](https://img.shields.io/badge/Contact%20Us-%23F36D2E?style=for-the-badge&logo=maildotru&logoColor=white&labelColor=F36D2E)](https://curiosum.com/contact) [![Visit Curiosum](https://img.shields.io/badge/Visit%20Curiosum-%236819E6?style=for-the-badge&logo=elixir&logoColor=white&labelColor=6819E6)](https://curiosum.com/services/elixir-software-development) [![License: MIT](https://img.shields.io/badge/License-MIT-1D0642?style=for-the-badge&logo=open-source-initiative&logoColor=white&labelColor=1D0642)]()

Purpose and usage

Provide a single source of truth of action permissions throughout your codebase, making use of Ecto to have your Phoenix Controllers and LiveViews authorize access to resources without having to repeat yourself.

Permit supports multiple integration points across the Elixir ecosystem:

Hex version badgeActions StatusCode coverage badgeLicense badge

Configure & define your permissions

Required package: :permit.

defmodule MyApp.Authorization do
  use Permit, permissions_module: MyApp.Permissions
end

defmodule MyApp.Permissions do
  use Permit.Permissions, actions_module: Permit.Phoenix.Actions

  def can(%{role: :admin} = user) do
    permit()
    |> all(MyApp.Blog.Article)
  end

  def can(%{id: user_id} = user) do
    permit()
    |> all(MyApp.Blog.Article, author_id: user_id)
    |> read(MyApp.Blog.Article) # allows :index and :show
  end

  def can(user), do: permit()
end

Set up your controller

Requires :permit_phoenix package, and optionally :permit_ecto for sourcing authorization data from the DB.

defmodule MyAppWeb.Blog.ArticleController do
  use MyAppWeb, :controller

  use Permit.Phoenix.Controller,
    authorization_module: MyApp.Authorization,
    resource_module: MyApp.Blog.Article

  # assumption: current user is in assigns[:current_user] - this is configurable

  def show(conn, _params) do
    # At this point, the Article has already been preloaded by Ecto and checked for authorization
    # based on action name (:show).
    # It's available as the @loaded_resource assign.

    render(conn, "show.html")
  end

  def index(conn, _params) do
    # The list of Articles accessible by current user has been preloaded by Ecto
    # into the @loaded_resources assign.

    render(conn, "index.html")
  end

  # Optionally, implement the handle_unauthorized/1 callback to deal with authorization denial.
end

Set up your LiveView

Requires :permit_phoenix, and optionally :permit_ecto.

defmodule MyAppWeb.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  live_session :authenticated, on_mount: Permit.Phoenix.LiveView.AuthorizeHook do
    live("/articles", MyAppWeb.Blog.ArticlesLive, :index)
    live("/articles/:id", MyAppWeb.Blog.ArticlesLive, :show)
  end
end

defmodule MyAppWeb.Blog.ArticleLive do
  use Phoenix.LiveView

  use Permit.Phoenix.LiveView,
    authorization_module: MyApp.Authorization,
    resource_module: MyApp.Blog.Article,
    use_stream?: true  # Enable LiveView 1.0 Streams support

  @impl true
  def fetch_subject(session), do: # load current user

  # Both in the mount/3 callback and in a hook attached to the handle_params event,
  # authorization will be performed based on assigns[:live_action].
  # With streams enabled, :index actions will use streams instead of assigns.

  # Optionally, implement the handle_unauthorized/1 callback to deal with authorization denial.
end

Set up your GraphQL API with Absinthe

Requires :permit_absinthe, whereas :permit_ecto is automatically retrieved to provide Dataloader support - see Permit.Absinthe docs.

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

  object :article do
    permit schema: MyApp.Blog.Article

    field :id, :id
    field :title, :string
    field :content, :string
    field :author_id, :id
  end

  query do
    field :article, :article do
      permit action: :read
      arg :id, non_null(:id)
      resolve &load_and_authorize/2  # Automatically loads and authorizes based on permissions
    end

    field :articles, list_of(:article) do
      permit action: :read
      resolve &load_and_authorize/2  # Returns only articles accessible by current user
    end
  end

  mutation do
    field :create_article, :article do
      permit action: :create
      arg :title, non_null(:string)
      arg :content, non_null(:string)

      # Use middleware for complex authorization scenarios
      middleware Permit.Absinthe.Middleware.LoadAndAuthorize

      resolve fn _, args, %{context: %{current_user: user}} ->
        MyApp.Blog.create_article(user, args)
      end
    end
  end
end

Quick authorization checks

Requires :permit for the basic checks, and :permit_ecto for accessible_by!/3.

# Check permissions directly
can(current_user) |> update?(article)

# Generate Ecto queries based on permissions
MyApp.Authorization.accessible_by!(current_user, :read, Article)

# Use the friendly API for multiple actions
can(current_user) |> do([:read, :update], article)

Ecosystem

Permit is designed as a modular ecosystem with multiple packages:

Package Version Description
permitHex.pm Core authorization library
permit_ectoHex.pm Ecto integration for database queries
permit_phoenixHex.pm Phoenix Controllers & LiveView integration
permit_absintheHex.pm GraphQL API authorization via Absinthe

Recent Updates

Version 0.3.3 fixed a bug in the predicate function behaviour with has-many associations not matching Permit.Ecto behaviour.

Version 0.3.2 fixed a bug in the predicate functions respecting action grouping.

Version 0.3.1 fixed a bug in the loader function.

Version 0.3 brought several major improvements:

See our recent blog posts for more details:

Roadmap

An outline of our development goals for both the "MVP" and further releases.

Milestone 1

The following features of Permit (along with its companion packages), originally intended as an initial backlog, has already been fulfilled:

Future plans

This list of planned items relates to the main Permit repository as well as to Permit.Ecto, Permit.Phoenix, Permit.Absinthe and possible future offspring repositories related to the Permit project.

Installation

If available in Hex, the package can be installed by adding permit to your list of dependencies in mix.exs:

def deps do
  [
    {:permit, "~> 0.3.3"}
  ]
end

For additional integrations, add the relevant packages:

def deps do
  [
    {:permit, "~> 0.3.3"},
    {:permit_ecto, "~> 0.2.4"},     # For Ecto integration
    {:permit_phoenix, "~> 0.3.0"},  # For Phoenix & LiveView
    {:permit_absinthe, "~> 0.1.0"}     # For GraphQL (Absinthe)
  ]
end

Documentation

Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development setup

Just clone the repository, install dependencies normally, develop and run tests. When running Credo and Dialyzer, please use MIX_ENV=test to ensure tests and support files are validated, too.

Media

Community

Contact

License

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