feat: 补齐 Agent 配置变更审计
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(/\/+$/, '');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user