Skip to content

Commit be7ae6a

Browse files
committed
fix: adds unlessCan method to ForbiddenError and reused it mongoose package to construct ForbiddenError
1 parent 546bc6e commit be7ae6a

File tree

8 files changed

+42
-22
lines changed

8 files changed

+42
-22
lines changed

packages/casl-ability/spec/pack_rules.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import './spec_helper';
1+
import './spec_helper'
22
import { packRules, unpackRules } from '../src/extra'
33

44
describe('Ability rules packing', () => {

packages/casl-ability/spec/rulesToAST.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defineAbility } from '../src'
22
import { rulesToAST } from '../src/extra'
3-
import './spec_helper';
3+
import './spec_helper'
44

55
describe('rulesToAST', () => {
66
it('returns empty "and" `Condition` if there are no conditions in `Ability`', () => {

packages/casl-ability/spec/rulesToQuery.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defineAbility } from '../src'
22
import { rulesToQuery } from '../src/extra'
3-
import './spec_helper';
3+
import './spec_helper'
44

55
function toQuery(ability, action, subject) {
66
const convert = rule => rule.inverted ? { $not: rule.conditions } : rule.conditions

packages/casl-ability/spec/rules_to_fields.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { rulesToFields } from '../src/extra'
22
import { defineAbility, PureAbility } from '../src'
3-
import './spec_helper';
3+
import './spec_helper'
44

55
describe('rulesToFields', () => {
66
it('returns an empty object for an empty `Ability` instance', () => {

packages/casl-ability/spec/spec_helper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import chai from 'chai'
22
import spies from 'chai-spies'
3-
import '../../dx/lib/spec_helper';
3+
import '../../dx/lib/spec_helper'
44

55
chai.use(spies)
66

packages/casl-ability/src/ForbiddenError.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { AnyAbility } from './PureAbility';
2-
import { Normalize } from './types';
2+
import { Normalize, Subject } from './types';
33
import { Generics } from './RuleIndex';
44
import { getSubjectTypeName } from './utils';
55

66
export type GetErrorMessage = (error: ForbiddenError<AnyAbility>) => string;
7+
/** @deprecated will be removed in the next major release */
78
export const getDefaultErrorMessage: GetErrorMessage = error => `Cannot execute "${error.action}" on "${error.subjectType}"`;
89

910
const NativeError = function NError(this: Error, message: string) {
@@ -25,7 +26,7 @@ export class ForbiddenError<T extends AnyAbility> extends NativeError {
2526
this._defaultErrorMessage = typeof messageOrFn === 'string' ? () => messageOrFn : messageOrFn;
2627
}
2728

28-
static from<U extends AnyAbility>(ability: U) {
29+
static from<U extends AnyAbility>(ability: U): ForbiddenError<U> {
2930
return new this<U>(ability);
3031
}
3132

@@ -39,26 +40,33 @@ export class ForbiddenError<T extends AnyAbility> extends NativeError {
3940
}
4041
}
4142

42-
setMessage(message: string) {
43+
setMessage(message: string): this {
4344
this.message = message;
4445
return this;
4546
}
4647

47-
throwUnlessCan(...args: Parameters<T['can']>) {
48-
const rule = this.ability.relevantRuleFor(...args);
48+
throwUnlessCan(...args: Parameters<T['can']>): void
49+
throwUnlessCan(action: string, subject?: Subject, field?: string): void {
50+
const error = (this as any).unlessCan(action, subject, field);
51+
if (error) throw error;
52+
}
53+
54+
unlessCan(...args: Parameters<T['can']>): this | undefined
55+
unlessCan(action: string, subject?: Subject, field?: string): this | undefined {
56+
const rule = this.ability.relevantRuleFor(action, subject, field);
4957

5058
if (rule && !rule.inverted) {
5159
return;
5260
}
5361

54-
this.action = args[0];
55-
this.subject = args[1];
56-
this.subjectType = getSubjectTypeName(this.ability.detectSubjectType(args[1]));
57-
this.field = args[2];
62+
this.action = action;
63+
this.subject = subject;
64+
this.subjectType = getSubjectTypeName(this.ability.detectSubjectType(subject));
65+
this.field = field;
5866

5967
const reason = rule ? rule.reason : '';
6068
// eslint-disable-next-line no-underscore-dangle
6169
this.message = this.message || reason || (this.constructor as any)._defaultErrorMessage(this);
62-
throw this; // eslint-disable-line
70+
return this; // eslint-disable-line consistent-return
6371
}
6472
}

packages/casl-mongoose/spec/accessible_records.spec.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Ability, defineAbility, ForbiddenError, SubjectType } from '@casl/ability'
1+
import { Ability, createMongoAbility, defineAbility, ForbiddenError, MongoAbility, SubjectType } from '@casl/ability'
22
import mongoose from 'mongoose'
33
import { AccessibleRecordModel, accessibleRecordsPlugin, toMongoQuery } from '../src'
44

@@ -195,14 +195,29 @@ describe('Accessible Records Plugin', () => {
195195
})
196196
})
197197

198-
it('throws `ForbiddenError` for `countDocuments` request', async () => {
198+
it('throws `ForbiddenError` for `estimatedDocumentCount` request', async () => {
199199
await query.estimatedDocumentCount()
200200
.then(() => fail('should not execute'))
201201
.catch((error: any) => {
202202
expect(error).toBeInstanceOf(ForbiddenError)
203203
expect(error.message).toMatch(/cannot execute/i)
204204
})
205205
})
206+
207+
it('throws `ForbiddenError` with correct message when subjectType is a model', async () => {
208+
const anotherAbility = createMongoAbility([
209+
{ action: 'read', subject: Post }
210+
], {
211+
detectSubjectType: o => o.constructor,
212+
})
213+
214+
await Post.find().accessibleBy(anotherAbility, 'update')
215+
.then(() => fail('should never be called'))
216+
.catch((error: unknown) => {
217+
expect(error).toBeInstanceOf(ForbiddenError)
218+
expect((error as ForbiddenError<MongoAbility>).message).toBe('Cannot execute "update" on "Post"')
219+
})
220+
})
206221
})
207222
})
208223

packages/casl-mongoose/src/accessible_records.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Normalize, AnyMongoAbility, Generics, ForbiddenError, getDefaultErrorMessage } from '@casl/ability';
1+
import { Normalize, AnyMongoAbility, Generics, ForbiddenError } from '@casl/ability';
22
import { Schema, QueryWithHelpers, Model, Document, HydratedDocument, Query } from 'mongoose';
33
import { toMongoQuery } from './mongo';
44

@@ -13,10 +13,7 @@ function failedQuery(
1313

1414
if (typeof anyQuery.pre === 'function') {
1515
anyQuery.pre((cb: (error?: Error) => void) => {
16-
const error = ForbiddenError.from(ability);
17-
error.action = action;
18-
error.subjectType = modelName;
19-
error.setMessage(getDefaultErrorMessage(error));
16+
const error = ForbiddenError.from(ability).unlessCan(action, modelName);
2017
cb(error);
2118
});
2219
}

0 commit comments

Comments
 (0)