MilosMosovsky

MilosMosovsky

terminator - Granular elixir ACL/permissions library suggestions/thoughts

Hello everyone! None of the provided authorization libraries worked for me in a way that I needed (I need granular permissions per Role, User, Entity) therefore I created small library for ACL permissions.

Initially I was doing the code inside my project but then I created library from it. I would love to hear any suggestions/thoughts about that. Initially I went with existing libraries like authorize or canary but as I need many actions to be performed I ended up with ~20 custom can? methods just for 1 schema and it was almost impossible to build admin panel for it to manage those permissions.

https://github.com/MilosMosovsky/terminator
https://hex.pm/packages/terminator

What terminator includes?

Database based permission system

When you are building large app with many actions and each action needs to have different permissions + you need some admin panel to manage those permissions existing libraries are just not enough.

Role based permissions

With existing libraries it was really hard to introduce 5 custom roles with different permissions (e.g. admin can done everything, editor can edit post description, super_editor can delete posts, writer can write new posts and registered user can view them. Terminator allows me to create as much roles as I need with assigned permissions to them

Compatibility with ecto projects

I already had existing project without any permissions therefore it was crucial to have something which I can plug-in with several lines without modyfing existing code. Performer which is main actor in terminator can be plugged to any existing schema (I have it plugged to Account schema)

Easy to read DSL

When I tried to create permission with existing libraries after a while I felt like a compiler in my head. You have to read extensively through multiple can? implementations and pattern match them in head to see easily which permission you are modifying. I created easily readable DSL:

permissions do
  has_ability(:delete)
  has_role(:admin)
end

as_authorized do
  "I can safely proceed"
end

Full code coverage

As I understand how ACL is crucial for app I am maintaining 100% code coverage and keep library “over-tested”

Future ideas

  • I am using ueberauth, absinthe in my app, I want to do easy plugs to load performer from plugs (session) or absinthe context.

  • Currently I have WIP version for field based authorization in GraphQL (e.g. you have user shape but only admins and owner of an account can query email field, you can solve it with multiple shaped queries but I created middleware on the top of terminator which protects resulting shape and returns nil on particular field) this allows you to have only 1 query
    query { account { id, email } } and terminator protects email field in resolver.

As I am originally react developer I realize that code is probably not perfect but I would love to hear any suggestions/ideas and try-outs! Thank you!

Most Liked

Eiji

Eiji

I have some found problems/questions related to your READM.md file …

#1 Missing do keyword at line 1:
defmodule Sample.Post -> defmodule Sample.Post do

#2 Wrong module call at line 20:
Sample.Repo.get(Sample.Post, id) |> Sample.repo.delete() -> Sample.Repo.get(Sample.Post, id) |> Sample.Repo.delete()

#3 Wrong module call at line 26:
:ok -> Sample.Repo.get(Sample.Post, id) |> Sample.repo.delete() > :ok -> Sample.Repo.get(Sample.Post, id) |> Sample.Repo.delete()

#4 Firstly you give example:

    permissions do
      has_role(:admin) # or
      has_role(:editor) # or
      has_ability(:delete_posts) # or
    end

and then you give this one:

    permissions do
      calculated(:confirmed_email)
      calculated(:is_owner, [post])
    end

so it will succeed when owner of specified Post does not had confirmed email, right? It does not looks like a perfect example here :slight_smile:

#5 Another problem is in this example:

defmodule Sample.Post do
  def create() do
    user = Sample.Repo.get(Sample.User, 1)
    post = %Post{owner_id: 1}
    load_and_authorize_performer(user)

    permissions do
      has_role(:editor)
    end

    as_authorized do
      case is_owner(performer, post) do
        :ok -> ...
        {:error, message} -> ...
      end
    end
  end

  def is_owner(performer, post) do
    load_and_authorize_performer(performer)

    permissions do
      calculated(fn p, [post] ->
        p.id == post.owner_id
      end)
    end

    is_authorized?
  end
end

Here performer in case statement is completely magic. Newbies would not get how it’s actually working.

#6 Also as_authorized do … case … end looks too complicated comparing to or examples.

Personally I would suggest some compile time scenarios like:

defmodule Example.MyModel do
  def_scenario :scenario_id do
    abilities([…])
    roles([…])
  end

  def_scenario :another_scenario_id do
    any_of(abilities: […], roles: […], scenarios: […]) # and
    all(abilities: […], roles: […], scenarios: […])
  end
end

and use it in with like:

defmodule Example do
  def sample(post_id, user_id) do
    with user <- Sample.Repo.get(Sample.User, 1),
      performer <- load_and_authorize_performer(user),
      :ok <- validate_scenario(performer, :scenario_name), # and
      :ok <- validate_role(performer, :role_name), # and
      :ok <- validate_ability(performer, :ability_name) do
      # here goes contents of `:ok` case result
    end
  end
end

What to do when you have performer User which is in Company in many to many relation? When you have function like def is_owner(performer, post) do … end there is no way to read company or company_id.

#7 Session plug to get current_user

Again, what if you have authorization based on multiple models?

#8 Will you provide any way to solve dynamic ecto queries?
Let’s say that somehow you have received not trusted generated ecto query which you want to validate, but you do not want to fetch millions of records. Instead you want to validate it properly on database level. Maybe there should be something like:

defmodule Example.MyModel do
  def_auth_check(user_id) do
    ensure_join(…, args: [user_id]) # join args here
    # continue ensure_join(…) in other models until reaching final model
  end
end

defmodule Example.MyFinalModel do
  def_auth_check(user_id) do
    ensure_join(…, as: :joined_name, …, on: [id: ^user_id]) # join args here
  end
end

defmodule Example.MyAuthModel do
  def_auth_check do
    check_ability(:read)
    # this would filter everything which in any depth joins this model
  end
end
# this is of course example written in "5 min"

In short I believe that there could be such changes:

  1. More compile-time data - limit run-time for calculated functions which would be called manually anyway.
  2. Think about some way to validate ecto queries without fetching records (as a second way - of course doing it for delete as you show is also good, but think about typical get and list REST API)
  3. Consider remove some “magic” in order to have library which could be faster to understand for everyone
  4. Consider making API more easy for and checks - not only for or cases

Let me know what do you think about it.

MilosMosovsky

MilosMosovsky

Awesome feedback! Yes your points are valid, I was also thinking about AND rules, either to introduce some terminating words or signatures like :next or :stop but your any_of and all is looking good. It’s good example as you can have defined more scenarios and validate only those which are needed inside function. I love it actually.

I will fix README.md :slight_smile:

#7 I didn’t get the question “authorization” based on multiple models" do you mean that you have for example User -> Company but both user and company are performers ?

#8 Understood makes sense, but for now I didn’t run to such case, but nice thing to put in roadmap :slight_smile:

Again really thank you for your feedback, sometimes is really hard when you work on something too long, everything seems “obvious”, now I see where it is missing more clarity. Thank you! I will definitely implement something like scenarios. I like it.

OvermindDL1

OvermindDL1

That would only be for the specific account that was auth’d. The user should always convert that to some local ID that multiple sources all reify into, otherwise you get a set of disparate accounts.

Does that mean it hits the database on every request?

Where does it cache the information for the authorize calls? Hmm, looks like it uses an ETS table. I’m not seeing where it gets cleared out, will this table infinitely fill up to the unique ID count (I have a few tens of thousands of accounts in my system of which most are not logged in at a time except occasional times where ‘most’ of them log in within a short time period). Is it never purged over time?

I go a different route where instead of ‘abilities’ like edit+ah+record+pidm+etc I combine those into a singular record. This means that I have full knowledge of every possible combination at compile-time for the admin view (and others) generation. Thus I generally only test a singular ‘ability’/permission at a time. I guess mine kind of combine your ability/role into a singular well-typed unit.

Hmm, it looks like every authorization check hits ETS quite a number of times, how well is that handled with filtering out records that a user should not be able to see, it seems like it would cause a bit of a slowdown?

That’s what my groups are for, there is a many<->many account<->group binding in the database, and both accounts and groups have permission set, which get aggregated together appropriately (in the database layer actually).

I have a few tens of thousands of account (actually I can check, hold on… 18054, there should be about 30k but that means a lot of people aren’t logging in that should be logging in as the accounts are created on first access ^.^), with a few dozen permissions (each permission covers a HUGE range of access capabilities as they are configurable).

My specific use-case is a college if you are curious, I write the backend system. :slight_smile:

Eh, it’s a very tiny library, I just wanted something rock solid with a minimal feature set that I needed. You could certainly do something better for something more specific to the user-case. That is the library that my permission matching is built on though.

I never released my overarching system that uses it though because I’m not happy with it, not the design or use, but I just can’t seem to come up with something better. It’s efficient enough that my server is the fastest of all that we have so I haven’t worried too much about that even during heavy load times, and the configurable permission structures have covered every case I’ve needed so far (and a great deal more), so I haven’t felt the need to try to iterate further on it.

For note, I’m poking at this because I’d really really want to see it replace my system. The less I have to manage and keep up to date myself, the better. I’ve also been very unhappy at all the other authorization frameworks I’ve seen in Elixir as well (this is something java does really well…). :slight_smile:

EDIT: Oh, and another note, mine also pulls permission data from other servers as well, not just the database, but also a LDAP and CAS systems so any replacement I use needs to be able to have pluggable ‘stores’.

bjunc

bjunc

I created a library a little while back called Access Decision Manager. I think it might do what you’re looking for. It’s based on logic by the same name in PHP’s Symfony framework. Quick examples:

granted?(viewer, "CREATE_FOO", some_entity) # true / false

This uses a “voter” system that allows for per-entity permissions. You can also use it for simpler scenarios like this:

granted?(viewer, "ROLE_SUPER_ADMIN")

This is where the “subject” and “entity” are the same (eg. you want to know if the viewer has the super admin role).

We’re currently using this in production, in both Guardian plugs, as well as Absinthe resolvers. Slightly paired back resolver example:

def update(%{input: %{id: floor_plan_id}}, %{context: %{viewer: viewer}}) do
    case FloorPlan.get(floor_plan_id) do  
      %FloorPlan{} = floor_plan ->
        if granted?(viewer, "EDIT_FLOOR_PLAN", floor_plan.project) do
          # ... update floor plan

        else
          {:error, message: "Permission denied.", code: 403}
        end

      nil ->
        {:error, message: "Floor Plan doesn't exist.", code: 404}
    end
  end
OvermindDL1

OvermindDL1

For note, ueberauth integration is not needed at all. ueberauth is an authentication library, not an authorization, and thus its purview ends where terminators begins. :slight_smile:

Overall, a lot to take in here. I wouldn’t really use roles as I do detailed testing on everything, so perhaps an example. Right now I do a lot of checks like this:

    conn
    |> can(%Permissions.AH.Requirement{action: :edit, tag: ah_tag, id: id, pidm: pidm})
    ~> case do conn ->
      # Do stuff...

      records =
        Something.get_records(...)
        |> Enum.filter(&can?(conn, %Permissions.AH.Requirement{action: :edit, tag: ah_tag, id: id, pidm: pidm, record: &1.name}))

      # Do more stuff...
    end

Where can/2 takes an environment (whether a conn, channel socket, token, etc…) and an ‘ability’ structure (to use a terminator term, just called a ‘permission’ here) and it returns either the environment back out (possible modified with cache data if allowed, but in general I use Cachex a lot instead) or it returns an exception structure (which is what the ~> is handling via the exceptional library, but that can be easily tested anyway), or I can use can?/2, which is the same but returns true/false.

In the admin interface all permission structures are listed in every account that can be added/removed/modified on a key-by-key basis. When an environment is looked up it’s permission data for the specific account is looked up in the database as well as the permissions for the groups and then they are merged in a way that works for my permissions_ex library (everything is either Allow/NotAllowed/Deny where not defined means NotAllowed and Allow overrides NotAllowed to allow but Deny overrides all others to always Deny regardless of all other settings).

But the above example tests if the current user has access to the AH Requirement of the proper tag, ID and for the PIDM record then filters the specific record names that the user has access to before proceeding. I use lots and lots of these checks everywhere and can is very well optimized for lots of use (a cache, database sends an event when permissions updated, etc… etc…). How would this pattern be done in terminator? I’m having an interesting time understanding the README.md, like is load_and_authorize_performer a magic function that does something, what do the permissions and as_authorized blocks actually do, how does it handle permission failure (like in my system an exception causes the system to redirect to the login page along with a message saying what permission they failed and to log in to an account that has such a permission for example), etc…? The ‘abilities’ in my system are hard-coded (it makes no sense to make them dynamic as the functionality everywhere only uses what it knows anyway, thus they are structs that the system can gather a list of via behaviour implementations).

Where Next?

Popular in Libraries Top

wmnnd
Hi there, for my project DBLSQD, I needed a file storage solution that is a bit more flexible than Arc. Because I thought others might f...
New
praveenperera
FastRSS Parse RSS feeds very quickly: This is rust NIF built using rustler Uses the RSS rust crate to do the actual RSS parsing Speed...
New
mathieuprog
Hello :wave: Allow me to introduce you to Tz, an alternative time zone database support to Tzdata. Why another library? First and fore...
New
oltarasenko
Dear Elixir community, After a year of development, bug fixes, and improvements, we are proudly ready to share the release of Crawly 0.1...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
New
archan937
It is a well-know topic within the Elixir community: “To mock or not to mock? :)” Every alchemist probably has his / her own opinion con...
New
michalmuskala
Another small library today. PersistentEts Hex: persistent_ets | Hex GitHub: GitHub - michalmuskala/persistent_ets Ets table backed by...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
Crowdhailer
Experimenting with this code. OK.try do user &lt;- fetch_user(1) cart &lt;- fetch_cart(1) order = checkout(cart, user) save_or...
New
anshuman23
Hello all, I have been working on my proposed project called Tensorflex as part of Google Summer of Code 2018.. Tensorflex can be used f...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
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
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New

Sub Categories:

We're in Beta

About us Mission Statement