Chuyển đến nội dung

API permission và lời gọi nội bộ

Đường dẫn import: @sdcorejs/nestjs/auth

Entrypoint này cung cấp hai guard độc lập: AuthGuard xác thực JWT và kiểm tra permission của route; InternalGuard xác thực lời gọi service-to-service bằng provider shared-secret có thể xoay vòng.

Các export permission

ExportLoạiMục đích
IPermissionStrategyinterfaceTải và tùy chọn diễn giải các mã permission
DefaultPermissionStrategyclassMặc định deny-all
PERMISSION_STRATEGYvalueDI token của strategy
PERMISSION_METADATA_KEYvalueKhóa metadata của decorator
HasPermissiondecoratorYêu cầu một mã
HasAnyPermissiondecoratorYêu cầu bất kỳ mã nào được cung cấp (OR)
AuthGuardclassGuard Passport jwt cùng kiểm tra danh tính đã xác minh và permission
PermissionModule, PermissionModuleOptionsclass/typeĐăng ký strategy và guard ở phạm vi global
ts
@Injectable()
export class AppPermissionStrategy implements IPermissionStrategy {
  async load(ctx: RequestContext): Promise<string[]> {
    if (!ctx.userId || !ctx.tenant) return [];
    return permissionsFor(ctx.tenant, ctx.userId);
  }

  check(codes: string[], required: string): boolean {
    const [resource] = required.split(':');
    return codes.includes(required) || codes.includes(`${resource}:*`);
  }
}

PermissionModule.forRoot({ strategy: AppPermissionStrategy });
ts
@UseGuards(AuthGuard)
@HasAnyPermission('product:read', 'product:admin')
@Get(':id')
detail() {}

AuthGuard luôn chạy xác thực Passport, kể cả khi route không có metadata permission. Nó ánh xạ req.user đã xác minh vào ContextService, từ chối xung đột với danh tính trusted-gateway hiện có, rồi đánh giá permission. Các mã bắt buộc dùng ngữ nghĩa OR. Permission được tải tối đa một lần cho mỗi request và được phản chiếu vào context. Mảng permissions rõ ràng của principal đã xác minh có thể thỏa cache mà không gọi load.

check mặc định là Array.includes chính xác. Thiếu permission sẽ ném lỗi 403 với code core.permission.forbidden; principal đã xác minh bị thiếu hoặc không hợp lệ sẽ ném lỗi 401 không tiết lộ thông tin định danh.

Các export cho lời gọi nội bộ

ExportLoạiMục đích
IInternalSecretProviderinterfaceKhóa hiện tại và tập khóa xoay vòng tùy chọn
INTERNAL_SECRET_PROVIDERvalueDI token của secret-provider
EnvInternalSecretProviderclassĐọc biến môi trường; mặc định INTERNAL_SECRET_KEY
IInternalContextEnricherinterfaceHook context tin cậy được gọi sau khi xác minh secret
INTERNAL_CONTEXT_ENRICHERvalueDI token của enricher
InternalGuardclassGuard shared-secret so sánh constant-time
INTERNAL_SECRET_HEADERvalueHeader mặc định: x-internal-secret

Cấu hình provider xoay vòng

ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import type { IncomingMessage } from 'node:http';

function requiredIdentityHeader(value: string | string[] | undefined, name: string): string {
  if (typeof value !== 'string') throw new UnauthorizedException(`Missing ${name}`);
  const normalized = value.trim();
  if (!normalized || normalized.length > 64 || !/^[A-Za-z0-9._:-]+$/.test(normalized)) {
    throw new UnauthorizedException(`Invalid ${name}`);
  }
  return normalized;
}

@Injectable()
export class RotatingInternalSecrets implements IInternalSecretProvider {
  async getKey(): Promise<string> {
    return secretStore.current();
  }

  async getKeys(): Promise<string[]> {
    return secretStore.currentAndPrevious();
  }
}

@Injectable()
export class InternalIdentity implements IInternalContextEnricher {
  constructor(private readonly context: ContextService) {}

  enrich(req: IncomingMessage): void {
    const tenant = requiredIdentityHeader(req.headers['x-tenant'], 'x-tenant');
    const caller = requiredIdentityHeader(req.headers['x-caller'], 'x-caller');
    this.context.set('tenant', tenant);
    this.context.set('custom', { caller });
  }
}

SdCoreModule.forRoot({
  providers: [
    { provide: INTERNAL_SECRET_PROVIDER, useClass: RotatingInternalSecrets },
    { provide: INTERNAL_CONTEXT_ENRICHER, useClass: InternalIdentity },
  ],
});

Nếu có getKeys, InternalGuard chấp nhận bất kỳ khóa nào được trả về và không gọi getKey. Nó dùng timingSafeEqual; độ dài khác nhau sẽ fail trước khi so sánh. Enricher chỉ chạy sau khi có secret hợp lệ.

ts
@UseGuards(InternalGuard)
@Post('reindex')
reindex() {}

Lỗi

Điều kiệnTrạng tháiCode
Provider chưa đăng ký500core.permission.internal-secret-provider-missing
Thiếu header403core.permission.internal-secret-missing
Không khóa nào khớp403core.permission.internal-secret-mismatch

Lưu ý bảo mật

  • Nên dùng getKeys() trong thời gian xoay vòng, sau đó xóa khóa cũ khi các caller đã chuyển đổi.
  • EnvInternalSecretProvider không trả khóa hợp lệ khi biến môi trường vắng mặt, vì vậy route vẫn đóng. Coi lỗi 500 là cấu hình vận hành sai.
  • Chỉ gửi internal secret tới origin tin cậy chính xác. HttpService loại bỏ secret khỏi request và redirect không tin cậy nhưng không bao giờ tự tạo secret header cho bạn.
  • Không suy ra tenant/user context tin cậy trước khi kiểm tra secret; đặt công việc đó trong IInternalContextEnricher.
  • Decorator permission của route là metadata, không phải đăng ký guard. Luôn áp dụng AuthGuard cục bộ hoặc global.

Phát hành theo giấy phép MIT.