Definitely one of my favourite problems too! Some additional context for those who don't think about this all the time: in many cases, the solution is as simple as "write some SQL where clauses to do the filtering I care about". e.g. I suspect the vast majority of people have logic like `where tenant_id = ?` or similar and they pass in a tenant ID on every query.
Where things get challenging is when you want to decouple the logic (e.g. have an abstraction in your code, or centralize logic in a service). Because then you're in the world of what's the decoupled API that allows me to filter my database.
The easiest way to do that is just generate return a big list of IDs the user can see, and put `id in (... list of ids)` on the query. But that involves (a) syncing the data to the central service and (b) that list can get pretty long.
And so that's why you would even need to think about turning rules into WHERE clauses in the first place :)
Permissions have three moving parts, who wants to do it, what do they want to do, and on what object. Any good permission system has to be able to efficiently answer any permutation of those variables. Given this person and this object, what can they do? Given this object and this action, who can do it? Given this person and this action, which objects can they act upon?
We’ve found most permissioning systems end up with a pick-2 approach, and the most common one to be abused is given a person and an action, give me the collection. This leads to implementing permissions twice, once in code, and once as a query.
> Permissions have three moving parts, who wants to do it, what do they want to do, and on what object. Any good permission system has to be able to efficiently answer any permutation of those variables. Given this person and this object, what can they do? Given this object and this action, who can do it? Given this person and this action, which objects can they act upon?
> We’ve found most permissioning systems end up with a pick-2 approach, and the most common one to be abused is given a person and an action, give me the collection. This leads to implementing permissions twice, once in code, and once as a query.
I love the way you put this! I'm always looking for good ways to talk about authorization without falling back on jargon and I've never come up with a way to talk about the difference between authorizing an action on a single resource and returning a list of authorized resources that I've been happy with. Would you mind if I adapted this in future writing?
- Actor - who is performing an action
- Policy - what types of possible actions are permitted on a resource for a type of actor
- Permission - actions an actor has been granted to perform.
With the intersection of these three objects you can determine if an action can be performed and actors can be granted granular access.
Unfortunately, there are a few issues not worth getting into here other than to say I feel that the industry has a very frustratingly uneven set of solutions for authorization, no doubt in part due to it being complicated.
1) build the rules programmatically, based on what you observe in the request context. This works fine until you want users to be able to create and assign custom policies and load them from a database.
2) put placeholders in the rule’s conditions, and swap them with the current contextual values when rehydrating the rules for a given request. Fine, except this obviates caching except for the raw rule from a database, and rehydrating dozens or hundreds of rules for every request starts to add up in terms of overhead.
I wish rule conditions could reference a “context” parameter (name not important) so I could create a condition like {userId: context.user.id} and at runtime I could pass the current context when I call can. That way I can rehydrate the rule once. I realize this creates all sorts of complications with serializing a rule to be stored in a database or sent over the wire, but that’s where some special placeholders could be understood natively by an Ability and hooked into a passed in context. However, that still creates an issue if I want to create a condition that is purely based on context ( e.g. {context.user.isTrialUser: false} )
The other thing I have been struggling with is that can with a subject name only (rather than an instance) will match a rule with no conditions along with rules that have matching conditions. I understand the author’s rationalization, but can potentially create unexpected results particularly when you offer a system that lets users build their own policies.
The last issue is CASL is nodejs only and I may need to support multiple platforms. I’ve looked at Casbin because of its multiplatform support and customizable model, but I’ve found it extraordinarily hard to use beyond simple RBAC or claims-based authorization, and it still doesn’t offer solutions like conditions-to-query filters or field-level authorizations.
I've done this with out-of-the-box Keycloak Authorization Services. There is an entire standards based framework for authorization piggy-backed on OAUTH called UMA2. Keycloak provides an implementation of this. It's young and the documentation is a bit thin, and the learning curve is cliff shaped, but it does what is says on the tin.
My case involved authorizing verbs on a set of resources. The backend generates a permission ticket with an arbitrary list of resources and scopes and obtains (from Keycloak) an otherwise ordinary OAUTH access token containing a UMA2 RPT (Requesting Party Token) claim. That is cycled through the "token introspection" endpoint of Keycloak which returns a clean, simple JSON response with the subset of resources and scopes that are authorized. Net result is two requests for any arbitrary subset of resources and scopes. Nothing is stored or managed by the backend system: all the authorization stuff is in Keycloak. You can submit an open ended request that just dumps everything the user is authorized for.
It's simple enough that, foregoing signature checks that are otherwise performed, I prototype and test this stuff using shell scripts, curl and jq. Since it's all piggy-backed on the existing OAUTH system there is no additional infrastructure.
how it would look like for example if a user has access to 10000 objects: are all of them baked into a token as claims?
For rails specifically, https://github.com/polleverywhere/moat was built with this in mind. It's heavily inspired by Pundit, but let's you write policies at the `ActiveRecord::Relation` level. So `policy_filter(Article).find_by!(id: params[:id])` would run something like `select * from articles where id = ? and id in (select id from articles where owner_id = ?);`.
e.g.: many objects <-> object tag <-> policy <-> user group attachments <-> user
you can run SQL locally using sqlite or something for authorization decisions and list filtering (give me everything I have access to)
Their solution is to let you configure rules that get turned into SQL fragments that you can run against your own database: https://www.osohq.com/docs/guides/integrate/filter-lists#lis... - example Rails app here: https://github.com/osohq/rails_list_filtering_sample_app
A team I worked with in the past came to the same conclusion - turning authorization rules into WHERE clauses is a very efficient way to solve this problem, if you can figure out a clean way to do it.