Help with authorization system based on roles and permissions

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:

11 Likes