fix(identity): 完善统一认证配对恢复与安全退役

修复 credentials_saved 状态无法恢复、配对与激活并发冲突,以及 SSF 和身份 Secret 生命周期不完整的问题。新增持久化协调器、取消与清理状态机、事务级并发门禁、受控 SSF 凭据交接、禁用后的延迟 Secret 清理,并对生产环境统一认证及 Discovery 端点强制 HTTPS。

验证:go test ./...;go test -race ./internal/auth ./internal/identity ./internal/identityruntime ./internal/securityevents ./internal/httpapi ./internal/store -count=1;go vet ./...;真实 PostgreSQL 并发及清理成功/冲突回滚测试;pnpm openapi。
This commit is contained in:
2026-07-17 18:31:12 +08:00
parent cdfca61304
commit a312ad880d
55 changed files with 9225 additions and 419 deletions
+63 -16
View File
@@ -11,10 +11,13 @@ import (
)
var (
ErrRevisionNotFound = errors.New("identity configuration revision not found")
ErrRevisionConflict = errors.New("identity configuration revision conflicts with current state")
ErrBreakGlassRequired = errors.New("a local break-glass manager credential is required")
ErrLocalTenantInvalid = errors.New("local tenant mapping is invalid")
ErrRevisionNotFound = errors.New("identity configuration revision not found")
ErrRevisionConflict = errors.New("identity configuration revision conflicts with current state")
ErrBreakGlassRequired = errors.New("a local break-glass manager credential is required")
ErrLocalTenantInvalid = errors.New("local tenant mapping is invalid")
ErrActiveConfigurationHandoffRequired = errors.New("active identity configuration requires an explicit remote resource handoff before re-pairing")
ErrRollbackConfigurationHandoffRequired = errors.New("rollback requires a fresh remote resource handoff")
ErrSecurityEventRetirementPending = errors.New("active security event connection must retire before identity can be disabled")
)
type RevisionPolicy struct {
@@ -111,15 +114,16 @@ type ManifestApplication struct {
SessionEncryptionKeyRef string
TraceID string
AuditID string
AppEnv string
}
func NewDraft(input PairingInput) (Revision, error) {
if _, err := input.ConsumerMetadata(false); err != nil {
func NewDraft(input PairingInput, appEnv string) (Revision, error) {
if _, err := input.ConsumerMetadata(false, appEnv); err != nil {
return Revision{}, err
}
authCenter, _ := exactBaseURL(input.AuthCenterURL)
publicBase, _ := exactBaseURL(input.PublicBaseURL)
webBase, _ := exactBaseURL(input.WebBaseURL)
authCenter, _ := exactBaseURL(input.AuthCenterURL, appEnv)
publicBase, _ := exactBaseURL(input.PublicBaseURL, appEnv)
webBase, _ := exactBaseURL(input.WebBaseURL, appEnv)
return Revision{
ID: uuid.NewString(), State: RevisionDraft, SchemaVersion: 1,
AuthCenterURL: authCenter, RolePrefix: "gateway.", LocalTenantKey: strings.TrimSpace(input.LocalTenantKey),
@@ -133,7 +137,7 @@ func ApplyManifest(revision Revision, input ManifestApplication) (Revision, erro
if revision.State != RevisionDraft {
return Revision{}, ErrRevisionConflict
}
if err := input.Manifest.Validate(); err != nil {
if err := input.Manifest.Validate(input.AppEnv); err != nil {
return Revision{}, err
}
capabilities := make(map[string]bool, len(input.Manifest.Capabilities))
@@ -187,17 +191,17 @@ func CanTransition(from, to RevisionState) bool {
}
}
func (input PairingInput) ConsumerMetadata(sessionRevocation bool) (ConsumerMetadata, error) {
authCenter, err := exactBaseURL(input.AuthCenterURL)
func (input PairingInput) ConsumerMetadata(sessionRevocation bool, appEnv string) (ConsumerMetadata, error) {
authCenter, err := exactBaseURL(input.AuthCenterURL, appEnv)
if err != nil {
return ConsumerMetadata{}, errors.New("auth center URL is invalid")
}
_ = authCenter
publicBase, err := exactBaseURL(input.PublicBaseURL)
publicBase, err := exactBaseURL(input.PublicBaseURL, appEnv)
if err != nil {
return ConsumerMetadata{}, errors.New("public base URL is invalid")
}
webBase, err := exactBaseURL(input.WebBaseURL)
webBase, err := exactBaseURL(input.WebBaseURL, appEnv)
if err != nil {
return ConsumerMetadata{}, errors.New("web base URL is invalid")
}
@@ -215,7 +219,7 @@ func (input PairingInput) ConsumerMetadata(sessionRevocation bool) (ConsumerMeta
return metadata, nil
}
func exactBaseURL(raw string) (string, error) {
func exactBaseURL(raw, appEnv string) (string, error) {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil || parsed.Opaque != "" || parsed.User != nil || parsed.Host == "" || parsed.RawQuery != "" || parsed.Fragment != "" {
return "", errors.New("invalid public URL")
@@ -225,7 +229,7 @@ func exactBaseURL(raw string) (string, error) {
return "", errors.New("base URL must not contain a path")
}
scheme := strings.ToLower(parsed.Scheme)
if scheme != "https" && !(scheme == "http" && isLoopbackHost(hostname)) {
if scheme != "https" && !(scheme == "http" && isLocalIdentityEnvironment(appEnv) && isLoopbackHost(hostname)) {
return "", errors.New("public URL must use HTTPS")
}
port := parsed.Port()
@@ -242,6 +246,49 @@ func exactBaseURL(raw string) (string, error) {
return scheme + "://" + host, nil
}
func isLocalIdentityEnvironment(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "development", "dev", "local", "test":
return true
default:
return false
}
}
// ValidateRevisionURLs re-applies the deployment environment URL policy when
// constructing a Runtime. This protects against legacy or directly persisted
// revisions bypassing the pairing boundary.
func ValidateRevisionURLs(revision Revision, appEnv string) error {
urls := []struct {
label string
raw string
base bool
}{
{label: "auth center", raw: revision.AuthCenterURL, base: true},
{label: "issuer", raw: revision.Issuer},
{label: "public base", raw: revision.PublicBaseURL, base: true},
{label: "web base", raw: revision.WebBaseURL, base: true},
}
for _, candidate := range urls {
var err error
if candidate.base {
_, err = exactBaseURL(candidate.raw, appEnv)
} else {
err = validatePublicIdentityURL(candidate.raw, appEnv)
}
if err != nil {
return errors.New("identity " + candidate.label + " URL must use HTTPS in this environment")
}
}
if revision.SessionRevocation || revision.SecurityEventIssuer != "" || revision.SecurityEventConfigURL != "" {
if validatePublicIdentityURL(revision.SecurityEventIssuer, appEnv) != nil ||
validatePublicIdentityURL(revision.SecurityEventConfigURL, appEnv) != nil {
return errors.New("identity security event URLs must use HTTPS in this environment")
}
}
return nil
}
func isLoopbackHost(host string) bool {
if host == "localhost" {
return true