forked from wangziqi/gongxue-base
80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { CHECK_POLICIES_KEY } from '../decorators/check-policies.decorator';
|
|
import { IS_PUBLIC_KEY } from '../../auth/decorators/public.decorator';
|
|
import { CaslAbilityFactory } from '../casl-ability.factory';
|
|
import { AppAbility, AuthorizationRequest, PolicyHandler } from '../interfaces';
|
|
|
|
/**
|
|
* Evaluates CASL policies declared via @CheckPolicies().
|
|
*
|
|
* Registered as a global APP_GUARD — runs after JwtAuthGuard and
|
|
* PermissionGuard in the guard chain. Only activates when a route
|
|
* carries @CheckPolicies metadata.
|
|
*
|
|
* ## Interaction with other guards
|
|
*
|
|
* - @Public() → PoliciesGuard skips (same as other guards).
|
|
* - @CheckPolicies alone (no @RequirePermission) → PermissionGuard
|
|
* passes through if user is authenticated; PoliciesGuard evaluates.
|
|
* - @CheckPolicies + @RequirePermission → both guards run independently;
|
|
* both must pass.
|
|
*
|
|
* ## Handler types
|
|
*
|
|
* Two forms are supported:
|
|
*
|
|
* ```ts
|
|
* // Callback form
|
|
* @CheckPolicies((ability) => ability.can('read', 'Student'))
|
|
*
|
|
* // Class-based form (testable, NestJS official pattern)
|
|
* class ReadStudentPolicyHandler implements IPolicyHandler {
|
|
* handle(ability: AppAbility) { return ability.can('read', 'Student'); }
|
|
* }
|
|
* @CheckPolicies(new ReadStudentPolicyHandler())
|
|
* ```
|
|
*/
|
|
@Injectable()
|
|
export class PoliciesGuard implements CanActivate {
|
|
constructor(
|
|
private reflector: Reflector,
|
|
private abilityFactory: CaslAbilityFactory,
|
|
) {}
|
|
|
|
canActivate(context: ExecutionContext): boolean {
|
|
// @Public() routes skip all authorization
|
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
]);
|
|
if (isPublic) return true;
|
|
|
|
const handlers = this.reflector.getAllAndMerge<PolicyHandler[]>(CHECK_POLICIES_KEY, [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
]);
|
|
|
|
// No @CheckPolicies → let other guards decide
|
|
if (!handlers || handlers.length === 0) return true;
|
|
|
|
const request = context.switchToHttp().getRequest<AuthorizationRequest>();
|
|
const user = request.user;
|
|
if (!user) return false;
|
|
|
|
const ability = this.abilityFactory.createForUser(user);
|
|
|
|
return handlers.every((handler) => this.execHandler(handler, ability));
|
|
}
|
|
|
|
/**
|
|
* Execute a policy handler — supports both callback and class-based forms.
|
|
*/
|
|
private execHandler(handler: PolicyHandler, ability: AppAbility): boolean {
|
|
if (typeof handler === 'function') {
|
|
return handler(ability);
|
|
}
|
|
return handler.handle(ability);
|
|
}
|
|
}
|