巢式JS + CASL + Mongoose :CASL无法从Mongoose架构推断主题类型

izkcnapc  于 2023-03-08  发布在  Go
关注(0)|答案(2)|浏览(181)

背景

我已经使用Mongoose和NestJS定义了一个Cat模式:

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);

我还使用CASL定义了一个能力工厂来处理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>,
    });
  }
}

问题

当我尝试检查服务中的权限时(注意:用户不是管理员):

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)总是返回false,而它应该是true

附加信息

当用户不是管理员时,ability.can为任何操作返回false
我猜CASL无法使用能力构建器InferSubject接口中的Cat类来推断Mongoose模型的主题类型,但我不知道使用哪个类来正确推断主题类型。
我错过了什么?

plicqrtu

plicqrtu1#

所以关键是在CASL能力工厂中的Nest JS注入之上注入Cat模型,所以像这样的工厂应该可以工作:

import {
    Ability,
    AbilityBuilder,
    AbilityClass,
    ExtractSubjectType,
    InferSubjects,
  } from '@casl/ability';
  import { Injectable } from '@nestjs/common';
  import { InjectModel } from '@nestjs/mongoose';
  import { Model } from 'mongoose';
  import { Cat, CatDocument } from '../cats/schemas/cat.schema';
  import { User } from '../users/models/user.model';
  
  export enum Action {
    Manage = 'manage',
    Create = 'create',
    Read = 'read',
    Update = 'update',
    Delete = 'delete',
  }
  
  @Injectable()
  export class CaslAbilityFactory {
    constructor(
        @InjectModel(Cat.name)
        private catModel: Model<CatDocument>,
    ) {}
    createForUser(user: User) {
      const { can, build } = new AbilityBuilder(
        Ability as AbilityClass<
          Ability<[Action, InferSubjects<typeof this.catModel> | 'all']>
        >,
      );
  
      can(Action.Read, this.catModel);
      can(Action.Create, this.catModel);
      can(Action.Update, this.catModel, {
        createdBy: user.id,
      });
  
      if (user.isAdmin()) {
        can(Action.Manage, 'all');
      }
  
      return build({
        detectSubjectType: (item) =>
          item.constructor as ExtractSubjectType<InferSubjects<typeof this.catModel> | 'all'>,
      });
    }
  }

完整示例项目:https://github.com/Wenish/nestjs-mongoose-casl

oewdyzsn

oewdyzsn2#

我的解决方案是丑陋的,但它是我发现的最好的,不涉及从Mongoose迁移。
基本上,问题的根源是mongoDB查询(如findOne)返回Documents,这是类型为Cat & Document的并集的普通javascript对象。在模式文件中,如果您遵循默认指令,则会出现类似export type CatDocument = Cat & Document的内容,其中Cat是您的模式,CatDocument只是为了Typescript而生成的类型。
但是,Casl需要一个类来验证,因此尝试使用该文档将导致类似'CatDocument' only refers to a type, but is being used as a value here.的结果
我的解决方案并不漂亮,但它很简单而且有效,当你得到一个CatDocument时,把它传递给一个文档,这个文档把它变成一个类,因为你的模式没有构造函数,你必须这样做:

catDocToSchema(catDoc: CatDocument): Cat {
    const cat = new Cat();
    const forbidden = ['__v', '_id']; // Removes the __v and _id properties created by mongoose/mongodb
    for (const prop in catDoc) {
      if (!forbidden.includes(prop)) cat[prop] = catDoc[prop];
    }
    return cat;
  }

并在将对象传递给“can”函数之前运行它:

userAbility.can(
     'Action',
     this.catService.catDocToSchema(catDoc),
   )

在另一个建议的解决方案中,这个.catModel可以编译,但实际上并不像注解中提到的那样进行验证。我也尝试过使用js-mixer来创建一个在Cat和Document之间具有多重继承的类的解决方案。然而,使用contractor几乎是不可能的,无论如何仍然需要一些转换。
我希望有一天我们能找到更好的解决办法!

相关问题