Integración con NestJS

En esta sección, aprenderás cómo configurar y utilizar @casl/ability en un proyecto NestJS para gestionar permisos y autorizaciones basados en roles y permisos almacenados en una base de datos MongoDB

Paso 1: Instalación de dependencias

Primero, instala las dependencias necesarias para @casl/ability y Mongoose.

npm install @casl/ability @nestjs/casl @nestjs/mongoose mongoose

Paso 2: Configuración de Mongoose

Configura Mongoose en tu módulo principal (app.module.ts).

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserModule } from './user/user.module';
import { AbilityModule } from './ability/ability.module';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/nest'),
    UserModule,
    AbilityModule,
  ],
})
export class AppModule {}

Paso 3: Definición de Esquemas y Modelos

Crea los esquemas y modelos para rol, permission, y rol_permission.

Role Schema:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { Permission } from './permission.schema';

@Schema()
export class Role extends Document {
  @Prop({ required: true })
  name: string;

  @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Permission' }] })
  permissions: Permission[];
}

export const RoleSchema = SchemaFactory.createForClass(Role);

Permission Schema:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema()
export class Permission extends Document {
  @Prop({ required: true })
  codename: string;

  @Prop({ required: true })
  section: string;

  @Prop({ required: true })
  name: string;

  @Prop({ required: true })
  action: string;

  @Prop({ required: true })
  description: string;
}

export const PermissionSchema = SchemaFactory.createForClass(Permission);

Paso 4: Servicios para Obtener Roles y Permisos

Crea un servicio para obtener los roles y permisos del usuario.

Role Service:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Role } from '../schemas/role.schema';

@Injectable()
export class RoleService {
  constructor(@InjectModel(Role.name) private roleModel: Model<Role>) {}

  async getRolesForUser(rolUserId: string): Promise<Role[]> {
    return this.roleModel.find({ rolUserId }).populate('permissions').exec();
  }
}

Paso 5: Definición de Habilidades

Define las habilidades basadas en los roles y permisos.

Ability Factory:

import {
  Ability,
  AbilityBuilder,
  AbilityClass,
  ExtractSubjectType,
  InferSubjects
} from '@casl/ability';
import { RoleService } from '../services/role.service';
import { User } from '../schemas/user.schema';

type Actions = 'manage' | 'create' | 'read' | 'update' | 'delete';
type Subjects = InferSubjects<any> | 'all';

export type AppAbility = Ability<[Actions, Subjects]>;

export async function defineAbilityFor(user: User, roleService: RoleService) {
  const { can, cannot, build } = new AbilityBuilder<AppAbility>(
    Ability as AbilityClass<AppAbility>
  );

  const roles = await roleService.getRolesForUser(user.role_id);

  roles.forEach(role => {
    role.permissions.forEach(permission => {
      can(permission.action, permission.section);
    });
  });

  return build({
    detectSubjectType: (item) =>
      item.constructor as ExtractSubjectType<Subjects>,
  });
}

Paso 6: Middleware para Inyectar Habilidades

Inyecta las habilidades en el contexto de la solicitud.

Ability Middleware:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { RoleService } from '../services/role.service';
import { defineAbilityFor } from './ability.factory';

@Injectable()
export class AbilityMiddleware implements NestMiddleware {
  constructor(private roleService: RoleService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const user = req.user; // Asegúrate de que el usuario esté presente en la solicitud
    req.ability = await defineAbilityFor(user, this.roleService);
    next();
  }
}

Paso 7: Uso de CASL en Controladores

Utiliza los guards y decoradores en tus controladores.

Abilities Guard:

import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ForbiddenError } from '@casl/ability';
import { CHECK_ABILITY, RequiredRule } from './ability.decorator';
import { AppAbility } from './ability.factory';

@Injectable()
export class AbilitiesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const rules = this.reflector.get<RequiredRule[]>(CHECK_ABILITY, context.getHandler()) || [];
    const { user, ability } = context.switchToHttp().getRequest();

    try {
      rules.forEach(rule =>
        ForbiddenError.from(ability).throwUnlessCan(rule.action, rule.subject)
      );
      return true;
    } catch (error) {
      if (error instanceof ForbiddenError) {
        throw new ForbiddenException(error.message);
      }
      return false;
    }
  }
}

Ability Decorator:

import { SetMetadata } from '@nestjs/common';

export const CHECK_ABILITY = 'check_ability';

export interface RequiredRule {
  action: string;
  subject: string;
}

export const CheckAbilities = (...rules: RequiredRule[]) =>
  SetMetadata(CHECK_ABILITY, rules);

Ejemplo de Controlador:

import { Controller, Get, Post, Param, UseGuards } from '@nestjs/common';
import { AbilitiesGuard } from './abilities.guard';
import { CheckAbilities } from './ability.decorator';
import { Action } from './ability.factory';
import { Article } from '../articles/article.entity';

@Controller('articles')
@UseGuards(AbilitiesGuard)
export class ArticlesController {
  @Get(':id')
  @CheckAbilities({ action: 'read', subject: Article })
  findOne(@Param('id') id: string) {
    // lógica para encontrar un artículo
  }

  @Post()
  @CheckAbilities({ action: 'create', subject: Article })
  create() {
    // lógica para crear un artículo
  }
}

Last updated