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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user