feat: 补齐 Agent 配置变更审计

This commit is contained in:
湛兮
2026-06-12 06:22:23 +08:00
parent 51a3565412
commit 303a8648bc
3 changed files with 81 additions and 16 deletions
+31 -3
View File
@@ -1,11 +1,24 @@
import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Get,
Post,
Put,
Req,
UseGuards,
} from '@nestjs/common';
import { ApiBody, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AdminOnlyMessage } from '../auth/admin-only-message.decorator';
import { AuthGuard } from '../auth/auth.guard';
import { AuthenticatedUser } from '../auth/auth.types';
import {
AuthenticatedUser,
AuthRequestContext,
RequestWithUser,
} from '../auth/auth.types';
import { CurrentUser } from '../auth/current-user.decorator';
import { PasswordChangeGuard } from '../auth/password-change.guard';
import { SuperAdminGuard } from '../auth/super-admin.guard';
import { resolveRequestId } from '../common/http/request-id';
import { ZodValidationPipe } from '../common/pipes/zod-validation.pipe';
import {
SaveAgentConfigDto,
@@ -37,11 +50,16 @@ export class AgentConfigController {
@ApiBody({ type: SaveAgentConfigDto })
@ApiOkResponse({ description: '保存 Agent 配置。' })
async saveConfig(
@Req() request: RequestWithUser,
@CurrentUser() user: AuthenticatedUser,
@Body(new ZodValidationPipe(saveAgentConfigSchema))
body: SaveAgentConfigInput,
): Promise<AgentConfigSummary> {
return this.agentConfigService.saveConfig(user, body);
return this.agentConfigService.saveConfig(
user,
body,
this.auditContextFor(request),
);
}
@Post('test')
@@ -51,4 +69,14 @@ export class AgentConfigController {
): Promise<AgentConnectionTestResult> {
return this.agentConfigService.testConnection(user);
}
private auditContextFor(request: RequestWithUser): AuthRequestContext {
const userAgent = request.headers['user-agent'];
return {
requestId: resolveRequestId(request.headers),
sourceIp: request.ip ?? request.socket?.remoteAddress,
userAgent: Array.isArray(userAgent) ? userAgent[0] : userAgent,
};
}
}
+16 -3
View File
@@ -84,13 +84,26 @@ describe('AgentConfigService', () => {
expect(audit.record).toHaveBeenCalledWith(
expect.objectContaining({
before: expect.objectContaining({
hasBaseURL: true,
hasKey: true,
model: 'gpt-test',
}),
after: expect.objectContaining({
hasBaseURL: true,
hasKey: true,
model: 'gpt-new',
changedFields: expect.arrayContaining(['key', 'model']),
}),
parameterDigest: expect.objectContaining({
keyProvided: true,
baseURLProvided: true,
}),
}),
);
expect(JSON.stringify(audit.record.mock.calls)).not.toContain(
'sk-new-abcdef123456',
);
const auditPayload = JSON.stringify(audit.record.mock.calls);
expect(auditPayload).not.toContain('sk-new-abcdef123456');
expect(auditPayload).not.toContain('https://api.example.com/v1');
});
});
+34 -10
View File
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { AuditService } from '../audit/audit.service';
import { AuthenticatedUser } from '../auth/auth.types';
import { AuthenticatedUser, AuthRequestContext } from '../auth/auth.types';
import { AppError } from '../common/errors/app-error';
import { redactSensitive } from '../common/security/redact-sensitive';
import { SaveAgentConfigInput } from './agent-config.dto';
@@ -29,26 +29,36 @@ export class AgentConfigService {
async saveConfig(
user: AuthenticatedUser,
input: SaveAgentConfigInput,
context: AuthRequestContext = {},
): Promise<AgentConfigSummary> {
this.assertSuperAdmin(user);
const before = await this.agentConfigRepository.getRuntimeConfig();
const savedConfig = await this.agentConfigRepository.saveConfig(input);
const changedFields = this.changedFields(
before,
savedConfig,
Boolean(input.key),
);
await this.auditService.record({
action: 'AGENT_CONFIG_UPDATED',
resourceType: 'agent_config',
resourceId: 'system-agent-config',
actorId: user.id,
actorName: user.account,
before: {
baseURL: before.baseURL,
model: before.model,
hasKey: Boolean(before.key),
},
requestId: context.requestId,
sourceIp: context.sourceIp,
userAgent: context.userAgent,
before: this.auditConfigSummary(before),
after: {
baseURL: savedConfig.baseURL,
model: savedConfig.model,
hasKey: Boolean(savedConfig.key),
changedFields: this.changedFields(before, savedConfig, Boolean(input.key)),
...this.auditConfigSummary(savedConfig),
changedFields,
},
parameterDigest: {
changedFields,
keyProvided: Boolean(input.key),
baseURLProvided: Boolean(input.baseURL),
model: input.model,
},
});
@@ -168,6 +178,20 @@ export class AgentConfigService {
return fields;
}
/**
* Agent 配置审计只记录状态和模型名,禁止把 key 或 baseURL 原文写入审计表。
*/
private auditConfigSummary(
config: Partial<AgentRuntimeConfig> & { updatedAt?: string },
) {
return {
hasKey: Boolean(config.key),
hasBaseURL: Boolean(config.baseURL),
model: config.model,
updatedAt: config.updatedAt,
};
}
private trimSlash(value: string): string {
return value.replace(/\/+$/, '');
}