View Source The Basics

Setup

Generator

$ mix janus.gen.policy
* creating lib/my_app/policy.ex

Generate a starting policy module for your application. For more information, see the Generated Policy Modules guide.

Policy module

Your policy module is the interface used by the rest of your application. This is usually the only place you should be referring to Janus directly.

lib/my_app/policy.ex

defmodule MyApp.Policy do
  use Janus

  @impl true
  def build_policy(policy, actor) do
    policy
    |> # authorization rules
  end
end

For more information on defining policies, see the Policy Definition Cheatsheet

Core concepts

Schemas

Schemas are modules that use Ecto.Schema.

Used when defining policies

policy
|> allow(Post, :edit, ...)
#        ^^^^

Used when scoping a query

MyApp.Policy.scope(Post, :edit, current_user)
#                  ^^^^

Used when checking for permissions

MyApp.Policy.any_authorized?(Post, :edit, current_user)
#                            ^^^^

Resources

Resources are loaded structs defined by one of your schemas.

Used when authorizing an action

MyApp.Policy.authorize(%Post{}, :edit, current_user)
#                      ^^^^^^^

Actors

Actors are the users of your application. They can be a %User{} struct, but they don't have to be. Actors are converted to a policy using build_policy/2, so an actor can be anything that you want to use to differentiate between types of user. They can even be a simple atom like :normal_user or :admin_user.

In build_policy/2

def build_policy(policy, %User{}) do
  #                      ^^^^^^^
end

Used when calling any authorization function

MyApp.Policy.authorize(%Post{}, :edit, current_user)
#                                      ^^^^^^^^^^^^

MyApp.Policy.scope(Post, :edit, current_user)
#                               ^^^^^^^^^^^^

Actions

Actions are what actors do to resources in your application. Janus doesn't care how you represent actions, but atoms usually do the trick.

Used when defining policies

policy
|> allow(Post, :edit, ...)
#              ^^^^^

Used when calling any authorization function

MyApp.Policy.authorize(%Post{}, :edit, current_user)
#                               ^^^^^

Can be any term except a list

policy
|> allow(Post, :edit, ...)
|> allow(Post, "edit", ...)
|> allow(Post, %Action{type: :edit}, ...)
# lists are special-cased to allow multiple
# actions to share conditions
|> allow(Post, [:read, :edit],...)

Defining rules

Overview

Authorization rules are attached to policies in the build_policy/2 callback.

Grant permission for all resources of schema

policy
|> allow(Post, :read)
|> allow(Post, :edit)
|> allow(Post, :archive)
|> allow(Comment, :read)
|> allow(Comment, :edit)

Using lists of actions

policy
|> allow(Post, [:read, :edit, :archive], Post)
|> allow(Comment, [:read, :edit], Comment)

Grant permission based on attributes

policy
|> allow(Post, :read, where: [archived: false])

# or define using :where_not
|> allow(Post, :read, where_not: [archived: true])

# or override a blanket permission using deny
|> allow(Post, :read)
|> deny(Post, :read, where: [archived: true])

Use deny to override a previous allow

policy
|> allow(Post, :read)
|> deny(Post, :read, where: [archived: true])

Grant permission if the user is associated with the resource

def build_policy(policy, %User{role: :member} = user) do
  policy
  |> allow(Comment, :edit, where: [user_id: user.id])
end

Grant permission based on association attributes

policy
|> allow(Comment, :edit, where: [user: [role: :member]])

Use allows to delegate permission to an association

policy
|> allow(Post, :read, where: [archived: false])
|> allow(Comment, :read, where: [post: allows(:read)])

Multiple allow combines as a logical-or

# This will always allow reading all posts
policy
|> allow(Post, :read)
|> allow(Post, :read, where: [archived: false]) # has no effect

Hooks

Overview

Hooks are attached to policies in the build_policy/2 callback. They are called prior to authorize or scope and can modify the resource/query or halt authorization altogether.

Run prior to authorizing any schema

policy
|> attach_hook(:my_hook, fn
  :authorize, resource, _action ->
    {:cont, resource}

  :scope, query, _action ->
    {:cont, query}
end)

Run prior to authorizing a specific schema

policy
|> attach_hook(:my_hook, Post, fn
  :authorize, resource, _action ->
    {:cont, resource}

  :scope, query, _action ->
    {:cont, query}
end)

Used to preload fields

policy
|> attach_hook(:preload_user, Post, fn
  :authorize, post, _action ->
    {:cont, Repo.preload(post, :user)}

  :scope, query, _action ->
    {:cont, query}
end)

Remove attached hooks with detach_hook/3

policy
|> attach_hook(:my_hook, &my_hook/3)
|> detach_hook(:my_hook)
policy
|> attach_hook(:my_hook, Post, &my_hook/3)
|> detach_hook(:my_hook, Post)

Attach hook if it's new using attach_new_hook/4

# second call has no effect
policy
|> attach_new_hook(:my_hook, &my_hook/3)
|> attach_new_hook(:my_hook, &my_other_hook/3)
# second call has no effect
policy
|> attach_new_hook(:my_hook, Post, &my_hook/3)
|> attach_new_hook(:my_hook, Post, &my_other_hook/3)
# attaches second hook because :my_hook not added for Post
policy
|> attach_new_hook(:my_hook, &my_hook/3)
|> attach_new_hook(:my_hook, Post, &my_other_hook/3)

Run a late check before each authorization call

@impl true
def build_policy(policy, user) do
  policy
  |> attach_hook(:ensure_unbanned, fn _, object, _ ->
    if Accounts.banned?(user.id) do
      :halt
    else
      {:cont, object}
    end
  end)
  |> ...
end

This can be useful if policies are being cached. If the call to Accounts.banned?(user.id) occurred in the callback body instead, the policy could not react to any change in account status after it was built.

Structuring your policies

Pattern-match to give different permissions to different actors

def build_policy(policy, %User{role: :member}) do
  # member permissions
end

def build_policy(policy, %User{role: :moderator}) do
  # moderator permissions
end

Delegate to context-specific policies

def build_policy(policy, actor) do
  policy
  |> CommunityForum.Policy.build_policy(actor)
  |> Storefront.Policy.build_policy(actor)
end

For larger applications with well-defined boundaries, a policy can be constructed by threading it through multiple build_policy calls.