Hi - Id like to allow permissions that only require access check at the top level of certain fields e.g:
fields: [
"address"
]
will allow for all fields: address.street, address.city etc...
I couldn't find an easy way to implement this into the existing ability is it possible?
Hi
CASL doesn鈥檛 support this out of the box but you can read https://github.com/stalniy/casl/issues/143 to get some guidance of how to do what you want
Thanks - I read that issue, but am having a hard time understanding how I could apply it to a set of rules that i save in a DB as a JSON object.
I'm using CASL isomorphically with React and Mongoose.
Would I have to use a schema to automatically add all the sub fields to the rule set or is there an easier method?
I would love to see this in the base functionality and would be happy to PR if a design could be agreed on, something such as:
fields: [address.*]
would could then be checked in ability rulesFor by testing last character of field for *
The changes should be done in Rule.isRelevantFor method.
I'm a bit concerned about performance of this solution but we can check it after implementation. This is important because nodejs has single thread and can be blocked by long running synchronous operations.
Probably what you will need to do is to convert field into some kind of mask the easiest way is to create a regular expression but then we ensure that all special characters are properly escaped).
Asterisk can be in different places, so lets consider several examples:
| Case | Example | Result | Note
| -------- | -------------| --------- | ------ |
| address.* | ability.can('read', 'User', 'address.*') | true | |
| address.* | ability.can('read', 'User', 'address') | true | Is it expected? |
| address.* | ability.can('read', 'User', 'address.city') | true | |
| address.* | ability.can('read', 'User', 'address.city.name') | false | Is it expected?
| address..name | ability.can('read', 'User', 'address.*.name') | true | |
| address..name | ability.can('read', 'User', 'address.city.name') | true | |
| *.name | ability.can('read', 'User', '*.name') | true | |
| *.name | ability.can('read', 'User', 'city.name') | true | |
| *.name | ability.can('read', 'User', 'address.city.name') | false | Is it expected? |
What do you think about cases noted with "is it expected?"?
Asterisk can be in different places, so lets consider several examples:
I had only considered the most common use case - which is to allow someone who is allowed access to a field - often will have access to any sub field - otherwise you would specify those fields explicitly Or not allow any access to sub-fields.
The other use cases seem edge cases to me and would cause a heavier implementation and may become a performance issue.
In answer to your examples:
address.*
ability.can('read', 'User', 'address')trueIs it expected?
Yes - * matches any including absence.
address.*
ability.can('read', 'User', 'address.city.name')falseIs it expected?
No - I would expect * to match one or many trailing fields, the risk otherwise becomes that you have potentially infinite mask levels.
address..name
ability.can('read', 'User', 'address.*.name')true
address..nameability.can('read', 'User', 'address.city.name')true
*.nameability.can('read', 'User', '*.name')true
*.nameability.can('read', 'User', 'city.name')true
*.nameability.can('read', 'User', 'address.city.name')falseIs it expected?
No - in line with above - this should match i.e: true.
IF a more complex (thorough?) solution is desirable - then my answers would likely not hold. But I expect this solves 90% of missing use cases.
I think it鈥檇 be good to implement mask behavior as in glob patterns:
* means any character except .** means any characterSo,
address.* matches only first level fields
address.** matches any level of fields
Ok so logic would be to string replace:
.** with
(\..+)?
Then
.* with:
(\.[^.]*)?
Then do a regex match. Does this sound correct?
Yes, looks good. What else you can do is to properly escape regexp and cache compiled regexp (somewhere)
For caching...I think it makes sense to parse the fields in the constructor for Rule and then join them:
const patterns = params.fields.map( //turn fields into regex)
this.fieldpattern = new RegExp(this.patterns.join('|'));
The isRelevantFor function could then simply do
return this.fieldpattern.test(field)
If above is good - ill submit PR.
Not sure whether it鈥檚 a good idea to create one big regexp. Need to do perf measurements.
About escaping. If you don鈥檛 escape regexp then the lib will have a potential security vulnerability. Especially when somebody creates ui for specifying permissions and will allow users to define which fields they can change. In this case, user may write something like .+ instead of field name and will get access to all fields
EDIT: After looking at the relevant code I think the issue is elsewhere in my code, but I still think this is a great idea as I can see where we will be running into this limitation down the line.
I wanted to chime in that this would be an extremely useful enhancement. I'm in the process of migrating our ad-hoc authorization engine to use casl and this is a big sticking point for me at this point.
In our use case, we have a specific user type that is restricted from seeing any "Personal Information" (eg: email, phone, address, etc) of other users. I have a rule constructed as follows:
disallow('read', 'workers', [
'pii',
'phoneNumber',
'phone',
'email',
'address',
]);
and using casl-react, I check this via:
<Can I="read pii" of="workers"> ...
This works great for that user type, but since there's no wildcard support, and my "core" users don't define fields in their rules, the casl-react tag returns false when the user has the following rule:
allow('crud', 'workers');
Unless I'm missing something, I believe I'm in need of the wildcard support and would like to avoid the various workarounds available.
Ok, so in terms of requirements this is what I came out with:
| Rule | Example | Result |
| -------- | -------------| --------- |
| address.* |
| | ability.can('read', 'User', 'address') | true |
| | ability.can('read', 'User', 'address.city') | true |
| | ability.can('read', 'User', 'address.city.name') | false |
| address.* |
| | ability.can('read', 'User', 'address') | true |
| | ability.can('read', 'User', 'address.city') | true |
| | ability.can('read', 'User', 'address.city.name') | true |
| | ability.can('read', 'User', 'address.city.location.lat') | true |
| address..name |
| | ability.can('read', 'User', 'address.*.name') | true |
| | ability.can('read', 'User', 'address.city.name') | true |
| | ability.can('read', 'User', 'address.city.location.name') | false |
| address.*.name |
| | ability.can('read', 'User', 'address.*.name') | true |
| | ability.can('read', 'User', 'address.city.name') | true |
| | ability.can('read', 'User', 'address.city.location.name') | true |
| *.name |
| | ability.can('read', 'User', '*.name') | true |
| | ability.can('read', 'User', 'city.name') | true |
| | ability.can('read', 'User', 'address.city.name') | false |
| | ability.can('read', 'User', 'address.city.location.name') | false |
| *.name |
| | ability.can('read', 'User', '*.name') | true |
| | ability.can('read', 'User', 'city.name') | true |
| | ability.can('read', 'User', 'address.city.name') | true |
| | ability.can('read', 'User', 'address.city.location.name') | true |
Benchmarks:
Big regexp is much faster then iterating over an array of regexps:

published in @casl/[email protected]
@stalniy Thanks for adding this and apologies for dropping off on it, I unfortunately got distracted on something but really appreciate you looking at it. Looks great!
Most helpful comment
The changes should be done in
Rule.isRelevantFormethod.I'm a bit concerned about performance of this solution but we can check it after implementation. This is important because nodejs has single thread and can be blocked by long running synchronous operations.
Probably what you will need to do is to convert field into some kind of mask the easiest way is to create a regular expression but then we ensure that all special characters are properly escaped).
Asterisk can be in different places, so lets consider several examples:
| Case | Example | Result | Note
| -------- | -------------| --------- | ------ |
| address.* |
ability.can('read', 'User', 'address.*')|true| || address.* |
ability.can('read', 'User', 'address')|true| Is it expected? || address.* |
ability.can('read', 'User', 'address.city')|true| || address.* |
ability.can('read', 'User', 'address.city.name')|false| Is it expected?| address..name |
ability.can('read', 'User', 'address.*.name')|true| || address..name |
ability.can('read', 'User', 'address.city.name')|true| || *.name |
ability.can('read', 'User', '*.name')|true| || *.name |
ability.can('read', 'User', 'city.name')|true| || *.name |
ability.can('read', 'User', 'address.city.name')|false| Is it expected? |What do you think about cases noted with "is it expected?"?