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:
@@ -0,0 +1,76 @@
|
||||
package identityruntime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
type tenantContextSyncRepository interface {
|
||||
DueOIDCTenantBindingSyncs(context.Context, string, string, int) ([]store.OIDCTenantBindingSyncTarget, error)
|
||||
ApplyOIDCTenantBindingSync(context.Context, string, identity.TenantContext, bool, time.Time) error
|
||||
RejectOIDCTenantBinding(context.Context, string, string, time.Time) error
|
||||
FailOIDCTenantBindingSync(context.Context, string, string, time.Time) error
|
||||
}
|
||||
|
||||
type tenantContextSyncReader interface {
|
||||
Get(context.Context, string, string) (identity.TenantContext, bool, error)
|
||||
}
|
||||
|
||||
func runTenantContextSynchronizer(ctx context.Context, repository tenantContextSyncRepository, reader tenantContextSyncReader, issuer, applicationID string) {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case now := <-ticker.C:
|
||||
_ = synchronizeDueTenantContexts(ctx, repository, reader, issuer, applicationID, now.UTC())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func synchronizeDueTenantContexts(ctx context.Context, repository tenantContextSyncRepository, reader tenantContextSyncReader, issuer, applicationID string, now time.Time) error {
|
||||
targets, err := repository.DueOIDCTenantBindingSyncs(ctx, issuer, applicationID, 50)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, target := range targets {
|
||||
tenant, unchanged, readErr := reader.Get(ctx, target.ExternalTenantID, target.ETag)
|
||||
switch {
|
||||
case readErr == nil && unchanged:
|
||||
err = repository.ApplyOIDCTenantBindingSync(ctx, target.ID, identity.TenantContext{}, true, now)
|
||||
case readErr == nil && tenant.Active():
|
||||
err = repository.ApplyOIDCTenantBindingSync(ctx, target.ID, tenant, false, now)
|
||||
case readErr == nil:
|
||||
err = repository.RejectOIDCTenantBinding(ctx, target.ID, "tenant_inactive", now)
|
||||
case errors.Is(readErr, identity.ErrTenantContextNotFound):
|
||||
err = repository.RejectOIDCTenantBinding(ctx, target.ID, "tenant_not_found", now)
|
||||
default:
|
||||
err = repository.FailOIDCTenantBindingSync(
|
||||
ctx, target.ID, "tenant_context_unavailable", now.Add(tenantContextRetryDelay(target.FailureCount)),
|
||||
)
|
||||
}
|
||||
if err != nil && !errors.Is(err, store.ErrOIDCTenantUnavailable) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tenantContextRetryDelay(failureCount int) time.Duration {
|
||||
if failureCount < 0 {
|
||||
failureCount = 0
|
||||
}
|
||||
delay := 30 * time.Second
|
||||
for index := 0; index < failureCount && delay < 5*time.Minute; index++ {
|
||||
delay *= 2
|
||||
}
|
||||
if delay > 5*time.Minute {
|
||||
return 5 * time.Minute
|
||||
}
|
||||
return delay
|
||||
}
|
||||
Reference in New Issue
Block a user