feat(identity): 增加统一认证管理接口
提供接入码配对、配置查询、本地策略修改、验证、激活、回滚、禁用和公开运行时状态接口。写操作强制 Idempotency-Key 与 If-Match,后台 Exchange 支持重启恢复和过期收敛,并记录脱敏 Trace 与审计。\n\n验证:go test 相关 identity、store、identityruntime、httpapi 包;go vet ./apps/api/...
This commit is contained in:
@@ -17,6 +17,27 @@ var (
|
||||
ErrLocalTenantInvalid = errors.New("local tenant mapping is invalid")
|
||||
)
|
||||
|
||||
type RevisionPolicy struct {
|
||||
LocalTenantKey string `json:"localTenantKey"`
|
||||
RolePrefix string `json:"rolePrefix"`
|
||||
JITEnabled bool `json:"jitEnabled"`
|
||||
LegacyJWTEnabled bool `json:"legacyJwtEnabled"`
|
||||
SessionIdleSeconds int `json:"sessionIdleSeconds"`
|
||||
SessionAbsoluteSeconds int `json:"sessionAbsoluteSeconds"`
|
||||
SessionRefreshSeconds int `json:"sessionRefreshSeconds"`
|
||||
}
|
||||
|
||||
func (policy RevisionPolicy) Validate() error {
|
||||
if strings.TrimSpace(policy.LocalTenantKey) == "" || strings.TrimSpace(policy.RolePrefix) == "" {
|
||||
return errors.New("local tenant mapping and role prefix are required")
|
||||
}
|
||||
if policy.SessionIdleSeconds <= 0 || policy.SessionAbsoluteSeconds <= policy.SessionIdleSeconds ||
|
||||
policy.SessionRefreshSeconds <= 0 || policy.SessionRefreshSeconds >= policy.SessionIdleSeconds {
|
||||
return errors.New("identity session policy is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RevisionState string
|
||||
|
||||
const (
|
||||
|
||||
@@ -129,6 +129,15 @@ func (service *PairingService) Continue(ctx context.Context, pairingID string) (
|
||||
if pairing.Status == PairingCompleted || pairing.Status == PairingFailed || pairing.Status == PairingExpired {
|
||||
return pairing, nil
|
||||
}
|
||||
if !pairing.ExpiresAt.After(time.Now()) {
|
||||
expired, updateErr := service.repository.UpdateIdentityPairingExchange(ctx, pairing.ID, pairing.Version, PairingExchangeUpdate{
|
||||
Status: PairingExpired, RemoteVersion: pairing.RemoteVersion, LastErrorCategory: "exchange_expired",
|
||||
})
|
||||
if updateErr == nil {
|
||||
_ = service.secrets.Delete(ctx, pairing.ExchangeTokenRef)
|
||||
}
|
||||
return expired, updateErr
|
||||
}
|
||||
revision, err := service.repository.IdentityConfigurationRevision(ctx, pairing.RevisionID)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
|
||||
@@ -154,3 +154,26 @@ func TestPairingRetriesWithRotatedCredentialAfterSecretStoreFailure(t *testing.T
|
||||
t.Fatalf("revision did not retain SecretStore references: %#v", repository.revision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairingExpirationIsPersistedAndExchangeTokenIsDestroyed(t *testing.T) {
|
||||
repository := &pairingRepositoryFake{exchange: PairingExchange{
|
||||
ID: "pairing", RevisionID: "revision", ExchangeTokenRef: "identity-exchange-pairing",
|
||||
Status: PairingPreparing, RemoteVersion: 2, ExpiresAt: time.Now().Add(-time.Minute), Version: 4,
|
||||
}}
|
||||
secrets := &secretStoreFake{values: map[string][]byte{"identity-exchange-pairing": []byte("temporary-token")}}
|
||||
service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) {
|
||||
t.Fatal("expired exchange must not call the remote service")
|
||||
return nil, nil
|
||||
}, nil)
|
||||
|
||||
expired, err := service.Continue(context.Background(), "pairing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expired.Status != PairingExpired || expired.LastErrorCategory != "exchange_expired" {
|
||||
t.Fatalf("expired pairing was not persisted: %#v", expired)
|
||||
}
|
||||
if _, exists := secrets.values["identity-exchange-pairing"]; exists {
|
||||
t.Fatal("expired exchange token remained in SecretStore")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user