feat(ssf): 托管机器凭据并支持动态恢复
Gateway 连接表单接收认证中心一次性交付的 machine Client 凭据,后端立即写入 SecretStore,数据库和公开响应只保留引用及公开 Client ID。SSF 管理 Token、Verification 和 RFC 7662 内省统一从动态凭据读取,支持现有连接无重启修复及进程重启恢复,环境变量仅保留为旧部署回退。\n\n验证:go test ./apps/api/...;pnpm nx test web;真实重启、OIDC 登录与 session-revoked 1.29 秒失效验收。
This commit is contained in:
@@ -64,7 +64,7 @@ export function SystemSettingsPanel(props: {
|
||||
onSaveClientCustomizationSettings: (input: ClientCustomizationSettingsUpdateRequest) => Promise<void>;
|
||||
onSaveFileStorageChannel: (input: FileStorageChannelUpsertRequest, channelId?: string) => Promise<void>;
|
||||
onSaveFileStorageSettings: (input: FileStorageSettingsUpdateRequest) => Promise<void>;
|
||||
onConnectSecurityEvents: (transmitterIssuer: string) => Promise<void>;
|
||||
onConnectSecurityEvents: (transmitterIssuer: string, managementClientId?: string, managementClientSecret?: string) => Promise<void>;
|
||||
onDisconnectSecurityEvents: () => Promise<void>;
|
||||
onRefreshSecurityEvents: () => Promise<void>;
|
||||
onRotateSecurityEventsCredential: () => Promise<void>;
|
||||
@@ -411,7 +411,7 @@ function SecurityEventConnectionPanel(props: {
|
||||
issuer: string;
|
||||
loading: boolean;
|
||||
onIssuerChange(value: string): void;
|
||||
onConnect(issuer: string): Promise<void>;
|
||||
onConnect(issuer: string, managementClientId?: string, managementClientSecret?: string): Promise<void>;
|
||||
onDisconnect(): void;
|
||||
onRefresh(): Promise<void>;
|
||||
onRotate(): Promise<void>;
|
||||
@@ -419,9 +419,13 @@ function SecurityEventConnectionPanel(props: {
|
||||
}) {
|
||||
const connection = props.connection?.connection;
|
||||
const prerequisites = props.connection?.prerequisites;
|
||||
const [managementClientId, setManagementClientId] = useState('');
|
||||
const [managementClientSecret, setManagementClientSecret] = useState('');
|
||||
useEffect(() => {
|
||||
if (!managementClientId && prerequisites?.managementClientId) setManagementClientId(prerequisites.managementClientId);
|
||||
}, [managementClientId, prerequisites?.managementClientId]);
|
||||
const missing = [
|
||||
!prerequisites?.oidcConfigured ? '先完成 OIDC Issuer、Tenant 和 Audience 配置' : '',
|
||||
!prerequisites?.introspectionClientConfigured ? '配置 RFC 7662 机器 Client ID / Secret' : '',
|
||||
!prerequisites?.publicBaseUrlConfigured ? '配置 AI_GATEWAY_PUBLIC_BASE_URL' : '',
|
||||
].filter(Boolean);
|
||||
return (
|
||||
@@ -446,15 +450,22 @@ function SecurityEventConnectionPanel(props: {
|
||||
<p className="mutedText">请先在认证中心 Application 的“安全事件流”中完成“准备 Gateway 接入”。</p>
|
||||
</div>
|
||||
{missing.length > 0 && <div className="formMessage">{missing.join(';')}</div>}
|
||||
<div className="fileStorageMeta">
|
||||
<span>复用机器 Client: {prerequisites?.managementClientId || '未配置'}</span>
|
||||
</div>
|
||||
<Label>
|
||||
SSF Transmitter Issuer
|
||||
<Input value={props.issuer} onChange={(event) => props.onIssuerChange(event.target.value)} placeholder="https://auth.51easyai.com/ssf" />
|
||||
<small>页面只提交公开 Issuer;Endpoint、Scope、Push Bearer 和 Stream 由后端自动处理。</small>
|
||||
<small>Endpoint、Scope、Push Bearer 和 Stream 由后端自动处理。</small>
|
||||
</Label>
|
||||
<Button type="button" disabled={props.loading || missing.length > 0 || !props.issuer.trim()} onClick={() => void props.onConnect(props.issuer.trim())}>
|
||||
<Label>
|
||||
Machine Client ID
|
||||
<Input value={managementClientId} onChange={(event) => setManagementClientId(event.target.value)} placeholder="从认证中心一次性连接凭据复制" autoComplete="off" />
|
||||
</Label>
|
||||
<Label>
|
||||
Machine Client Secret
|
||||
<Input type="password" value={managementClientSecret} onChange={(event) => setManagementClientSecret(event.target.value)} placeholder="仅本次提交,保存后不再显示" autoComplete="new-password" />
|
||||
<small>浏览器只负责本次传递;Gateway 后端会立即写入 SecretStore,数据库、响应和日志均不保存明文。</small>
|
||||
</Label>
|
||||
<Button type="button" disabled={props.loading || missing.length > 0 || !props.issuer.trim() || !managementClientId.trim() || !managementClientSecret}
|
||||
onClick={() => void props.onConnect(props.issuer.trim(), managementClientId.trim(), managementClientSecret).then(() => setManagementClientSecret(''))}>
|
||||
<Link2 size={15} />连接认证中心
|
||||
</Button>
|
||||
</CardContent>
|
||||
@@ -475,6 +486,21 @@ function SecurityEventConnectionPanel(props: {
|
||||
{props.connection?.auditId && <span>最近操作 Audit ID: {props.connection.auditId}</span>}
|
||||
{connection.lastErrorCategory && <span>最近错误: {connection.lastErrorCategory}</span>}
|
||||
</div>
|
||||
{(connection.lastErrorCategory === 'credential_unavailable' || connection.lastErrorCategory === 'management_token_failed') && <div className="pageStack">
|
||||
<div className="formMessage">认证中心机器凭据不可用。请在认证中心重新生成一次性连接凭据,并在这里更新;无需断开 Stream。</div>
|
||||
<Label>
|
||||
Machine Client ID
|
||||
<Input value={managementClientId} onChange={(event) => setManagementClientId(event.target.value)} placeholder="从认证中心一次性连接凭据复制" autoComplete="off" />
|
||||
</Label>
|
||||
<Label>
|
||||
Machine Client Secret
|
||||
<Input type="password" value={managementClientSecret} onChange={(event) => setManagementClientSecret(event.target.value)} placeholder="仅本次提交,保存后不再显示" autoComplete="new-password" />
|
||||
</Label>
|
||||
<Button type="button" disabled={props.loading || !managementClientId.trim() || !managementClientSecret}
|
||||
onClick={() => void props.onConnect(connection.transmitterIssuer, managementClientId.trim(), managementClientSecret).then(() => setManagementClientSecret(''))}>
|
||||
<Link2 size={15} />保存凭据并恢复连接
|
||||
</Button>
|
||||
</div>}
|
||||
<div className="fileStorageToolbar">
|
||||
{connection.lifecycleStatus === 'error' && (
|
||||
<Button type="button" size="sm" disabled={props.loading} onClick={() => void props.onConnect(connection.transmitterIssuer)}><Link2 size={14} />重试连接</Button>
|
||||
|
||||
Reference in New Issue
Block a user