View Source Policy Usage

Authorization

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?, scope, etc.
  import MyApp.Policy

  alias MyApp.Repo

  # ...
end

Authorizing an action using authorize/4

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.update()

    {:error, :not_authorized} ->
      {:error, :not_authorized}
  end
end
iex> update_post(authorized_post, %{}, some_user)
{:ok, %Post{}}

iex> update_post(unauthorized_post, %{}, some_user)
{:error, :not_authorized}

Fetching authorized resources

def authorized_posts(user_or_policy) do
  if any_authorized?(Post, :read, user_or_policy) do
    posts =
      Post
      |> scope(: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

scope(Post, :read, user_or_policy,
  preload_authorized: :comments
)
scope(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

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

scope(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:

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

Caching a policy

Call build_policy/1 to get the policy for a user

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

Pass a policy anywhere you'd pass in an actor

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

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

Cache a policy in a Plug.Conn

Call in a plug

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

  conn
  |> assign(:current_policy, MyApp.Policy.build_policy(user))
end

Controller action

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