我不知道这是否是最好的方法,但这似乎是一个实用的方法(适用于比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,
) {
}
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) {
return true;
}
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";
export interface IRelationResolver<T, U> {
getSupportedRelations(): Promise<RelationRole[]>;
getRelatedObject(request: any): Promise<U>;
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场景(请将其视为概念性的 - 没有添加健壮性功能)。