question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

casl Infer subject types from @nestjs/mongoose models

See original GitHub issue

Fr, 04.02.2022 20:21 Describe the bug If you cast ability.can(Action, object) with a mongoose model it returns false but it should return true. the error is that it dosnt infer the subject correctly.

I referring to this part of the docs: https://casl.js.org/v5/en/guide/subject-type-detection#how-does-casl-detect-subject-type

Its says it takes it from object.constructor.modelName if its defined

with the defineSubjectType function it should work with classes like described here: https://casl.js.org/v5/en/guide/subject-type-detection#use-classes-as-subject-types

and with typescript you have to pass second argument as true on InferSubjects https://casl.js.org/v5/en/advanced/typescript#infer-subject-types-from-interfaces-and-classes

To Reproduce Steps to reproduce the behavior: I made a github repo: https://github.com/Wenish/nestjs-mongoose-casl

  1. clone the repo
  2. run command npm ci
  3. run command npm test

you will see the test ‘user should be able to read specific offer’ fails: https://github.com/Wenish/nestjs-mongoose-casl/blob/main/src/casl/casl-ability.factory.spec.ts#L51

Expected behavior I would expect that this return true: https://github.com/Wenish/nestjs-mongoose-casl/blob/main/src/casl/casl-ability.factory.spec.ts#L62

Interactive example (optional, but highly desirable) https://github.com/Wenish/nestjs-mongoose-casl

CASL Version

@casl/ability - v5.4.3

Environment: node - v16.13.0 TypeScript - 4.3.5

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:2
  • Comments:13 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
Justinio99commented, Feb 4, 2022

I have exactly the same problem using nestjs…

1reaction
Wenishcommented, Feb 5, 2022

@mathibla @Justinio99 hey guys this is the solution i came up with.

@stalniy and thanks very much with pointing that out that i should use the mongoose model as ref

the key with nestjs is to inject the model in the factory and use the injected model

no idea if there is away to simplify the definitions since atm wrote them 2 times.

import { InferSubjects, Ability, AbilityBuilder, AbilityClass, ExtractSubjectType } from "@casl/ability";
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { Offer, OfferDocument, OfferStatus } from "../database/schemas/offer.schema";

export enum Action {
    Manage = 'manage',
    Create = 'create',
    Read = 'read',
    Update = 'update',
    Delete = 'delete',
}

@Injectable()
export class CaslAbilityFactory {
    constructor(
        @InjectModel(Offer.name)
        private offerModel: Model<OfferDocument>,
    ) { }
    
    createForUser(user: any) {
        const { can, cannot, build } = new AbilityBuilder(Ability as AbilityClass<Ability<[Action, InferSubjects<typeof this.offerModel> | 'all']>>);

        can(Action.Read, this.offerModel, {
            publishDate: { $lte: new Date() },
            status: { $in: [OfferStatus.Approved] },
        });

        can(Action.Create, this.offerModel);

        if (user) {
            if (user?.uid) {
                can(Action.Manage, this.offerModel, { creator: user.uid });
            }

            const userRoles: Role[] = user?.roles || [];

            if (userRoles.includes(Role.CONTENT_MANAGER)) {
                can(Action.Manage, this.offerModel);
            }

            if (userRoles.includes(Role.SYSTEM_ADMIN)) {
                can(Action.Manage, 'all');
            }
        }

        return build({
            // Read https://casl.js.org/v5/en/guide/subject-type-detection#use-classes-as-subject-types for details
            detectSubjectType: object => {
                return object.constructor as ExtractSubjectType<InferSubjects<typeof this.offerModel> | 'all'>
            }
        });
    }
}

export enum Role {
    CONTENT_MANAGER = 'ContentManager',
    SYSTEM_ADMIN = 'SystemAdmin',
}

also that how the controller looks where i use the factory

import { Controller, Delete, Get, NotFoundException, Param, Patch, Post, UnauthorizedException } from '@nestjs/common';
import { ApiResponse } from '@nestjs/swagger';
import { Action, CaslAbilityFactory } from '../casl/casl-ability.factory';
import { OffersService } from './offers.service';
import { Offer, OfferDocument } from '../database/schemas/offer.schema';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';

@Controller('offers')
export class OffersController {
    constructor(
        @InjectModel(Offer.name)
        private offerModel: Model<OfferDocument>,
        private readonly offersService: OffersService,
        private readonly caslAbilityFactory: CaslAbilityFactory
    ) { }

    @Post()
    @ApiResponse({ type: Offer })
    create() {
        const user = {}
        const ability = this.caslAbilityFactory.createForUser(user);
        const canCreateOffers = ability.can(Action.Create, this.offerModel);
        console.log(this.timeString(),'can create offer', canCreateOffers)

        if (!canCreateOffers) throw new UnauthorizedException();

        return this.offersService.create();
    }

    @Get()
    @ApiResponse({ type: Offer, isArray: true })
    async readAll() {
        const user = {
            // roles: ['SystemAdmin']
        }
        const ability = this.caslAbilityFactory.createForUser(user);
        const canReadOffers = ability.can(Action.Read, this.offerModel);
        console.log(this.timeString(), 'can read offers', canReadOffers)

        if (!canReadOffers) throw new UnauthorizedException();

        return (await this.offersService.findAll()).filter((offer) => {
            const canReadOffer = ability.can(Action.Read, offer)
            console.log(offer.id)
            console.log(this.timeString(),'can read offer', canReadOffer)
            return canReadOffer
        });
    }

    @Get(':id')
    @ApiResponse({ type: Offer })
    async read(@Param('id') id: string) {
        const user = {}
        const ability = this.caslAbilityFactory.createForUser(user);
        const offer = await this.offersService.findOne(id)
        const canReadOffer = ability.can(Action.Read, offer)
        console.log(this.timeString(),'can read offer', canReadOffer)

        if (!canReadOffer) throw new UnauthorizedException();
        if (!offer) throw new NotFoundException();

        return offer;
    }

    @Patch(':id')
    @ApiResponse({ type: Offer })
    async update(@Param('id') id: string) {
        const user = {}
        const ability = this.caslAbilityFactory.createForUser(user);
        const offer = await this.offersService.findOne(id)
        const canUpdateOffer = ability.can(Action.Update, offer)
        console.log(this.timeString(),'can update offer', canUpdateOffer)

        if (!canUpdateOffer) throw new UnauthorizedException();
        if (!offer) throw new NotFoundException();
        
        return this.offersService.update(id)
    }

    @Delete(':id')
    @ApiResponse({ type: Offer })
    async delete(@Param('id') id: string) {
        const user = {}
        const ability = this.caslAbilityFactory.createForUser(user);
        const offer = await this.offersService.findOne(id)
        const canDeleteOffer = ability.can(Action.Delete, offer)
        console.log(this.timeString(),'can delete offer', canDeleteOffer)

        if (!canDeleteOffer) throw new UnauthorizedException();
        if (!offer) throw new NotFoundException();

        return this.offersService.delete(id);
    }

    private timeString() {
        const date = new Date()
        return `[${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}]`
    }
}

the schema is in a seperate database module else offers module and the casl module would have circular dependencies

Read more comments on GitHub >

github_iconTop Results From Across the Web

NestJS + CASL + Mongoose: CASL cannot infer subject type ...
My guess is that CASL cannot infer the subject type of my Mongoose model using the Cat class in InferSubject interface of the...
Read more >
Casl/mongoose and official mongoose types #436 - GitHub
3 import { Schema, DocumentQuery, Model, Document } from 'mongoose'; ... I have @casl/mongoose@5.0.0 and I'm using NestJs.
Read more >
CASL Prisma
This package allows to define CASL permissions on Prisma models using Prisma WhereInput . And that brings a lot of power in terms...
Read more >
Authorization | NestJS - A progressive Node.js framework
CASL is an isomorphic authorization library which restricts what resources a given client is allowed to access. It's designed to be incrementally adoptable...
Read more >
stalniy-casl/casl - Gitter
I saw that I could handover a function for the subject but I'm filling my rules by a JSON array. ... I need...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found