Files
gongxue-base/apps/server/src/authorization/guards/policies.guard.ts

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