Skip to content

Commit

Permalink
fix: ensure acessible plugins can work with Ability instance that use…
Browse files Browse the repository at this point in the history
…s classes as SubjectTypes

closes #656
  • Loading branch information
stalniy committed Aug 28, 2022
1 parent cf70ab3 commit 7e9b634
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 25 deletions.
16 changes: 14 additions & 2 deletions packages/casl-mongoose/spec/accessible_fields.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineAbility, Ability } from '@casl/ability'
import { defineAbility, Ability, SubjectType } from '@casl/ability'
import mongoose from 'mongoose'
import { accessibleFieldsPlugin, AccessibleFieldsModel } from '../src'

Expand Down Expand Up @@ -30,7 +30,7 @@ describe('Accessible fields plugin', () => {
const post = new Post()

expect(typeof Post.accessibleFieldsBy).toBe('function')
expect(post.accessibleFieldsBy).toBe(Post.accessibleFieldsBy)
expect(typeof post.accessibleFieldsBy).toBe('function')
})

describe('`accessibleFieldsBy` method', () => {
Expand Down Expand Up @@ -77,6 +77,18 @@ describe('Accessible fields plugin', () => {

expect(post.accessibleFieldsBy(ability, 'update')).toEqual(['title'])
})

it('returns fields for Ability that uses classes as subject type', () => {
const ability = defineAbility((can) => {
can('update', Post, ['title', 'state'], { state: 'draft' })
can('update', Post, ['title'], { state: 'public' })
}, {
detectSubjectType: o => o.constructor as SubjectType
})
const post = new Post({ state: 'public' })

expect(post.accessibleFieldsBy(ability, 'update')).toEqual(['title'])
})
})

describe('when plugin options are provided', () => {
Expand Down
21 changes: 19 additions & 2 deletions packages/casl-mongoose/spec/accessible_records.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ability, defineAbility, ForbiddenError } from '@casl/ability'
import { Ability, defineAbility, ForbiddenError, SubjectType } from '@casl/ability'
import mongoose from 'mongoose'
import { AccessibleRecordModel, accessibleRecordsPlugin, toMongoQuery } from '../src'

Expand Down Expand Up @@ -33,7 +33,6 @@ describe('Accessible Records Plugin', () => {

it('injects `accessibleBy` query method', () => {
expect(typeof Post.find().accessibleBy).toBe('function')
expect(Post.find().accessibleBy).toBe(Post.accessibleBy)
})

describe('`accessibleBy` method', () => {
Expand Down Expand Up @@ -90,6 +89,24 @@ describe('Accessible Records Plugin', () => {
expect(expectedQueryType).not.toBeUndefined()
})

it('returns query for Ability that uses classes as subject type', () => {
ability = defineAbility<Ability>((can) => {
can('read', Post, { state: 'draft' })
can('update', Post, { state: 'published' })
}, {
detectSubjectType: o => o.constructor as SubjectType
})
const query = Post.accessibleBy(ability).getQuery()

expect(query).toEqual({
$and: [
{
$or: [{ state: 'draft' }]
}
]
})
})

describe('when ability disallow to perform an action', () => {
let query: mongoose.QueryWithHelpers<Post, Post, any, any>

Expand Down
17 changes: 11 additions & 6 deletions packages/casl-mongoose/src/accessible_fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,20 @@ export function accessibleFieldsPlugin(
): void {
const options = { getFields: getSchemaPaths, ...rawOptions };
const fieldsFrom = modelFieldsGetter();
type ModelOrDoc = Model<AccessibleFieldsDocument> | AccessibleFieldsDocument;

function accessibleFieldsBy(this: ModelOrDoc, ability: AnyMongoAbility, action?: string) {
const subject = typeof this === 'function' ? this.modelName : this;
return permittedFieldsOf(ability, action || 'read', subject, {
function istanceAccessibleFields(this: Document, ability: AnyMongoAbility, action?: string) {
return permittedFieldsOf(ability, action || 'read', this, {
fieldsFrom: fieldsFrom(schema, options)
});
}

schema.statics.accessibleFieldsBy = accessibleFieldsBy;
schema.method('accessibleFieldsBy', accessibleFieldsBy);
function modelAccessibleFields(this: Model<unknown>, ability: AnyMongoAbility, action?: string) {
const document = { constructor: this };
return permittedFieldsOf(ability, action || 'read', document, {
fieldsFrom: fieldsFrom(schema, options)
});
}

schema.statics.accessibleFieldsBy = modelAccessibleFields;
schema.method('accessibleFieldsBy', istanceAccessibleFields);
}
39 changes: 24 additions & 15 deletions packages/casl-mongoose/src/accessible_records.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Normalize, AnyMongoAbility, Generics, ForbiddenError, getDefaultErrorMessage } from '@casl/ability';
import type { Schema, QueryWithHelpers, Model, Document, HydratedDocument } from 'mongoose';
import mongoose from 'mongoose';
import { Schema, QueryWithHelpers, Model, Document, HydratedDocument, Query } from 'mongoose';
import { toMongoQuery } from './mongo';

function failedQuery(
Expand All @@ -26,27 +25,25 @@ function failedQuery(
}

function accessibleBy<T extends AnyMongoAbility>(
this: any,
baseQuery: Query<any, any>,
ability: T,
action?: Normalize<Generics<T>['abilities']>[0]
): QueryWithHelpers<Document, Document> {
let modelName: string | undefined = this.modelName;
const subjectType = ability.detectSubjectType({
constructor: baseQuery.model
});

if (!modelName) {
modelName = 'model' in this ? this.model.modelName : null;
if (!subjectType) {
throw new TypeError(`Cannot detect subjec type of "${baseQuery.model.modelName}" to return accessible records`);
}

if (!modelName) {
throw new TypeError('Cannot detect model name to return accessible records');
}

const query = toMongoQuery(ability, modelName, action);
const query = toMongoQuery(ability, subjectType, action);

if (query === null) {
return failedQuery(ability, action || 'read', modelName, this.where());
return failedQuery(ability, action || 'read', subjectType, baseQuery.where());
}

return this instanceof mongoose.Query ? this.and([query]) : this.where({ $and: [query] });
return baseQuery.and([query]);
}

type GetAccessibleRecords<T, TQueryHelpers, TMethods, TVirtuals> = <U extends AnyMongoAbility>(
Expand Down Expand Up @@ -83,7 +80,19 @@ export interface AccessibleRecordModel<
>
}

function modelAccessibleBy(this: Model<unknown>, ability: AnyMongoAbility, action?: string) {
return accessibleBy(this.where(), ability, action);
}

function queryAccessibleBy(
this: Query<unknown, unknown>,
ability: AnyMongoAbility,
action?: string
) {
return accessibleBy(this, ability, action);
}

export function accessibleRecordsPlugin(schema: Schema<any>): void {
(schema.query as Record<string, unknown>).accessibleBy = accessibleBy;
schema.statics.accessibleBy = accessibleBy;
(schema.query as Record<string, unknown>).accessibleBy = queryAccessibleBy;
schema.statics.accessibleBy = modelAccessibleBy;
}

0 comments on commit 7e9b634

Please sign in to comment.