dorgan

dorgan

Help with authorization system based on roles and permissions

Hi,
I’m working on an authorization system for my phoenix app, and I need some help designing it..
I want it to be based on roles and permissions, and I also want to be able to create, update and delete roles at runtime and have them persisted in the database.
Permissions wouldn’t change very often as they would imply changes to the codebase, so I think they can be hardcoded.
I’ve seen bodyguard, canary and policy_wonk but neither of them seem to suit my needs so I prefer to roll my own.

This is the approach I would follow:

  • Create roles and role_user tables for a many_to_many association between users and roles
  • As the permissions are hardcoded, the roles table would have a column for each of the permissions(ie: a boolean create_post column) or a comma delimited permissions string column. I don’t really know which is better, but I like the latter because I don’t need to run migrations if my permissions ever change.
  • Write an authorization module that exposes an API like BodyGuard’s, for example: Authorization.can(:create_post, user)

Is this, in general, a good approach I should go for? How would you approach this?
Thanks!

Marked As Solved

OvermindDL1

OvermindDL1

Similar to what I wrote at work it sounds like (I really should generify it and put it out as a library someday…).

Essentially what I did was this:

  • A set of Permission structures (MyServer.Permissions.SomeBlahPermission for example), each of which has at least an ‘action’ field, among other (also a ‘default’ function for displaying in the UI for default values, which are different from default values when instancing it in code).
  • A can/can? set of functions (and others) that takes an environment (usually a conn or socket or so that grabs the information from a protocol).
  • I perform a test like:
    conn
    |> can(%Perms.SomeBlah{action: :list, id: :_})
    ~> case do conn ->
    end
    
  • The can? returns a boolean (great for Enum.filter!), the can returns either the environment itself if it passes or an exception structure (not raising it, returning it, I use the Exceptional library a lot, that’s where ~> comes from, it pipes into if the value is ‘good’ otherwise returns it directly). The can’s tend to be called many times in a single call so efficient cache’ing is pretty important (it can add an inline cache to the, for example, ‘conn’ for faster lookup on future calls as well so there is no ETS hit too via Cachex)
  • The permission test in can accesses a Cachex cache that calls into the database. All Cachex caches of a given account_id are wiped everywhere anytime the permissions are written.
  • The database side stores a json structure of the serialized permission structure, comparisons happen via my permissions_ex library on hex.pm.
  • Each account_id has an associated set of permissions as well as associated groups, each group has an associated permissions as well, all are combined for the person as well as from some other sources like LDAP and some other information in another database, this is why cache’ing is important as looking up all those sources can easily take a few hundred milliseconds to a second when the other sources like LDAP are being particularly slow.

One big change I’d make would probably make the format in the database a bit different, right now the permissions tables for account_id/group is the account_id/group_id, the structure name, and a jsonb of the structure data. I’d probably change it to allow for direct record comparisons more easily in ‘some’ cases in the database so I wouldn’t have to filter in-code so often, though sadly I wouldn’t actually be able to do that in ‘most’ of my cases because so much data comes from the Oracle and LDAP systems, but would be more useful for setups that are more usual.

But with this style I can test to the logged in user, I can test via a variety of other things, all with simple can/2/can?/2 calls. It’s efficient enough that even looping over 2k records for a report is still less than a second with the database lookup and all, which is significantly faster than the system it was replacing anyway. :slight_smile:

On the admin side the permissions are taken a structure to display to the user based on the defaults call on each permissions module, so I can set permissions from the UI in detail. In addition there is a ‘matrix’ view for mass editing of many account permissions via some pre-built sets with ease, all dynamically generated based on the permission structures that are detected in the system (based on their @behaviour module attributes).

EDIT: Oh, also thanks to the permission_ex library there is an admin lookup for easy overriding and a blacklist key for deny’ing, even if other permissions would allow for it. It makes it very easy. :slight_smile:

12
Post #2

Also Liked

OvermindDL1

OvermindDL1

Ugh, must have happened when I updated ex_doc, wonder how that happened as the docs haven’t changed other than just updating that… ^.^;

And fixed it, looks like ex_doc removed a feature I was using, so that was fun… >.>

dorgan

dorgan

I took a look at your permission_ex, in essence it’s pretty similar to the module I was coding, and I also lookup for an admin permission for easy overriding too :slightly_smiling_face:

So, if I understood correctly, you store the permissions per user and per group(what I call roles), serialized in a jsonb column, then retrieve every permission for a given user and match against them. As you match against a list of permissions, you can also get permissions from other sources(eg ldap). You also cache the permissions per user to improve performance.
Is this correct?

If so then I think I will go that way, your approach has enlightened me :slightly_smiling_face:

Right now I’m storing the permissions per group and assigning groups to users, I also wrote a plug that checks in the conn struct for a perms assign and match the permissions list against it. Since the user is retrieved from the db on every request and by extension it’s permissions, I guess it’s a good idea to cache it as you do as an improvement, but I think that overall yes, we’re doing similar things :smile:

Thank you!

OvermindDL1

OvermindDL1

In essence. I just have Cachex with a fallback function do all the lookups when there is a cache miss, it’s a nice singular place to put it all. :slight_smile:

If it’s just hitting the database once per connection it’s not really a big deal to not cache it, I’m mostly caching it because initial lookup can be crazy slow at times because of the way old servers I have to interact with. It’s still good to put it all behind a module API so you can always swap it out with a cache later if necessary without needing to change your API, that is why I pass the 'env’ironment in/out of each function so I can cache on it as well transparently without changing the API. :slight_smile:

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call t...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Other popular topics Top

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
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
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
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
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36128 110
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

We're in Beta

About us Mission Statement