fix(identity): 完善统一认证配对恢复与安全退役

修复 credentials_saved 状态无法恢复、配对与激活并发冲突,以及 SSF 和身份 Secret 生命周期不完整的问题。新增持久化协调器、取消与清理状态机、事务级并发门禁、受控 SSF 凭据交接、禁用后的延迟 Secret 清理,并对生产环境统一认证及 Discovery 端点强制 HTTPS。

验证:go test ./...;go test -race ./internal/auth ./internal/identity ./internal/identityruntime ./internal/securityevents ./internal/httpapi ./internal/store -count=1;go vet ./...;真实 PostgreSQL 并发及清理成功/冲突回滚测试;pnpm openapi。
This commit is contained in:
2026-07-17 18:31:12 +08:00
parent cdfca61304
commit a312ad880d
55 changed files with 9225 additions and 419 deletions
+198 -5
View File
@@ -2918,7 +2918,7 @@
"BearerAuth": []
}
],
"description": "保留 Superseded Revision 供回滚,清理 BFF Session,并继续允许本地管理登录。",
"description": "将 Revision 转为只读 Superseded 审计历史,清理 BFF Session,并继续允许本地管理登录;重新接入必须使用新的接入码。",
"produces": [
"application/json"
],
@@ -3116,6 +3116,174 @@
}
}
},
"/api/admin/system/identity/pairings/{pairingID}/cancel": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "原子封存未激活 Revision,并异步清理由该 Revision 拥有的临时 Secret 与 SSF 连接。不会撤销已经完成的远端 Exchange;下一次凭据交付会轮换机器凭据。",
"produces": [
"application/json"
],
"tags": [
"identity"
],
"summary": "放弃本地统一认证配对",
"parameters": [
{
"type": "string",
"description": "配对 ID",
"name": "pairingID",
"in": "path",
"required": true
},
{
"type": "string",
"description": "幂等键",
"name": "Idempotency-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "当前 Pairing ETag",
"name": "If-Match",
"in": "header",
"required": true
}
],
"responses": {
"202": {
"description": "Accepted",
"schema": {
"$ref": "#/definitions/identity.PairingExchange"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"412": {
"description": "Precondition Failed",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"428": {
"description": "Precondition Required",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
}
}
}
},
"/api/admin/system/identity/pairings/{pairingID}/retire-conflicting-security-event": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "仅当指定 Pairing 仍因 owner 冲突停在 credentials_saved 时,退役不属于该 Revision 的旧连接;陈旧请求不会退役当前 Pairing 已创建的新连接。",
"produces": [
"application/json"
],
"tags": [
"identity"
],
"summary": "安全退役阻塞配对的旧 SSF 连接",
"parameters": [
{
"type": "string",
"description": "配对 ID",
"name": "pairingID",
"in": "path",
"required": true
},
{
"type": "string",
"description": "幂等键",
"name": "Idempotency-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "当前 Pairing ETag",
"name": "If-Match",
"in": "header",
"required": true
}
],
"responses": {
"202": {
"description": "Accepted",
"schema": {
"$ref": "#/definitions/identity.PairingExchange"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"412": {
"description": "Precondition Failed",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"428": {
"description": "Precondition Required",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
}
}
}
},
"/api/admin/system/identity/revisions/{revisionID}": {
"patch": {
"security": [
@@ -3290,13 +3458,14 @@
"BearerAuth": []
}
],
"description": "当前远端 OAuth/SSF 资源未版本化,接口会拒绝直接回滚并要求禁用后使用新接入码完成交接。",
"produces": [
"application/json"
],
"tags": [
"identity"
],
"summary": "回滚统一认证 Revision",
"summary": "请求恢复历史统一认证 Revision",
"parameters": [
{
"type": "string",
@@ -4792,7 +4961,7 @@
},
"/api/v1/auth/login": {
"post": {
"description": "使用用户名或邮箱登录本地账号,并返回 24 小时 JWT。",
"description": "使用用户名或邮箱登录本地账号,并返回 24 小时 JWT。非本地身份模式只允许本地应急 Manager 登录。",
"consumes": [
"application/json"
],
@@ -11492,12 +11661,34 @@
}
}
},
"identity.PairingCleanupStatus": {
"type": "string",
"enum": [
"none",
"pending",
"completed"
],
"x-enum-varnames": [
"PairingCleanupNone",
"PairingCleanupPending",
"PairingCleanupCompleted"
]
},
"identity.PairingExchange": {
"type": "object",
"properties": {
"authCenterAuditId": {
"type": "string"
},
"cancelledAt": {
"type": "string"
},
"cleanupCompletedAt": {
"type": "string"
},
"cleanupStatus": {
"$ref": "#/definitions/identity.PairingCleanupStatus"
},
"createdAt": {
"type": "string"
},
@@ -11565,7 +11756,8 @@
"credentials_saved",
"completed",
"failed",
"expired"
"expired",
"cancelled"
],
"x-enum-varnames": [
"PairingMetadataPending",
@@ -11574,7 +11766,8 @@
"PairingCredentialsSaved",
"PairingCompleted",
"PairingFailed",
"PairingExpired"
"PairingExpired",
"PairingCancelled"
]
},
"identity.Revision": {
+134 -3
View File
@@ -932,10 +932,26 @@ definitions:
example: manual recharge
type: string
type: object
identity.PairingCleanupStatus:
enum:
- none
- pending
- completed
type: string
x-enum-varnames:
- PairingCleanupNone
- PairingCleanupPending
- PairingCleanupCompleted
identity.PairingExchange:
properties:
authCenterAuditId:
type: string
cancelledAt:
type: string
cleanupCompletedAt:
type: string
cleanupStatus:
$ref: '#/definitions/identity.PairingCleanupStatus'
createdAt:
type: string
expiresAt:
@@ -983,6 +999,7 @@ definitions:
- completed
- failed
- expired
- cancelled
type: string
x-enum-varnames:
- PairingMetadataPending
@@ -992,6 +1009,7 @@ definitions:
- PairingCompleted
- PairingFailed
- PairingExpired
- PairingCancelled
identity.Revision:
properties:
activatedAt:
@@ -4616,7 +4634,7 @@ paths:
- identity
/api/admin/system/identity/disable:
post:
description: 保留 Superseded Revision 供回滚,清理 BFF Session,并继续允许本地管理登录。
description: 将 Revision 转为只读 Superseded 审计历史,清理 BFF Session,并继续允许本地管理登录;重新接入必须使用新的接入码
parameters:
- description: 幂等键
in: header
@@ -4746,6 +4764,118 @@ paths:
summary: 查询统一认证配对状态
tags:
- identity
/api/admin/system/identity/pairings/{pairingID}/cancel:
post:
description: 原子封存未激活 Revision,并异步清理由该 Revision 拥有的临时 Secret 与 SSF 连接。不会撤销已经完成的远端
Exchange;下一次凭据交付会轮换机器凭据。
parameters:
- description: 配对 ID
in: path
name: pairingID
required: true
type: string
- description: 幂等键
in: header
name: Idempotency-Key
required: true
type: string
- description: 当前 Pairing ETag
in: header
name: If-Match
required: true
type: string
produces:
- application/json
responses:
"202":
description: Accepted
schema:
$ref: '#/definitions/identity.PairingExchange'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"403":
description: Forbidden
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"404":
description: Not Found
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"409":
description: Conflict
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"412":
description: Precondition Failed
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"428":
description: Precondition Required
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
security:
- BearerAuth: []
summary: 放弃本地统一认证配对
tags:
- identity
/api/admin/system/identity/pairings/{pairingID}/retire-conflicting-security-event:
post:
description: 仅当指定 Pairing 仍因 owner 冲突停在 credentials_saved 时,退役不属于该 Revision
的旧连接;陈旧请求不会退役当前 Pairing 已创建的新连接。
parameters:
- description: 配对 ID
in: path
name: pairingID
required: true
type: string
- description: 幂等键
in: header
name: Idempotency-Key
required: true
type: string
- description: 当前 Pairing ETag
in: header
name: If-Match
required: true
type: string
produces:
- application/json
responses:
"202":
description: Accepted
schema:
$ref: '#/definitions/identity.PairingExchange'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"403":
description: Forbidden
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"404":
description: Not Found
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"409":
description: Conflict
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"412":
description: Precondition Failed
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"428":
description: Precondition Required
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
security:
- BearerAuth: []
summary: 安全退役阻塞配对的旧 SSF 连接
tags:
- identity
/api/admin/system/identity/revisions/{revisionID}:
patch:
consumes:
@@ -4857,6 +4987,7 @@ paths:
- identity
/api/admin/system/identity/revisions/{revisionID}/rollback:
post:
description: 当前远端 OAuth/SSF 资源未版本化,接口会拒绝直接回滚并要求禁用后使用新接入码完成交接。
parameters:
- description: Revision ID
in: path
@@ -4902,7 +5033,7 @@ paths:
$ref: '#/definitions/httpapi.ErrorEnvelope'
security:
- BearerAuth: []
summary: 回滚统一认证 Revision
summary: 请求恢复历史统一认证 Revision
tags:
- identity
/api/admin/system/identity/revisions/{revisionID}/validate:
@@ -5825,7 +5956,7 @@ paths:
post:
consumes:
- application/json
description: 使用用户名或邮箱登录本地账号,并返回 24 小时 JWT。
description: 使用用户名或邮箱登录本地账号,并返回 24 小时 JWT。非本地身份模式只允许本地应急 Manager 登录。
parameters:
- description: 登录请求,account 可为用户名或邮箱
in: body