mirror of
https://github.com/ruvnet/RuView
synced 2026-08-07 20:01:43 +00:00
c2bd33e649
Wires `ruview-auth` into `bearer_auth.rs`. `RUVIEW_OAUTH_ISSUER` enables it;
unset, nothing changes.
Layering, in order:
1. `RUVIEW_API_TOKEN` set and the bearer matches exactly -> allow. Byte-for-
byte today's behaviour.
2. Otherwise, if OAuth is configured, verify the bearer as a Cognitum access
token and require the scope the route needs.
3. Otherwise 401.
The static compare goes first for compatibility, not security: a matching
static token is not a JWT and a JWT never matches the static token. It means an
existing deployment behaves identically even with OAuth switched on.
Scope gate (`required_scope_for`), split by blast radius per ADR-060 —
"can this destroy something", not how many routes it covers:
sensing:admin /api/v1/train/* (hours of Pi CPU, writes models)
DELETE /api/v1/models/{id} (irreversible)
DELETE /api/v1/recording/{id} (irreversible)
sensing:read everything else
Deliberately NOT admin: model load/unload and recording start. They mutate
server state but destroy nothing, and gating them would push routine dashboard
use into requesting delete capability — the opposite of least privilege.
The legacy static token stays un-scope-gated. It predates scopes and carries no
claims, so narrowing it would be a silent breaking change to deployments using
it; migrating to OAuth is how an operator opts into the finer split.
FAIL CLOSED at boot. If OAuth is requested but cannot work — empty issuer, or a
JWKS we cannot fetch — the server logs why and exits rather than serving.
Starting anyway would silently downgrade an operator who asked for OAuth to
either an open API or a shared-secret one, with no signal it happened. The JWKS
is warmed eagerly for the same reason: a bad `jwks_uri` should die at boot with
a legible message, not surface as a puzzling 401 an hour later.
The verified `Principal` is attached to request extensions, so handlers and
audit logs can attribute a request (`sub`, `account_id`, `org_id`,
`workspace_id`, `jti`) instead of knowing only "someone had the secret". That
is the point of moving off a shared bearer.
Verification failures are logged with the reason and returned as a flat 401 —
the reason is useful to an operator and equally useful to an attacker probing
for which claim to forge next.
Also aligns `ruview_auth::extract_bearer` to match the scheme
case-insensitively (RFC 7235 §2.1). The sensing server has always done this
deliberately, with a comment saying why; the two layers disagreeing about what
a valid header looks like would be a latent bug.
Tests: 16 new in `bearer_auth::oauth_tests`, driving a real Router end to end
(request -> middleware -> verifier -> handler) with ES256 tokens signed by a
runtime-generated key. Covers the scope policy as a pure function, read-scoped
tokens refused on delete and train, admin-scoped tokens allowed, an
`inference`-only token from another Cognitum product refused on every route,
garbage and absent bearers, both legacy-token layering directions, the
principal reaching a handler, and the unset case remaining a no-op.
`cargo test -p wifi-densepose-sensing-server --lib --no-default-features`:
501 passed, 0 failed. `ruview-auth`: 43 passed across both feature configs.
Co-Authored-By: Ruflo & AQE
137 lines
6.2 KiB
TOML
137 lines
6.2 KiB
TOML
[package]
|
|
name = "wifi-densepose-sensing-server"
|
|
version = "0.3.4"
|
|
edition.workspace = true
|
|
description = "Lightweight Axum server for WiFi sensing UI with RuVector signal processing"
|
|
license.workspace = true
|
|
authors = ["rUv <ruv@ruv.net>", "WiFi-DensePose Contributors"]
|
|
repository.workspace = true
|
|
documentation = "https://docs.rs/wifi-densepose-sensing-server"
|
|
keywords = ["wifi", "sensing", "server", "websocket", "csi"]
|
|
categories = ["web-programming::http-server", "science"]
|
|
readme = "README.md"
|
|
|
|
[lib]
|
|
name = "wifi_densepose_sensing_server"
|
|
path = "src/lib.rs"
|
|
|
|
[[bin]]
|
|
name = "sensing-server"
|
|
path = "src/main.rs"
|
|
|
|
[dependencies]
|
|
# Web framework
|
|
axum = { workspace = true }
|
|
tower-http = { version = "0.6", features = ["fs", "cors", "set-header"] }
|
|
tokio = { workspace = true, features = ["full", "process"] }
|
|
futures-util = "0.3"
|
|
ruvector-mincut = { workspace = true }
|
|
|
|
# Serialization
|
|
serde = { workspace = true }
|
|
serde_json.workspace = true
|
|
|
|
# Logging
|
|
tracing.workspace = true
|
|
tracing-subscriber = { workspace = true }
|
|
|
|
# Time
|
|
chrono = { version = "0.4", features = ["serde"] }
|
|
|
|
# CLI
|
|
clap = { workspace = true }
|
|
|
|
# Multi-BSSID WiFi scanning pipeline (ADR-022 Phase 3)
|
|
wifi-densepose-wifiscan = { version = "0.3.0", path = "../wifi-densepose-wifiscan" }
|
|
|
|
# Signal processing with RuvSense pose tracker (accuracy sprint).
|
|
# default-features = false drops the optional ndarray-linalg/BLAS chain so that
|
|
# `--no-default-features` at the workspace root can produce a Windows-friendly
|
|
# build without vcpkg/openblas (issue #366, #415).
|
|
wifi-densepose-signal = { version = "0.3.1", path = "../wifi-densepose-signal", default-features = false }
|
|
|
|
# Hardware crate — SyncPacket decoder for ADR-110 §A0.12 mesh-aligned timestamps.
|
|
wifi-densepose-hardware = { version = "0.3.0", path = "../wifi-densepose-hardware" }
|
|
|
|
# Governed streaming engine (ADR-135..146): fusion + privacy demotion +
|
|
# WorldGraph belief + deterministic witness. The live server data runs through
|
|
# this as a governed path whose Restricted-class decision strips per-node raw
|
|
# amplitudes from the live publish; full output gating is a tracked follow-up —
|
|
# see engine_bridge.rs ("Honest scope of the live-path governance").
|
|
wifi-densepose-engine = { version = "0.3.0", path = "../wifi-densepose-engine" }
|
|
wifi-densepose-worldgraph = { version = "0.3.0", path = "../worldgraph/wifi-densepose-worldgraph" }
|
|
wifi-densepose-bfld = { version = "0.3.1", path = "../wifi-densepose-bfld", default-features = false }
|
|
wifi-densepose-geo = { version = "0.1.0", path = "../worldgraph/wifi-densepose-geo" }
|
|
|
|
# ADR-262 P3: live RuField surface. The thin anti-corruption bridge that turns
|
|
# this server's governed sensing cycle into signed RuField `FieldEvent`s on
|
|
# `/api/field` + `/ws/field`. It path-deps the standalone `vendor/rufield`
|
|
# submodule (it is the single coupling point — ADR-262 §5.4) and pulls in no
|
|
# RuView internal crate, so the dep surface added here is just the bridge.
|
|
wifi-densepose-rufield = { version = "0.3.0", path = "../wifi-densepose-rufield" }
|
|
|
|
# midstream — real-time introspection / low-latency tap (ADR-099 D1).
|
|
# Two crates only, on purpose: scheduler / neural-solver / strange-loop are
|
|
# explicitly out of scope of ADR-099 (D5).
|
|
midstreamer-temporal-compare = "0.2" # DTW / LCS / Edit-Distance pattern matching
|
|
midstreamer-attractor = "0.2" # Lyapunov + regime classification
|
|
|
|
# ADR-102: Edge Module Registry — fetch the canonical Cognitum cog catalog
|
|
# at `https://storage.googleapis.com/cognitum-apps/app-registry.json`,
|
|
# cache with TTL, surface via /api/v1/edge/registry. ureq is the smallest
|
|
# blocking HTTP client we can use without dragging a tokio HTTP stack in;
|
|
# rustls is enabled implicitly via the `tls` default feature.
|
|
ureq = { version = "2", default-features = false, features = ["tls", "json"] }
|
|
sha2 = "0.10"
|
|
thiserror = "1"
|
|
|
|
# ADR-271 — Cognitum OAuth access-token verification. Reuses the `ureq`
|
|
# transport above rather than pulling a second HTTP stack: `ruview-auth`'s
|
|
# JWKS fetch sits behind a trait, and its default feature is the ureq one.
|
|
ruview-auth = { path = "../ruview-auth" }
|
|
|
|
# ADR-115 §3.8 — MQTT publisher (HA-DISCO).
|
|
# Gated behind the `mqtt` feature so the default binary stays small for users
|
|
# who don't need Home Assistant integration. `rumqttc` is the chosen Rust MQTT
|
|
# client (ADR-115 §10 references). `rustls` is preferred over openssl on
|
|
# Windows to keep parity with the rest of the workspace (`ureq` above also
|
|
# uses rustls).
|
|
rumqttc = { version = "0.24", default-features = false, features = ["use-rustls"], optional = true }
|
|
|
|
[features]
|
|
default = []
|
|
# Enables the ADR-115 §2 MQTT auto-discovery publisher. Without this feature
|
|
# all `--mqtt-*` CLI flags still parse (cli.rs declares them unconditionally),
|
|
# but enabling `--mqtt` at runtime logs a `WARN` and the publisher is a no-op.
|
|
mqtt = ["dep:rumqttc"]
|
|
# ADR-115 §3.11 — Matter Bridge (HA-FABRIC). Same gating principle: flags
|
|
# parse unconditionally; the bridge is a no-op without this feature.
|
|
# matter-rs is added in P7; intentionally absent in P1 to keep the dep
|
|
# surface small until the SDK choice is validated.
|
|
matter = []
|
|
|
|
[dev-dependencies]
|
|
tempfile = "3.10"
|
|
# `tower::ServiceExt::oneshot` for in-process Router tests (bearer_auth).
|
|
tower = { workspace = true }
|
|
# ADR-115 P9 — micro-benchmarks for MQTT hot paths + semantic bus.
|
|
# Heavy dep tree (~80 transitive crates) so it's dev-only; benches live
|
|
# behind --features mqtt because they bench the mqtt module.
|
|
criterion = { version = "0.5", features = ["html_reports"] }
|
|
# ADR-115 P9 — property-based fuzzing for the wire-boundary security
|
|
# audit. Catches edge cases the example-based unit tests would miss
|
|
# (random Unicode, control chars, etc.). Pinned to a small version that
|
|
# doesn't pull in proptest-derive (we don't need it).
|
|
proptest = { version = "1.5", default-features = false, features = ["std"] }
|
|
# ADR-271 — sign real ES256 tokens so the middleware's OAuth path is exercised
|
|
# end to end (router → middleware → verifier), not just mocked at the seam.
|
|
# Keys are generated at test runtime; none are committed.
|
|
jsonwebtoken = "9"
|
|
p256 = { version = "0.13", features = ["ecdsa", "pkcs8"] }
|
|
base64 = "0.21"
|
|
|
|
[[bench]]
|
|
name = "mqtt_throughput"
|
|
harness = false
|
|
required-features = ["mqtt"]
|