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?: falsewhen callingAsh.can?/can_*?. If a decision can’t be made definitively, Ash will use the value of themaybe_isoption — 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
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 ![]()
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
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
What if you had a cache counter to keep track or already created resources?
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









