Skip to content

Enforce authorization checks in LiveView callbacks#2549

Open
lawik wants to merge 7 commits into
mainfrom
enforce-authz-checks-2
Open

Enforce authorization checks in LiveView callbacks#2549
lawik wants to merge 7 commits into
mainfrom
enforce-authz-checks-2

Conversation

@lawik

@lawik lawik commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

The goal of this is to pro-actively prevent leaving LiveView callbacks open to security issues. The mechanism should force you to consider what authorization your functionality actually requires. This retrofits a lot of authorization checks. We may find that some of them are too tight or too loose. But in some cases they weren't there at all. It is intended to harmonize with the use of Scopes.

@lawik
lawik requested review from joshk, kybishop and nshoes March 12, 2026 16:52
@lawik

lawik commented Mar 12, 2026

Copy link
Copy Markdown
Contributor Author

So the authorisation checks should not just check that you have the role but that you have the role on the relevant entity.

@lawik

lawik commented Mar 13, 2026

Copy link
Copy Markdown
Contributor Author

For checking a DeploymentGroup-focused permission. Should we add the deployment group to the scope and pass the scope or should we just pass the deployment group? @joshk I feel like you'll have some thoughts on this PR overall but also these specifics.

I'm very happy for us to overhaul things in this. What I want to prevent is that we can introduce authz failures inadvertently.

lawik added 7 commits May 21, 2026 09:43
The decorator was unquoting the entire arg AST (e.g. `%{...} = socket`)
which re-injected the pattern match into the wrapped body and shadowed
any variables bound in the head. Extract just the bare socket variable
via socket_var/1 so head pattern matching on the function head works
normally.

Also replace the `{reply, socket} = body` destructure with a
mark_socket/2 helper that handles every LiveView callback return shape:
`{:noreply, _}`, `{:reply, _, _}`, `{:ok, _}`, `{:ok, _, opts}`,
`{:halt, _}`, `{:cont, _}`. The old version only matched 2-tuples and
silently produced MatchError for `{:reply, msg, socket}` returns.

The auth-check no longer rebinds `socket` inside the wrapper; it only
captures the resulting private flags and re-merges them into whatever
socket the body returned.
After rebasing onto main two upstream changes collide with the original
enforcement strategy:

- PR #2681 added self-removal-by-non-admin for org users via a
  can_remove!/3 private check. The `requires_permission(:"org_user:delete")`
  decorator (admin-only) blocks that case, so the handler is moved to
  `special_permission(:enforced_by_can_remove)` and lets the existing
  can_remove! call act as the gate.

- PR #2543 added Product.Notifications, a LiveView that did not exist
  when this branch was opened and therefore has no decorators. The
  wrapper now raises AuthorizationNotApplied on each callback. Decorate
  mount/handle_params/paginate/set-paginate-opts with
  `requires_permission(:"product:view")`, and `dismiss-all` with
  `special_permission(:enforced_by_authorized_bang)` (its body already
  calls authorized!/2).

- The "view role can't dismiss notifications" test asserted the old
  process-exit behaviour via catch_exit/1. Update it to assert the
  flash-error response the new wrapper produces, matching the
  convention used elsewhere in the PR's denied-access tests.
…ply/opts tuples

Adds four new tests to AuthorizedLiveViewTest:

- head-matched scope variables survive `requires_permission` unchanged
- `special_permission(reason)` marks the socket authorized with the
  supplied annotation
- `{:reply, msg, socket}` 3-tuple event returns are preserved
- `{:ok, socket, opts}` 3-tuple mount returns are preserved

These exercise the new socket_var/1 extraction and mark_socket/2
helper in NervesHubWeb.Access.AuthDecorator and would have failed
against the previous decorator implementation.

Also drops the unused default arg on `build_scope/3` (all callers
pass a product).
`AuthDecorator.mark_socket/2` is called from inside `quote do ... end`
blocks, so it has to be referenced by its full module name (the
generated code runs in the user's module, not ours). Adding
`alias __MODULE__` at the top lets the body use the short
`AuthDecorator.mark_socket/2` form, which also silences credo's
Design.AliasUsage warning.
The original PR avoided destructuring scope in the function head
because the decorator's `unquote(socket)` injected the whole pattern
match AST into the body, shadowing the head's bindings. With the
decorator's socket_var/1 extraction in place, the standard
`%{assigns: %{current_scope: scope}} = socket` pattern works
correctly. Convert the workaround back to the idiomatic form across
the LiveViews touched by this PR.
@lawik
lawik force-pushed the enforce-authz-checks-2 branch from 25e3a48 to 71b021d Compare May 21, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant