Files
story/internal/onboarding/runtime.go
T

94 lines
4.4 KiB
Go
Raw Normal View History

package onboarding
import (
"data-terminal-cloud/internal/deviceauth"
"encoding/json"
"net/http"
"regexp"
"strings"
)
var runtimePath = regexp.MustCompile(`^/device/v(?:2/conversations|3/agent-sessions)/([0-9a-f-]{36})(?:/(events|heartbeat|stop|activate|turns:batch|upload-batches))?$`)
func (s *Service) runtimeAdmission(r *http.Request, d Device, raw []byte) (bool, error) {
create := r.URL.Path == "/device/v3/agent-sessions"
match := runtimePath.FindStringSubmatch(r.URL.Path)
if r.URL.Path == "/device/v2/conversations" {
return false, deviceauth.ErrDeviceInactive
}
if len(match) > 0 {
var current bool
err := s.pool.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM conversation WHERE public_id=$1::uuid AND device_id=$2::uuid AND onboarding_generation=$3)`, match[1], d.ID, d.Generation).Scan(&current)
if err != nil {
return false, err
}
if !current {
return false, deviceauth.ErrDeviceInactive
}
}
if !create && len(match) == 0 {
parts := strings.Split(r.URL.Path, "/")
if len(parts) >= 5 && (parts[3] == "upload-batches" || parts[3] == "media-segments") {
if !uuidValid(parts[4]) {
return false, deviceauth.ErrCredentialInvalid
}
table := "upload_batch"
if parts[3] == "media-segments" {
table = "media_segment"
}
var current bool
err := s.pool.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM `+table+` m JOIN conversation c ON c.id=m.conversation_id WHERE m.public_id=$1::uuid AND c.device_id=$2::uuid AND c.onboarding_generation=$3)`, parts[4], d.ID, d.Generation).Scan(&current)
if err != nil {
return false, err
}
if !current {
return false, deviceauth.ErrDeviceInactive
}
}
return false, nil
}
var intent, activation, attempt, token string
if create {
var input struct {
Intent string `json:"intent"`
Activation string `json:"activation_id"`
Attempt string `json:"attempt_id"`
Token string `json:"permit_token"`
}
if err := json.Unmarshal(raw, &input); err != nil {
return false, deviceauth.ErrCredentialInvalid
}
intent = input.Intent
activation = input.Activation
attempt = input.Attempt
token = input.Token
}
var ready bool
err := s.pool.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM device_activation_job j JOIN device_binding b ON b.device_id=j.device_id AND b.generation=j.binding_generation AND b.unbound_at IS NULL WHERE j.device_id=$1::uuid AND j.binding_generation=$2 AND j.status='READY' AND j.config_ack_at IS NOT NULL AND j.config_expires_at>now())`, d.ID, d.Generation).Scan(&ready)
if err != nil {
return false, err
}
if ready && intent != "activation_test" {
return false, nil
}
var valid bool
if create {
if intent != "activation_test" || !uuidV4(activation) || !uuidV4(attempt) || (token != "" && !validB64(token, 32)) {
return false, deviceauth.ErrDeviceInactive
}
err = s.pool.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM device_activation_job j JOIN device_activation_attempt a ON a.activation_id=j.id JOIN device_binding b ON b.device_id=j.device_id AND b.generation=j.binding_generation AND b.unbound_at IS NULL WHERE j.device_id=$1::uuid AND j.id=$2::uuid AND a.id=$3::uuid AND j.binding_generation=$4 AND a.binding_generation=$4 AND a.config_version=j.config_version AND j.status IN ('REGISTERED_OBSERVED','CONTACT_POSSIBLE','READY') AND j.config_ack_at IS NOT NULL AND j.engine_ready_at IS NOT NULL AND j.config_expires_at>now() AND a.stage='CONVERSATION_TEST' AND a.executor='DEVICE' AND a.status='CONTACT_POSSIBLE' AND (a.permit_hash IS NULL OR a.permit_hash=$5) AND (a.permit_expires_at>now() OR a.conversation_id IS NOT NULL))`, d.ID, activation, attempt, d.Generation, digest([]byte(token))).Scan(&valid)
} else {
if len(match) > 2 && !oneOf(match[2], "", "events", "heartbeat", "stop") {
return false, deviceauth.ErrDeviceInactive
}
err = s.pool.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM conversation c JOIN device_activation_attempt a ON a.conversation_id=c.id JOIN device_activation_job j ON j.id=a.activation_id WHERE c.public_id=$1::uuid AND c.device_id=$2::uuid AND j.binding_generation=$3 AND a.binding_generation=$3 AND a.config_version=j.config_version AND a.stage='CONVERSATION_TEST' AND a.status IN ('CONTACT_POSSIBLE','SUCCESS_OBSERVED','UNKNOWN'))`, match[1], d.ID, d.Generation).Scan(&valid)
}
if err != nil {
return false, err
}
if !valid {
return false, deviceauth.ErrDeviceInactive
}
return true, nil
}