mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-03 00:33:13 +00:00
fix(security-audit): 解密失败不再吞掉整份配置,修复升级后配置消失且无法保存的死锁 (#4887)
根因:prompt audit 是共享 TOTP_ENCRYPTION_KEY 加密器的功能中唯一不校验 EncryptionKeyConfigured 的落点。未配置固定密钥的部署每次重启自动生成新 密钥,v162 保存的节点 Token 密文在升级重启后永久无法解密,Reload 中 ActiveFromStorage 整体失败导致快照永远装不上:管理端 GET 回退默认 v1 (v166 起为 503),而保存路径直读数据库做 CAS 版本对比,必然冲突—— 配置既看不见也改不掉。PR #4893 仅改变了报错形态,未修复根因。 修复: - ActiveFromStorage 对单节点解密失败降级容忍:该节点运行时禁用并标记 TokenInvalid,配置整体照常激活;管理端恢复显示真实版本号,重新输入 Token 即可自愈(密文保留,密钥恢复后自动复原) - blocking 意图下零可用节点时 evaluator 仍返回 unavailable,请求照旧 被拒,fail-closed 语义不回归;async 意图下 enqueue 直接 drop 并告警 - Save 在未配置固定加密密钥时拒绝保存新 Token(与 TOTP/Ollama/备份 一致的门控),错误码 prompt_audit_encryption_key_required - token_status 新增 invalid 状态,前端凭据列与编辑框提示重新输入 - 新增 config_token_invalid 告警日志(集合变化时记录一次,不随 5s 刷新刷屏)
This commit is contained in:
1 parent
59ce11c780
commit
bfbe113f5e
15 files changed
+244
-42
No files matched your search
@@ -50,6 +50,21 @@ describe('Prompt Audit components', () => {
|
||||
expect(wrapper.emitted('probe')?.[0]?.[0]).toMatchObject({ id: 'guard-1' })
|
||||
})
|
||||
|
||||
it('surfaces an undecryptable saved credential and prompts for re-entry', async () => {
|
||||
const invalidEndpoint = { ...endpoint(), token_status: 'invalid' }
|
||||
const wrapper = mount(EndpointPool, {
|
||||
props: { endpoints: [invalidEndpoint], probeResults: {}, probingIds: [] },
|
||||
global: { stubs: { BaseDialog: DialogStub } },
|
||||
})
|
||||
expect(wrapper.text()).toContain('admin.promptAudit.pool.invalid')
|
||||
expect(wrapper.text()).not.toContain('admin.promptAudit.pool.configured')
|
||||
|
||||
const edit = wrapper.findAll('button').find((button) => button.text().includes('common.edit'))
|
||||
await edit!.trigger('click')
|
||||
const token = wrapper.get<HTMLInputElement>('[aria-label="admin.promptAudit.pool.apiKey"]')
|
||||
expect(token.attributes('placeholder')).toContain('admin.promptAudit.pool.reenterSecret')
|
||||
})
|
||||
|
||||
it('supports group search, stale configured groups, nine scanners, and bounded worker inputs', async () => {
|
||||
const draft: PromptAuditDraft = {
|
||||
enabled: true, blocking_enabled: false, store_pass_events: false, effective_mode: 'async_audit', strategy: 'priority',
|
||||
|
||||
@@ -68,9 +68,9 @@
|
||||
|
||||
<div class="min-w-0">
|
||||
<p class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400 xl:hidden">{{ t('admin.promptAudit.pool.credential') }}</p>
|
||||
<div class="flex items-center gap-1.5 text-xs font-medium" :class="hasCredential(endpoint) ? 'text-emerald-700 dark:text-emerald-300' : 'text-gray-500 dark:text-dark-400'">
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="hasCredential(endpoint) ? 'bg-emerald-500' : 'bg-gray-300 dark:bg-dark-500'" aria-hidden="true" />
|
||||
{{ hasCredential(endpoint) ? t('admin.promptAudit.pool.configured') : t('admin.promptAudit.pool.missing') }}
|
||||
<div class="flex items-center gap-1.5 text-xs font-medium" :class="credentialInvalid(endpoint) ? 'text-red-600 dark:text-red-300' : hasCredential(endpoint) ? 'text-emerald-700 dark:text-emerald-300' : 'text-gray-500 dark:text-dark-400'">
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="credentialInvalid(endpoint) ? 'bg-red-500' : hasCredential(endpoint) ? 'bg-emerald-500' : 'bg-gray-300 dark:bg-dark-500'" aria-hidden="true" />
|
||||
{{ credentialInvalid(endpoint) ? t('admin.promptAudit.pool.invalid') : hasCredential(endpoint) ? t('admin.promptAudit.pool.configured') : t('admin.promptAudit.pool.missing') }}
|
||||
</div>
|
||||
<p v-if="probingIds.includes(endpoint.id)" class="mt-1.5 text-xs text-primary-600 dark:text-primary-300">
|
||||
{{ t('admin.promptAudit.pool.probeProgress') }}
|
||||
@@ -108,7 +108,7 @@
|
||||
</label>
|
||||
<label class="space-y-1 text-sm text-gray-700 dark:text-dark-200 sm:col-span-2">
|
||||
<span>{{ t('admin.promptAudit.pool.apiKey') }}</span>
|
||||
<input v-model="editing.token" class="input w-full" type="password" autocomplete="new-password" :placeholder="editing.has_token ? t('admin.promptAudit.pool.keepSecret') : ''" :aria-label="t('admin.promptAudit.pool.apiKey')" />
|
||||
<input v-model="editing.token" class="input w-full" type="password" autocomplete="new-password" :placeholder="editing.has_token ? (editing.token_status === 'invalid' ? t('admin.promptAudit.pool.reenterSecret') : t('admin.promptAudit.pool.keepSecret')) : ''" :aria-label="t('admin.promptAudit.pool.apiKey')" />
|
||||
<span class="block text-xs text-gray-500 dark:text-dark-400">{{ t('admin.promptAudit.pool.secretHint') }}</span>
|
||||
</label>
|
||||
<label v-if="editing.has_token" class="flex items-center gap-2 text-sm text-red-600 dark:text-red-300 sm:col-span-2">
|
||||
@@ -190,4 +190,7 @@ function removeEndpoint(endpoint: PromptAuditEndpointDraft) {
|
||||
function hasCredential(endpoint: PromptAuditEndpointDraft): boolean {
|
||||
return Boolean(endpoint.token.trim() || (endpoint.has_token && !endpoint.clear_token))
|
||||
}
|
||||
function credentialInvalid(endpoint: PromptAuditEndpointDraft): boolean {
|
||||
return endpoint.token_status === 'invalid' && !endpoint.token.trim() && !endpoint.clear_token
|
||||
}
|
||||
</script>
|
||||
@@ -12,7 +12,7 @@ export interface PromptAuditEndpoint {
|
||||
input_limit: number
|
||||
enabled: boolean
|
||||
has_token: boolean
|
||||
token_status: 'configured' | 'missing' | string
|
||||
token_status: 'configured' | 'missing' | 'invalid' | string
|
||||
}
|
||||
|
||||
export interface PromptAuditEndpointDraft extends PromptAuditEndpoint {
|
||||
|
||||
Reference in new issue
Block a user