woylie

woylie

LetMe - authorization DSL

I just released LetMe 1.0, marking the first stable release.

LetMe (hex)
LetMe (Github)

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.

Most Liked

woylie

woylie

LetMe 1.1.0 was released, which adds a metadata macro and a metadata field to the LetMe.Rule struct, which allows you to extend the functionality of the library. Thanks to Stephen for the contribution.

derpycoder

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. :sweat_smile:

woylie

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 v1.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.

woylie

woylie

[1.2.0] - 2023-06-19

Added

  • Added an optional opts argument to the authorize functions, so that
    additional options can be passed to pre-hooks.
  • Updated LetMe.filter_rules/2 to allow filtering by meta data.

Changed

  • Pre-hook options are now expected to be passed as a keyword list.
woylie

woylie

I’d prefer for the policy to be linked explicitly to the checks, something like use LetMe.Policy, checks: SomeModule (unless this is already the case?),

You can set a different check module: Check module

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?).

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 :grinning:

Maybe rename action macro to something else?

It could have been operation, but I guess that ship has sailed.

Where Next?

Popular in Libraries Top

Qqwy
TypeCheck: Fast and flexible runtime type-checking for your Elixir projects. Core ideas Type- and function specifications are const...
336 13801 100
New
kelvinst
Hey everyone! Well, we made this lib a while ago and now we decided to finally go out and public with it! It’s a tool for creating and m...
New
OvermindDL1
Been making an MLElixir thing (not released yet…) for fun in spare time in the past day. I’m just trying to see how much I can get an ML...
132 13487 106
New
tmbb
I’ve published the first version of my Makeup library. It’s a syntax highlighter for Elixir in the spirit of Pygments, Currently it highl...
New
woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
OvermindDL1
I created a new library (rather I pulled out a couple files from my big project), it manages an operating system PID file for the BEAM. ...
New
Azolo
Hey everyone, I just released WebSockex which is a Elixir WebSocket client. WebSockex strives to work as a OTP special process, be RFC6...
New
KallDrexx
For a good number of months I've been working on creating a very basic RTMP live video streaming server. Now that I have a very, very ba...
New
mischov
import Meeseeks.CSS html = HTTPoison.get!("https://news.ycombinator.com/").body for story &lt;- Meeseeks.all(html, css("tr.athing")) do...
New
wojtekmach
Hey everyone! Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New

Other popular topics Top

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
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New

Sub Categories:

We're in Beta

About us Mission Statement