# Upgrading to v0.21

## Bump Your Deps

Update Backpex to the latest version:

```elixir
defp deps do
  [
    {:backpex, "~> 0.21.0"}
  ]
end
```

v0.21 makes authorization something Backpex enforces rather than something every
call site has to remember. `c:Backpex.LiveResource.can?/3` is now evaluated
centrally — in `Backpex.Resource` before any mutation, and directly before every
item and resource action runs.

If you never call `Backpex.Resource` yourself and never wrote a custom item
action, the only change you may notice is that a few forged or stale interactions
now raise instead of silently doing nothing. If you do, read on: two function
signatures changed.

## 1. Security fix: the item action key no longer comes from the DOM

> #### Security {: .error}
>
> Before v0.21, submitting an item action modal read the action key from the
> `phx-value-action-key` DOM parameter. The `can?/3` check ran against that
> client-supplied key while the server executed the module stored in
> `action_to_confirm` — a client could pass the key of an action it *is* allowed
> to perform and have a different, unauthorized action executed. This was
> actively bypassable, not merely easy to forget.

The key is now taken from `socket.assigns.action_to_confirm.key`, which the view
sets when it opens the modal, and `phx-value-action-key` has been removed from
the form. Nothing to do on your side unless you rendered Backpex's form component
yourself with a hand-built `action-key` value.

Client-supplied action keys are also no longer passed through
`String.to_existing_atom/1`. They are matched against the registered
`item_actions/1` and `resource_actions/0` keys, so an unknown key raises
`Backpex.NoResultsError` (404) instead of an `ArgumentError`.

### Removed upload files are validated

> #### Security {: .error}
>
> Before v0.21, removing an existing file in an upload field accepted any
> client-supplied file key and passed it on to your `remove_uploads/3` callback.
> A callback that deletes the file from disk (as in the upload field docs) could
> be made to delete another record's file, or any file reachable by path
> traversal.

A file key is now only accepted when `list_existing_files/1` returns it for the
item being edited and the upload field is not readonly. Anything else is ignored,
so `remove_uploads/3` only ever receives files the item had before the edit.
Nothing to do on your side.

## 2. `delete_all/2` → `delete_all/4`

`delete_all` needs the assigns to authorize the deletion.

```diff
- Backpex.Resource.delete_all(items, socket.assigns.live_resource)
+ Backpex.Resource.delete_all(items, socket.assigns, socket.assigns.live_resource)
```

## 3. `update_all/3` and `update_all/4` → `update_all/5`

`update_all` also needs the assigns, and `event_name` moved into the options.
The old head had a default argument (`event_name \\ "updated"`), so it defined
both an arity-3 and an arity-4 function. Both are gone:

```diff
- Backpex.Resource.update_all(items, updates, MyAppWeb.UserLive)
+ Backpex.Resource.update_all(items, updates, socket.assigns, MyAppWeb.UserLive)

- Backpex.Resource.update_all(items, updates, "deleted", MyAppWeb.UserLive)
+ Backpex.Resource.update_all(items, updates, socket.assigns, MyAppWeb.UserLive, event_name: "deleted")
```

> #### Watch the argument order {: .warning}
>
> The two old forms fail differently, and neither fails silently:
>
> * `update_all(items, updates, MyAppWeb.UserLive)` raises
>   `UndefinedFunctionError` — there is no `update_all/3` any more.
> * `update_all(items, updates, "deleted", MyAppWeb.UserLive)` has the same arity
>   as the new `update_all(items, updates, assigns, live_resource)`. A guard
>   (`is_map(assigns) and not is_struct(assigns)`) makes it raise
>   `FunctionClauseError` instead of quietly authorizing against the event name
>   string.
>
> The same guard also rejects `socket` where `socket.assigns` belongs. If you see
> either error, you missed a call site.

## 4. Central enforcement and default actions

Every mutation in `Backpex.Resource` now authorizes before it does anything else
— before the changeset is built and before `c:Backpex.Field.before_changeset/6`
runs, so your own code never executes for an unauthorized request.

| function | authorizes | item passed to `can?/3` |
| --- | --- | --- |
| `insert/6` | `:new` | `nil` |
| `update/6` | `:edit` | the item |
| `update_all/5` | `:edit` | each item |
| `delete_all/4` | `:delete` | each item |

`can?/3` must return a boolean. Only `true` allows; `false` and `nil` deny. Any
other value raises an `ArgumentError`, so a policy result like
`{:error, :unauthorized}` can never pass as permission. If you delegate to a
policy library, convert its answer, e.g. `Policy.authorize(...) == :ok`.

Note that `insert/6` checks with `nil`, consistent with every other `:new` check
in Backpex. A clause like `def can?(_assigns, :new, nil), do: false` is now
honored on save, not only when the form is opened.

Reads (`list/4`, `get/4`, `count/4`) are **not** authorized here. `:index` and
`:show` are still enforced in the view layer — filtering rows after pagination
would corrupt counts and select-all.

### What an item action's `handle/3` may assume

Backpex calls `c:Backpex.ItemAction.handle/3` only after the gate authorized
**exactly those items under exactly that action's key**. The items you receive
are the ones the gate re-read from the data layer, not the snapshot that was
rendered. Writing those same items back is the decision the gate already made,
so pass `authorize?: false` instead of paying for a second evaluation of your
`can?/3`:

```elixir
def handle(socket, items, _data) do
  # already authorized by Backpex before this ran
  Backpex.Resource.delete_all(items, socket.assigns, socket.assigns.live_resource, authorize?: false)

  {:ok, socket}
end
```

The guarantee covers nothing else. Writes to *other* items, or to another
resource, keep the default gate.

### Overriding the action: `:authorization_action`

When an action writes items the gate did not cover, `:authorization_action`
picks the key to check instead of the `:new` / `:edit` / `:delete` default:

```elixir
Backpex.Resource.update_all(other_items, updates, socket.assigns, socket.assigns.live_resource,
  authorization_action: socket.assigns.item_action_key
)
```

`assigns.item_action_key` is new in v0.21. Backpex sets it immediately before
calling `c:Backpex.ItemAction.handle/3` and clears it again afterwards, so it is
meaningful exactly for the duration of one dispatch and the action does not need
to know which key it was registered under.

The option must be a non-nil atom; anything else raises `ArgumentError` rather
than reaching a permissive catch-all `can?/3` clause.

### Skipping the check: `authorize?: false`

System and cascade writes are not user-initiated actions on the resource they
touch. Skip the check explicitly:

```elixir
# nullify the foreign key on another resource
Backpex.Resource.update_all(item.posts, [set: [user_id: nil]], socket.assigns, MyAppWeb.PostLive,
  event_name: "updated",
  authorize?: false
)
```

The option is deliberately explicit and greppable. Reach for it only when the
write is already covered by the gate, or when it really is not the user's action
on that resource.

## 5. Item actions are strict now

Previously, an item action silently filtered unauthorized items out of the
selection and then ran `handle/3` with what was left — including an empty list,
which usually reported success. That is gone.

* A selection containing a single unauthorized item raises
  `Backpex.ForbiddenError`. Nothing runs.
* A stale or forged item id raises `Backpex.NoResultsError`. `nil` never
  reaches your `can?/3`.
* An unknown action key raises `Backpex.NoResultsError`.
* `handle/3` receives the **full** list of selected items, and is not called at
  all for an empty selection.
* The bulk action button in the toolbar is disabled when the selection is empty
  **or** contains any unauthorized item, so the UI never offers a click that
  would raise. Its `title` says why.
* A row that is authorized for none of the bulk actions can no longer be
  selected: its checkbox is disabled, and "select all" skips it. Without this a
  user could build a selection whose every action is disabled, with no way to
  tell which row caused it.

Three consequences worth knowing:

* **A raised gate is not an error page.** Both exceptions carry a `plug_status`,
  but LiveView only maps that to an HTTP status while a view mounts. From an
  event handler on a connected socket the LiveView process crashes and the client
  reloads the page — no 403 page, no flash. That is intended: the gate exists so
  the write does not happen, not to explain itself. The preflight checks are what
  keep a user from reaching a gate through the UI at all.
* **Double-clicking a delete button** after the item is gone now raises
  `Backpex.NoResultsError` — the LiveView crashes and reconnects instead of
  flashing "0 items deleted". This is a deliberate trade-off for not leaking
  whether an id exists.
* **The execution gate checks re-read records, not the rendered ones.** Right
  before it authorizes, Backpex re-reads every selected item by its primary key
  through the adapter, checks `can?/3` against those records and passes them to
  `handle/3`. A row another actor changed while the confirmation modal was open
  is judged by its current values; a row that was deleted, or that has left the
  resource's `item_query/3` scope, comes back as `nil` and raises
  `Backpex.NoResultsError`. The check when the modal *opens* still uses the
  rendered items — it only decides whether the dialog appears.

  This is not a lock. A window remains between the re-read and whatever your
  `handle/3` writes; Backpex opens no transaction and takes no row locks there.
  An action that needs strict atomicity has to re-read and lock inside its own
  `handle/3` (`c:Ecto.Repo.transaction/2` plus a `lock: "FOR UPDATE"` query).

## 6. `handle_item_action/5` behavior change

`Backpex.ItemAction.handle_item_action/5` is public. It no longer filters items;
it re-reads them, authorizes them and raises. If you call it yourself, expect
`Backpex.ForbiddenError` / `Backpex.NoResultsError` where you previously got a
shorter list, and expect `c:Backpex.ItemAction.handle/3` to receive the re-read
records rather than the list you passed in. `Backpex.ItemAction.authorize_fresh!/3`
is that re-read-then-authorize step on its own, and `Backpex.ItemAction.dispatch/5`
remains the unguarded "just run it" primitive.

## 7. Do not let a `rescue` swallow the gate

This applies to `Backpex.Resource` calls that are still gated — the ones you did
*not* pass `authorize?: false`. A broad `rescue` around such a call now catches
`Backpex.ForbiddenError` and `Backpex.NoResultsError` as well, turning a refused
write into an ordinary flash message. That is worse than the crash it replaces:
the request looks handled, and the failure is filed under "something went wrong"
instead of "you may not do this". Reraise them:

```elixir
def handle(socket, items, _data) do
  # ...
rescue
  error in [Backpex.ForbiddenError, Backpex.NoResultsError] ->
    reraise error, __STACKTRACE__

  error ->
    # your existing error handling
end
```

## 8. Readonly fields

`readonly` is now a global field option. Existing configurations keep working,
but check these changes when upgrading:

- Disabled checkboxes and toggles no longer submit a hidden `false` value.
  Custom form handlers must preserve existing values when these parameters are absent.
- Nested fields inherit a readonly parent; a child's `readonly: false` cannot override it.
- Review custom CSS and DOM selectors for the updated readonly styling and
  non-interactive dropdown markup.
- Code inspecting field schemas must include `Backpex.Field.default_config_schema/0`
  to find the global `readonly` option.

See the [Readonly guide](../fields/readonly.md) for supported fields and behavior,
and [Readonly for custom fields](../fields/readonly.md#readonly-for-custom-fields)
for custom implementations.

## Checklist for custom actions

- [ ] Every `delete_all/2` call updated to `delete_all/4`.
- [ ] Every `update_all/3` **and** `update_all/4` call updated to `update_all/5`,
      with `event_name:` moved into the options. The arity-3 form raises
      `UndefinedFunctionError`, the arity-4 form `FunctionClauseError`.
- [ ] Item actions that write back the items they were handed pass
      `authorize?: false` — the gate already covered them.
- [ ] Writes to *other* items pass `authorization_action:` when the default
      `:new` / `:edit` / `:delete` is not the right key.
- [ ] Cascade or system writes pass `authorize?: false`.
- [ ] Broad `rescue` clauses around a *still-gated* `Backpex.Resource` call
      reraise `Backpex.ForbiddenError` and `Backpex.NoResultsError`.
- [ ] `handle/3` implementations cope with receiving the full selection (they are
      no longer handed a pre-filtered list, and are never called with `[]`).
