linusdm

linusdm

Enforce a constraint on the number of Resources created in an Ash Policy

I’m trying to enforce the following policy: a User can only create up to 10 related Resources. When the 11th Resources is being created the policy should disallow this (another Resource should first be destroyed before creating this new one).

This is a simplification of a real-world problem, but the gist is that I want to enforce a rule that needs to take into account “sibling” Resources that already exist, and are not necessarily loaded. 10 is just an arbitrary number, it might as wel be 1000, in which case loading them all would be not be feasible anyway.

My best guess now would be to define an aggregate on the User resource that counts the number of related Resources, and use that in a policy. But I’m not sure aggregates are available in policy checks. I’m also not sure if that’s the correct way to do it, since the aggregate count will be stale by the time the it’s being used to enforce the policy. Or will the aggregate count be part of the atomic action somehow and be reloaded just-in-time?

An alternative might be to query the existing Resources (or at least query the count) in a custom policy check. But in the Ash Framework book I read this advice:

Beware the policy check that performs queries! […] For a LiveView app, if that related data isn’t pre-loaded and stored in memory, it’ll be re-fetched to recalculate the authorization on every page render, which would be disastrous for performance! Ash make a best guess about authorization using data already loaded, if you use run_queries?: false when calling Ash.can?/can_*?. If a decision can’t be made definitively, Ash will use the value of the maybe_is option — by default this is true, but you can fail closed by setting it to false.

Which makes me think this is not a good idea. Or maybe it can be done, but then I don’t understand how this works exactly.

I want to use the can_*? functions to drive the UI in a LiveView. If the policy doesn’t allow creating new Resources, because a limit is reached, I don’t want to make that possibility available in the UI. I’m perfectly fine with some trade-offs: the UI could be on best-effort basis, deciding to offert create functionality based on (somewhat) stale data. In the unfortunate event that the event did offer the create functionality, but a Resource was created out-of-band, then a suitable error should be shown (and the UI should be updated to reflect the new situation, now without the possibility to create a new Resource). It makes me think of Ecto’s unsafe_validate_unique/4 functionality that also works on a best-effort basis, but still enforces strict uniqueness with a unique constraint when using it in combination with unique_constraint/3.

Maybe I’m not thinking about this straight, and maybe this functionality shouldn’t be in a policy in the first place. I’m curious if I overlooked something obvious, or if this pattern of checking an invariant relative to other data (not the actor, nor the request parameters/context for the Resource being created) can be solved in a elegant way.

Most Liked

zachdaniel

zachdaniel

Creator of Ash

So you can do it in a policy, but there are concurrency concerns (as you’ve pointed out) that come into play here that will likely require at least one other thing. Either locking, or some application logic for handling there being more rows than expected. You’ll regardless want a relationship from a resource to all “other” resources for that user

That relationship:

relationships do
  has_many :other_resources_for_user, __MODULE__ do
    source_attribute :user_id
    destination_attribute :user_id
    filter expr(parent(id) != id) # where they aren't me
  end
end

With Locking

changes do
  change TakeAdvisoryLock, on: [:create]
end

You could add a change that takes an advisory lock in a before action hook, and apply it above. Then a policy like this would work:

authorize_if expr(count(other_resources_for_user) < 10)

The lock ensures that any create action is serialized. This does not handle cases where you directly insert a row i.e via a database console concurrently because likely whoever is doing that is not taking the same advisory lock. Easy solve there is just not to do that in a production environment :smiley:

With app logic

You could, for example, have just the policy I showed above, and somewhere in your UI you could see “do they have too many of X? If so, show a banner saying you’ve got too many, please upgrade or your newest ones may be deleted” for example. This “overage” application state is often necessary to handle things like canceled plans, failed payments, downgrades etc. Unless they can manage to create a whole bunch of these things in the same instant, there isn’t any significant risk of them having more than they should, except maybe at most one or two. But your application will be modeled to handle that situation.

zachdaniel

zachdaniel

Creator of Ash

Correct, it would cause a query to be made on every time you check the policies if done that way. But you can also assign the value on mount. So it would be checked on page load, and once again on submit.

egze

egze

What if you had a cache counter to keep track or already created resources?

Where Next?

Popular in Questions Top

beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; somethi...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement