96 lines
3.2 KiB
Go
96 lines
3.2 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
type PendingView struct {
|
|
ID string `json:"candidateId"`
|
|
Generation uint64 `json:"generation"`
|
|
Section Section `json:"section"`
|
|
SameSession bool `json:"sameSession"`
|
|
Committed bool `json:"committed"`
|
|
Candidate RuntimeConfig `json:"-"`
|
|
}
|
|
|
|
// Pending 返回独立快照,不向调用者泄露候选操作者的会话标识。
|
|
func (m *ConfigManager) Pending(actor AuditActor) *PendingView {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if m.pending == nil {
|
|
return nil
|
|
}
|
|
return &PendingView{
|
|
ID: m.pending.ID,
|
|
Generation: m.pending.Candidate.Generation, Section: m.pending.Section,
|
|
SameSession: actor.UserID != "" && actor.UserID == m.pending.Actor.UserID && actor.SessionID != "" && actor.SessionID == m.pending.Actor.SessionID,
|
|
Committed: m.pending.Committed, Candidate: cloneRuntimeConfig(m.pending.Candidate),
|
|
}
|
|
}
|
|
|
|
// ManagePending 显式恢复或取消指定代次;跨会话必须重新验证近期身份并确认操作。
|
|
func (m *ConfigManager) ManagePending(ctx context.Context, generation uint64, id string, actor AuditActor, confirmation StepUpConfirmation, cancel bool) error {
|
|
if err := contextError(ctx); err != nil {
|
|
return err
|
|
}
|
|
if m == nil || actor.UserID == "" || actor.SessionID == "" {
|
|
return ErrAuthorizationRequired
|
|
}
|
|
m.writeMu.Lock()
|
|
defer m.writeMu.Unlock()
|
|
m.mu.RLock()
|
|
pending := m.pending
|
|
current := m.current.Generation
|
|
m.mu.RUnlock()
|
|
if pending == nil || id == "" || pending.ID != id || pending.Candidate.Generation != generation {
|
|
return ErrGenerationConflict
|
|
}
|
|
if err := m.authorize(ctx, actor, ConfigOperationUpdate, pending.Section); err != nil {
|
|
return err
|
|
}
|
|
sameSession := actor.UserID == pending.Actor.UserID && actor.SessionID == pending.Actor.SessionID
|
|
systemRecovery := pending.Committed && pending.Actor.UserID == "system" && actor.Role == "super_admin"
|
|
if actor.UserID != pending.Actor.UserID && !systemRecovery && (!cancel || actor.Role != "super_admin") {
|
|
return ErrAuthorizationRequired
|
|
}
|
|
if !sameSession && (!actor.RecentAuth || !validStepUpConfirmation(confirmation)) {
|
|
return ErrStepUpRequired
|
|
}
|
|
if err := m.confirm(ctx, actor, confirmation, ConfigOperationUpdate, pending.Section); err != nil {
|
|
return err
|
|
}
|
|
if cancel && pending.Committed {
|
|
return ErrRuntimeSnapshotUnavailable
|
|
}
|
|
field := "pending.resume"
|
|
if cancel {
|
|
field = "pending.cancel"
|
|
}
|
|
if err := m.writeAudit(ctx, ConfigAuditEvent{
|
|
Operation: ConfigOperationUpdate, Section: pending.Section, ActorID: actor.UserID, ActorRole: actor.Role,
|
|
FromGeneration: current, ToGeneration: current, ChangedFields: []string{field},
|
|
ConfirmationMethod: confirmation.Method, Result: "success", CreatedAt: time.Now().UTC(),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
m.mu.Lock()
|
|
activeSSL := m.current.SSL.TLS
|
|
if cancel {
|
|
m.pending = nil
|
|
m.status.PendingRestart = append([]ServiceName(nil), pending.PreviousRestart...)
|
|
} else {
|
|
m.pending.Actor = actor
|
|
m.pending.Confirmation = confirmation
|
|
}
|
|
m.mu.Unlock()
|
|
if cancel {
|
|
releaseManagedSSL(m.loader.root, pending.Candidate.SSL.TLS)
|
|
maintainManagedSSL(m.loader.root, activeSSL)
|
|
}
|
|
return nil
|
|
}
|