feat(rls): add TenantContextInterceptor for PostgreSQL RLS
Implement global interceptor that sets tenant and user context in PostgreSQL session before each authenticated request. This enables Row Level Security policies to filter data automatically. - Create TenantContextInterceptor in common/interceptors/ - Register as global interceptor in AppModule - Call auth.set_current_tenant() and auth.set_current_user() - Clear context after request completion to prevent leaks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a671697d35
commit
ff7fc41449
@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
@ -9,6 +10,9 @@ import { BullModule } from '@nestjs/bullmq';
|
||||
import { envConfig, validationSchema } from '@config/env.config';
|
||||
import { databaseConfig } from '@config/database.config';
|
||||
|
||||
// Interceptors
|
||||
import { TenantContextInterceptor } from './common/interceptors';
|
||||
|
||||
// Modules
|
||||
import { AuthModule } from '@modules/auth/auth.module';
|
||||
import { TenantsModule } from '@modules/tenants/tenants.module';
|
||||
@ -99,5 +103,12 @@ import { MlmModule } from '@modules/mlm/mlm.module';
|
||||
GoalsModule,
|
||||
MlmModule,
|
||||
],
|
||||
providers: [
|
||||
// Global interceptor for RLS tenant context
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: TenantContextInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
1
src/common/index.ts
Normal file
1
src/common/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './interceptors';
|
||||
1
src/common/interceptors/index.ts
Normal file
1
src/common/interceptors/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './tenant-context.interceptor';
|
||||
123
src/common/interceptors/tenant-context.interceptor.ts
Normal file
123
src/common/interceptors/tenant-context.interceptor.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
/**
|
||||
* TenantContextInterceptor
|
||||
*
|
||||
* Establece el contexto de tenant y usuario en PostgreSQL para aplicar politicas RLS.
|
||||
* Se ejecuta ANTES de cada request autenticado y llama a las funciones:
|
||||
* - auth.set_current_tenant(uuid) - Establece app.current_tenant_id
|
||||
* - auth.set_current_user(uuid) - Establece app.current_user_id
|
||||
*
|
||||
* Las variables de sesion son utilizadas por las politicas RLS en PostgreSQL
|
||||
* para filtrar automaticamente las filas segun el tenant del usuario.
|
||||
*
|
||||
* IMPORTANTE: Usa set_config con local=FALSE para que el contexto persista
|
||||
* durante toda la conexion. Al final del request, limpiamos el contexto
|
||||
* para evitar fugas entre requests.
|
||||
*
|
||||
* @see database/ddl/03-functions.sql - Funciones auth.set_current_tenant/user
|
||||
* @see database/ddl/05-policies.sql - Politicas RLS que usan el contexto
|
||||
*/
|
||||
@Injectable()
|
||||
export class TenantContextInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(TenantContextInterceptor.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Promise<Observable<unknown>> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
// Si no hay usuario autenticado, continuar sin establecer contexto RLS
|
||||
if (!user) {
|
||||
this.logger.debug('No authenticated user, skipping RLS context setup');
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const tenantId = user.tenant_id;
|
||||
const userId = user.id;
|
||||
|
||||
// Validar que tenemos tenant_id
|
||||
if (!tenantId) {
|
||||
this.logger.warn(
|
||||
'User authenticated but tenant_id is missing in JWT payload',
|
||||
);
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
// Validar formato UUID para prevenir injection
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
if (!uuidRegex.test(tenantId)) {
|
||||
this.logger.error(`Invalid tenant_id format: ${tenantId}`);
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
if (userId && !uuidRegex.test(userId)) {
|
||||
this.logger.error(`Invalid user_id format: ${userId}`);
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
try {
|
||||
// Establecer contexto de tenant usando la funcion del DDL
|
||||
await this.dataSource.query(`SELECT auth.set_current_tenant($1)`, [
|
||||
tenantId,
|
||||
]);
|
||||
|
||||
this.logger.debug(`RLS tenant context set: tenant_id=${tenantId}`);
|
||||
|
||||
// Establecer contexto de usuario si existe
|
||||
if (userId) {
|
||||
await this.dataSource.query(`SELECT auth.set_current_user($1)`, [
|
||||
userId,
|
||||
]);
|
||||
this.logger.debug(`RLS user context set: user_id=${userId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to set RLS context: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
// Continuar sin RLS si falla - los servicios tienen filtros manuales como fallback
|
||||
}
|
||||
|
||||
// Procesar el request y limpiar contexto al finalizar
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: () => {
|
||||
this.clearContext();
|
||||
},
|
||||
error: () => {
|
||||
this.clearContext();
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia el contexto RLS al finalizar el request.
|
||||
* Esto previene fugas de contexto entre requests en conexiones pooled.
|
||||
*/
|
||||
private async clearContext(): Promise<void> {
|
||||
try {
|
||||
await this.dataSource.query(`SELECT auth.clear_context()`);
|
||||
this.logger.debug('RLS context cleared');
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to clear RLS context: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user