Context
I have defined a Cat schema using Mongoose and NestJS:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type CatDocument = Cat & Document;
@Schema()
export class Cat {
@Prop({ required: true })
name: string;
@Prop({ required: true })
breed: string;
@Prop({ required: true })
createdBy: string;
// Other fields...
}
export const CatSchema = SchemaFactory.createForClass(Cat);
I have also defined an ability factory using CASL to handle authorization for my API:
import {
Ability,
AbilityBuilder,
AbilityClass,
ExtractSubjectType,
InferSubjects,
} from '@casl/ability';
import { Injectable } from '@nestjs/common';
import { Cat } from '../cats/schemas/cat.schema';
import { User } from '../users/models/user.model';
type Subjects = InferSubjects<typeof Cat | typeof User> | 'all';
export enum Action {
Manage = 'manage',
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
export type AppAbility = Ability<[Action, Subjects]>;
@Injectable()
export class CaslAbilityFactory {
createForUser(user: User) {
const { can, build } = new AbilityBuilder<
Ability<[Action, Subjects]>
>(Ability as AbilityClass<AppAbility>);
can(Action.Read, Cat);
can(Action.Create, Cat);
can(Action.Update, Cat, {
createdBy: user.id,
});
if (user.isAdmin()) {
can(Action.Manage, 'all');
}
return build({
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}
}
Problem
When I try to check permissions in my service (note: the user is not an administrator):
import {
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { EditCatInput } from './dto/edit-cat.input';
import { Cat, CatDocument } from './schemas/cat.schema';
import { Action, CaslAbilityFactory } from '../casl/casl-ability.factory';
import { User } from '../users/models/user.model';
@Injectable()
export class CatsService {
constructor(
private readonly configService: ConfigService,
private readonly caslAbilityFactory: CaslAbilityFactory,
@InjectModel(Cat.name) private catModel: Model<CatDocument>,
) {}
// Other methods...
async update(id: string, input: EditCatInput, user: User): Promise<Cat> {
const updatedCat = await this.catModel
.findOne({
_id: { $eq: id },
deleted: { $eq: false },
})
.exec();
const ability = this.caslAbilityFactory.createForUser(user);
if (!ability.can(Action.Update, updatedCat)) {
throw new ForbiddenException(
'You cannot edit this cat.',
);
}
Object.assign(updatedCat, input);
await updatedCat.save();
return updatedCat;
}
}
ability.can(Action.Update, cat) always return false, while it should be true.
Additional information
ability.can returns false for any action when the user is not administrator.
My guess is that CASL cannot infer the subject type of my Mongoose model using the Cat class in InferSubject interface of the ability builder. But I don't know which class to use to infer subject type correctly.
What did I miss?