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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user