View Source Permit behaviour (permit v0.2.0)

Permit is an extensible, DSL-less library allowing the coder to define authorization rules in plain Elixir.

It can run on its own, but is also integrated with widely used Elixir libraries and frameworks.

Libraries and repositories

The Permit library can run on its own as a standalone library, but it can also be used alongside Ecto and Phoenix integrations.

  • Permit - provides a syntax to define permissions to perform actions (defined as atoms) on objects (structs) by a specific user (subject) using functions or keyword lists, matching against an object's attributes.

  • Permit.Ecto - provides a resolver using Ecto to build and execute singular or collection Ecto queries based on defined permissions, also extending the syntax with a possibility to define more sophisticated permissions convertible to Ecto queries.

  • Permit.Phoenix - uses Permit and Permit.Ecto to retrieve records via loader functions or queries generated by Permit.Ecto (if installed), based on data accessible in current context defined by a Plug conn or a LiveView socket.

Paradigm and Extensibility

At the core of authorization resolution, there's always the question of:

  • What action is being performed (for Phoenix, it's most likely a controller action)
  • What subject performs the action (usually, the current user)
  • What object the action is performed on

Once answers to these three questions are found, authorization or lack thereof is determined based on the set of permission definitions, defined as expressions in disjunctive normal form (DNF) expressions - that is, a set of sufficient conditions, with each condition defined as a conjunction of predicates, for example:

   Subject     |   Action   | Object
--------------------------------------------
A **user** can | **update** | an **article**

...if user's ID = article author's ID AND the article is not published,
...if user's ID = article author's ID AND the article type is a live ticker,
...if user's role is editor-in-chief AND the article is not published,
...if user's role is editor-in-chief ID AND the article type is a live ticker,
...or if the use has a super-admin role.

Which, in Permit syntax, is translated to the following. Note the usage of pattern matching on the current user's (subject's) attributes, which allows to create function clauses for each user role. Permit does not enforce a specific structure of the can/1 function, but as pattern matching usage is convenient in this case, it is naturally encouraged.

def can(%User{role: :editor_in_chief} = _current_user) do
  permit()
  |> update(Article, state: {:not, :published})
  |> update(Article, type: :live_ticker)
end

def can(%User{id: user_id} = _current_user) do
  permit()
  |> update(Article, author_id: user_id, state: {:not, :published})
  |> update(Article, author_id: user_id, type: :live_ticker)
end

def can(%User{id: user_id, role: :super_admin} = _current_user) do
  permit()
  |> update(Article)
end

The library is written with extensibility in mind. Analogously to Phoenix interoperatbility, the developer may define their own integration with different frameworks.

For more details on interoperability, see Permit.ResolverBase.

Configuration and usage

For more details on Ecto and Phoenix usage, visit permit_ecto and permit_phoenix documentations, respectively.

Configure & define your permissions

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

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

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

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

Note that in the permission definitions module the read function is generated based on configuration provided as the :actions_module option - in this case, CrudActions generates create, read, update and delete. For more on this, see Permit.Actions and Permit.Permissions.

Check a user's authorization to perform an action on a resource

iex(1)> import MyApp.Authorization
iex(2)> can(%MyApp.User{id: 1}) |> read?(%MyApp.Article{author_id: 1})
true
iex(3)> can(%MyApp.User{id: 1}) |> read?(%MyApp.Article{author_id: 2})
true
iex(4)> can(%MyApp.User{id: 1}) |> update?(%MyApp.Article{author_id: 2})
false
iex(4)> can(%MyApp.User{role: :admin}) |> delete?(%MyApp.Article{author_id: 2})
true

Functions such as MyApp.Authorization.read?/2, MyApp.Authorization.update?/2, etc. are also generated based on the :actions_module option. See more in Permit.Actions.

Summary

Callbacks

@callback resolver_module() :: Permit.Types.resolver_module()