MilosMosovsky
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,absinthein my app, I want to do easy plugs to load performer from plugs (session) or absinthe context. -
Currently I have WIP version for
fieldbased authorization in GraphQL (e.g. you have user shape but only admins and owner of an account can queryemailfield, you can solve it with multiple shaped queries but I created middleware on the top of terminator which protects resulting shape and returnsnilon particular field) this allows you to have only 1 query
query { account { id, email } }and terminator protectsemailfield 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!
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #ai
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming











Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
Eiji
I have some found problems/questions related to your
READM.mdfile …#1 Missing
dokeyword at line1: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:
and then you give this one:
so it will succeed when
ownerof specifiedPostdoes not had confirmed email, right? It does not looks like a perfect example here#5 Another problem is in this example:
Here
performerincasestatement is completely magic. Newbies would not get how it’s actually working.#6 Also
as_authorized do … case … endlooks too complicated comparing toorexamples.Personally I would suggest some compile time scenarios like:
and use it in
withlike:What to do when you have performer
Userwhich is inCompanyinmany to manyrelation? When you have function likedef is_owner(performer, post) do … endthere is no way to readcompanyorcompany_id.#7
Session plug to get current_userAgain, what if you have authorization based on multiple models?
#8 Will you provide any way to solve dynamic
ectoqueries?Let’s say that somehow you have received not trusted generated
ectoquery 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:In short I believe that there could be such changes:
calculatedfunctions which would be called manually anyway.ectoqueries without fetching records (as a second way - of course doing it for delete as you show is also good, but think about typicalgetandlistREST API)andchecks - not only fororcasesLet me know what do you think about it.
MilosMosovsky
Awesome feedback! Yes your points are valid, I was also thinking about AND rules, either to introduce some terminating words or signatures like
:nextor:stopbut yourany_ofandallis 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#7 I didn’t get the question “authorization” based on multiple models" do you mean that you have for example
User→Companybut both user and company areperformers?#8 Understood makes sense, but for now I didn’t run to such case, but nice thing to put in roadmap
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.
Eiji
I had even more complicated scenario
Of course I can’t share project details, but some general info about
use caseshould be ok.Imagine that you have typical
Usermodel. EveryUsercould create or joinCompany. When it joinsCompanyit can access, add and modify some data based onmany-to-manyrelation betweenUser<->Companyand itsenumfield calledrole(of course you are going to replace such field with your solution), so what we need to properly determine scenario is:Plug(s) for handlingUserandCompanytokens (under different request headers).Useris notnilandCompanyis notnilwe firstly need to check if there is unique relation between specificUserandCompanyrolefield (for exampleeditor) and usecompany_#{role}scenario (for examplecompany_editor).Companytoken was passedCompanytoken is passed usebasic_userscenario or return 400 error ifUsertoken was incorrectguest_userscenarioI would like to see your example solution for that use case.
MilosMosovsky
Got it, to simplify solution, let’s eject
tokenhandling out of the scope of solution and let’s assume that we have some function which needs to be authorized and we will passuser/companyas arguments:edit_companyfor example:basic_userTerminator.Performer.grant(user, role)to ecto changeset on user creationUserwill joinCompanyso I would doiex> user = %User{}
iex> ability = %Ability{identifier: “edit_company”}
iex> company = %Company{}
iex> Terminator.Performer.grant(user, ability, company)
Now this particular user has ability to edit this particular company once he join it. Also this user has
:userrole as we inserted related record when he registered. Now let’s illustrate protected action:If you can assure calling
grantmethod on user creation / company creation / company join. Example should work. What do you think? But I think introducing :scenarios as you posted in first post will make it much cleanerEiji
Your solution looks ok, but personally I would do it a bit differently.
Assuming that we are after token validation then everything is even more simplified.
Look that if we already have
CompanyUserthen storing reference toUserandCompanyseparately is bad idea. Assuming that we want to deleteCompanythen we are going to remove all its members anyway. When we are going to delete account then we need to delete reference toCompanyas well. So instead of 2 checks (i.e.Companydelete andUserdelete) we can have just one (CompanyUserdelete) since it’s required to remove associated database rows before remove target row.In case when
CompanyUser(many to many) relation is not needed (i.e. no other data thancompany_id,user_idand acl is stored) then we do not needCompanyUserand your solution is better in such case. If you agree with me then I believe that those 2 examples (with and withoutCompanyUser) should be mentioned somewhere as an usage in bigger projects.OvermindDL1
For note,
ueberauth integrationis not needed at all.ueberauthis an authentication library, not an authorization, and thus its purview ends where terminators begins.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:
Where
can/2takes 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 theexceptionallibrary, but that can be easily tested anyway), or I can usecan?/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/Denywhere 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
canis very well optimized for lots of use (a cache, database sends an event when permissions updated, etc… etc…). How would this pattern be done interminator? I’m having an interesting time understanding theREADME.md, like isload_and_authorize_performera magic function that does something, what do thepermissionsandas_authorizedblocks 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).MilosMosovsky
@OvermindDL1 Thanks for taking a look!
%Uebear.Authstruct with UID (which is unique external ID for example of google/twitter account) This can be used directly asperformerso you even don’t need to createUserstable in app, so that was my IDEA of integrating ueberauth is not like integration bot more like compatibility .Your solution looks very similar to mine (in fact I think there are living many solutions like mine/yours nowadays) as it’s like normal scenario in any app.
Regarding
load_and_authorize_performer, many people are coming from rails, evencanada/canarylibrary took rails as example GitHub - ryanb/cancan: Authorization Gem for Ruby on Rails. · GitHub which is very common gem to be used ascan?library in rails. It has similarload_and_authorize_resourcemethod as AR models are OOP you always have instance of model bound to DB record soload_and_authorize_resourcejust load it from database and do the authorization. I took this example forload_and_authorize_performerwhich setup something like new “auth” session in context of Terminator and this performer is used for any subsequent calls of abilities. That means insidepermissionsmacro, each call is checked against entity loaded withload_and_authorize_performerso instead of writingability(performer, :view)you can write justability(:view)andperformeris fetched from “current session”Regarding roles, they are not needed for terminator at all, they can be omitted and it would look almost same as your snippet
It’s just sugar on top if you want to assign for example 30 abilities to different user you can just group them to Role. If you want to reject 1 ability from all those users you revoke it from role and you don’t have to update all users in database
as_authorizedis just macro foris_authorized?which wraps passed block. There is no difference betweenis_authorized?andas_authorizedmacro.Regarding dynamic abilities, it really depends on use case. If you have small codebase and 1000 users it’s easy to handle everything in structs. If you have 100 000 users and you need to assign 200 users to view particular entity, fun is just starting.
If I would have application with relatively small amount of “users” and small amount of permissions Terminator looks like an overkill. But for example if you have some enterprise app where companies can sign up, and you have like 100 000 companies, and 500 staff members as “Sales” reps, and you need to assign some sales people to operate with that company, it’s hard to prepare good architecture only with structs. Not saying it’s impossible but when 200 developers are working on same codebase it’s usually really magical to find correct place.
I didn’t roll out Terminator on large codebase yet so I can’t say how optimized it is and how it would perform if single page load would call let’s say 30 times
:D. My goal to achieve is call
is_authorized?, probably after that I will come to a decision to drop Terminatorload_and_authorize_performeronce on every request which will prepare all abilities with 1 DB load and all subsequent calls to authorization will be done against cache.MilosMosovsky
Oh, and btw @OvermindDL1 I have been looking to GitHub - OvermindDL1/permission_ex · GitHub and probably I will try to build terminator on the top of that
As my primary goal wasn’t to test permissions (as you did in your lib very nicely) but have dynamic management system around it in database and way how to very easily “prepare” arguments for something like your
test_permissionfunction. When I prepare everything from database I have my owntest_permissionfunction which I think I will just call from your library.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+etcI 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.
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…).
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’.
MilosMosovsky
Yes it hits DB all the time.
ETS table is very small as it is cleared on each
load_and_authorize_performerandpermissionsmacro. But I imaginage to have some cache there and sweeper which will truncate the cache after some time.Yeah I see that you can achieve the SAME with
permissions_exlib + some database layer, and that’s the point of Terminator. To take this responsibility from devs who just don’t care and want have simple API interface around permissions.So final notes are probably like this: You can achieve same with can? or without can? + permission_ex + DB. But there is no complex solution which does it. Something which will prepare everything together as 1 to go solution. There are nice libs as canary/canada/permission_ex which does this checking very nicely BUT ONLY WHEN you know what to pass there (those tags, groups, ids, entities). So my goal is to extract this part to simple
So my idea is not to focus on perfect solution which will do permission check as probably you can go with existing libraries. But perfect solution which will PREPARE and translate those arguments to appropriate calls and returns 1 aggregated boolean.
has_ability?(user, :ability)andgrant(user, :ability)which will do everything else under the cover. I hope I made my goal more clear now