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 callingAsh.can?/can_*?
. If a decision can’t be made definitively, Ash will use the value of themaybe_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.