Authorization
Copy MarkdownIncant 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:
:current_scope
:current_user
:current_admin
:actor
:userConfigure an explicit assign and policy on the admin root when needed:
defmodule MyApp.Admin do
use Incant.Admin,
policy: MyApp.Admin.Policy,
actor_assign: :current_scope
endFor custom extraction, use actor: with a one-argument function or {Module, :function} callback that receives LiveView assigns:
use Incant.Admin,
policy: MyApp.Admin.Policy,
actor: {MyApp.Admin.Actor, :from_assigns}Policies use a Bodyguard-compatible callback shape:
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)
endReturn true/:ok to allow or false/:error/{:error, reason} to deny. Without a configured policy, Incant allows all actions.
See Architecture for the runtime authorization/data-loading flow.
Policies may also scope data:
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))
endTo bridge Bodyguard, delegate from the Incant policy:
def authorize(action, actor, context) do
Bodyguard.permit(MyApp.Admin.Authz, action, actor, context)
endPhoenix 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:
defmodule MyApp.Admin.Resources.Product do
use Incant.Resource,
schema: MyApp.Catalog.Product,
repo: MyApp.Repo,
policy: MyApp.Admin.Policies.Product
endA 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:
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
endFor Bodyguard, keep Incant thin and delegate:
def authorize(action, actor, context) do
Bodyguard.permit(MyApp.Admin.Authz, action, actor, context)
end