Current section
Files
Jump to
Current section
Files
lib/bodyguard.ex
defmodule Bodyguard do
@moduledoc """
Bodyguard imposes a simple module naming convention to express authorization
policies.
For easy integration with Plug-based web applications (e.g. Phoenix), check
out `Bodyguard.Controller`.
"""
require Logger
@doc """
Given a data structure, determines the policy module to call for authorization
checks, following the Bodyguard convention.
Returns an atom of the policy module if passed a struct or atom, by appending
".Policy" to the module name
policy_module(MyApp.User) # => MyApp.User.Policy
policy_module(%MyApp.User{}) # => MyApp.User.Policy
Returns `:error` otherwise.
policy_module("Derp") # => :error
"""
@spec policy_module(term) :: module | :error
# Unable to determine for nil
def policy_module(nil), do: :error
# For Ecto queries
def policy_module(%{from: {source, schema}})
when is_binary(source) and is_atom(schema), do: policy_module(schema)
# For structs
def policy_module(%{__struct__: s}), do: policy_module(s)
# For schemas
def policy_module(term) when is_atom(term), do: String.to_atom("#{term}.Policy")
# Unable to determine
def policy_module(_), do: :error
@doc """
Returns a value determining if the user's action is authorized via the appropriate
policy module for that resource.
The result is returned directly from the `can?/3` callback.
`policy_module/1` is used to find the module, then calls `can?(user, action, term)` on it.
user = %MyApp.User{}
post = %MyApp.Post{}
Bodyguard.authorized?(user, :show, post)
Bodyguard.authorized?(user, :index, MyApp.Post)
Available options:
* `policy` (atom) - override the policy determined from the term
"""
@spec authorized?(term, atom, term, keyword) :: boolean | :ok | :error | {:error, atom}
def authorized?(user, action, term, opts \\ []) do
module = opts[:policy] || policy_module(term)
apply(module, :can?, [user, action, term])
end
@doc """
Scope resources based on the current user.
See also: `Bodyguard.Controller.scope/3`
Define a `scope(user, action, scope)` callback on the policy module to return
the appropriate scope for that user.
If the `scope` argument is a struct, module name, or an Ecto query, the schema
can be automatically inferred. Otherwise, you must pass the `policy` option to
explicitly determine the policy.
This example scopes an Ecto query of posts a user can see.
# post_policy.ex
defmodule MyApp.Post.Policy
# A user can only see their own posts, but an admin can see all posts
def scope(%{role: "user", id: user_id}, _action, scope) do
scope |> where(user_id: user_id)
end
def scope(%{role: "admin"}, _action, scope), do: scope
end
# elsewhere
posts = Bodyguard.scoped(current_user, :index, MyApp.Post) |> Repo.all
"""
@spec scoped(term, atom, term, keyword) :: term
def scoped(user, action, scope, opts \\ []) do
module = opts[:policy] || policy_module(scope)
# TODO v1.0: Remove deprecation warning
if Keyword.keyword?(opts) && Keyword.keys(opts) != [] && Keyword.keys(opts) != [:policy] do
Logger.warn("Passing opts to the #{module}.scope/3 callback is deprecated. The new callback function signature is #{module}.scope(user, action, scope). Use the scope argument instead.")
end
apply(module, :scope, [user, action, scope])
end
@doc """
Specify which schema attributes may be updated by the current user.
The policy module must define a function `permitted_attributes(user, term)`
which returns a list of atoms corresponding to the fields that may
be updated. This resulting list is often passed to `Ecto.Changeset.cast/3`
to whitelist the parameters being passed in to the changeset.
Available options:
* `policy` (atom) - override the policy determined from the term
"""
@spec permitted_attributes(term, term, keyword) :: [atom]
def permitted_attributes(user, term, opts \\ []) do
module = opts[:policy] || policy_module(term)
apply(module, :permitted_attributes, [user, term])
end
end