View Source Cheatsheet

installation-and-setup

Installation and setup

dependency

Dependency

mix.exs

defp deps do
  [
    {:ex_janus, github: "zachallaun/ex_janus"}
  ]
end

Ecto v3.9.4 or later and a database that supports lateral joins (like PostgreSQL) is required to use all Janus features.

policy-module

Policy module

lib/my_app/policy.ex

defmodule MyApp.Policy do
  use Janus

  @impl true
  def policy_for(policy, user) do
    policy
    |> # authorization rules
  end
end

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.

formatter-optional

Formatter (optional)

.formatter.exs

[
  import_deps: [:ex_janus]
]

This may be desired if you're using hooks. See Janus.Policy.before_policy_for/1 for more.

reference-schemas

Reference schemas

users

Users

defmodule User do
  use Ecto.Schema

  schema "users" do
    field :email, :string
    field :display_name, :string

    field :role, Ecto.Enum,
      values: [:member, :moderator, :admin]

    has_many :posts, Post
    has_many :comments, Comment
  end
end

posts

Posts

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    field :content, :string

    field :archived, :boolean,
      default: false

    belongs_to :user, User
    has_many :comments, Comment
  end
end

comments

Comments

defmodule Comment do
  use Ecto.Schema

  schema "comments" do
    field :content, :string

    belongs_to :user, User
    belongs_to :post, Post
  end
end

core-concepts

Core concepts

schemas

Schemas

Schemas are modules that invoke use Ecto.Schema and are used to identify different resources in your application.

When defining policies

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

When generating a query

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

When checking whether any auth rules are defined

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

resources

Resources

Resources are loaded structs defined by one of your schemas.

When authorizing an action

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

actors

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 policy_for/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 policy_for/2

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

When calling any authorization function

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

actions

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.

When defining policies

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

When calling any authorization function

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

Can be any term (except a list)

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

permitting-actions

Permitting actions

grant-permission-for-all-resources-of-schema

Grant permission for all resources of schema

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

using-lists-of-actions

Using lists of actions

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

grant-permission-based-on-attributes

Grant permission based on attributes

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

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

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

use-deny-to-override-a-previous-allow

Use deny to override a previous allow

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

grant-permission-if-the-user-is-associated-with-the-resource

Grant permission if the user is associated with the resource

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

grant-permission-based-on-association-attributes

Grant permission based on association attributes

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

use-allows-to-delegate-permission-to-an-association

Use allows to delegate permission to an association

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

multiple-allow-combines-as-a-logical-or

Multiple allow combines as a logical-or

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

structuring-your-policy-definition

Structuring your policy definition

pattern-match-to-give-different-permissions-to-different-actors

Pattern-match to give different permissions to different actors

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

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

delegate-to-context-specific-policies

Delegate to context-specific policies

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

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

authorization

Authorization

using-policies-in-a-context-module

Using policies in a context module

Policies should most often be used in context modules, since they provide the interface to actions and resources that the rest of your application uses.

defmodule MyApp.MessageBoard do
  @moduledoc """
  Context module for the message board.
  """
  import Ecto.Query

  # imports authorize, any_authorized?, filter_authorized, etc.
  import MyApp.Policy

  alias MyApp.Repo

  # ...
end

authorizing-an-action-on-a-resource

Authorizing an action on a resource

def update_post(%Post{} = post, attrs \\ %{}, user_or_policy) do
  case authorize(post, :edit, user_or_policy) do
    {:ok, post} ->
      post
      |> Post.changeset(attrs)
      |> Repo.insert()

    :error ->
      {:error, :not_authorized}
  end
end

fetching-authorized-resources

Fetching authorized resources

def authorized_posts(user_or_policy) do
  if any_authorized?(Post, :read, user_or_policy) do
    posts =
      Post
      |> filter_authorized(:read, user_or_policy)
      |> Repo.all()

    {:ok, posts}
  else
    {:error, :not_authorized}
  end
end

Use any_authorized?/3 to differentiate between a result that is empty because there are no resources that match the policy conditions and a result that is empty because the user isn't authorized to view any resources.

preloading-authorized-associations

Preloading authorized associations

filter_authorized(Post, :read, user_or_policy,
  preload_authorized: :comments
)
filter_authorized(Post, :read, user_or_policy,
  preload_authorized: [comments: :user]
)

The :preload_authorized option can be passed to preload only those associated resources that are authorized for the given action.

applying-a-query-to-preloads

Applying a query to preloads

latest_comment_query =
  from Comment,
    order_by: [desc: :inserted_at],
    limit: 1

filter_authorized(Post, :read, user_or_policy,
  preload_authorized: [comments: latest_comment_query]
)

A query can be applied to associated authorized resources. It is scoped per-association, so it applies to comments of each post instead of the comments of all posts. The above would return all :read-able posts preloaded with their latest :read-able comment.

You can still include nested preloads using a tuple:

filter_authorized(Post, :read, user_or_policy,
  preload_authorized: [comments: {latest_comment_query, [:user]}]
)

caching-a-policy

Caching a policy

call-policy_for-1-to-get-the-policy-for-a-user

Call policy_for/1 to get the policy for a user

iex> policy = MyApp.Policy.policy_for(current_user)
%Janus.Policy{...}

pass-a-policy-anywhere-you-d-pass-in-an-actor

Pass a policy anywhere you'd pass in an actor

iex> MyApp.Policy.authorize(post, :read, policy)
{:ok, post}

iex> MyApp.Policy.filter_authorized(Post, :read, policy)
%Ecto.Query{}

cache-a-policy-in-a-plug-conn

Cache a policy in a Plug.Conn

def assign_current_policy(conn) do
  %{assigns: %{current_user: user}} = conn

  conn
  |> assign(:current_policy, MyApp.Policy.policy_for(user))
end
def index(conn, _params) do
  %{assigns: %{current_policy: policy}} = conn

  # Pass the policy to your context
  case MessageBoard.authorized_posts(policy) do
    {:ok, posts} ->
      ...

    {:error, :not_authorized} ->
      ...
  end
end