diff --git a/src/modules/rbac/decorators/permission.decorator.ts b/src/modules/rbac/decorators/permission.decorator.ts new file mode 100644 index 0000000..dc633b6 --- /dev/null +++ b/src/modules/rbac/decorators/permission.decorator.ts @@ -0,0 +1,7 @@ +import { SetMetadata } from '@nestjs/common'; + +import { PERMISSION_CHECKERS } from '../constants'; +import { PermissionChecker } from '../types'; + +export const Permision = (...checkers: PermissionChecker[]) => + SetMetadata(PERMISSION_CHECKERS, checkers); diff --git a/src/modules/rbac/guards/rbac.guard.ts b/src/modules/rbac/guards/rbac.guard.ts new file mode 100644 index 0000000..87c2fa9 --- /dev/null +++ b/src/modules/rbac/guards/rbac.guard.ts @@ -0,0 +1,100 @@ +import { createMongoAbility } from '@casl/ability'; +import { ExecutionContext, ForbiddenException } from '@nestjs/common'; +import { ModuleRef, Reflector } from '@nestjs/core'; + +import { isNil } from 'lodash'; + +import { JwtAuthGuard } from '@/modules/user/guards'; + +import { UserRepository } from '@/modules/user/repositories'; +import { TokenService } from '@/modules/user/services'; + +import { PERMISSION_CHECKERS } from '../constants'; +import { PermissionEntity } from '../entities/permission.entity'; +import { RbacResolver } from '../rbac.resolver'; + +import { CheckerParams, PermissionChecker } from '../types'; + +export class RbacGuard extends JwtAuthGuard { + constructor( + protected reflector: Reflector, + protected resolver: RbacResolver, + protected tokenService: TokenService, + protected userRepository: UserRepository, + protected modeleRef: ModuleRef, + ) { + super(reflector, tokenService); + } + + async canActivate(context: ExecutionContext): Promise { + const authCheck = await super.canActivate(context); + + if (!authCheck) { + throw new ForbiddenException(); + } + + const checkers = this.reflector.getAllAndOverride( + PERMISSION_CHECKERS, + [context.getHandler(), context.getClass()], + ); + if (isNil(checkers) || checkers.length < 1) { + return true; + } + + const result = await checkPermissions({ + resolver: this.resolver, + repository: this.userRepository, + checkers, + moduleRef: this.modeleRef, + request: context.switchToHttp().getRequest(), + }); + if (!result) { + throw new ForbiddenException(); + } + return result; + } +} + +/** + * 检查权限 + * @param CheckerParams + */ +export async function checkPermissions({ + checkers, + moduleRef, + resolver, + repository, + request, +}: CheckerParams) { + if (isNil(request.user)) { + return false; + } + const user = await repository.findOneOrFail({ + relations: ['roles.permissions', 'permissions'], + where: { id: request.user.id }, + }); + let permissions = user.permissions as PermissionEntity[]; + for (const role of user.roles) { + permissions = [...permissions, ...role.permissions]; + } + permissions = permissions.reduce((o, n) => { + if (o.find(({ name }) => name === n.name)) { + return o; + } + return [...o, n]; + }, []); + + const ability = createMongoAbility( + permissions.map(({ rule, name }) => { + const resolve = resolver.permissions.find((p) => p.name === name); + if (!isNil(resolve) && !isNil(resolve.rule.conditions)) { + return { ...rule, conditions: resolve.rule.conditions(user) }; + } + return rule; + }), + ); + const results = await Promise.all( + checkers.map(async (checker) => checker(ability, moduleRef, request)), + ); + return results.every((r) => !!r); +} diff --git a/src/modules/rbac/types.ts b/src/modules/rbac/types.ts index 2e626f9..198b97e 100644 --- a/src/modules/rbac/types.ts +++ b/src/modules/rbac/types.ts @@ -7,8 +7,11 @@ import { FastifyRequest as Request } from 'fastify'; import { UserEntity } from '../user/entities'; +import { UserRepository } from '../user/repositories'; + import { PermissionEntity } from './entities/permission.entity'; import { RoleEntity } from './entities/role.entity'; +import { RbacResolver } from './rbac.resolver'; export type Role = Pick, 'name' | 'label' | 'description'> & { permissions: string[]; @@ -29,3 +32,11 @@ export type PermissionChecker = ( ref?: ModuleRef, request?: Request, ) => Promise; + +export type CheckerParams = { + resolver: RbacResolver; + repository: UserRepository; + checkers: PermissionChecker[]; + moduleRef?: ModuleRef; + request?: any; +};