We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
A single source of truth for authorization in Elixir. Define rules as code, source conditions from Ecto/SQL, auto-generate queries, and plug authorization seamlessly into Phoenix, LiveView, and Absinthe.
Quick reference for core scenarios across Permit, Ecto, Phoenix and Absinthe.
# lib/my_app/permissions.ex
defmodule MyApp.Permissions do
use Permit.Permissions, actions_module: Permit.Phoenix.Actions
def can(%{role: :admin}), do: permit() |> all(MyApp.Blog.Article)
def can(%{id: user_id}) do
permit()
|> all(MyApp.Blog.Article, author_id: user_id)
|> read(MyApp.Blog.Article)
end
def can(_), do: permit()
end
# Does user have permission to read a specific article?
can(user) |> do?(:read, article)
# Does user have permission to create an Article in general?
can(user) |> do?(:create, MyApp.Blog.Article)
# Convenience predicates also exist for all defined actions:
can(user) |> create?(MyApp.Blog.Article)
# All supported operators (examples)
can(user) |> # ...
# Equality / inequality
read(MyApp.Blog.Article, state: {:==, :published})
read(MyApp.Blog.Article, state: {:eq, :published})
read(MyApp.Blog.Article, state: {:!=, :archived})
read(MyApp.Blog.Article, state: {:neq, :archived})
# Comparisons
read(MyApp.Blog.Article, views: {:>, 100})
read(MyApp.Blog.Article, views: {:gt, 100})
read(MyApp.Blog.Article, views: {:>=, 100})
read(MyApp.Blog.Article, views: {:ge, 100})
read(MyApp.Blog.Article, views: {:<, 100})
read(MyApp.Blog.Article, views: {:lt, 100})
read(MyApp.Blog.Article, views: {:<=, 100})
read(MyApp.Blog.Article, views: {:le, 100})
# Pattern operators: converted to regular expressions in Elixir,
# generating LIKE/ILIKE queries
read(MyApp.Blog.Article, name: {:like, "%Guide%"})
read(MyApp.Blog.Article, name: {:ilike, "%guide%"})
# LIKE/ILIKE with escape option for literal % and _
read(MyApp.Blog.Article, name: {:like, "%!!%!%%!_%", escape: "!"})
# Regular expressions (not convertible to Ecto queries)
read(MyApp.Blog.Article, name: {:=~, ~r/Guide|Tutorial/})
read(MyApp.Blog.Article, name: {:match, ~r/guides?\\d+/i})
# Membership
read(MyApp.Blog.Article, tag: {:in, ["elixir", "phoenix"]})
# Nil checks
read(MyApp.Blog.Article, deleted_at: nil) # is_nil(deleted_at)
read(MyApp.Blog.Article, deleted_at: {:not, nil}) # not is_nil(deleted_at)
# Negation with operators
read(MyApp.Blog.Article, name: {{:not, :like}, "%admin%"})
# Match on associated records using nested maps.
# Example: allow updating an Article when its author's group matches user's group
def can(%{group_id: group_id}) do
permit()
|> update(MyApp.Blog.Article, author: [group_id: group_id])
end
# Build an Ecto query that reflects your Permit rules
MyApp.Authorization.accessible_by!(user, :read, MyApp.Blog.Article)
Repo.all(q)
# Examples of generated queries:
can(user) |> read(MyApp.Blog.Article)
# Query equivalent to:
from a in Article
can(user) |> read(MyApp.Blog.Article, views: {:>=, 100})
# Query equivalent to:
from a in Article, where: a.views >= 100
can(user) |> read(MyApp.Blog.Article, author: [group_id: user.group_id])
# Query equivalent to:
from a in Article,
join: u in assoc(a, :author),
where: u.group_id == ^user.group_id
# Some operators (e.g. :match / :=~ on regex) are not convertible to SQL automatically.
# You can supply an explicit Ecto dynamic query alongside the runtime predicate.
defmodule MyApp.Permissions do
use Permit.Ecto.Permissions, actions_module: Permit.Phoenix.Actions
import Ecto.Query
def can(_user) do
permit()
|> read(MyApp.Blog.Article, [
# Provide both: runtime predicate and equivalent Ecto dynamic
{fn _subject, article -> article.title =~ ~r/Guide|Tutorial/i end,
fn _subject, _object -> dynamic([a], ilike(a.title, ^"%guide%") or ilike(a.title, ^"%tutorial%")) end}
])
end
end
# lib/my_app_web/controllers/article_controller.ex
defmodule MyAppWeb.ArticleController do
use MyAppWeb, :controller
use Permit.Phoenix.Controller,
authorization_module: MyApp.Authorization,
resource_module: MyApp.Blog.Article
def show(conn, _params) do
# conn.assigns[:loaded_resource]
render(conn, :show)
end
def index(conn, _params) do
# conn.assigns[:loaded_resources]
render(conn, :index)
end
end
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
import Phoenix.LiveView.Router
scope "/", MyAppWeb do
pipe_through :browser
resources "/articles", ArticleController
live_session :authz, on_mount: Permit.Phoenix.LiveView.AuthorizeHook do
live "/articles", ArticleLive, :index
live "/articles/:id", ArticleLive, :show
end
end
end
# lib/my_app_web/live/article_live.ex
defmodule MyAppWeb.ArticleLive do
use Phoenix.LiveView
use Permit.Phoenix.LiveView,
authorization_module: MyApp.Authorization,
resource_module: MyApp.Blog.Article,
use_stream?: true
def fetch_subject(session), do: MyApp.Accounts.fetch_user(session)
end
# Default grouping for Phoenix controllers/LiveViews:
# :read => [:index, :show]
# :create => [:new, :create]
# :update => [:edit, :update]
# :delete => [:delete]
defmodule MyApp.Permissions do
use Permit.Permissions, actions_module: Permit.Phoenix.Actions
# define rules using :read/:create/:update/:delete
end
# Merge action names defined in your Phoenix router
# into the default grouping schema.
defmodule MyApp.Actions do
use Permit.Phoenix.Actions, router: MyAppWeb.Router
end
defmodule MyApp.Permissions do
use Permit.Permissions, actions_module: MyApp.Actions
end
# Override grouping_schema/0 and merge routing actions
defmodule MyApp.Actions do
use Permit.Phoenix.Actions
@impl true
def grouping_schema do
Permit.Phoenix.Actions.grouping_schema()
|> Permit.Phoenix.Actions.merge_from_router(MyAppWeb.Router)
|> Map.put(:publish, [:publish])
end
end
# Controllers infer from action_name(conn)
# LiveViews infer from assigns[:live_action]
# lib/my_app_web/schema.ex
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
end
query do
field :article, :article do
arg :id, non_null(:id)
# loads and checks permissions
resolve &load_and_authorize/2
end
field :articles, list_of(:article) do
# returns only accessible records
resolve &load_and_authorize/2
end
end
end
# Inside the schema module
@prototype_schema Permit.Absinthe.Schema.Prototype
query do
field :articles_via_directive, list_of(:article), directives: [:load_and_authorize] do
permit action: :read
resolve fn _, _, %{context: %{loaded_resources: articles}} ->
{:ok, articles}
end
end
end
# Object mapping with authorized dataloader
object :article do
permit schema: MyApp.Blog.Article
field :id, :id
field :title, :string
field :comments, list_of(:comment), resolve: &authorized_dataloader/3
end
# Add plugins for dataloader
@impl true
def plugins do
[Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end
# When using dataloader, set up context for fields that need it
field :article, :article do
permit action: :read
middleware Permit.Absinthe.Middleware.DataloaderSetup
arg :id, non_null(:id)
resolve &load_and_authorize/2
end
# lib/my_app_web/schema.ex
mutation do
@desc "Update an article"
field :update_article, :article do
permit action: :update
arg :id, non_null(:id)
arg :title, non_null(:string)
arg :content, non_null(:string)
# Loads the resource into context and authorizes access
middleware Permit.Absinthe.Middleware.LoadAndAuthorize
resolve fn _, %{title: title, content: content}, %{context: %{loaded_resource: article}} ->
case MyApp.Blog.update_article(article, %{title: title, content: content}) do
{:ok, article} -> {:ok, article}
{:error, _changeset} -> {:error, "Could not update article"}
end
end
end
end
Speed up permission checks with precomputed rule structures and caching strategies.
Detect unreachable rules, overlaps and missing cases right in CI.
Experiment with subjects/resources and see rule evaluation paths visually.
Scaffold common query conditions and record-loading strategies quickly.
First-class adapters for Ash and Commanded beyond Phoenix/Absinthe.
Simplify wiring by inferring actions straight from Phoenix routes.
Explore feasibility of safely constraining access to individual fields.
Investigate bridging Permit’s rules with database row-level security.
Permit's development depends on you.
We invite you to discuss, contribute and share Permit with others.
Join #permit to ask questions and share feedback.
Open-ended conversations about features and usage.
Open and track issues in each repository.
Hang out with the Curiosum community.