/** * Capability Guard * * Enforces capability-based authorization for federation operations. * Fail-closed security model: denies access unless capability is explicitly granted. */ import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, ForbiddenException, } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { FederationConnectionStatus } from "@prisma/client"; import type { Request } from "express"; import type { FederationCapabilities } from "../types/instance.types"; import { ConnectionService } from "../connection.service"; import { FederationAuditService } from "../audit.service"; /** * Metadata key for required capability */ const REQUIRED_CAPABILITY_KEY = "federation:requiredCapability"; /** * Guard that enforces capability-based authorization * * Security properties: * - Fail-closed: Denies access if capability is undefined or not explicitly true * - Connection validation: Verifies connection exists and is CONNECTED * - Audit logging: Logs all capability denials for security monitoring */ @Injectable() export class CapabilityGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly connectionService: ConnectionService, private readonly auditService: FederationAuditService ) {} async canActivate(context: ExecutionContext): Promise { // Check if capability requirement is set on endpoint const requiredCapability = this.reflector.get( REQUIRED_CAPABILITY_KEY, context.getHandler() ); // If no capability required, allow access // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!requiredCapability) { return true; } const request = context.switchToHttp().getRequest(); // Extract connection ID from request const connectionId = this.extractConnectionId(request); if (!connectionId) { throw new UnauthorizedException("Federation connection not identified"); } // Load connection const connection = await this.connectionService.getConnectionById(connectionId); if (!connection) { throw new UnauthorizedException("Invalid federation connection"); } // Verify connection is active if (connection.status !== FederationConnectionStatus.ACTIVE) { throw new ForbiddenException("Connection is not active"); } // Check if capability is granted (fail-closed: must be explicitly true) const capabilities = connection.remoteCapabilities as unknown as FederationCapabilities; const hasCapability = capabilities[requiredCapability] === true; if (!hasCapability) { // Log capability denial for security monitoring this.auditService.logCapabilityDenied( connection.remoteInstanceId, requiredCapability, request.url ); throw new ForbiddenException(`Operation requires capability: ${requiredCapability}`); } return true; } /** * Extract connection ID from request * Checks body, headers, and params in order */ private extractConnectionId(request: Request): string | undefined { // Check body first (most common for POST/PATCH) const body = request.body as { connectionId?: string } | undefined; if (body?.connectionId) { return body.connectionId; } // Check headers (for federated requests) const headerConnectionId = request.headers["x-federation-connection-id"]; if (headerConnectionId) { return Array.isArray(headerConnectionId) ? headerConnectionId[0] : headerConnectionId; } // Check route params (for GET/DELETE operations) const params = request.params as { connectionId?: string } | undefined; if (params?.connectionId) { return params.connectionId; } return undefined; } } /** * Decorator to mark endpoints that require specific federation capabilities * * @example * ```typescript * @Post("execute-command") * @RequireCapability("supportsCommand") * async executeCommand(@Body() dto: ExecuteCommandDto) { * // Only reachable if remote instance has supportsCommand = true * } * ``` */ export const RequireCapability = (capability: keyof FederationCapabilities) => Reflector.createDecorator({ key: REQUIRED_CAPABILITY_KEY, transform: () => capability, });