-
-
Notifications
You must be signed in to change notification settings - Fork 274
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds accessibleBy and accessibleFieldsBy to casl-mongoose
BREAKING CHANGE: preparation to remove deprecated mongoose plugins * `accessibleBy` is now just a POJO which has `ofType` method to get query for specific type * mongoose `accessibleRecordsPlugin` doesn't throw exception anymore if ability forbids to do an action and instead it sends empty result query to MongoDB
- Loading branch information
Showing
12 changed files
with
331 additions
and
403 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,154 @@ | ||
import { defineAbility } from "@casl/ability" | ||
import { MongoAbility, defineAbility } from "@casl/ability" | ||
import { accessibleBy } from "../src" | ||
import { testConversionToMongoQuery } from "./mongo_query.spec" | ||
|
||
declare module '../src' { | ||
interface RecordTypes { | ||
Post: true | ||
describe('accessibleBy', () => { | ||
type AppAbility = MongoAbility<[string, Post['kind'] | Post]> | ||
interface Post { | ||
kind: 'Post'; | ||
_id: string; | ||
state: string; | ||
private: boolean; | ||
isPublished: boolean | null; | ||
authorId: number; | ||
views: number; | ||
'comments.author': string; | ||
} | ||
} | ||
|
||
describe('accessibleBy', () => { | ||
it('returns `{ $expr: false }` when there are no rules for specific subject/action', () => { | ||
const ability = defineAbility((can) => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('read', 'Post') | ||
}) | ||
|
||
const query = accessibleBy(ability, 'update').Post | ||
const query = accessibleBy(ability, 'update').ofType('Post') | ||
|
||
expect(query).toEqual({ $expr: { $eq: [0, 1] } }) | ||
}) | ||
|
||
it('returns `{ $expr: false }` if there is a rule that forbids previous one', () => { | ||
const ability = defineAbility((can, cannot) => { | ||
const ability = defineAbility<AppAbility>((can, cannot) => { | ||
can('update', 'Post', { authorId: 1 }) | ||
cannot('update', 'Post') | ||
}) | ||
|
||
const query = accessibleBy(ability, 'update').Post | ||
const query = accessibleBy(ability, 'update').ofType('Post') | ||
|
||
expect(query).toEqual({ $expr: { $eq: [0, 1] } }) | ||
}) | ||
|
||
describe('it behaves like `toMongoQuery` when converting rules', () => { | ||
testConversionToMongoQuery((ability, subjectType, action) => | ||
accessibleBy(ability, action)[subjectType]) | ||
it('accepts ability action as third argument', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('update', 'Post', { _id: 'mega' }) | ||
}) | ||
const query = accessibleBy(ability, 'update').ofType('Post') | ||
|
||
expect(query).toEqual({ | ||
$or: [{ _id: 'mega' }] | ||
}) | ||
}) | ||
|
||
it('OR-es conditions for regular rules and AND-es for inverted ones', () => { | ||
const ability = defineAbility<AppAbility>((can, cannot) => { | ||
can('read', 'Post', { _id: 'mega' }) | ||
can('read', 'Post', { state: 'draft' }) | ||
cannot('read', 'Post', { private: true }) | ||
cannot('read', 'Post', { state: 'archived' }) | ||
}) | ||
const query = accessibleBy(ability).ofType('Post') | ||
|
||
expect(query).toEqual({ | ||
$or: [ | ||
{ state: 'draft' }, | ||
{ _id: 'mega' } | ||
], | ||
$and: [ | ||
{ $nor: [{ state: 'archived' }] }, | ||
{ $nor: [{ private: true }] } | ||
] | ||
}) | ||
}) | ||
|
||
describe('can find records where property', () => { | ||
it('is present', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('read', 'Post', { | ||
isPublished: { $exists: true, $ne: null } | ||
}) | ||
}) | ||
const query = accessibleBy(ability).ofType('Post') | ||
|
||
expect(query).toEqual({ $or: [{ isPublished: { $exists: true, $ne: null } }] }) | ||
}) | ||
|
||
it('is blank', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('read', 'Post', { isPublished: { $exists: false } }) | ||
can('read', 'Post', { isPublished: null }) | ||
}) | ||
const query = accessibleBy(ability).ofType('Post') | ||
|
||
expect(query).toEqual({ | ||
$or: [ | ||
{ isPublished: null }, | ||
{ isPublished: { $exists: false } } | ||
] | ||
}) | ||
}) | ||
|
||
it('is defined by `$in` criteria', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('read', 'Post', { state: { $in: ['draft', 'archived'] } }) | ||
}) | ||
const query = accessibleBy(ability).ofType('Post') | ||
|
||
expect(query).toEqual({ $or: [{ state: { $in: ['draft', 'archived'] } }] }) | ||
}) | ||
|
||
it('is defined by `$all` criteria', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('read', 'Post', { state: { $all: ['draft', 'archived'] } }) | ||
}) | ||
const query = accessibleBy(ability).ofType('Post') | ||
|
||
expect(query).toEqual({ $or: [{ state: { $all: ['draft', 'archived'] } }] }) | ||
}) | ||
it('is defined by `$lt` and `$lte` criteria', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('read', 'Post', { views: { $lt: 10 } }) | ||
can('read', 'Post', { views: { $lt: 5 } }) | ||
}) | ||
const query = accessibleBy(ability).ofType('Post') | ||
|
||
expect(query).toEqual({ $or: [{ views: { $lt: 5 } }, { views: { $lt: 10 } }] }) | ||
}) | ||
|
||
it('is defined by `$gt` and `$gte` criteria', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('read', 'Post', { views: { $gt: 10 } }) | ||
can('read', 'Post', { views: { $gte: 100 } }) | ||
}) | ||
const query = accessibleBy(ability).ofType('Post') | ||
|
||
expect(query).toEqual({ $or: [{ views: { $gte: 100 } }, { views: { $gt: 10 } }] }) | ||
}) | ||
|
||
it('is defined by `$ne` criteria', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('read', 'Post', { authorId: { $ne: 5 } }) | ||
}) | ||
const query = accessibleBy(ability).ofType('Post') | ||
|
||
expect(query).toEqual({ $or: [{ authorId: { $ne: 5 } }] }) | ||
}) | ||
|
||
it('is defined by dot notation fields', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('read', 'Post', { 'comments.author': 'Ted' }) | ||
}) | ||
const query = accessibleBy(ability).ofType('Post') | ||
|
||
expect(query).toEqual({ $or: [{ 'comments.author': 'Ted' }] }) | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { MongoAbility, createMongoAbility, defineAbility } from "@casl/ability" | ||
import { accessibleFieldsBy } from "../src" | ||
import mongoose from "mongoose" | ||
|
||
describe('accessibleFieldsBy', () => { | ||
type AppAbility = MongoAbility<[string, Post | mongoose.Model<Post> | 'Post']> | ||
interface Post { | ||
_id: string; | ||
title: string; | ||
state: string; | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-redeclare | ||
const Post = mongoose.model('Post', new mongoose.Schema<Post>({ | ||
title: String, | ||
state: String, | ||
})) | ||
|
||
describe('when subject type is a mongoose model', () => { | ||
testWithSubjectType(Post, Post) | ||
}) | ||
|
||
describe('when subject type is a mongoose model name', () => { | ||
testWithSubjectType('Post', Post) | ||
}) | ||
|
||
function testWithSubjectType(type: mongoose.Model<Post> | 'Post', Model: mongoose.Model<Post>) { | ||
it('returns empty array for empty `Ability` instance', () => { | ||
const fields = accessibleFieldsBy(createMongoAbility<AppAbility>()).ofType(type) | ||
|
||
expect(fields).toBeInstanceOf(Array) | ||
expect(fields).toHaveLength(0) | ||
}) | ||
|
||
it('returns all fields for model if ability does not have restrictions on rules', () => { | ||
const ability = defineAbility<AppAbility>(can => can('read', type)) | ||
|
||
expect(accessibleFieldsBy(ability).ofType(type).sort()) | ||
.toEqual(['_id', '__v', 'title', 'state'].sort()) | ||
}) | ||
|
||
it('returns fields for `read` action by default', () => { | ||
const ability = defineAbility<AppAbility>(can => can('read', type, ['title', 'state'])) | ||
|
||
expect(accessibleFieldsBy(ability).ofType(type)).toEqual(['title', 'state']) | ||
}) | ||
|
||
it('returns fields for an action specified as 2nd parameter', () => { | ||
const ability = defineAbility<AppAbility>(can => can('update', type, ['title', 'state'])) | ||
|
||
expect(accessibleFieldsBy(ability, 'update').ofType(type)).toEqual(['title', 'state']) | ||
}) | ||
|
||
it('returns fields permitted for the instance when called on model instance', () => { | ||
const ability = defineAbility<AppAbility>((can) => { | ||
can('update', type, ['title', 'state'], { state: 'draft' }) | ||
can('update', type, ['title'], { state: 'public' }) | ||
}) | ||
const post = new Model({ state: 'public' }) | ||
|
||
expect(accessibleFieldsBy(ability, 'update').of(post)).toEqual(['title']) | ||
}) | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.