我一直在学习udemy的在线课程,并玩guard
middleware
我还创建了admin.guard
auth.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场景(请认为它是概念性的-没有添加健壮性特性)。