package management import ( "context" "database/sql" "encoding/json" "errors" "net/http" "strings" "time" "github.com/gofiber/fiber/v3" ) var requiredVerificationChecks = []string{"transport", "registration_auth", "caller_id_rules", "codec", "capacity", "whitelist"} func (s *Store) AddVerification(ctx context.Context, trunkID string, revision int64, input VerificationInput, actor, requestID string) error { if !validID(trunkID) || revision < 1 || !containsString(requiredVerificationChecks, input.CheckName) { return newAppError(422, "INVALID_VERIFICATION", "check_name or revision is invalid", nil) } if input.Result != "confirmed" && input.Result != "failed" && input.Result != "unknown" && input.Result != "not_applicable" { return newAppError(422, "INVALID_VERIFICATION_RESULT", "result is invalid", nil) } if input.CheckedBy == "" { input.CheckedBy = actor } if strings.ContainsAny(input.EvidenceRef+input.CheckedBy, "\r\n") || len(input.EvidenceRef) > 512 { return newAppError(422, "INVALID_VERIFICATION_EVIDENCE", "verification evidence is invalid", nil) } now := utcString(time.Now()) return s.withTx(ctx, func(tx *sql.Tx) error { var exists int if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM trunk_versions WHERE trunk_id=? AND revision=?", trunkID, revision).Scan(&exists); err != nil { return err } if exists == 0 { return newAppError(404, "VERSION_NOT_FOUND", "trunk version does not exist", nil) } _, err := tx.ExecContext(ctx, "INSERT INTO trunk_verifications(trunk_id,revision,check_name,result,evidence_ref,checked_by,checked_at) VALUES(?,?,?,?,?,?,?) ON CONFLICT(trunk_id,revision,check_name) DO UPDATE SET result=excluded.result,evidence_ref=excluded.evidence_ref,checked_by=excluded.checked_by,checked_at=excluded.checked_at", trunkID, revision, input.CheckName, input.Result, input.EvidenceRef, input.CheckedBy, now) if err != nil { return err } details, _ := json.Marshal(map[string]any{"check_name": input.CheckName, "result": input.Result, "evidence_ref": input.EvidenceRef}) return insertAudit(ctx, tx, "trunk", trunkID, "verification", revision, actor, requestID, string(details), now) }) } func (s *Store) RealVerificationComplete(ctx context.Context, trunkID string, revision int64) (bool, []string, error) { results := map[string]string{} err := s.withRead(ctx, func(db *sql.DB) error { rows, err := db.QueryContext(ctx, "SELECT check_name,result FROM trunk_verifications WHERE trunk_id=? AND revision=?", trunkID, revision) if err != nil { return err } defer rows.Close() for rows.Next() { var name, result string if err := rows.Scan(&name, &result); err != nil { return err } results[name] = result } return rows.Err() }) if err != nil { return false, nil, err } missing := make([]string, 0) for _, name := range requiredVerificationChecks { if results[name] != "confirmed" { missing = append(missing, name) } } return len(missing) == 0, missing, nil } func (s *Store) ListVerifications(ctx context.Context, trunkID string, revision int64) ([]map[string]any, error) { var out []map[string]any err := s.withRead(ctx, func(db *sql.DB) error { rows, err := db.QueryContext(ctx, "SELECT check_name,result,evidence_ref,checked_by,checked_at FROM trunk_verifications WHERE trunk_id=? AND revision=? ORDER BY check_name", trunkID, revision) if err != nil { return err } defer rows.Close() for rows.Next() { var name, result, evidence, by, at string if err := rows.Scan(&name, &result, &evidence, &by, &at); err != nil { return err } out = append(out, map[string]any{"check_name": name, "result": result, "evidence_ref": evidence, "checked_by": by, "checked_at": at}) } return rows.Err() }) return out, err } func (s *Server) addVerification(c fiber.Ctx) error { trunkID := c.Params("trunk_id") p, authErr := s.authorize(c, "admin", "sip.trunk.write", "trunk", trunkID) if authErr != nil { return s.fail(c, authErr) } expected, err := parseIfMatch(c) if err != nil { return s.fail(c, err) } requestID, err := requireHeader(c, "X-Request-ID") if err != nil { return s.fail(c, err) } var body struct { Revision int64 `json:"revision"` VerificationInput } if err := decodeJSON(c, &body); err != nil { return s.fail(c, err) } if body.Revision < 1 { return s.fail(c, newAppError(422, "REVISION_REQUIRED", "revision is required", nil)) } ctx, cancel := s.context(c) defer cancel() trunk, trunkErr := s.store.GetTrunk(ctx, trunkID, s.cfg.PublicMode()) if errors.Is(trunkErr, sql.ErrNoRows) { return s.fail(c, newAppError(404, "TRUNK_NOT_FOUND", "trunk does not exist", nil)) } if trunkErr != nil { return s.fail(c, asAppError(trunkErr)) } if trunk.LatestRevision != expected || body.Revision > trunk.LatestRevision { return s.fail(c, newAppError(409, "REVISION_CONFLICT", "verification must target the current or an existing revision", map[string]any{"current_revision": trunk.LatestRevision})) } op, replay, beginErr := s.beginOperation(ctx, p, "trunk.verification", "trunk", trunkID, requestID, body, expected) if replay || beginErr != nil { return s.replayOrError(c, op, beginErr) } if err := s.store.AddVerification(ctx, trunkID, body.Revision, body.VerificationInput, p.Principal, requestID); err != nil { appErr := asAppError(err) _ = s.store.FinishOperation(ctx, op.OperationID, "failed", appErr.Status, "", errorJSON(appErr, requestID)) return s.fail(c, appErr) } checks, checksErr := s.store.ListVerifications(ctx, trunkID, body.Revision) if checksErr != nil { return s.fail(c, asAppError(checksErr)) } response := map[string]any{"mode": s.cfg.PublicMode(), "trunk_id": trunkID, "revision": body.Revision, "verifications": checks} if err := s.finishSuccess(ctx, op, http.StatusOK, response); err != nil { return s.fail(c, asAppError(err)) } return c.JSON(response) }