5708127dbb
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>
81 lines
2.1 KiB
TypeScript
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),
|
|
);
|
|
}
|