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