Scert/backend/internal/deployment/recovery_disposition.go

115 lines
4.3 KiB
Go

package deployment
import (
"context"
"encoding/hex"
"errors"
"strings"
"time"
)
var (
ErrRecoveryDispositionInvalid = errors.New("人工处置记录参数无效")
ErrRecoveryDispositionConflict = errors.New("人工处置请求与已有记录不一致")
ErrRecoveryDispositionBusy = errors.New("该事务已有未释放的人工处置占用")
ErrRecoveryDispositionUnavailable = errors.New("人工处置记录暂不可用,未授权执行远端动作")
)
// RecoveryDispositionIntent 仅保存绑定摘要;不能包含历史证据、凭据或确认令牌原文。
type RecoveryDispositionIntent struct {
ID string
TransactionID string
ServerID string
Operator string
Action RecoveryAction
RequestKey string
RecordDigest string
ServerDigest string
ObservationDigest string
}
type RecoveryDisposition struct {
Intent RecoveryDispositionIntent `json:"-"`
Status string `json:"status"`
ResultCode string `json:"result_code"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
const (
RecoveryResultObserved = "observed"
RecoveryResultCommitted = "committed"
RecoveryResultRolledBack = "rolled_back"
RecoveryResultRejected = "rejected"
RecoveryResultUncertain = "uncertain"
)
// RecoveryDispositionJournal 的开始成功且新建返回值为真,才允许执行器继续核验。
// 记录本身不是远端写入授权;执行器仍需一次性确认、完整互斥及动作专属核验。
type RecoveryDispositionJournal interface {
BeginRecoveryDisposition(context.Context, RecoveryDispositionIntent) (RecoveryDisposition, bool, error)
FinishRecoveryDisposition(context.Context, string, string) (RecoveryDisposition, error)
GetRecoveryDisposition(context.Context, string) (RecoveryDisposition, error)
HasRecoveryClaim(context.Context, string) (bool, error)
}
func (r RecoveryReview) DispositionIntent(id, requestKey string) (RecoveryDispositionIntent, error) {
if r.service == nil || r.service.now == nil || !r.service.now().Before(r.summary.ExpiresAt) {
return RecoveryDispositionIntent{}, ErrRecoveryReviewExpired
}
intent := RecoveryDispositionIntent{
ID: id, TransactionID: r.summary.TransactionID, ServerID: r.summary.ServerID,
Operator: r.operator, Action: r.summary.Action, RequestKey: requestKey,
RecordDigest: hex.EncodeToString(r.recordDigest[:]),
ServerDigest: hex.EncodeToString(r.serverDigest[:]),
ObservationDigest: hex.EncodeToString(r.observationDigest[:]),
}
return intent, intent.Validate()
}
func (i RecoveryDispositionIntent) Validate() error {
if !validInspectionID(i.TransactionID) || !validRecoverySummary(i.ServerID) ||
!validReviewOperator(i.Operator) || !validRecoveryAction(i.Action) {
return ErrRecoveryDispositionInvalid
}
for _, value := range []string{i.ID, i.RequestKey, i.RecordDigest, i.ServerDigest, i.ObservationDigest} {
if len(value) != 64 || value != strings.ToLower(value) || value == strings.Repeat("0", 64) {
return ErrRecoveryDispositionInvalid
}
if _, err := hex.DecodeString(value); err != nil {
return ErrRecoveryDispositionInvalid
}
}
return nil
}
// SameRequest 忽略重试时新生成的记录标识,但必须匹配原请求的全部绑定值。
func (i RecoveryDispositionIntent) SameRequest(other RecoveryDispositionIntent) bool {
i.ID, other.ID = "", ""
return i == other
}
func (i RecoveryDispositionIntent) MatchesRecord(record TransactionRecord) bool {
if record.ID != i.TransactionID || record.ServerID != i.ServerID ||
(record.Status != TransactionStatusPending && record.Status != TransactionStatusRecoveryRequired) ||
(i.Action != RecoveryActionRecord && record.Evidence == nil) {
return false
}
digest, err := reviewDigest(record)
return err == nil && hex.EncodeToString(digest[:]) == i.RecordDigest
}
func (i RecoveryDispositionIntent) AllowsResult(result string) bool {
switch result {
case RecoveryResultRejected, RecoveryResultUncertain:
return validRecoveryAction(i.Action)
case RecoveryResultObserved:
return i.Action == RecoveryActionRecord
case RecoveryResultCommitted:
return i.Action == RecoveryActionCommit
case RecoveryResultRolledBack:
return i.Action == RecoveryActionRollback
}
return false
}