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.