feat(center): enforce API key on all APIs except node register
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { buildTypeOrmConfig } from './database.config';
|
||||
import { HealthController } from './common/health.controller';
|
||||
@@ -7,15 +8,24 @@ import { MetricsService } from './common/metrics.service';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { NodesModule } from './nodes/nodes.module';
|
||||
import { AuditModule } from './audit/audit.module';
|
||||
import { CenterApiKeyGuard } from './common/center-api-key.guard';
|
||||
import { GuildNode } from './entities/guild-node.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRoot(buildTypeOrmConfig()),
|
||||
TypeOrmModule.forFeature([GuildNode]),
|
||||
AuditModule,
|
||||
AuthModule,
|
||||
NodesModule,
|
||||
],
|
||||
controllers: [HealthController, MetricsController],
|
||||
providers: [MetricsService],
|
||||
providers: [
|
||||
MetricsService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: CenterApiKeyGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
43
src/common/center-api-key.guard.ts
Normal file
43
src/common/center-api-key.guard.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { GuildNode } from '../entities/guild-node.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CenterApiKeyGuard implements CanActivate {
|
||||
constructor(
|
||||
@InjectRepository(GuildNode)
|
||||
private readonly nodeRepo: Repository<GuildNode>,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest<{
|
||||
path?: string;
|
||||
method?: string;
|
||||
headers: Record<string, string | string[] | undefined>;
|
||||
}>();
|
||||
|
||||
const path = req.path ?? '';
|
||||
const method = (req.method ?? 'GET').toUpperCase();
|
||||
|
||||
// only guild registration is exempt from API key; it is protected by HMAC secret
|
||||
if (method === 'POST' && (path === '/nodes/register' || path.endsWith('/nodes/register'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const received = req.headers['x-api-key'];
|
||||
const apiKey = Array.isArray(received) ? received[0] : received;
|
||||
if (!apiKey) throw new UnauthorizedException('missing api key');
|
||||
|
||||
const nodes = await this.nodeRepo.find({ where: { status: 'active' } });
|
||||
for (const node of nodes) {
|
||||
if (!node.apiKeyHash) continue;
|
||||
const ok = await bcrypt.compare(apiKey, node.apiKeyHash);
|
||||
if (ok) return true;
|
||||
}
|
||||
|
||||
throw new UnauthorizedException('invalid api key');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ export class GuildNode {
|
||||
@Column()
|
||||
endpoint!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
apiKeyHash!: string | null;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['active', 'offline', 'revoked'],
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { GuildNode } from '../entities/guild-node.entity';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { RegisterNodeDto } from './dto.register-node.dto';
|
||||
@@ -95,7 +97,10 @@ export class NodesController {
|
||||
name: body.name,
|
||||
endpoint: body.endpoint,
|
||||
status: 'active',
|
||||
apiKeyHash: null,
|
||||
});
|
||||
const rawApiKey = `gk_${randomBytes(24).toString('hex')}`;
|
||||
node.apiKeyHash = await bcrypt.hash(rawApiKey, 10);
|
||||
const saved = await this.nodeRepo.save(node);
|
||||
await this.audit.write({
|
||||
action: 'node.register',
|
||||
@@ -114,6 +119,7 @@ export class NodesController {
|
||||
endpoint: saved.endpoint,
|
||||
status: saved.status,
|
||||
},
|
||||
apiKey: rawApiKey,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user