Current section
Files
Jump to
Current section
Files
lib/janus.ex
defmodule Janus do
@moduledoc """
Authorization superpowers for your `Ecto` schemas.
Janus prioritizes:
* Single source of truth: authorization rules should be defined once
and used for authorizing individual actions as well as composing
Ecto queries.
* Minimal library footprint: expose a small set of useful functions
that application authors can use to set up their own optimal
authorization API.
* Minimal application footprint: "hide" Janus behind application-
controlled policy modules that define the interface for the rest
of the application.
* Escape hatches: easily "drop down" to your own code when the
declarative API doesn't cut it.
Janus is split into two primary components:
* `Janus.Policy` - functions and behaviour for defining _policy
modules_, which describe the allowed actors, actions, and resources
in your application. This is where you look if you're writing a
policy module.
* `Janus.Authorization` - functions and behaviour used by the rest
of your application to authorize and load resources. This is where
you look if you're using a policy module.
Janus defines a Mix task to generate the basic policy module that will
get you started:
$ mix janus.gen.policy
## Policies
Policy modules are created by invoking `use Janus`, which implements
both the `Janus.Policy` and `Janus.Authorization` behaviours:
defmodule Policy do
use Janus
@impl true
def build_policy(policy, _user) do
policy
end
end
When you invoke `use Janus`, default implementations are injected for
required callbacks, except for `c:Janus.Policy.build_policy/2`. This
callback is your foundation, as it returns the authorization policy
for an individual user of your application.
The policy above is not very useful -- it doesn't allow anyone to do
anything -- but that can be changed by using the `Janus.Policy` API to
define actions, resources, and conditions that make up your
authorization rules.
def build_policy(policy, %User{role: :moderator} = mod) do
policy
|> allow(:read, Post)
|> allow([:edit, :archive, :unarchive], Post, where: [user: [role: :member]])
|> allow([:edit, :archive, :unarchive], Post, where: [user_id: mod.id])
|> deny(:unarchive, Post, where: [archived_by: [role: :admin]])
end
See the `Janus.Policy` documentation for more on defining policies.
## Authorization
With our policy module defined, it can now be used to load and
authorize resources.
iex> Policy.authorize(some_post, :archive, moderator)
{:ok, some_post}
iex> Policy.authorize(post_archived_by_admin, :unarchive, moderator)
{:error, :not_authorized}
iex> Policy.scope(Post, :read, moderator)
%Ecto.Query{}
iex> Policy.scope(Post, :read, moderator) |> Repo.all()
[ ... posts the moderator can read ]
iex> Policy.any_authorized?(Post, :edit, moderator)
true # there are rules allowing moderators to edit posts
iex> Policy.any_authorized?(Post, :delete, moderator)
false # there are no rules that allow moderators to delete posts
These functions make up the `Janus.Authorization` behaviour, and their
definitions were injected by default when we invoked `use Janus`. This
is the "public API" that the rest of your application will use to
authorize resources.
See the `Janus.Authorization` documentation for more.
## Integration with `Ecto.Query`
The primary assumption that Janus makes is that your resources are
backed by an `Ecto.Schema`. Using Ecto's schema reflection
capabilities, Janus is able to use the same policy to authorize a
single resource and to construct a composable Ecto query that is aware
of field types and associations.
# This query would result in the 5 latest posts that the current
# user is authorized to see, preloaded with the user # who made
# the post (but only if the current user is allowed to see that
# user).
Post
|> Policy.scope(:read, current_user,
preload_authorized: :user
)
|> order_by(desc: :inserted_at)
|> limit(5)
This integration with Ecto queries is main reason Janus exists.
## Configuration
Some defaults can be configured by passing them as options when
invoking `use Janus`. Those are:
* `:repo` - `Ecto.Repo` used to load associations when required by
your authorization rules
* `:load_associations` - Load associations when required by your
authorization rules (requires `:repo` config option to be set or
to be passed explicitly at the call site), defaults to `false`
For example:
defmodule MyApp.Policy do
use Janus,
repo: MyApp.Repo,
load_associations: true
# ...
end
These defaults will be referenced in the `Janus.Authorization`
documentation where they are used.
"""
require Ecto.Query
@type action :: any()
@type schema_module :: module()
@type actor :: any()
@doc """
Sets up a module to implement the `Janus.Policy` and
`Janus.Authorization` behaviours.
Using `use Janus` does the following:
* adds the `Janus.Policy` behaviour, imports functions used to
define the required callback `c:Janus.Policy.build_policy/2`, and
defines a `build_policy/1` helper
* adds the `Janus.Authorization` behaviour and injects default
(overridable) implementations for all callbacks
## Options
* `:load_associations` - Load associations when required by your
authorization rules (requires `:repo` config option to be set or
to be passed explicitly at the call site), defaults to `false`
* `:repo` - `Ecto.Repo` used to load associations when required by
your authorization rules
See "Configuration" section for details.
## Example
defmodule MyApp.Policy do
use Janus, repo: MyApp.Repo
@impl true
def build_policy(policy, _actor) do
policy
# |> allow(...)
end
end
"""
defmacro __using__(opts \\ []) do
quote location: :keep do
@behaviour Janus.Authorization
use Janus.Policy, unquote(opts)
require Janus
@impl Janus.Authorization
def authorize(resource, action, actor, opts \\ []) do
Janus.Authorization.authorize(resource, action, build_policy(actor), opts)
end
@impl Janus.Authorization
def any_authorized?(schema, action, actor) do
Janus.Authorization.any_authorized?(schema, action, build_policy(actor))
end
@impl Janus.Authorization
def scope(query_or_schema, action, actor, opts \\ []) do
Janus.Authorization.scope(query_or_schema, action, build_policy(actor), opts)
end
defoverridable authorize: 4, any_authorized?: 3, scope: 4
end
end
end