woylie
I just released LetMe 1.0, marking the first stable release.
LetMe is an authorization DSL with introspection capabilities.
Why?
There are plenty of authorization libraries, but when an application grows and with it the number of authorization rules, it becomes harder to see at a glance what is allowed under which circumstances. I wanted a solution that a) provides me an easily readable format to define authorization rules, and b) allows me to list and filter those rules, so that I can dynamically generate documentation pages and help texts for permission forms.
Example
A simple policy module would look like this:
defmodule MyApp.Policy do
use LetMe.Policy
object :article do
action :create do
desc "allows a user to create a new article"
allow role: :editor
allow role: :writer
end
action :read do
desc "allows a user to read an article and to see a list of articles"
allow true
deny :banned
end
action :update do
allow role: :editor
allow [:own_resource, role: :writer]
end
action :delete do
allow role: :editor
end
end
end
The allow and deny conditions only reference check functions which you have to define on your own. This means the DSL is much simpler than a full-fledged policy language. In the end it’s just a means of combining custom checks.
defmodule MyApp.Policy.Checks do
alias MyApp.Accounts.User
@doc """
Returns `true` if the `banned` flag is set on the user.
"""
def banned(%User{banned: banned}, _, _), do: banned
@doc """
Checks whether the user ID of the object matches the ID of the current user.
Assumes that the object has a `:user_id` field.
"""
def own_resource(%User{id: id}, %{user_id: id}, _opts) when is_binary(id), do: true
def own_resource(_, _, _), do: false
@doc """
Checks whether the user role matches the role passed as an option.
## Usage
allow role: :editor
or
allow {:role, :editor}
"""
def role(%User{role: role}, _object, role), do: true
def role(_, _, _), do: false
end
LetMe compiles the defined rules into authorization and introspection functions.
iex> MyApp.Policy.authorize?(:article_read, current_user)
true
iex> MyApp.Policy.get_rule(:article_create)
%LetMe.Rule{
action: :create,
allow: [
[role: :admin],
[role: :writer]
],
name: :article_create,
object: :article,
# ...
}
You can also get a list of rules and apply filters on them. There are some more features including a Schema behaviour for query scoping and field redactions.
Trending in Announcing
Other Trending 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
- #blog-post
- #ai
- #phoenix_html
- #iex
- #elixirconf-us
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 9- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
derpycoder
Awesome Library. Looks succinct. I loved the introspection aspect and redaction of information!
I was curious about how the library will handle Role explosion and some of the scenarios mentioned below?
Say a customer comes to bar, instead of having a role depicting that they can consume alcohol, they claim that they can_drink.
We can take the claim and restrict it with policy saying, age must be drinking age, based on attribute country.
And maybe add a scope, saying that the customer can only access the drinks available on counter.
Later, bouncer might grant or revoke access on the fly!
Maybe the customer gets recognised as a VIP, so now he should have access to drinks from cellar! Perhaps they fancy a private lounge. (Instead of creating another role for people who can enter the lounge or drink special wine, they can be assigned claims like: can_enter_private_lounge)
I have a verbose way achieving the above scenarios:
P.S. I am excited about this. I won’t have to write much code. I am just trying to piece together how I can make use of the library.
woylie
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:
With these check functions:
With this, you pass all the necessary information to the authorize function:
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: :vipinstead ofallow :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.
stefanchrobot
This looks great! I really like the idea of separating the rules from the implementation. Couple of notes:
use LetMe.Policy, checks: SomeModule(unless this is already the case?),I think it would read better as
deny :banned?. Still, first it’s allow everything but then there are some “conflicting” rules. Would be great if it was obvious from the code (not the docs) how the rules are combined and what’s the behavior when no rules are specified (is it sum or intersection, allow by default or deny by default?). Maybe renameactionmacro to something else?woylie
You can set a different check module: Check module
An action is allowed if any allow rule evaluates to true and no deny rule evaluates to true. I think that’s a fairly standard way of dealing with access control rules. To make this completely explicit, you’d have to string together all rules and checks with boolean operators. But I’m pretty happy with the API as it is
It could have been
operation, but I guess that ship has sailed.woylie
Just released patch version 1.0.2. Nothing exciting, only documentation updates. ex_doc’s cheat sheet feature is nice, though: Rules and Checks — LetMe v3.0.1
woylie
LetMe 1.1.0 was released, which adds a
metadatamacro and ametadatafield to theLetMe.Rulestruct, which allows you to extend the functionality of the library. Thanks to Stephen for the contribution.woylie
[1.2.0] - 2023-06-19
Added
optsargument to the authorize functions, so thatadditional options can be passed to pre-hooks.
LetMe.filter_rules/2to allow filtering by meta data.Changed
woylie
I just released LetMe 2.0.0!
What’s New?
Internal Policy Rule Representation
The internal representation of the policy rules has been changed to a tree-like expression format using
AllOf,AnyOf,Check,Literal, andNotstructs. An expression might look like this:Or this:
Or just:
These expressions are generated from the policy rules you define with the macro DSL. At compile time, a few basic normalization steps are applied, for example to remove unnecessary nesting or to factorize common checks in
AnyOfexpressions.Lazy Evaluation
All expressions are now evaluated lazily when an authorization check is performed.
Checks With Custom Return Values
Previously, all check functions had to return a boolean. Now, the return type is
boolean | :ok | :error | {:ok, term} | {:error, term}.Detailed Authorization Errors
By default,
c:LetMe.Policy.authorize/4still returns{:error, :unauthorized}if an authorization check fails. But there is a newerroroption you can pass touse LetMeto set a default value and toc:LetMe.Policy.authorize/4andc:LetMe.Policy.authorize!/4to override the default. The available values are::detailedIf you set the value to
:detailed,authorize/4returns anUnauthorizedErrorstruct with the parts of the expression that were evaluated until a decision was made. You can find the exact return value of the check function in theresultfield of theCheckstruct. TheUnauthorizedErrorexception raised byauthorize!/4also contains the expression with this option value.:simpleIf you set the value to
:simple,authorize/4returns anUnauthorizedErrorstruct without the expression. Likewise,authorize!/4raises anUnauthorizedErrorexception without it.anyAny other value will be used directly in the error tuple:
Upgrade from v1
The policy DSL and authorization API remain unchanged. The only breaking changes are:
allowanddenyfields of theLetMe.Rulestruct in favor of the newexpressionfield.error_reasonanderror_messageoptions fromuse LetMe.Policyin favor of the newerroroption.allowanddenyfilter options onLetMe.filter_rules/2andc:LetMe.Policy.list_rules/1in favor of a singlecheckoption.For more details, refer to the changelog.
woylie
Since the release of version 2, I extracted the expressions, evaluation, and optimization logic into a separate library called Spek. I just released version 3.0.0 of LetMe, which removes that code from the library and adds a dependency on Spek. That means that now you can reuse LetMe authorization rules in larger Spek expressions, and you can pass the expressions defined with LetMe to the Spek functions, for example to collect error reasons from the expression tree into a flat list.
The update should be straightforward. Code changes are only necessary if you used the LetMe expression structs directly. See changelog for details.