mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-23 13:31:10 +00:00
ccf929af69
Batch 15 ships the four foundation tickets sitting on top of AZ-675 (gRPC server) and AZ-667 (mapobjects_store hydrate): * AZ-676: telemetry_stream video path (rtsp_forward + bytes_inline) with ai_locked atomic + session counter, SubscribeVideo RPC. * AZ-677: MapObjects snapshot-on-subscribe + diff broadcast + reconnect-resync (StartThen stream-prepend pattern). * AZ-678: HmacOperatorValidator with per-session monotonic seq, in-process session registry + TTL, constant-time HMAC compare, rejection-reason counters, sliding 60 s sig-failure red-health gate. Trait OperatorCommandValidator in shared::contracts::operator_auth. * AZ-679: PoiSurfaceMapper produces OperatorPoiEvent per architecture §7.10; PoiDequeued events on rotate/age-out/complete; pushed via new TelemetrySink::push_operator_event extension on Topic::OperatorEvent. Cross-task wiring: TelemetrySink trait extended with push_operator_event; OperatorBridge gets optional builder methods with_telemetry_sink / with_validator (composition root wires in AZ-680). Workspace deps: hmac = "0.12"; per-crate adds bytes, serde_json, parking_lot, chrono, uuid, sha2, thiserror. Tests: 14/14 ACs verified locally (4 + 3 + 5 + 3 by AC) plus 6 supporting unit tests + 7 integration tests + 2 shared serde roundtrips. cargo clippy clean on touched crates. Cumulative review for batches 13-15 produced; verdict PASS_WITH_WARNINGS (0 Critical, 0 High, 1 Medium, 4 Low — all carry-overs or deferred-producer notes for AZ-680/AZ-684). Co-authored-by: Cursor <cursoragent@cursor.com>
100 lines
3.9 KiB
Rust
100 lines
3.9 KiB
Rust
//! AZ-678 — operator command authentication contract.
|
|
//!
|
|
//! `OperatorCommandValidator` is the boundary every operator-bound
|
|
//! command crosses before any business logic runs. The default
|
|
//! implementation (`HmacOperatorValidator` in
|
|
//! `operator_bridge::internal::auth`) uses HMAC-SHA256 over
|
|
//! `(session_token || sequence_number || payload_bytes)`. The trait
|
|
//! lives here so the dispatch surface (`scan_controller`,
|
|
//! `mission_executor`) can depend on the contract without importing
|
|
//! `operator_bridge`.
|
|
|
|
use thiserror::Error;
|
|
|
|
use crate::models::operator::{OperatorCommand, OperatorCommandKind};
|
|
|
|
/// A command as it arrives over the operator-link, prior to any
|
|
/// authentication. Mirrors the validated `OperatorCommand` shape
|
|
/// closely so a successful `validate` is a near-identity transform.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SignedCommand {
|
|
pub session_token: String,
|
|
pub sequence_number: u64,
|
|
pub kind: OperatorCommandKind,
|
|
pub payload: serde_json::Value,
|
|
/// HMAC over `(session_token || sequence_number || canonical
|
|
/// JSON of payload)`. Length depends on the scheme; for HMAC-SHA256
|
|
/// this is exactly 32 bytes.
|
|
pub signature: Vec<u8>,
|
|
/// Wall-clock time the Ground Station stamped the command. Carried
|
|
/// through `validate` for downstream audit logging; not used by
|
|
/// the auth check itself.
|
|
pub issued_at_wallclock: chrono::DateTime<chrono::Utc>,
|
|
pub command_id: uuid::Uuid,
|
|
}
|
|
|
|
impl SignedCommand {
|
|
/// Convert into a canonical [`OperatorCommand`] once validation
|
|
/// has succeeded. The signature is retained on the result for
|
|
/// downstream audit logging.
|
|
pub fn into_command(self) -> OperatorCommand {
|
|
OperatorCommand {
|
|
command_id: self.command_id,
|
|
session_token: self.session_token,
|
|
sequence_number: self.sequence_number,
|
|
issued_at_wallclock: self.issued_at_wallclock,
|
|
kind: self.kind,
|
|
payload: self.payload,
|
|
signature: self.signature,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Validated command. Returned by `OperatorCommandValidator::validate`
|
|
/// on the happy path. Holding a `ValidatedCommand` is the proof that
|
|
/// dispatching the inner command is safe.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ValidatedCommand {
|
|
pub command: OperatorCommand,
|
|
}
|
|
|
|
/// Why an operator command was rejected. Each variant maps 1-1 to a
|
|
/// `auth_rejections_total{reason}` metric counter and to a structured
|
|
/// log line. Order MUST match
|
|
/// `operator_bridge::internal::auth::REJECTION_REASONS` for the
|
|
/// counter array layout.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
|
pub enum AuthError {
|
|
#[error("signature does not match computed HMAC")]
|
|
SignatureInvalid,
|
|
#[error("replay detected — sequence number not greater than last seen")]
|
|
ReplayDetected,
|
|
#[error("session token unknown or never established")]
|
|
SessionUnknown,
|
|
#[error("session token expired (TTL elapsed)")]
|
|
SessionExpired,
|
|
}
|
|
|
|
impl AuthError {
|
|
/// Stable kebab-case label for the rejection-reason metric.
|
|
pub fn reason_label(&self) -> &'static str {
|
|
match self {
|
|
Self::SignatureInvalid => "signature_invalid",
|
|
Self::ReplayDetected => "replay_detected",
|
|
Self::SessionUnknown => "session_unknown",
|
|
Self::SessionExpired => "session_expired",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Contract every operator-command validator must satisfy. The
|
|
/// default `HmacOperatorValidator` lives in `operator_bridge`; other
|
|
/// schemes (e.g. Q9 resolution to a JWS-based one) implement the
|
|
/// same trait and can be swapped behind the same callsite.
|
|
pub trait OperatorCommandValidator: Send + Sync {
|
|
/// Validate one signed command. On `Ok`, the per-session
|
|
/// sequence-number tracker advances; on `Err`, it does NOT
|
|
/// advance (so the rejected `seq` does not poison the session).
|
|
fn validate(&self, cmd: SignedCommand) -> Result<ValidatedCommand, AuthError>;
|
|
}
|