fix(identity): 支持生效配置安全重验证
Active Revision 重验证成功后更新验证元数据并原子替换同配置 Runtime;验证失败时不修改数据库状态或当前运行时。\n\n验证:go test ./apps/api/internal/identityruntime ./apps/api/internal/store ./apps/api/internal/httpapi
This commit is contained in:
@@ -17,6 +17,7 @@ type Repository interface {
|
|||||||
IdentityConfigurationRevision(context.Context, string) (identity.Revision, error)
|
IdentityConfigurationRevision(context.Context, string) (identity.Revision, error)
|
||||||
ActiveIdentityConfigurationRevision(context.Context) (identity.Revision, error)
|
ActiveIdentityConfigurationRevision(context.Context) (identity.Revision, error)
|
||||||
MarkIdentityRevisionValidated(context.Context, string, int64, string, string) (identity.Revision, error)
|
MarkIdentityRevisionValidated(context.Context, string, int64, string, string) (identity.Revision, error)
|
||||||
|
RevalidateActiveIdentityRevision(context.Context, string, int64, string, string) (identity.Revision, error)
|
||||||
MarkIdentityRevisionFailed(context.Context, string, int64, string, string, string) (identity.Revision, error)
|
MarkIdentityRevisionFailed(context.Context, string, int64, string, string, string) (identity.Revision, error)
|
||||||
ActivateIdentityRevision(context.Context, string, int64, string, string) (identity.Revision, bool, error)
|
ActivateIdentityRevision(context.Context, string, int64, string, string) (identity.Revision, bool, error)
|
||||||
DisableActiveIdentityRevision(context.Context, int64, string, string) (identity.Revision, error)
|
DisableActiveIdentityRevision(context.Context, int64, string, string) (identity.Revision, error)
|
||||||
@@ -84,14 +85,27 @@ func (manager *Manager) Validate(ctx context.Context, id string, expectedVersion
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return identity.Revision{}, err
|
return identity.Revision{}, err
|
||||||
}
|
}
|
||||||
if revision.Version != expectedVersion || revision.State != identity.RevisionDraft && revision.State != identity.RevisionSuperseded {
|
if revision.Version != expectedVersion || revision.State != identity.RevisionDraft && revision.State != identity.RevisionSuperseded && revision.State != identity.RevisionActive {
|
||||||
return identity.Revision{}, identity.ErrRevisionConflict
|
return identity.Revision{}, identity.ErrRevisionConflict
|
||||||
}
|
}
|
||||||
candidate, buildErr := manager.builder.Build(ctx, revision)
|
candidate, buildErr := manager.builder.Build(ctx, revision)
|
||||||
if buildErr != nil {
|
if buildErr != nil {
|
||||||
_, _ = manager.repository.MarkIdentityRevisionFailed(ctx, id, expectedVersion, "validation_failed", traceID, auditID)
|
if revision.State != identity.RevisionActive {
|
||||||
|
_, _ = manager.repository.MarkIdentityRevisionFailed(ctx, id, expectedVersion, "validation_failed", traceID, auditID)
|
||||||
|
}
|
||||||
return identity.Revision{}, buildErr
|
return identity.Revision{}, buildErr
|
||||||
}
|
}
|
||||||
|
if revision.State == identity.RevisionActive {
|
||||||
|
revalidated, err := manager.repository.RevalidateActiveIdentityRevision(ctx, id, expectedVersion, traceID, auditID)
|
||||||
|
if err != nil {
|
||||||
|
candidate.Close()
|
||||||
|
return identity.Revision{}, err
|
||||||
|
}
|
||||||
|
candidate.Revision = revalidated
|
||||||
|
old := manager.current.Swap(candidate)
|
||||||
|
retireRuntime(old)
|
||||||
|
return revalidated, nil
|
||||||
|
}
|
||||||
candidate.Close()
|
candidate.Close()
|
||||||
return manager.repository.MarkIdentityRevisionValidated(ctx, id, expectedVersion, traceID, auditID)
|
return manager.repository.MarkIdentityRevisionValidated(ctx, id, expectedVersion, traceID, auditID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ func (f *runtimeRepositoryFake) MarkIdentityRevisionValidated(_ context.Context,
|
|||||||
f.revisions[id] = revision
|
f.revisions[id] = revision
|
||||||
return revision, nil
|
return revision, nil
|
||||||
}
|
}
|
||||||
|
func (f *runtimeRepositoryFake) RevalidateActiveIdentityRevision(_ context.Context, id string, expected int64, traceID, auditID string) (identity.Revision, error) {
|
||||||
|
revision := f.revisions[id]
|
||||||
|
if revision.Version != expected || revision.State != identity.RevisionActive {
|
||||||
|
return identity.Revision{}, identity.ErrRevisionConflict
|
||||||
|
}
|
||||||
|
revision.Version, revision.LastTraceID, revision.LastAuditID = revision.Version+1, traceID, auditID
|
||||||
|
f.revisions[id], f.active = revision, revision
|
||||||
|
return revision, nil
|
||||||
|
}
|
||||||
func (f *runtimeRepositoryFake) MarkIdentityRevisionFailed(_ context.Context, id string, expected int64, category, _, _ string) (identity.Revision, error) {
|
func (f *runtimeRepositoryFake) MarkIdentityRevisionFailed(_ context.Context, id string, expected int64, category, _, _ string) (identity.Revision, error) {
|
||||||
revision := f.revisions[id]
|
revision := f.revisions[id]
|
||||||
if revision.Version != expected {
|
if revision.Version != expected {
|
||||||
@@ -136,3 +145,20 @@ func TestRollbackValidatesAndAtomicallyReplacesActiveRuntime(t *testing.T) {
|
|||||||
t.Fatalf("rollback did not replace active runtime: revision=%#v current=%#v", rolledBack, manager.Current())
|
t.Fatalf("rollback did not replace active runtime: revision=%#v current=%#v", rolledBack, manager.Current())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActiveRevalidationSwapsOnlyAfterSuccessfulValidation(t *testing.T) {
|
||||||
|
active := identity.Revision{ID: "active", State: identity.RevisionActive, Version: 4}
|
||||||
|
repository := &runtimeRepositoryFake{revisions: map[string]identity.Revision{"active": active}, active: active}
|
||||||
|
builder := &runtimeBuilderFake{}
|
||||||
|
manager := NewManager(repository, builder)
|
||||||
|
original := &Runtime{Revision: active}
|
||||||
|
manager.current.Store(original)
|
||||||
|
|
||||||
|
revalidated, err := manager.Validate(context.Background(), active.ID, active.Version, "trace-new", "audit-new")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if revalidated.State != identity.RevisionActive || revalidated.Version != 5 || manager.Current() == original || manager.Current().Revision.LastAuditID != "audit-new" {
|
||||||
|
t.Fatalf("active runtime was not safely revalidated: %#v", revalidated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,6 +153,18 @@ RETURNING `+identityRevisionColumns, id, expectedVersion, traceID, auditID))
|
|||||||
return revision, err
|
return revision, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) RevalidateActiveIdentityRevision(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
|
||||||
|
revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `
|
||||||
|
UPDATE gateway_identity_configuration_revisions SET validated_at=now(),last_error_category=NULL,
|
||||||
|
last_trace_id=NULLIF($3,''),last_audit_id=NULLIF($4,''),version=version+1,updated_at=now()
|
||||||
|
WHERE id=$1::uuid AND version=$2 AND state='active'
|
||||||
|
RETURNING `+identityRevisionColumns, id, expectedVersion, traceID, auditID))
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return identity.Revision{}, identity.ErrRevisionConflict
|
||||||
|
}
|
||||||
|
return revision, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) MarkIdentityRevisionFailed(ctx context.Context, id string, expectedVersion int64, category, traceID, auditID string) (identity.Revision, error) {
|
func (s *Store) MarkIdentityRevisionFailed(ctx context.Context, id string, expectedVersion int64, category, traceID, auditID string) (identity.Revision, error) {
|
||||||
revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `
|
revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `
|
||||||
UPDATE gateway_identity_configuration_revisions SET state='failed',last_error_category=NULLIF($3,''),
|
UPDATE gateway_identity_configuration_revisions SET state='failed',last_error_category=NULLIF($3,''),
|
||||||
|
|||||||
Reference in New Issue
Block a user