Files
elmprodvpn/selective-vpn-api/app/transport_handlers_policy_mutations_validate.go

123 lines
4.3 KiB
Go

package app
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
func handleTransportPoliciesValidateExec(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body TransportPolicyValidateRequest
if r.Body != nil {
defer r.Body.Close()
if err := json.NewDecoder(io.LimitReader(r.Body, 2<<20)).Decode(&body); err != nil && err != io.EOF {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
}
transportMu.Lock()
clientsState := loadTransportClientsState()
current := loadTransportPolicyState()
locks := TransportOwnerLockState{}
if transportPolicyKernelConntrackStickyEnabled() {
locks = loadTransportOwnerLocksState()
}
transportMu.Unlock()
clients := transportPolicyClientsWithVirtualTargets(clientsState.Items)
result := validateTransportPolicy(body.Intents, current.Intents, clients)
ownerSwitchConflicts := detectTransportOwnerSwitchConflicts(current.Intents, result.Normalized)
if len(ownerSwitchConflicts) > 0 {
result.Conflicts = append(result.Conflicts, ownerSwitchConflicts...)
result.Conflicts = dedupeTransportConflicts(result.Conflicts)
result.Summary = summarizeTransportConflicts(result.Conflicts)
result.Valid = result.Summary.BlockCount == 0
}
ownerLockConflicts := detectTransportOwnerLockConflicts(current.Intents, result.Normalized, clients)
if len(ownerLockConflicts) > 0 {
result.Conflicts = append(result.Conflicts, ownerLockConflicts...)
result.Conflicts = dedupeTransportConflicts(result.Conflicts)
result.Summary = summarizeTransportConflicts(result.Conflicts)
result.Valid = result.Summary.BlockCount == 0
}
destinationLockConflicts := detectTransportDestinationLockConflicts(current.Intents, result.Normalized, locks)
if len(destinationLockConflicts) > 0 {
result.Conflicts = append(result.Conflicts, destinationLockConflicts...)
result.Conflicts = dedupeTransportConflicts(result.Conflicts)
result.Summary = summarizeTransportConflicts(result.Conflicts)
result.Valid = result.Summary.BlockCount == 0
}
if body.BaseRevision > 0 && body.BaseRevision != current.Revision {
result.Conflicts = append(result.Conflicts, TransportConflictRecord{
Key: "base_revision",
Type: "stale_base",
Severity: "warn",
Reason: fmt.Sprintf("base_revision=%d differs from current=%d", body.BaseRevision, current.Revision),
SuggestedResolution: "refresh policy and retry validate/apply",
})
result.Summary.WarnCount++
result.Conflicts = dedupeTransportConflicts(result.Conflicts)
result.Summary = summarizeTransportConflicts(result.Conflicts)
result.Valid = result.Summary.BlockCount == 0
}
plan, compileConflicts := compileTransportPolicyPlan(result.Normalized, clients, current.Revision)
if len(compileConflicts) > 0 {
result.Conflicts = append(result.Conflicts, compileConflicts...)
result.Conflicts = dedupeTransportConflicts(result.Conflicts)
result.Summary = summarizeTransportConflicts(result.Conflicts)
result.Valid = result.Summary.BlockCount == 0
}
digest := digestTransportIntents(result.Normalized)
confirmToken := issueTransportConfirmToken(current.Revision, digest)
state := TransportConflictState{
Version: transportStateVersion,
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
HasBlocking: result.Summary.BlockCount > 0,
Items: append([]TransportConflictRecord(nil), result.Conflicts...),
}
transportMu.Lock()
_ = saveTransportConflictsState(state)
transportMu.Unlock()
msg := "validation complete"
code := ""
if result.Summary.BlockCount > 0 {
msg = "policy has blocking conflicts"
code = "POLICY_CONFLICT_BLOCK"
}
events.push("transport_policy_validated", map[string]any{
"valid": result.Valid,
"block_count": result.Summary.BlockCount,
"warn_count": result.Summary.WarnCount,
})
if result.Summary.BlockCount > 0 {
events.push("transport_conflict_detected", map[string]any{
"count": result.Summary.BlockCount,
})
}
writeJSON(w, http.StatusOK, TransportPolicyValidateResponse{
OK: true,
Message: msg,
Code: code,
Valid: result.Valid,
BaseRevision: current.Revision,
ConfirmToken: confirmToken,
Summary: result.Summary,
Conflicts: result.Conflicts,
Diff: result.Diff,
Plan: &plan,
})
}