README.md

# Datacop

An authorization library with Dataloader and Absinthe support.

This library is heavily inspired by [bodyguard](https://hex.pm/packages/bodyguard).
Our authorization rules not always simple, so `datacop` allows you to deal with n+1 queries using `dataloader`.

## Installation

The package can be installed by adding `datacop` and optionally `absinthe` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:datacop, "~> 0.1"},
    {:absinthe, "~> 1.6"}
  ]
end
```

## Usage
### Define a Policy module with authorization rules
This module should contain authorization rules or redirect resolution to `dataloader` for batch resolutions.
Try to keep authorize callback pure and redirect side effects to `dataloader`.

```elixir
defmodule MyApp.Blog.Policy do
  @behaviour Datacop.Policy

  @imp true
  def authorize(:delete_post, actor, _post), do: actor.id == post.author_id or actor.admin?

  def authorize(:view_stats, actor, post) do
    if actor.admin? do
      {:dataloader,
       %{
         source_name: MyApp.Blog,
         batch_key: {:one, MyApp.Blog.Post},
         inputs: [{{:can_admin_view_stats?, actor.id}, post.id}]
       }}
    else
      false
    end
  end
end
```

### Define a Data module for integration with `dataloader`
A typical module for working with `dataloader`. In this example we use [`Dataloader.Ecto`](https://hexdocs.pm/dataloader/Dataloader.Ecto.html).
See documentation for this module for detailed explanation how it works.
Batch query should return a list of boolean values in the same order which `post_ids` has.
```elixir
defmodule MyApp.Blog.Data do
  def data do
    Dataloader.Ecto.new(MyApp.Repo, run_batch: &run_batch/5)
  end

  def run_batch(queryable, _query, {:can_admin_view_stats?, admin_id}, post_ids, repo_opts, _params) do
    result =
      queryable
      |> very_complex_query_returns_posts_which_are_managed_by_admin(admin_id, post_ids)
      |> select([posts], {posts.id, true})
      |> MyApp.Repo.all(repo_opts)
      |> Map.new()

    Enum.map(post_ids, &Map.get(result, &1, false))
  end
end
```

### Use context module as a proxy
It is not necessary to do this, but otherwise you'll have to refer to Data and Policy modules directly in places
where corresponding functions are invoked.

```elixir
defmodule MyApp.Blog do
  defdelegate authorize(action, actor, params), to: __MODULE__.Policy
  defdelegate data, to: __MODULE__.Data
end
```

### Setup dataloader in Absinthe schema
See [this guide](https://hexdocs.pm/absinthe/dataloader.html#usage) for reference.
In general implementation of `c:Absinthe.Schema.context/1` should look like this:
```elixir
def context(ctx) do
  loader =
    Dataloader.new() |> Dataloader.add_source(MyApp.Blog, MyApp.Blog.data())

  Map.put(ctx, :loader, loader)
end
```

### Use as a single action
Because absinthe defines `:loader` in `c:Absinthe.Schema.context/1` callback, we can reuse it in resolver functions by passing `:loader` option explicitly:
```elixir
def delete_post(params, %{context: %{actor: actor, loader: loader}}) do
  with {:ok, post} <- MyApp.Blog.fetch_post(params.post_id),
       :ok <- Datacop.permit(MyApp.Blog, :delete_post, actor, subject: post, loader: loader) do
    MyApp.Blog.delete_post(post, params)
  end
end
```
If you don't pass `:loader`, then `datacop` checks if passed module (in example above it is `MyApp.Blog`) has `data/0` function. If yes, then loader can be lazily initiated by `datacop` for single source with passed module as a `:source_name`.

For our example this call will work:
```elixir
Datacop.permit(MyApp.Blog, :view_stats, actor, subject: post)
```
which is a short version of:
```elixir
Datacop.permit(MyApp.Blog, :view_stats, actor,
  subject: post,
  loader: Dataloader.new() |> Dataloader.add_source(MyApp.Blog, MyApp.Blog.data())
)
```
but this won't (`MyApp.Blog.Policy` doesn't implement `data/0`):
```elixir
Datacop.permit(MyApp.Blog.Policy, :view_stats, actor, subject: post)
```
The next example works fine, as `:delete_post` action doesn't use dataloader:
```elixir
Datacop.permit(MyApp.Blog.Policy, :delete_post, actor, subject: post)
```
With `Datacop.permit?/4` it's also possible to work with booleans:
```elixir
Datacop.permit?(MyApp.Blog, :search, actor, loader: loader)}
```

### Use as Absinthe middleware
In order to leverage full potential of `datacop` it is recommended to use it with `absinthe`.
```elixir
alias Datacop.AbsintheMiddleware.Authorize

object :post do
  field :id, :id
  field :stats, :stats do
    middleware(Authorize, {MyApp.Blog, :view_stats, loader: &(&1.loader), actor: &(&1.actor)})
    resolve(...)
  end
end
```
In order to DRY you may want to provide a custom middleware on top of existing one:
```elixir
defmodule MyApp.Schema.Middleware.Authorize do
  @behaviour Absinthe.Middleware

  @impl Absinthe.Middleware
  def call(resolution, {action, context_module}) do
    call(resolution, {action, context_module, []})
  end

  @impl Absinthe.Middleware
  def call(resolution, {action, context_module, opts}) do
    opts =
      opts
      |> Keyword.put_new(:actor, &(&1.actor))
      |> Keyword.put_new(:loader, &(&1.loader))

    params = {action, context_module, opts}

    %{resolution | middleware: [{Authorization.AbsintheMiddleware.Authorize, params} | resolution.middleware]}
  end
end
```

and a helper on top of it
```elixir
def authorize(action, module, opts \\ []) do
  {:middleware, PtWeb.Schema.Middleware.Authorize, {action, module, opts}}
end
```
so block with `:stats` contains less noise:
```elixir
field :stats, :stats do
  authorize(MyApp.Blog, :view_stats)
  resolve(...)
end
```

That's it. Now if you request list of posts, then authorization will be performed in batches.