The examples only use RBAC for illustration, but LetMe does not make any assumption about the kind of checks you run. If you need to make a decision based on one or multiple claims, you can just write checks for those claims. It also does not make any assumptions about the format of the subject or the object, which means you can also pass a tuple or a map with claims or anything else as a subject, and an object or multiple objects or more data relating to the object in any format you need, as long as your check functions understand them. You can also register pre-hooks to run before running the checks, e.g. to load more data necessary in multiple checks (LetMe.Policy — LetMe v3.0.1).
So in your example, assuming that there are multiple bars, the user can have different customer statuses in each bar, and you already loaded the location, drink, and customer status for the specific bar before running the permission checks, you could end up with a policy module like this:
defmodule MyApp.Policy do
use LetMe.Policy
object :drink do
action :consume do
allow storage: :counter
allow customer_status: :vip, storage: :cellar
# deny access to alcoholic drinks if user is below drinking age, no matter what
deny [:drink_is_alcoholic, :below_drinking_age]
# deny access to any banned user
deny customer_status: :banned
end
end
end
With these check functions:
defmodule MyApp.Policy.Checks do
def customer_status(%User{}, %{customer_statuses: statuses}, status) do
Enum.find_value(statuses, false, &(&1.type == status))
end
def storage(_, %{drink: %Drink{storage: storage}}, storage), do: true
def storage(_, %{drink: %Drink{}}, _), do: false
def drink_is_alcoholic(_, %{drink: %Drink{alcoholic: alcoholic}}),
do: alcoholic
def below_drinking_age(%User{} = user, %{location: %Location{} = location}) do
age = SomeModule.get_age(user.birthdate)
legal_drinking_age = SomeModule.get_legal_drinking_age(location.country)
age < legal_drinking_age
end
end
With this, you pass all the necessary information to the authorize function:
user = %User{id: 10, birthdate: ~D[2002-02-05]}
object = %{
# storage can be :counter, :cellar, :private_lounge
drink: %Drink{alcoholic: true, storage: :counter},
location: %Location{id: 20, country: "uk"},
customer_statuses: [
# type can be :vip, :banned, :private_lounge_access
%CustomerStatus{user_id: 10, location_id: 20, type: :vip}
]
}
Policy.authorize?(:drink_consume, user, object)
Alternatively, you could only pass the drink and the location, and preload the customer statuses with a pre-hook. Or maybe the location is preloaded in the drink struct. Whatever it is, LetMe doesn’t care about these details. It’s up to you!
In general, I would opt for parameterized rules if possible, as opposed to very specific rules (e.g. allow customer_status: :vip instead of allow :is_vip). Personally, I wouldn’t go too far with abstractions in favor of readability, but if you wanted, you could define more general check functions, e.g. one that checks for equality of a value in any nested map: allow match: {[:path, :to, :value], :value_to_check}.
This doesn’t cover every single rule you listed, but should be enough to illustrate how you can compose complex rule sets.


















