Current section
Files
Jump to
Current section
Files
docs/authorization.md
# 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
```