Where is the best place to put the permissions checking logic in LiveView?

This is pretty much the same approach I’ve taken. Authorization belongs on the contexts. There’s at least a version of every CRUD type function or other mutation action that takes a user and authorizes accordingly.

I don’t think your approach is paranoid at all. (Of course if I am also paranoid I’d think that, lol.)

I case you have some system tasks or jobs that need the data how to solve this issue?

This is one area where building the core of your application should enforce the policy, regardless if it a JSON API, graphql, dead view or liveview.

The best example I am aware of is the Ash framework for building your application domain model inclusive of the access policy and not reimplemented inconsistenly in each access channel. Ash has a sophisticated policy engine with a clever sat solver for efficient evaluation, as well as lots of expressiveness in the policy rules. The logging and diagnostics on how a policy decision is reached is also awesome:

Policy Breakdown
A check status of `?` implies that the solver did not need to determine that check.
Some checks may look like they failed when in reality there was no need to check them.
Look for policies with `✘` and `✓` in check statuses.

A check with a `⬇` means that it didn’t determine if the policy was authorized or forbidden, and so moved on to the next check.
`🌟` and `⛔` mean that the check was responsible for producing an authorized or forbidden (respectively) status.

If no check results in a status (they all have `⬇`) then the policy is assumed to have failed. In some cases, however, the policy
may have just been ignored, as described above.

  Admins and managers can create posts | ⛔:
    authorize if: actor.admin == true | ✘ | ⬇
    authorize if: actor.manager == true | ✘ | ⬇

An example of a policy on a resource:

policies do
  # Anything you can use in a condition, you can use in a check, and vice-versa
  # This policy applies if the actor is a super_user
  # Additionally, this policy is declared as a `bypass`. That means that this check is allowed to fail without
  # failing the whole request, and that if this check *passes*, the entire request passes.
  bypass actor_attribute_equals(:super_user, true) do
    authorize_if always()
  end

  # This will likely be a common occurrence. Specifically, policies that apply to all read actions
  policy action_type(:read) do
    # unless the actor is an active user, forbid
    forbid_unless actor_attribute_equals(:active, true)
    # if the record is marked as public, authorize
    authorize_if attribute(:public, true)
    # if the actor is related to the data via that data's `owner` relationship, authorize
    authorize_if relates_to_actor_via(:owner)
  end
end

They just recently added a mix task for visually describing policy evaluations for documentation, an example:

policy flow

More on Ash policy basics here: Ash Framework

A comprehensive approach to domain modelling and access policy is one of the key things that got my attention with Ash, being able to build the domain model and business rules for access in a mostly declartive fashion with powerful access policy and changeset / action handling (even for nested resources) really takes the drudgery out of building very capable applications, plus you get APIs almost for free, multi tenancy, sane transactional aware notification handling (eg pub sub notifications don’t get sent unless the transaction actually suceeds), and admin UIs which are great in development and early prototyping.

It’s definitely a different way to go vs writing it all yourself or modifying outputs from code generators which you then have to maintain and test. No doubt Ash does take some getting used to but the productivity gains are real and there really isnt any lock-in and plenty of escape hatches. Ash is worth a good hard look IMO vs solving all of the “table stakes” commodity problems that customers take for granted, you actually get to focus on the real customer problem and both develop and demonstrate value quickly.

11 Likes

The way I’ve solved this, is to pass an on the fly created %User{role: :system} and then use that information in the permission layer.

The permission layer, is basically a module that defines a series of overloads to the auth? function:

defmodule Permission do
  def auth?(%User{role: :system}, :update, %Product{} = product), do: true
  def auth?(%User{role: :admin}, :update, %Product{} = product), do: true
  def auth?(%User{} = user, :update, %Product{} = product) do
     ...
  end
end

And from the calling side looks like

if Permission.auth?(user, :update, product) do

The benefit I see to this approach, is that there’s nothing to learn beyond basic elixir and you know everything that’s going on behind the curtains, because there are no curtains.

Rant mode: I got a moderate to strong bias towards writing 50-100 lines of code instead of using a library. Permissions is one of those cases. Another one is test factories.

I think you have the basics there with the user context, the action and the resource to make a logic based decision, but without knowing exactly which rule passed or failed.

What might be a little surprising is permit vs deny evaluation. if your auth? function matches on a rule that permits but you also could logically match with less specificity on a rule that denies, then the permit wins, which is probably not what you want.

This is one reason why I do prefer the approach that uses a formal Boolean satisfiability solver.

Another use case is using policy rules to scope queries and filter the results from the database to what you can actually see and effect for an action whilst also doing it efficiently by avoiding redundant or unnecessary queries without reimplementing the authorisation logic yet again.

With a sat solver, the simplest query can be determined and applied to scope results according to the declared policy. Ash provides this, meaning policies also define query scopes so that you can’t see and touch what you shouldn’t be allowed to see or touch at the query level.

With your approach, you will have to implement the policy rules again in your query scopes to control visibility of things.

Given Ash provides a lot of expressibility on calculations including aggregate functions which policies can use (e.g a rule that a hotel booking can only have a limited number of registered guests for the selected room), both authorisation and filtering can still be decided efficiently, and the authorisation decisions (permits and denies) is also fully explained in the logs.

Yes it is true that all this advanced stuff goes on beyond behind the curtain, and whilst I also won’t reach for dependencies that provide trivial value, I will reach for capable frameworks that deliver significant value.

Such things should not be feared with some level due diligence, but your use cases may be much simpler than mine.

1 Like