152 lines
5.8 KiB
Go
152 lines
5.8 KiB
Go
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)
|
|
}
|