如何为NestJS创建一个最佳的isAuthor守卫?

3
我一直在Udemy上学习在线课程,并试用了guard中间件。我还按照教程创建了admin.guard和auth.guard,但是现在我在思考,如果我想添加一个isAuthor.guard,不仅管理员可以更改post或其他内容,原始作者也能够进行编辑...应该如何更好地创建它?它应该是一个guard吗?还是middleware更好?
另外,是否可能有多个guards?例如isAdmin/isAuthor,这样就可以灵活使用,而不必拥有isAdminOrAuthor。
提前感谢任何建议/意见。同时,我尝试通过在Nest.JS中将服务注入到guard来访问服务,但对我没有用。
1个回答

1

我不知道这是否是最好的方法,但这似乎是一个实用的方法(适用于比isAdmin/isAuthor更大的范围)。注意:如果只需要isAdmin isAuthor情况,请将PostRelationResolver中的适当逻辑移至RolesGuard,并跳过整个通用方法。

提供通用方法是因为它允许覆盖更广泛的相同类型的情况(存在用户和任何特定实体 - 基于关系的限制需要应用)。

所以,为了解决这个问题。

假设阅读帖子(仅作为示例)受到限制,管理员可以看到所有帖子,而作者只能看到自己的帖子。

可以这样实现:

  @Get('read-post/:postId')
  @UseGuards(RolesGuard)
  @SetMetadata('general-roles', [GeneralRole.ADMIN])
  @SetMetadata('relation-roles', [RelationRole.POST_AUTHOR])
  readPostAsAuthor(
    @Param('postId') postId: number,
  ) {
    return this.postRepository.findPostById(postId);
  }

而对于帖子列表,可以像这样:

  @Get('read-all-posts')
  async readAllPosts(
    @Req() request
  ) {
    const posts = await this.postRepository.findAll();
    return this.rbacService.filterList(
      request, 
      posts, 
      [GeneralRole.ADMIN], 
      [RelationRole.POST_AUTHOR]
    );
  }

针对列表筛选的注意事项:应确保实现不响应未允许的帖子,并且仅在作为备份时才使用此筛选器(因为请求不包含足够的信息来限制调用)。
为了使其正常工作,需要实现RolesGuard。
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { GeneralRole } from "../role/general-role";
import { RelationRole } from "../role/relation-role";
import { RbacService } from "../rbac.service";

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private rbacService: RbacService,
  ) {
  }
  
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const contextHandler = context.getHandler();
    const request = context.switchToHttp().getRequest();
    
    const requestedGeneralRoles = this.reflector.get<GeneralRole[]>('general-roles', contextHandler);
    const requestedRelationRoles = this.reflector.get<RelationRole[]>('relation-roles', contextHandler);

    return this.rbacService.authorize(request, requestedGeneralRoles, requestedRelationRoles);
  }
}

实际授权的逻辑包含在rbacService中,如下所示:

import { Injectable } from "@nestjs/common";
import { GeneralRole } from "./role/general-role";
import { RelationRole } from "./role/relation-role";
import { UserRepository } from "./repository/user.repository";
import { CoreRelationResolver } from "./relation-resolver/core.relation-resolver";

@Injectable()
export class RbacService {

  constructor(
    private userRepository: UserRepository,
    private coreRelationResolver: CoreRelationResolver,
  ) {
  }

  // NOTE: This method should be implemented however token to user mapping is done - based on business requirement.
  async getUserByToken(token: string) {
    return await this.userRepository.findByToken(token);
  }

  async authorize(request: any, requestedGeneralRoles: GeneralRole[], requestedRelationRoles: RelationRole[]) {
    const user = await this.getUserByToken(request.headers['token']);

    if (!user) {
      return false;
    }

    if (requestedGeneralRoles && requestedGeneralRoles.indexOf(user.role) !== -1) {
      // If user is of general role, it is simply allowed - regardless of relationRoles.
      return true;
    }
    
    // Relation roles handling (user is not ADMIN - for example - but is author of post)
    if (requestedRelationRoles) {
      const relationRoles = await this.coreRelationResolver.getRelationRoles(user, requestedRelationRoles, request);
      return this.isAllowed(requestedRelationRoles, relationRoles);
    }

    return false;
  }
  
  isAllowed(requestedRelationRoles: RelationRole[], containingRelationRoles: RelationRole[]) {
    const matches = containingRelationRoles.filter(sr => {
      return !!requestedRelationRoles.find(rr => rr === sr);
    });

    return !!matches.length;
  }

  async filterList(
    request: any, 
    entities: any[], 
    requestedGeneralRoles: GeneralRole[], 
    requestedRelationRoles: RelationRole[]
  ): Promise<any[]> {
    const user = await this.getUserByToken(request.headers['token']);

    if (!user) {
      return [];
    }

    if (requestedGeneralRoles && requestedGeneralRoles.indexOf(user.role) !== -1) {
      return entities;
    }

    const result = [];
    const relationResolver = await this.coreRelationResolver.findRelationResolver(requestedRelationRoles);
    
    for (const entity of entities) {
      const singleEntityRelations = await relationResolver.getRelations(user, entity);
      if (this.isAllowed(requestedRelationRoles, singleEntityRelations)) {
        result.push(entity);
      } else {
        console.warn("WARNING: Check next entity and query that responds with it. It shouldn't be here!");
        console.warn(entity);
      }
    }
    
    return result;
  }
}

在继续逻辑之前,让我提供一个小描述。

授权逻辑停留在RbacService中。

CoreRelationResolver服务的主要作用是识别使用应用程序(发出请求)的用户和执行给定操作的对象实体之间的关系。

用户和特定实体之间可能存在的关系由RelationalRoles描述。通过RelationalRoles,可以定义限制,例如:“只有给定帖子的作者和协作者才能看到它”。

这里提供了CoreRelationResolver的实现:

import { Injectable } from "@nestjs/common";
import { RelationRole } from "../role/relation-role";
import { IRelationResolver } from "./i-relation-resolver";
import { PostRelationResolver } from "./post.relation-resolver";
import { UserEntity } from "../entity/user.entity";
import { ClientAppRelationResolver } from "./client-app.relation-resolver";

@Injectable()
export class CoreRelationResolver {

  private relationResolvers: IRelationResolver<UserEntity, unknown>[];

  constructor(
    private postRelationAuthorization: PostRelationResolver,
    private clientAppRelationResolver: ClientAppRelationResolver,
  ) {
    this.relationResolvers = [
      this.postRelationAuthorization,
      this.clientAppRelationResolver,
    ];
  }

  async getRelationRoles(user: UserEntity, requiredRelations: RelationRole[], request: any): Promise<RelationRole[]> {
    let relationRoles = [];

    const relationResolver = await this.findRelationResolver(requiredRelations);
    
    if (relationResolver) {
      const relatedObject = await relationResolver.getRelatedObject(request);

      if (relatedObject) {
        relationRoles = await relationResolver.getRelations(user, relatedObject);
      }
    }

    return relationRoles;
  }

  async findRelationResolver(requiredRelations: RelationRole[]): Promise<IRelationResolver<UserEntity, unknown>> {
    let result = null;
    
    for (const relationResolver of this.relationResolvers) {
      const supportedRelations = await relationResolver.getSupportedRelations();

      const matches = supportedRelations.filter(sr => {
        return !!requiredRelations.find(rr => rr === sr);
      });

      if (matches.length) {
        result = relationResolver;
        break;
      }
    }
    
    return result;
  }
}

它的设计方式是,在其构造函数中应注册并正确实现任何RelationResolver(IRelationResolver接口)。
IRelationResolver接口:
import { RelationRole } from "../role/relation-role";

/**
 * T - Type of user
 * U - Type of relatedObject
 */
export interface IRelationResolver<T, U> {
  /**
   * Return RelationRoles that this resolver is responsible to handle.
   */
  getSupportedRelations(): Promise<RelationRole[]>;

  /**
   * Retrieve related object from the request data.
   */
  getRelatedObject(request: any): Promise<U>;

  /**
   * Calculate and provide relation between user and related object.
   */
  getRelations(user: T, relatedObject: U): Promise<RelationRole[]>;
}

最后,在这里实现了检索相关对象并识别用户与给定对象之间的关系。
import { IRelationResolver } from "./i-relation-resolver";
import { Injectable } from "@nestjs/common";
import { RelationRole } from "../role/relation-role";
import { UserEntity } from "../entity/user.entity";
import { PostEntity } from "../entity/post.entity";
import { PostRepository } from "../repository/post.repository";

@Injectable()
export class PostRelationResolver implements IRelationResolver<UserEntity, PostEntity> {
  constructor(
    private postRepository: PostRepository
  ) {
  }
  
  async getSupportedRelations(): Promise<RelationRole[]> {
    return [RelationRole.POST_AUTHOR];
  }

  async getRelatedObject(request: any): Promise<PostEntity> {
    const postId: string = request.params.postId;
  
    return await this.postRepository.findPostById(parseInt(postId));
  }

  async getRelations(user: UserEntity, relatedObject: PostEntity): Promise<RelationRole[]> {
    const relations = [];
    
    if (relatedObject.authorId === user.id) {
      relations.push(RelationRole.POST_AUTHOR);
    }
    
    return relations;
  }
}

显然,自由在这里是实现任何所需内容和定义关系的方式。

对于所有下一个RBAC案例(针对不同实体类型),应该创建RelationResolver,实现它,并在CoreRelationResolver的构造函数中注册它。

总的来说,考虑到可用性范围,这种方法应该足够灵活,可以应用于许多RBAC场景(请将其视为概念性的 - 没有添加健壮性功能)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接