Skip to content

Commit 7452962

Browse files
authored
refactor: adds comments to rulesToQuery and Rule matching and adds more tests (#950)
1 parent 1acec8b commit 7452962

3 files changed

Lines changed: 79 additions & 38 deletions

File tree

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { defineAbility } from '../src'
22
import { rulesToQuery } from '../src/extra'
3-
import './spec_helper'
43

54
function toQuery(ability, action, subject) {
65
const convert = rule => rule.inverted ? { $not: rule.conditions } : rule.conditions
@@ -12,14 +11,8 @@ describe('rulesToQuery', () => {
1211
const ability = defineAbility(can => can('read', 'Post'))
1312
const query = toQuery(ability, 'read', 'Post')
1413

15-
expect(Object.keys(query)).to.be.empty
16-
})
17-
18-
it('returns `null` if empty `Ability` instance is passed', () => {
19-
const ability = defineAbility(() => {})
20-
const query = toQuery(ability, 'read', 'Post')
21-
22-
expect(query).to.be.null
14+
expect(query).toBeInstanceOf(Object)
15+
expect(Object.keys(query)).toHaveLength(0)
2316
})
2417

2518
it('returns empty `$or` part if at least one regular rule does not have conditions', () => {
@@ -29,7 +22,8 @@ describe('rulesToQuery', () => {
2922
})
3023
const query = toQuery(ability, 'read', 'Post')
3124

32-
expect(Object.keys(query)).to.be.empty
25+
expect(query).toBeInstanceOf(Object)
26+
expect(Object.keys(query)).toHaveLength(0)
3327
})
3428

3529
it('returns empty `$or` part if rule with conditions defined last', () => {
@@ -39,40 +33,48 @@ describe('rulesToQuery', () => {
3933
})
4034
const query = toQuery(ability, 'read', 'Post')
4135

42-
expect(Object.keys(query)).to.be.empty
36+
expect(query).toBeInstanceOf(Object)
37+
expect(Object.keys(query)).toHaveLength(0)
38+
})
39+
40+
it('returns `null` if empty `Ability` instance is passed', () => {
41+
const ability = defineAbility(() => {})
42+
const query = toQuery(ability, 'read', 'Post')
43+
44+
expect(query).toBe(null)
4345
})
4446

4547
it('returns `null` if specified only inverted rules', () => {
46-
const ability = defineAbility((can, cannot) => {
48+
const ability = defineAbility((_, cannot) => {
4749
cannot('read', 'Post', { private: true })
4850
})
4951
const query = toQuery(ability, 'read', 'Post')
5052

51-
expect(query).to.be.null
53+
expect(query).toBe(null)
5254
})
5355

54-
it('returns `null` if at least one inverted rule does not have conditions', () => {
55-
const ability = defineAbility((can, cannot) => {
56+
it('returns `null` if inverted rule does not have conditions and there are no direct rules', () => {
57+
const ability = defineAbility((_, cannot) => {
5658
cannot('read', 'Post', { author: 123 })
5759
cannot('read', 'Post')
5860
})
5961
const query = toQuery(ability, 'read', 'Post')
6062

61-
expect(query).to.be.null
63+
expect(query).toBe(null)
6264
})
6365

64-
it('returns `null` if at least one inverted rule does not have conditions even if direct condition exists', () => {
66+
it('returns `null` if at least one inverted rule does not have conditions even if direct rule exists', () => {
6567
const ability = defineAbility((can, cannot) => {
6668
can('read', 'Post', { public: true })
6769
cannot('read', 'Post', { author: 321 })
6870
cannot('read', 'Post')
6971
})
7072
const query = toQuery(ability, 'read', 'Post')
7173

72-
expect(query).to.be.null
74+
expect(query).toBe(null)
7375
})
7476

75-
it('returns non-`null` if there is at least one regular rule after last inverted one without conditions', () => {
77+
it('returns query if there is at least one regular rule after last inverted one without conditions', () => {
7678
const ability = defineAbility((can, cannot) => {
7779
can('read', 'Post', { public: true })
7880
cannot('read', 'Post', { author: 321 })
@@ -81,45 +83,45 @@ describe('rulesToQuery', () => {
8183
})
8284
const query = toQuery(ability, 'read', 'Post')
8385

84-
expect(query).to.deep.equal({
86+
expect(query).toEqual({
8587
$or: [
8688
{ author: 123 }
8789
]
8890
})
8991
})
9092

91-
it('OR-es conditions for regular rules', () => {
93+
it('OR-s conditions for regular rules', () => {
9294
const ability = defineAbility((can) => {
9395
can('read', 'Post', { status: 'draft', createdBy: 'someoneelse' })
9496
can('read', 'Post', { status: 'published', createdBy: 'me' })
9597
})
9698
const query = toQuery(ability, 'read', 'Post')
9799

98-
expect(query).to.deep.equal({
100+
expect(query).toEqual({
99101
$or: [
100102
{ status: 'published', createdBy: 'me' },
101103
{ status: 'draft', createdBy: 'someoneelse' }
102104
]
103105
})
104106
})
105107

106-
it('AND-es conditions for inverted rules', () => {
108+
it('AND-s conditions for inverted rules', () => {
107109
const ability = defineAbility((can, cannot) => {
108110
can('read', 'Post')
109111
cannot('read', 'Post', { status: 'draft', createdBy: 'someoneelse' })
110112
cannot('read', 'Post', { status: 'published', createdBy: 'me' })
111113
})
112114
const query = toQuery(ability, 'read', 'Post')
113115

114-
expect(query).to.deep.equal({
116+
expect(query).toEqual({
115117
$and: [
116118
{ $not: { status: 'published', createdBy: 'me' } },
117119
{ $not: { status: 'draft', createdBy: 'someoneelse' } }
118120
]
119121
})
120122
})
121123

122-
it('OR-es conditions for regular rules and AND-es for inverted ones', () => {
124+
it('OR-s conditions for regular rules and AND-es for inverted ones', () => {
123125
const ability = defineAbility((can, cannot) => {
124126
can('read', 'Post', { _id: 'mega' })
125127
can('read', 'Post', { state: 'draft' })
@@ -128,7 +130,7 @@ describe('rulesToQuery', () => {
128130
})
129131
const query = toQuery(ability, 'read', 'Post')
130132

131-
expect(query).to.deep.equal({
133+
expect(query).toEqual({
132134
$or: [
133135
{ state: 'draft' },
134136
{ _id: 'mega' }
@@ -140,14 +142,39 @@ describe('rulesToQuery', () => {
140142
})
141143
})
142144

143-
it('returns empty `$and` part if inverted rule with conditions defined before regular rule without conditions', () => {
145+
it('returns empty query if inverted rule with conditions defined before regular rule without conditions', () => {
144146
const ability = defineAbility((can, cannot) => {
145147
can('read', 'Post', { author: 123 })
146148
cannot('read', 'Post', { private: true })
147149
can('read', 'Post')
148150
})
149151
const query = toQuery(ability, 'read', 'Post')
150152

151-
expect(Object.keys(query)).to.be.empty
153+
expect(query).toBeInstanceOf(Object)
154+
expect(Object.keys(query)).toHaveLength(0)
155+
})
156+
157+
it('should ignore inverted rules with fields and conditions', () => {
158+
const ability = defineAbility((can, cannot) => {
159+
can('read', 'Post', { author: 123 })
160+
cannot('read', 'Post', 'description', { private: true })
161+
})
162+
const query = toQuery(ability, 'read', 'Post')
163+
164+
expect(query).toEqual({
165+
$or: [{ author: 123 }]
166+
})
167+
})
168+
169+
it('should ignore inverted rules with fields and without conditions', () => {
170+
const ability = defineAbility((can, cannot) => {
171+
can('read', 'Post', { author: 123 })
172+
cannot('read', 'Post', 'description')
173+
})
174+
const query = toQuery(ability, 'read', 'Post')
175+
176+
expect(query).toEqual({
177+
$or: [{ author: 123 }]
178+
})
152179
})
153180
})

packages/casl-ability/src/Rule.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,15 @@ export class Rule<A extends Abilities, C> {
9595
}
9696

9797
if (!field) {
98+
// if there is no field (i.e., checking whether user has access to at least one field on subject)
99+
// we ignore inverted rules because they disallow to do an action on it, so we are continue looking for regular rule
98100
return !this.inverted;
99101
}
100102

101-
if (this.fields && !this._matchField) {
103+
if (!this._matchField) {
102104
this._matchField = this._options.fieldMatcher!(this.fields);
103105
}
104106

105-
return this._matchField!(field);
107+
return this._matchField(field);
106108
}
107109
}

packages/casl-ability/src/extra/rulesToQuery.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CompoundCondition, Condition, buildAnd, buildOr } from '@ucast/mongo2js';
22
import { AnyAbility } from '../PureAbility';
3-
import { RuleOf } from '../RuleIndex';
3+
import { Generics, RuleOf } from '../RuleIndex';
44
import { ExtractSubjectType } from '../types';
55

66
export type RuleToQueryConverter<T extends AnyAbility, R = object> = (rule: RuleOf<T>) => R;
@@ -15,27 +15,39 @@ export function rulesToQuery<T extends AnyAbility, R = object>(
1515
subjectType: ExtractSubjectType<Parameters<T['rulesFor']>[1]>,
1616
convert: RuleToQueryConverter<T, R>
1717
): AbilityQuery<R> | null {
18-
const query: AbilityQuery<R> = {};
18+
const $and: Generics<T>['conditions'][] = [];
19+
const $or: Generics<T>['conditions'][] = [];
1920
const rules = ability.rulesFor(action, subjectType);
2021

2122
for (let i = 0; i < rules.length; i++) {
2223
const rule = rules[i];
23-
const op = rule.inverted ? '$and' : '$or';
24+
const list = rule.inverted ? $and : $or;
2425

2526
if (!rule.conditions) {
2627
if (rule.inverted) {
28+
// stop if inverted rule without fields and conditions
29+
// Example:
30+
// can('read', 'Post', { id: 2 })
31+
// cannot('read', "Post")
32+
// can('read', 'Post', { id: 5 })
2733
break;
2834
} else {
29-
delete query[op];
30-
return query;
35+
// if it allows reading all types then remove previous conditions
36+
// Example:
37+
// can('read', 'Post', { id: 1 })
38+
// can('read', 'Post')
39+
// cannot('read', 'Post', { status: 'draft' })
40+
return $and.length ? { $and } : {};
3141
}
3242
} else {
33-
query[op] = query[op] || [];
34-
query[op]!.push(convert(rule));
43+
list.push(convert(rule));
3544
}
3645
}
3746

38-
return query.$or ? query : null;
47+
// if there are no regular conditions and the where no rule without condition
48+
// then user is not allowed to perform this action on this subject type
49+
if (!$or.length) return null;
50+
return $and.length ? { $or, $and } : { $or };
3951
}
4052

4153
function ruleToAST(rule: RuleOf<AnyAbility>): Condition {

0 commit comments

Comments
 (0)