Files
tessera-ctl/apps/api/src/module-registry/module.guard.ts
T
schalli 5708127dbb fix(03): module guard uses JWT tenantId fallback for deactivation enforcement
ModuleGuard now reads tenantId from req.user?.tenantId as fallback
(same pattern as module-registry controller), and throws 403 instead
of silently allowing access when no tenant context exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:31:59 +02:00

81 lines
2.1 KiB
TypeScript

import {
applyDecorators,
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
SetMetadata,
UseGuards,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ModuleRegistryService } from './module-registry.service';
/**
* Metadata key for the module slug attached by @UseModule().
*/
export const MODULE_SLUG_KEY = 'moduleSlug';
/**
* Guard that checks whether the requesting tenant has an active
* module activation for the module identified by its slug.
*
* Per T-03-04: tenantId is sourced from JWT (via TenantMiddleware),
* not from user-supplied input, preventing elevation of privilege.
*/
@Injectable()
export class ModuleGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly moduleRegistryService: ModuleRegistryService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Get moduleSlug from metadata (set by @UseModule decorator)
const moduleSlug = this.reflector.getAllAndOverride<string>(
MODULE_SLUG_KEY,
[context.getHandler(), context.getClass()],
);
// If no module slug is set, allow (guard is not applicable)
if (!moduleSlug) {
return true;
}
const request = context.switchToHttp().getRequest();
const tenantId = request.tenantId ?? request.user?.tenantId;
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
const isActive = await this.moduleRegistryService.isModuleActive(
tenantId,
moduleSlug,
);
if (!isActive) {
throw new ForbiddenException(
`Module '${moduleSlug}' is not activated for this tenant`,
);
}
return true;
}
}
/**
* Decorator that protects a controller or route handler with the ModuleGuard.
* Ensures the specified module is activated for the requesting tenant.
*
* Usage:
* @UseModule('domaincheck')
* @Controller('domaincheck')
* export class DomaincheckController { ... }
*/
export function UseModule(slug: string) {
return applyDecorators(
SetMetadata(MODULE_SLUG_KEY, slug),
UseGuards(ModuleGuard),
);
}