# Authorization

Incant is experimental; authorization APIs may still change before a first stable release.

Incant reuses the host Phoenix application's existing authentication. It does not generate a user model. The LiveView context exposes an `actor`, detected from assigns in this order:

```elixir
:current_scope
:current_user
:current_admin
:actor
:user
```

Configure an explicit assign and policy on the admin root when needed:

```elixir
defmodule MyApp.Admin do
  use Incant.Admin,
    policy: MyApp.Admin.Policy,
    actor_assign: :current_scope
end
```

For custom extraction, use `actor:` with a one-argument function or `{Module, :function}` callback that receives LiveView assigns:

```elixir
use Incant.Admin,
  policy: MyApp.Admin.Policy,
  actor: {MyApp.Admin.Actor, :from_assigns}
```

Policies use a Bodyguard-compatible callback shape:

```elixir
defmodule MyApp.Admin.Policy do
  use Incant.Policy

  def authorize(:view_admin, actor, context), do: allow_admin?(actor, context)
  def authorize(:view_resource, actor, %{resource: resource}), do: can_view?(actor, resource)
  def authorize(:view_dashboard, actor, %{dashboard: dashboard}), do: can_view?(actor, dashboard)
  def authorize(:view_row, actor, %{resource: resource, selected_row: row}), do: can_view_row?(actor, resource, row)
  def authorize(:create, actor, %{resource: resource, attrs: attrs}), do: can_create?(actor, resource, attrs)
  def authorize(:edit, actor, %{resource: resource, row: row, attrs: attrs}), do: can_edit?(actor, resource, row, attrs)
  def authorize(:run_action, actor, %{action: action, row: row}), do: can_run?(actor, action, row)
  def authorize(:run_bulk_action, actor, %{action: action, selected_ids: ids}), do: can_run_bulk?(actor, action, ids)
  def authorize(:run_page_action, actor, %{action: action}), do: can_run_page?(actor, action)
end
```

Return `true`/`:ok` to allow or `false`/`:error`/`{:error, reason}` to deny. Without a configured policy, Incant allows all actions.

See [Architecture](architecture.md) for the runtime authorization/data-loading flow.

Policies may also scope data:

```elixir
def scope_query(actor, resource, queryable, context) do
  MyApp.Admin.Authorization.scope_query(actor, resource, queryable, context)
end

def scope_rows(actor, resource, rows, context) do
  Enum.filter(rows, &can_view_row?(actor, resource, &1))
end
```

To bridge Bodyguard, delegate from the Incant policy:

```elixir
def authorize(action, actor, context) do
  Bodyguard.permit(MyApp.Admin.Authz, action, actor, context)
end
```

Phoenix 1.8 `phx.gen.auth` apps should usually set `actor_assign: :current_scope` and write policies against the generated scope struct.

Resources and dashboards can override the admin policy locally:

```elixir
defmodule MyApp.Admin.Resources.Product do
  use Incant.Resource,
    schema: MyApp.Catalog.Product,
    repo: MyApp.Repo,
    policy: MyApp.Admin.Policies.Product
end
```

A local policy handles resource/dashboard actions and scoping for that surface; the admin policy remains the fallback.

A realistic account-scoped policy can use the Phoenix `current_scope` generated by `phx.gen.auth`:

```elixir
defmodule MyApp.Admin.Policy do
  use Incant.Policy
  import Ecto.Query

  def authorize(:view_admin, %{user: user}, _context), do: user.role in [:admin, :operator]
  def authorize(:view_resource, _scope, _context), do: true
  def authorize(:view_dashboard, _scope, _context), do: true
  def authorize(:view_row, _scope, %{selected_row: nil}), do: {:error, :not_found}
  def authorize(:view_row, scope, %{selected_row: row}), do: row.account_id == scope.user.account_id
  def authorize(:create, %{user: user}, %{attrs: attrs}), do: user.role == :admin and attrs["account_id"] == to_string(user.account_id)
  def authorize(:edit, scope, %{row: row}), do: row.account_id == scope.user.account_id
  def authorize(:run_action, scope, %{row: row}), do: row.account_id == scope.user.account_id
  def authorize(:run_bulk_action, %{user: user}, _context), do: user.role == :admin
  def authorize(:run_page_action, %{user: user}, _context), do: user.role in [:admin, :operator]

  def scope_query(scope, _resource, queryable, _context) do
    where(queryable, [row], field(row, :account_id) == ^scope.user.account_id)
  end

  def scope_rows(scope, _resource, rows, _context) do
    Enum.filter(rows, &(&1.account_id == scope.user.account_id))
  end
end
```

For Bodyguard, keep Incant thin and delegate:

```elixir
def authorize(action, actor, context) do
  Bodyguard.permit(MyApp.Admin.Authz, action, actor, context)
end
```
