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:
2026-07-17 12:10:44 +08:00
parent a9e23cb237
commit 1fce3a535d
10 changed files with 767 additions and 33 deletions
+36 -6
View File
@@ -18,8 +18,8 @@ type Repository interface {
ActiveIdentityConfigurationRevision(context.Context) (identity.Revision, error)
MarkIdentityRevisionValidated(context.Context, string, int64, string, string) (identity.Revision, error)
MarkIdentityRevisionFailed(context.Context, string, int64, string, string, string) (identity.Revision, error)
ActivateIdentityRevision(context.Context, string, int64) (identity.Revision, bool, error)
DisableActiveIdentityRevision(context.Context, int64) (identity.Revision, error)
ActivateIdentityRevision(context.Context, string, int64, string, string) (identity.Revision, bool, error)
DisableActiveIdentityRevision(context.Context, int64, string, string) (identity.Revision, error)
}
type Builder interface {
@@ -96,7 +96,7 @@ func (manager *Manager) Validate(ctx context.Context, id string, expectedVersion
return manager.repository.MarkIdentityRevisionValidated(ctx, id, expectedVersion, traceID, auditID)
}
func (manager *Manager) Activate(ctx context.Context, id string, expectedVersion int64) (identity.Revision, error) {
func (manager *Manager) Activate(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
manager.operation.Lock()
defer manager.operation.Unlock()
revision, err := manager.repository.IdentityConfigurationRevision(ctx, id)
@@ -110,7 +110,7 @@ func (manager *Manager) Activate(ctx context.Context, id string, expectedVersion
if err != nil {
return identity.Revision{}, err
}
activated, _, err := manager.repository.ActivateIdentityRevision(ctx, id, expectedVersion)
activated, _, err := manager.repository.ActivateIdentityRevision(ctx, id, expectedVersion, traceID, auditID)
if err != nil {
candidate.Close()
return identity.Revision{}, err
@@ -121,10 +121,40 @@ func (manager *Manager) Activate(ctx context.Context, id string, expectedVersion
return activated, nil
}
func (manager *Manager) Disable(ctx context.Context, expectedVersion int64) (identity.Revision, error) {
func (manager *Manager) Rollback(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
manager.operation.Lock()
defer manager.operation.Unlock()
disabled, err := manager.repository.DisableActiveIdentityRevision(ctx, expectedVersion)
revision, err := manager.repository.IdentityConfigurationRevision(ctx, id)
if err != nil {
return identity.Revision{}, err
}
if revision.Version != expectedVersion || revision.State != identity.RevisionSuperseded {
return identity.Revision{}, identity.ErrRevisionConflict
}
candidate, err := manager.builder.Build(ctx, revision)
if err != nil {
return identity.Revision{}, err
}
validated, err := manager.repository.MarkIdentityRevisionValidated(ctx, id, expectedVersion, traceID, auditID)
if err != nil {
candidate.Close()
return identity.Revision{}, err
}
activated, _, err := manager.repository.ActivateIdentityRevision(ctx, id, validated.Version, traceID, auditID)
if err != nil {
candidate.Close()
return identity.Revision{}, err
}
candidate.Revision = activated
old := manager.current.Swap(candidate)
retireRuntime(old)
return activated, nil
}
func (manager *Manager) Disable(ctx context.Context, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
manager.operation.Lock()
defer manager.operation.Unlock()
disabled, err := manager.repository.DisableActiveIdentityRevision(ctx, expectedVersion, traceID, auditID)
if err != nil {
return identity.Revision{}, err
}
@@ -45,7 +45,7 @@ func (f *runtimeRepositoryFake) MarkIdentityRevisionFailed(_ context.Context, id
f.revisions[id] = revision
return revision, nil
}
func (f *runtimeRepositoryFake) ActivateIdentityRevision(_ context.Context, id string, expected int64) (identity.Revision, bool, error) {
func (f *runtimeRepositoryFake) ActivateIdentityRevision(_ context.Context, id string, expected int64, traceID, auditID string) (identity.Revision, bool, error) {
f.activateCalled = true
revision := f.revisions[id]
if revision.Version != expected || revision.State != identity.RevisionValidated {
@@ -56,16 +56,16 @@ func (f *runtimeRepositoryFake) ActivateIdentityRevision(_ context.Context, id s
old.State = identity.RevisionSuperseded
f.revisions[old.ID] = old
}
revision.State, revision.Version = identity.RevisionActive, revision.Version+1
revision.State, revision.Version, revision.LastTraceID, revision.LastAuditID = identity.RevisionActive, revision.Version+1, traceID, auditID
f.active, f.revisions[id] = revision, revision
return revision, true, nil
}
func (f *runtimeRepositoryFake) DisableActiveIdentityRevision(_ context.Context, expected int64) (identity.Revision, error) {
func (f *runtimeRepositoryFake) DisableActiveIdentityRevision(_ context.Context, expected int64, traceID, auditID string) (identity.Revision, error) {
if f.active.Version != expected {
return identity.Revision{}, identity.ErrRevisionConflict
}
disabled := f.active
disabled.State, disabled.Version = identity.RevisionSuperseded, disabled.Version+1
disabled.State, disabled.Version, disabled.LastTraceID, disabled.LastAuditID = identity.RevisionSuperseded, disabled.Version+1, traceID, auditID
f.revisions[disabled.ID] = disabled
f.active = identity.Revision{}
return disabled, nil
@@ -110,7 +110,7 @@ func TestActivationSwapsRuntimeOnlyAfterRepositoryActivation(t *testing.T) {
manager := NewManager(repository, &runtimeBuilderFake{})
manager.current.Store(&Runtime{Revision: active})
activated, err := manager.Activate(context.Background(), candidate.ID, candidate.Version)
activated, err := manager.Activate(context.Background(), candidate.ID, candidate.Version, "trace", "audit")
if err != nil {
t.Fatal(err)
}
@@ -118,3 +118,21 @@ func TestActivationSwapsRuntimeOnlyAfterRepositoryActivation(t *testing.T) {
t.Fatalf("activation order failed: activated=%#v current=%#v", activated, manager.Current())
}
}
func TestRollbackValidatesAndAtomicallyReplacesActiveRuntime(t *testing.T) {
active := identity.Revision{ID: "current", State: identity.RevisionActive, Version: 5}
previous := identity.Revision{ID: "previous", State: identity.RevisionSuperseded, Version: 3}
repository := &runtimeRepositoryFake{
revisions: map[string]identity.Revision{"current": active, "previous": previous}, active: active,
}
manager := NewManager(repository, &runtimeBuilderFake{})
manager.current.Store(&Runtime{Revision: active})
rolledBack, err := manager.Rollback(context.Background(), previous.ID, previous.Version, "trace", "audit")
if err != nil {
t.Fatal(err)
}
if rolledBack.State != identity.RevisionActive || manager.Current().Revision.ID != previous.ID || repository.active.ID != previous.ID {
t.Fatalf("rollback did not replace active runtime: revision=%#v current=%#v", rolledBack, manager.Current())
}
}