为nestjs创建isAuthor保护的最好方法是什么?



我一直在学习udemy的在线课程,并玩guardmiddleware

我还创建了admin.guardauth.guard建议遵循教程,但我在想,如果我想添加一个isAuthor.guard,不仅管理员可以更改post或任何东西,但原作者也能够进行编辑…

有什么更好的方法来创建这个?应该是守卫吗?或者中间件会更好?

注:在Nest.JS中将服务注入到守卫中,但对我不起作用。

编辑:另外,有可能有或警卫吗?例如isAdmin/isAuthor所以它可以灵活使用而不是isAdminOrAuthor

提前感谢您的建议/意见。

我不知道这是否是最好的方法,但这个方法似乎是实用的(它适用于比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的授权逻辑停止。

corelelationresolver服务是关于识别使用应用程序(发出请求)的用户和作为给定操作对象(在其上执行操作)的实体之间的关系。

用户和特定实体之间可能的关系用RelationalRoles描述。使用RelationalRoles限制定义为:"只有给定帖子的AUTHOR和COLLABORATOR可以看到它"。

corelelationresolver实现在这里提供:

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

它的设计方式是,在它的构造函数中,任何关系解析器(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,实现它,并在corelelationresolver的构造函数中注册它。

总而言之,考虑到可用性范围,这种方法应该足够灵活,可以应用于许多RBAC场景(请认为它是概念性的-没有添加健壮性特性)。

相关内容

最新更新