defmodule Permit.Ecto do @moduledoc """ Integrates [`Permit`](https://github.com/curiosum-dev/permit) with Ecto, providing means to convert permissions to Ecto queries, automatically constructing `Ecto.Query` scopes to preload records that meet authorization criteria. ## Dependencies and related libraries `Permit.Ecto` depends on `Permit`. It can be used to build custom integrations or in conjunction with `Permit.Phoenix`, which uses the generated `accessible_by/4` functions to automatically preload, authorize and inject records loaded via Ecto into controller assigns (see more in [`Permit.Phoenix documentation`](https://github.com/curiosum-dev/permit_phoenix)). ## Configuration ```elixir defmodule MyApp.Authorization do use Permit.Ecto, permissions_module: MyApp.Permissions, repo: MyApp.Repo end defmodule MyApp.Permissions do use Permit.Ecto.Permissions, actions_module: Permit.Actions.CrudActions def can(%{role: :admin} = user) do permit() |> all(MyApp.Blog.Article) end def can(%{id: user_id} = user) do permit() |> all(MyApp.Blog.Article, author_id: user_id) |> read(MyApp.Blog.Article) end def can(user), do: permit() end iex> MyApp.Repo.all(MyApp.Blog.Article) [ %MyApp.Blog.Article{id: 1, author_id: 1}, %MyApp.Blog.Article{id: 2, author_id: 1}, %MyApp.Blog.Article{id: 3, author_id: 2} ] # The `accessible_by!/3` function also has a `accessible_by/3` variant which returns `{:ok, ...}` tuples. iex> MyApp.Permissions.accessible_by!(%MyApp.Users.User{id: 1}, :update, MyApp.Blog.Article) [%MyApp.Blog.Article{id: 1, ...}, %MyApp.Blog.Article{id: 2, ...}] iex> MyApp.Permissions.accessible_by!(%MyApp.Users.User{id: 1}, :read, MyApp.Blog.Article) [%MyApp.Blog.Article{id: 1, ...}, %MyApp.Blog.Article{id: 2, ...}, %MyApp.Blog.Article{id: 3, ...}] iex> MyApp.Permissions.accessible_by!(%MyApp.Users.User{id: 3, role: :admin}, :update, MyApp.Blog.Article) [%MyApp.Blog.Article{id: 1, ...}, %MyApp.Blog.Article{id: 2, ...}, %MyApp.Blog.Article{id: 3, ...}] ``` """ defmacro __using__(opts) do alias Permit.Types alias Permit.Ecto.Permissions alias Permit.Ecto.UnconvertibleConditionError alias Permit.Ecto.UndefinedConditionError quote do use Permit, unquote(opts) require Ecto.Query @spec repo() :: Ecto.Repo.t() def repo, do: unquote(opts[:repo]) @spec accessible_by( Types.subject(), Types.action_group(), Types.object_or_resource_module(), map() ) :: {:ok, Ecto.Query.t()} | {:error, term()} def accessible_by(current_user, action, resource, opts \\ %{}) do # base_query is (Types.object_or_resource_module() -> Ecto.Query.t()) opts = opts |> Map.put_new(:params, %{}) |> Map.put_new(:base_query, fn %{resource_module: resource_module} -> Ecto.Query.from(_ in resource_module) end) current_user |> can() |> Map.get(:permissions) |> Permissions.construct_query( action, resource, current_user, actions_module(), opts ) end @spec accessible_by!( Types.subject(), Types.action_group(), Types.object_or_resource_module(), map() ) :: Ecto.Query.t() def accessible_by!(current_user, action, resource, opts \\ %{}) do case accessible_by(current_user, action, resource, opts) do {:ok, query} -> query {:error, {:undefined_condition, key}} -> raise UndefinedConditionError, key {:error, errors} -> raise UnconvertibleConditionError, errors end end @impl Permit def resolver_module, do: Permit.Ecto.Resolver end end require Ecto.Query @doc false @spec from(Ecto.Queryable.t()) :: Ecto.Query.t() def from(queryable) do Ecto.Query.from(queryable) end @doc false def filter_by_field(query, field_name, value) do query |> Ecto.Query.where([it], field(it, ^field_name) == ^value) end end