View Source Janus.Authorization behaviour (Janus v0.3.2)

Authorize and load resources using policies.

Policy modules expose a minimal API that can be used to authorize and load resources throughout the rest of your application.

  • authorize/4 - authorize an individual, already-loaded resource

  • scope/4 - construct an Ecto query for a schema that will filter results to only those that are authorized

  • any_authorized?/3 - checks whether the given actor/policy has any access to the given schema for the given action

These functions will usually be called from your policy module directly, since wrappers that accept either a policy or an actor are injected when you invoke use Janus. Documentation examples will show usage from your policy module.

See individual function documentation for details.

Link to this section Summary

Functions

Checks whether any permissions are defined for the given schema, action, and actor.

Authorizes a loaded resource.

Create an %Ecto.Query{} that results in only authorized records.

Link to this section Types

@type filterable() :: Janus.schema_module() | Ecto.Query.t()

Link to this section Callbacks

Link to this callback

any_authorized?(filterable, action, arg3)

View Source
@callback any_authorized?(filterable(), Janus.action(), Janus.actor() | Janus.Policy.t()) ::
  boolean()
Link to this callback

authorize(t, action, arg3, keyword)

View Source
@callback authorize(
  Ecto.Schema.t(),
  Janus.action(),
  Janus.actor() | Janus.Policy.t(),
  keyword()
) ::
  {:ok, Ecto.Schema.t()} | {:error, :not_authorized}
Link to this callback

scope(filterable, action, arg3, keyword)

View Source
@callback scope(filterable(), Janus.action(), Janus.actor() | Janus.Policy.t(), keyword()) ::
  Ecto.Query.t()

Link to this section Functions

Link to this function

any_authorized?(schema_or_query, action, policy)

View Source
@spec any_authorized?(filterable(), Janus.action(), Janus.Policy.t()) :: boolean()

Checks whether any permissions are defined for the given schema, action, and actor.

This function is most useful in conjunction with scope/4, which builds an Ecto query that filters to only those resources the actor is authorized for. If you run the resulting query and receive [], it is not possible to determine whether the result is empty because the actor wasn't authorized for any resources or because of other restrictions on the query.

For example, you might use the following pattern to load all the resources a user is allowed to read that were inserted in the last day:

query = from(r in MyResource, where: r.inserted_at > from_now(-1, "day"))

if any_authorized?(query, :read, user) do
  {:ok, scope(query, :read, user) |> Repo.all()}
else
  {:error, :not_authorized}
end

This would result in {:ok, results} if the user is authorized to read any resources, even if the result set is empty, and would result in {:error, :not_authorized} if the user isn't authorized to read the resources at all.

examples

Examples

iex> MyPolicy.any_authorized?(MyResource, :read, actor)
true

iex> MyPolicy.any_authorized?(MyResource, :delete, actor)
false
Link to this function

authorize(resource, action, policy, opts \\ [])

View Source
@spec authorize(Ecto.Schema.t(), Janus.action(), Janus.Policy.t(), keyword()) ::
  {:ok, Ecto.Schema.t()} | {:error, :not_authorized}

Authorizes a loaded resource.

Expects to receive a struct, an action, and an actor or policy.

Returns {:ok, resource} if authorized, otherwise {:error, :not_authorized}.

options

Options

  • :load_associations - Whether to load associations required by policy authorization rules, defaults to false unless configured on your policy module

  • :repo - Ecto repository to use when loading required associations if :load_associations is set to true, defaults to nil unless configured on your policy module

examples

Examples

iex> MyPolicy.authorize(%MyResource{}, :read, actor) # accepts an actor
{:ok, %MyResource{}}

iex> MyPolicy.authorize(%MyResource{}, :read, policy) # or a policy
{:ok, %MyResource{}}

iex> MyPolicy.authorize(%MyResource{}, :delete, actor)
{:error, :not_authorized}
Link to this function

scope(query_or_schema, action, policy, opts \\ [])

View Source

Create an %Ecto.Query{} that results in only authorized records.

Like the Ecto.Query API, this function can accept a schema as the first argument or a query, in which case it will compose with that query. If a query is passed, the appropriate schema will be derived from that query's source.

scope(MyResource, :read, user)

query = from(r in MyResource, where: r.inserted_at > from_ago(1, "day"))
scope(query, :read, user)

If the query specifies the source as a string, we cannot derive the schema. For example, this will not work:

# Raises an ArgumentError
query = from(r in "my_resources", where: r.inserted_at > from_ago(1, "day"))
scope(query, :read, user)

options

Options

  • :preload_authorized - Similar to Ecto.Query.preload/3, but only preloads those associated records that are authorized. Note that this requires Ecto v3.9.4 or later and a database that supports lateral joins. See "Preloading authorized associations" for more information.

preloading-authorized-associations

Preloading authorized associations

The :preload_authorized option can be used to preload associated records, but only those that are authorized for the given actor. An additional query can be specified for each preloaded association that will be run as if scoped to its parent row.

This can simplify certain queries dramatically. For instance, imagine a user search interface that lists users along with their most recent comment. Naughty comments can be hidden by moderators, but those hidden comments should still be visible if a moderator is searching. Here's how that might be accomplished:

iex> last_comment = from(Comment, order_by: [desc: :inserted_at], limit: 1)

iex> User
...> |> search(search_params)
...> |> MyPolicy.scope(:read, current_user,
...>   preload_authorized: [comments: last_comment]
...> )
...> |> Repo.all()
[%User{comments: [%Comment{}]}, %User{comments: [%Comment{}]}, ...]

Some things to note about this example:

  • The last_comment query runs as if scoped to each user's comments. This means that the :limit applies to each user's comments, not the entire set of comments.

  • The comment will be the last inserted comment that is authorized to be read by the current_user. Moderators may be able to see hidden comments, while normal users may not.

It is also possible to nest authorized preloads. For instance, you could preload comments and their associated post.

MyPolicy.scope(User, :read, current_user,
  preload_authorized: [comments: :post]
)

This would load all comments. You could incorporate the last_comment query above by specifying it as the first element of a tuple, followed by the list of inner preloads:

MyPolicy.scope(User, :read, current_user,
  preload_authorized: [comments: {last_comment, [:post]}]
)

This would load only the latest comment as well as its associated post (assuming it too is authorized to be read by current_user).

examples

Examples

iex> MyPolicy.scope(MyResource, :read, actor)
%Ecto.Query{}

iex> MyPolicy.scope(MyResource, :read, actor) |> Repo.all()
[%MyResource{}, ...]

iex> MyResource
...> |> MyPolicy.scope(:read, actor)
...> |> order_by(inserted_at: :desc)
...> |> limit(1)
...> |> Repo.one()
%MyResource{}

iex> MyResource
...> |> MyPolicy.scope(:read, actor,
...>   preload_authorized: :other
...> )
...> |> Repo.all()
[%MyResource{other: %OtherResource{}}, ...]