diff --git a/apps/api/docs/swagger.json b/apps/api/docs/swagger.json index e4f5bc9..c50cfe6 100644 --- a/apps/api/docs/swagger.json +++ b/apps/api/docs/swagger.json @@ -9753,6 +9753,9 @@ "tenantKey": { "type": "string" }, + "tenantName": { + "type": "string" + }, "userGroupId": { "type": "string" }, @@ -12376,9 +12379,21 @@ "supersededAt": { "type": "string" }, + "tenantContextAudience": { + "type": "string" + }, + "tenantContextEndpoint": { + "type": "string" + }, + "tenantContextScope": { + "type": "string" + }, "tenantId": { "type": "string" }, + "tenantMode": { + "type": "string" + }, "tokenIntrospection": { "type": "boolean" }, diff --git a/apps/api/docs/swagger.yaml b/apps/api/docs/swagger.yaml index 514e8cb..33e5f5a 100644 --- a/apps/api/docs/swagger.yaml +++ b/apps/api/docs/swagger.yaml @@ -32,6 +32,8 @@ definitions: type: string tenantKey: type: string + tenantName: + type: string userGroupId: type: string userGroupKey: @@ -1858,8 +1860,16 @@ definitions: $ref: '#/definitions/identity.RevisionState' supersededAt: type: string + tenantContextAudience: + type: string + tenantContextEndpoint: + type: string + tenantContextScope: + type: string tenantId: type: string + tenantMode: + type: string tokenIntrospection: type: boolean updatedAt: diff --git a/apps/web/src/pages/admin/UnifiedIdentityPanel.test.tsx b/apps/web/src/pages/admin/UnifiedIdentityPanel.test.tsx index 605a8d2..53689a9 100644 --- a/apps/web/src/pages/admin/UnifiedIdentityPanel.test.tsx +++ b/apps/web/src/pages/admin/UnifiedIdentityPanel.test.tsx @@ -9,6 +9,7 @@ import { executeIdentityOperation, isStaleIdentityOperation, pairingFailureMessage, + requiresLocalTenantMapping, } from './UnifiedIdentityPanel'; describe('UnifiedIdentityPanel', () => { @@ -20,12 +21,26 @@ describe('UnifiedIdentityPanel', () => { expect(html).toContain('Gateway API 公网地址'); expect(html).not.toContain('value="/gateway-api"'); expect(html).toContain('Gateway Web 地址'); - expect(html).toContain('Gateway 本地租户映射'); + expect(html).toContain('Gateway 本地租户映射(仅单租户 V1)'); + expect(html).toContain('多租户 V2 无需填写'); + const localTenantInput = html.match(/]+placeholder="default"[^>]*>/)?.[0] ?? ''; + expect(localTenantInput).not.toContain('required'); + expect(localTenantInput).not.toContain('value="default"'); expect(html).not.toContain('Machine Client Secret'); expect(html).not.toContain('SSF Transmitter Issuer'); }); }); +describe('local tenant mapping compatibility', () => { + it('does not require a fixed mapping for multi-tenant V2 revisions', () => { + expect(requiresLocalTenantMapping('multi_tenant')).toBe(false); + }); + + it('keeps the fixed mapping requirement for single-tenant V1 revisions', () => { + expect(requiresLocalTenantMapping('single_tenant')).toBe(true); + }); +}); + describe('PairingProgressCard', () => { it('shows a safe actionable discovery failure and a recovery action', () => { const html = renderToStaticMarkup( undefined} pairing={{ @@ -192,6 +207,7 @@ function identityRevision( id: `${state}-revision`, state, schemaVersion: 1, + tenantMode: 'single_tenant', authCenterUrl: 'https://auth.example.com', issuer: 'https://issuer.example.com', scopes: ['openid'], diff --git a/apps/web/src/pages/admin/UnifiedIdentityPanel.tsx b/apps/web/src/pages/admin/UnifiedIdentityPanel.tsx index 215b316..fff69b6 100644 --- a/apps/web/src/pages/admin/UnifiedIdentityPanel.tsx +++ b/apps/web/src/pages/admin/UnifiedIdentityPanel.tsx @@ -24,7 +24,7 @@ export function UnifiedIdentityPanel(props: { token: string }) { const [configuration, setConfiguration] = useState(null); const [form, setForm] = useState(defaultPairingInput); const [showPairingForm, setShowPairingForm] = useState(false); - const [localTenantKey, setLocalTenantKey] = useState('default'); + const [localTenantKey, setLocalTenantKey] = useState(''); const [legacyJwtEnabled, setLegacyJwtEnabled] = useState(false); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); @@ -120,6 +120,7 @@ export function UnifiedIdentityPanel(props: { token: string }) { const previous = configuration?.previous; const pairing = configuration?.pairing; const runtime = configuration?.runtime; + const localTenantMappingRequired = requiresLocalTenantMapping(draft?.tenantMode); const cleanupPending = pairing?.status === 'cancelled' && pairing.cleanupStatus === 'pending'; const cleanupCompleted = pairing?.status === 'cancelled' && pairing.cleanupStatus === 'completed'; const shouldShowPairing = !active && (showPairingForm || !pairing || cleanupCompleted); @@ -188,7 +189,9 @@ export function UnifiedIdentityPanel(props: { token: string }) { {draft.state === 'draft' && ( - Gateway 本地租户映射 setLocalTenantKey(event.target.value)} /> + {localTenantMappingRequired + ? Gateway 本地租户映射 setLocalTenantKey(event.target.value)} placeholder="default" /> + : 多租户 V2 按 Token `tid` 动态解析租户,不使用固定的本地租户映射。} setLegacyJwtEnabled(event.target.checked)} /> 继续兼容 Legacy JWT仅在迁移期确有旧调用方时开启。 @@ -197,8 +200,8 @@ export function UnifiedIdentityPanel(props: { token: string }) { )} {draft.state === 'draft' && <> - void saveDraftPolicy()}>保存本地策略 - void saveDraftPolicy()}>保存本地策略 + void run(() => validateIdentityRevision(props.token, draft.id, draft.version), '统一认证配置验证成功,可以激活。')}> 验证配置 @@ -241,7 +244,11 @@ export function UnifiedIdentityPanel(props: { token: string }) { 一次性接入码 setForm({ ...form, onboardingCode: event.target.value })} placeholder="仅本次提交,不写入日志或数据库" /> Gateway API 公网地址 setForm({ ...form, publicBaseUrl: event.target.value })} placeholder="https://api.gateway.example.com" /> Gateway Web 地址 setForm({ ...form, webBaseUrl: event.target.value })} placeholder="https://gateway.example.com" /> - Gateway 本地租户映射 setForm({ ...form, localTenantKey: event.target.value })} placeholder="default" /> + + Gateway 本地租户映射(仅单租户 V1) + setForm({ ...form, localTenantKey: event.target.value })} placeholder="default" /> + 多租户 V2 无需填写;收到 Manifest 后会按 Token `tid` 动态解析。 + setForm({ ...form, legacyJwtEnabled: event.target.checked })} /> 继续兼容 Legacy JWT默认关闭;本地应急管理员登录始终保留。 @@ -368,7 +375,9 @@ function RevisionDetails({ revision }: { revision: IdentityConfigurationRevision Issuer: {revision.issuer || '等待 Manifest'} Audience: {revision.audience || '等待 Manifest'} Tenant: {revision.tenantId || '等待 Manifest'} - 本地租户: {revision.localTenantKey} + {revision.tenantMode === 'multi_tenant' + ? 租户模式: 多租户(按 tid 动态解析) + : 本地租户: {revision.localTenantKey}} 登录 Client: {revision.browserClientId || '未启用'} 服务 Client: {revision.machineClientId || '未启用'} 能力: {revision.capabilities.map(capabilityLabel).join('、') || '等待 Manifest'} @@ -392,11 +401,15 @@ function defaultPairingInput(): IdentityPairingInput { onboardingCode: '', publicBaseUrl: /^https?:\/\//i.test(configuredApiBaseUrl) ? configuredApiBaseUrl : '', webBaseUrl, - localTenantKey: 'default', + localTenantKey: '', legacyJwtEnabled: false, }; } +export function requiresLocalTenantMapping(tenantMode?: string): boolean { + return tenantMode !== 'multi_tenant'; +} + function pairingStatusLabel(status: string, cleanupStatus?: string) { if (status === 'cancelled' && cleanupStatus === 'pending') return '正在销毁临时凭据并清理由本次 Revision 创建的安全事件连接'; return ({ metadata_pending: '正在提交回调与 Receiver 地址', preparing: '认证中心正在创建或复用标准资源', ready: '正在领取并保存一次性机器凭据', credentials_saved: '正在建立安全事件能力并确认完成' } as Record)[status] ?? status; diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 37a6e19..da85862 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -1053,6 +1053,7 @@ export interface IdentityConfigurationRevision { id: string; state: IdentityRevisionState; schemaVersion: number; + tenantMode: 'single_tenant' | 'multi_tenant'; authCenterUrl: string; issuer?: string; tenantId?: string;
多租户 V2 按 Token `tid` 动态解析租户,不使用固定的本地租户映射。