feat(identity): 接入认证中心多租户登录

支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
This commit is contained in:
2026-07-28 17:28:35 +08:00
parent 0b02e62c72
commit 5c679ff13f
45 changed files with 2986 additions and 139 deletions
+58 -12
View File
@@ -16,6 +16,8 @@ type ApplySessionRevokedInput struct {
JTI string
TransactionID string
SubjectIssuer string
ApplicationID string
SubjectType string
TenantID string
Subject string
EventTimestamp time.Time
@@ -36,6 +38,9 @@ type SecurityEventEvaluation struct {
}
func (s *Store) ApplySessionRevoked(ctx context.Context, input ApplySessionRevokedInput) (ApplySecurityEventResult, error) {
if input.SubjectType == "" {
input.SubjectType = "principal"
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return ApplySecurityEventResult{}, err
@@ -44,13 +49,15 @@ func (s *Store) ApplySessionRevoked(ctx context.Context, input ApplySessionRevok
subjectHash := shortSecurityEventHash(input.Subject)
tag, err := tx.Exec(ctx, `
INSERT INTO gateway_security_event_receipts (
issuer, jti, audience, event_type, transaction_id, tenant_id, subject_hash, event_timestamp
issuer, jti, audience, event_type, transaction_id, tenant_id, subject_hash, event_timestamp,
application_id, subject_type
)
VALUES ($1, $2::uuid, $3, $4, NULLIF($5, ''), $6, $7, $8)
VALUES ($1, $2::uuid, $3, $4, NULLIF($5, ''), $6, $7, $8, NULLIF($9, ''), $10)
ON CONFLICT (issuer, jti) DO NOTHING`,
input.Issuer, input.JTI, input.Audience,
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
input.TransactionID, input.TenantID, subjectHash, input.EventTimestamp,
input.ApplicationID, input.SubjectType,
)
if err != nil {
return ApplySecurityEventResult{}, err
@@ -64,25 +71,56 @@ ON CONFLICT (issuer, jti) DO NOTHING`,
var revokedAt time.Time
err = tx.QueryRow(ctx, `
INSERT INTO gateway_oidc_revocation_watermarks (issuer, tenant_id, subject, revoked_at, source_jti)
VALUES ($1, $2, $3, $4, $5::uuid)
ON CONFLICT (issuer, tenant_id, subject) DO UPDATE
INSERT INTO gateway_oidc_revocation_watermarks (
issuer, application_id, tenant_id, subject_type, subject, revoked_at, source_jti
)
VALUES ($1, $2, $3, $4, $5, $6, $7::uuid)
ON CONFLICT (issuer, application_id, tenant_id, subject_type, subject) DO UPDATE
SET revoked_at = EXCLUDED.revoked_at, source_jti = EXCLUDED.source_jti, updated_at = now()
WHERE EXCLUDED.revoked_at > gateway_oidc_revocation_watermarks.revoked_at
RETURNING revoked_at`, input.SubjectIssuer, input.TenantID, input.Subject, input.EventTimestamp, input.JTI).Scan(&revokedAt)
RETURNING revoked_at`, input.SubjectIssuer, input.ApplicationID, input.TenantID, input.SubjectType,
input.Subject, input.EventTimestamp, input.JTI).Scan(&revokedAt)
watermarkMoved := err == nil
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return ApplySecurityEventResult{}, err
}
var sessionsDeleted int64
if watermarkMoved {
tag, err = tx.Exec(ctx, `
switch {
case input.ApplicationID != "" && input.SubjectType == "principal":
tag, err = tx.Exec(ctx, `
DELETE FROM gateway_oidc_sessions session
USING gateway_oidc_user_bindings user_binding,gateway_oidc_tenant_bindings tenant_binding
WHERE session.oidc_user_binding_id=user_binding.id
AND user_binding.tenant_binding_id=tenant_binding.id
AND tenant_binding.issuer=$1
AND tenant_binding.application_id=$2
AND tenant_binding.external_tenant_id=$3
AND user_binding.subject=$4`, input.SubjectIssuer, input.ApplicationID, input.TenantID, input.Subject)
case input.ApplicationID != "" && input.SubjectType == "tenant":
tag, err = tx.Exec(ctx, `
DELETE FROM gateway_oidc_sessions session
USING gateway_oidc_user_bindings user_binding,gateway_oidc_tenant_bindings tenant_binding
WHERE session.oidc_user_binding_id=user_binding.id
AND user_binding.tenant_binding_id=tenant_binding.id
AND tenant_binding.issuer=$1
AND tenant_binding.application_id=$2
AND tenant_binding.external_tenant_id=$3`, input.SubjectIssuer, input.ApplicationID, input.TenantID)
if err == nil {
_, err = tx.Exec(ctx, `UPDATE gateway_oidc_tenant_bindings
SET access_status='disabled',updated_at=now()
WHERE issuer=$1 AND application_id=$2 AND external_tenant_id=$3`,
input.SubjectIssuer, input.ApplicationID, input.TenantID)
}
default:
tag, err = tx.Exec(ctx, `
DELETE FROM gateway_oidc_sessions session
USING gateway_users gateway_user
WHERE session.gateway_user_id = gateway_user.id
AND gateway_user.source = 'oidc'
AND gateway_user.external_user_id = $1
AND gateway_user.tenant_id = $2`, input.Subject, input.TenantID)
}
if err != nil {
return ApplySecurityEventResult{}, err
}
@@ -95,7 +133,7 @@ WHERE session.gateway_user_id = gateway_user.id
AfterState: map[string]any{"watermarkMoved": watermarkMoved, "sessionsDeleted": sessionsDeleted},
Metadata: map[string]any{
"issuer": input.SubjectIssuer, "transmitterIssuer": input.Issuer, "tenantId": input.TenantID, "jtiHash": jtiHash,
"initiatingEntity": input.InitiatingEntity,
"applicationId": input.ApplicationID, "subjectType": input.SubjectType, "initiatingEntity": input.InitiatingEntity,
},
})
if err != nil {
@@ -314,7 +352,7 @@ WHERE issuer=$1 AND audience=$2 AND mode <> 'introspection_fallback'
return err
}
func (s *Store) EvaluateOIDCSecurityEvent(ctx context.Context, transmitterIssuer, audience, subjectIssuer, tenantID, subject string, issuedAt, now time.Time, staleAfter time.Duration) (SecurityEventEvaluation, error) {
func (s *Store) EvaluateOIDCSecurityEvent(ctx context.Context, transmitterIssuer, audience, subjectIssuer, applicationID, tenantID, subject string, issuedAt, now time.Time, staleAfter time.Duration) (SecurityEventEvaluation, error) {
var mode, streamStatus string
var lastVerification, bootstrapUntil *time.Time
var createdAt time.Time
@@ -322,9 +360,17 @@ func (s *Store) EvaluateOIDCSecurityEvent(ctx context.Context, transmitterIssuer
err := s.pool.QueryRow(ctx, `
SELECT state.mode,state.stream_status,state.last_verification_at,state.bootstrap_until,state.created_at,watermark.revoked_at
FROM gateway_security_event_stream_state state
LEFT JOIN gateway_oidc_revocation_watermarks watermark
ON watermark.issuer=$3 AND watermark.tenant_id=$4 AND watermark.subject=$5
WHERE state.issuer=$1 AND state.audience=$2`, transmitterIssuer, audience, subjectIssuer, tenantID, subject).Scan(
LEFT JOIN LATERAL (
SELECT max(revoked_at) AS revoked_at
FROM gateway_oidc_revocation_watermarks
WHERE issuer=$3 AND application_id=$4 AND tenant_id=$5
AND (
(subject_type='principal' AND subject=$6)
OR (subject_type='tenant' AND subject=$5)
)
) watermark ON true
WHERE state.issuer=$1 AND state.audience=$2`,
transmitterIssuer, audience, subjectIssuer, applicationID, tenantID, subject).Scan(
&mode, &streamStatus, &lastVerification, &bootstrapUntil, &createdAt, &revokedAt,
)
if err != nil {