-
-
Notifications
You must be signed in to change notification settings - Fork 284
Description
Hello!
First, thanks so much for this library! We've gotten a lot of use out of CASL so far. Up to this point, we've yet to convert resource collections of our REST API to be scoped based on the CASL ability instance for a given user (we're reliant on legacy code to do this). Making this leap is one of the last steps we need to take to deprecate our legacy permissions model implementation.
We use knex
and scope queries on REST resource collections before applying user-provided filters onto the query. We would instead like to use the interpreter in @ucast/sql
to process the Ability instance AST; producing a raw where constraint we can apply to our query builder.
For additive permissions, everything seems to work as expected. However, we need to be able to support negative (inverted) permissions as well. The ability instance handles this great when querying permissions on individual resources. However, something is lost in translation in our processing of the underlying AST when producing SQL and after tinkering for a while and doing some research, we're left thinking this HAS to be a common problem others have solved.
I've created a simple example that demonstrates what we're trying to achieve:
const { subject, createMongoAbility, AbilityBuilder } = require('@casl/ability')
const { rulesToAST } = require('@casl/ability/extra')
const { allInterpreters, createSqlInterpreter, mysql } = require('@ucast/sql')
const { can, cannot, build } = new AbilityBuilder(createMongoAbility)
cannot('manage', 'Post', { id: 1 })
can('manage', 'Post', { id: 1 })
const ability = build()
console.log(
`We can update a post with id === 1:`,
ability.can('update', subject('Post', { id: 1 }))
)
const condition = rulesToAST(ability, 'update', 'Post')
console.log(`Our condition AST is...`)
console.log(JSON.stringify(condition, null, 2))
const interpret = createSqlInterpreter(allInterpreters)
const [sql, replacements] = interpret(condition, {
...mysql,
paramPlaceholder: () => '?',
})
console.log(
`The matching SQL constraint would be:`,
sql,
replacements
)
Output when ordering is cannot
before can
We can update a post with id === 1: true
Our condition AST is...
{
"operator": "and",
"value": [
{
"operator": "not",
"value": [
{
"operator": "eq",
"value": 1,
"field": "id"
}
]
},
{
"operator": "eq",
"value": 1,
"field": "id"
}
]
}
The matching SQL constraint would be: (not (`id` = ?) and `id` = ?) [ 1, 1 ]
Output when ordering is can
before cannot
We can update a post with id === 1: false
Our condition AST is...
{
"operator": "and",
"value": [
{
"operator": "not",
"value": [
{
"operator": "eq",
"value": 1,
"field": "id"
}
]
},
{
"operator": "eq",
"value": 1,
"field": "id"
}
]
}
The matching SQL constraint would be: (not (`id` = ?) and `id` = ?) [ 1, 1 ]
A few important notes:
- If the order of rules is changed, the runtime
ability.can
check behaves as expected. I will lose access if thecannot
comes last and I retain access ifcan
is last. - Regardless of the order, the AST is the same; which makes sense! It should theoretically be the same. However, I'm lost on how to translate this to SQL in that case such that the ordering of the rules produces different SQL.
We've also come across https://gist.github.com/ygrishajev/9ef01444fdb5c386c43b6611400c0fc6 which uses rulesToQuery
to return AST nodes and constructs a compound condition manually. However, it seems to hit this same issue when there are inverted and non-inverted rules applied to a resource where you get a lossy translation to the database; differing from results you see calling ability.can
.