security: enforce TenantGuard and ThrottlerGuard globally, lock down CORS
Both guards were fully implemented but never actually wired in -- Nest doesn't enforce a guard just because its module is imported, it needs an explicit APP_GUARD registration. Registered both globally so every new controller is deny-by-default and rate-limited unless it opts out. Added a @Public() decorator (checked via Reflector in TenantGuard) for routes that legitimately have no session, applied it to /health so the global guard doesn't break it. CORS was wide open (enableCors() with no origin restriction, effectively allow-any-origin). Now reads an explicit CORS_ORIGINS allowlist from env, defaulting to localhost:3000 for local dev.
This commit is contained in:
parent
5656f1cc1c
commit
5a1206118a
6 changed files with 36 additions and 2 deletions
|
|
@ -1,6 +1,9 @@
|
|||
NODE_ENV=development
|
||||
PORT=3001
|
||||
|
||||
# Comma-separated list of allowed browser origins for CORS -- never use a wildcard in production
|
||||
CORS_ORIGINS=http://localhost:3000
|
||||
|
||||
# Supabase (CEO-OS project, Coolify)
|
||||
DATABASE_URL=
|
||||
SUPABASE_URL=
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { HealthController } from './health/health.controller';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { EventsModule } from './events/events.module';
|
||||
import { TenantGuard } from './auth/tenant.guard';
|
||||
|
||||
function parseRedisConnection(redisUrl: string | undefined) {
|
||||
if (!redisUrl) {
|
||||
|
|
@ -31,5 +33,9 @@ function parseRedisConnection(redisUrl: string | undefined) {
|
|||
EventsModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
{ provide: APP_GUARD, useClass: ThrottlerGuard },
|
||||
{ provide: APP_GUARD, useClass: TenantGuard },
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
8
src/auth/public.decorator.ts
Normal file
8
src/auth/public.decorator.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
|
||||
// Marchez explicit rutele fara sesiune (health checks, webhooks semnate separat).
|
||||
// Fara acest decorator, TenantGuard respinge implicit orice request -- deny-by-default,
|
||||
// per blueprint 11.3, in loc sa se bazeze pe fiecare controller nou sa adauge garda manual.
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import type { Request } from 'express';
|
||||
import { IS_PUBLIC_KEY } from './public.decorator';
|
||||
|
||||
export interface AuthenticatedSession {
|
||||
userId: string;
|
||||
|
|
@ -15,10 +17,22 @@ interface RequestWithSession extends Request {
|
|||
* Blueprint 11.3: tenant_id vine intotdeauna din sesiune, niciodata din
|
||||
* body/query/params. Orice request fara sesiune valida, sau care incearca
|
||||
* sa suprascrie tenant_id din client, este respins (deny-by-default).
|
||||
* Inregistrata global (APP_GUARD) -- rutele publice trebuie sa foloseasca
|
||||
* explicit @Public(), altfel raman blocate implicit.
|
||||
*/
|
||||
@Injectable()
|
||||
export class TenantGuard implements CanActivate {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest<RequestWithSession>();
|
||||
const session = request.session;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Public } from '../auth/public.decorator';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
@Public()
|
||||
@Get()
|
||||
check() {
|
||||
return { status: 'ok', service: 'ceo-api', timestamp: new Date().toISOString() };
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ async function bootstrap() {
|
|||
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
||||
app.useLogger(app.get(Logger));
|
||||
app.use(helmet());
|
||||
app.enableCors();
|
||||
const allowedOrigins = (process.env.CORS_ORIGINS ?? 'http://localhost:3000').split(',').map((o) => o.trim());
|
||||
app.enableCors({ origin: allowedOrigins, credentials: true });
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('CEO OS API')
|
||||
|
|
|
|||
Loading…
Reference in a new issue