From 9e09cd4b3d9449944afa041a441c8c6b5da3a4c2 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 20 Oct 2026 12:16:32 +0200 Subject: [PATCH] broker: initialize drops the dataset's bearer tokens and everything they protected Initialize is the fresh start between users. It already cleared the images, plots, scan result and live rugnux status; it now also drops the tokens, resets the dataset settings (name, file prefix, unit cell, ...) to their defaults and forgets the last collection followed through /live/events, so after it nothing of the previous dataset is readable, with or without its token - the broker is as open and as empty as a freshly started one. The auth test checks that after /initialize the data-collection statistics and the /statistics aggregate are open and no longer carry the previous dataset's name. SECURITY.md also lists /rugnux/status among the protected endpoints. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4 --- broker/JFJochStateMachine.cpp | 7 ++++++- broker/jfjoch_api.yaml | 1 + docs/SECURITY.md | 5 ++++- image_pusher/LiveCollection.cpp | 10 ++++++++++ image_pusher/LiveCollection.h | 2 ++ tests/BrokerHttpAuthTest.cpp | 18 ++++++++++++++++-- 6 files changed, 39 insertions(+), 4 deletions(-) diff --git a/broker/JFJochStateMachine.cpp b/broker/JFJochStateMachine.cpp index cdae150db..2ce46530e 100644 --- a/broker/JFJochStateMachine.cpp +++ b/broker/JFJochStateMachine.cpp @@ -330,8 +330,13 @@ void JFJochStateMachine::Initialize() { start_exception = nullptr; // Re-initialising discards a pending start failure // A fresh start: no images, plots or live rugnux status from before, and no live rugnux analysis - // or model left over from the previous user. + // or model left over from the previous user. The previous dataset's tokens go too, and with them + // everything they protected: its settings (name, cell, ...) and the record of its collection. services.ClearResults(); + tokens.Replace({}); + experiment.ImportDatasetSettings(DatasetSettings()); + if (auto *live = services.GetLiveCollection()) + live->Forget(); { std::unique_lock ul2(rugnux_mutex); RugnuxLiveSettings s = services.GetRugnuxLive(); diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index c492e795b..866d1a2e4 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -2918,6 +2918,7 @@ paths: During operation of the detector it is recommended to use the `POST /pedestal` operation instead. If storage cells are used, the execution time might be few minutes. It also clears the image buffer, the previous collection's plots and live rugnux status, disables live rugnux analysis and removes the rugnux model. + It drops the previous dataset's bearer tokens and resets its settings, so nothing of that dataset stays readable. This is async function - one needs to use `POST /wait_till_done` to ensure operation is done. responses: diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 4893b63a2..ee3ace785 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -101,7 +101,9 @@ users cannot read this one. No endpoint returns the tokens; the broker only comp An accepted `/start` also clears what the previous run left readable - its statistics, plots and buffered images - in the same step that installs the new tokens, so the previous run is never served under the new tokens, and the new run's name never under the old ones. A refused `/start` -(wrong state, invalid settings) changes nothing. +(wrong state, invalid settings) changes nothing. `/initialize` drops the tokens together with +everything they protected - the dataset's settings, results, images, plots, live rugnux status and +the record of its collection - so the broker is left as open and as empty as a freshly started one. | Endpoint | With tokens set | |----------|-----------------| @@ -110,6 +112,7 @@ served under the new tokens, and the new run's name never under the old ones. A | `/image_buffer/start.cbor`, `/image_buffer/image.cbor`, `/image_buffer/image.jpeg`, `/image_buffer/image.tiff` (the images and the start message) | 401 without a token | | `/preview/plot`, `/preview/plot.bin` (per-image plots, unit cell) | 401 without a token | | `/live/events` (the start message and the data files of the collection being written) | 401 without a token | +| `/rugnux/status` (live rugnux run of the collection: cell, space group, results) | 401 without a token | | `/statistics` (the aggregate the web UI polls) | 200, but the `measurement` block is omitted | | everything else (`/status`, `/config/*`, `/start`, `/cancel`, masks, pedestal, ...) | open | diff --git a/image_pusher/LiveCollection.cpp b/image_pusher/LiveCollection.cpp index d40f5ac56..6e40e3767 100644 --- a/image_pusher/LiveCollection.cpp +++ b/image_pusher/LiveCollection.cpp @@ -68,6 +68,16 @@ void LiveCollection::EndIfOpen(const std::string &error) { End(0, error); } +void LiveCollection::Forget() { + { + std::unique_lock ul(m); + state = Snapshot{}; + start_message = StartMessage{}; + start_cbor.clear(); + } + cv.notify_all(); +} + std::optional> LiveCollection::StartCBOR() const { std::unique_lock ul(m); if (!state.started) diff --git a/image_pusher/LiveCollection.h b/image_pusher/LiveCollection.h index d35cfbf67..4056c699d 100644 --- a/image_pusher/LiveCollection.h +++ b/image_pusher/LiveCollection.h @@ -51,6 +51,8 @@ public: void End(uint64_t images, const std::string &error = ""); // The measurement is over: ends a collection the image pusher did not end, with this error. void EndIfOpen(const std::string &error); + // Back to "no collection since the broker started" (/initialize): nothing of the last one is kept. + void Forget(); // The start message of the current collection; nothing before it is known. std::optional> StartCBOR() const; diff --git a/tests/BrokerHttpAuthTest.cpp b/tests/BrokerHttpAuthTest.cpp index 3af02f883..edebb0c0e 100644 --- a/tests/BrokerHttpAuthTest.cpp +++ b/tests/BrokerHttpAuthTest.cpp @@ -110,6 +110,20 @@ TEST_CASE("JFJochBrokerHttp_BearerTokens", "[broker]") { REQUIRE(client.Get("/live/events", good)->status == 409); } + SECTION("initialize drops the tokens and everything they protected") { + REQUIRE(client.Post("/initialize")->status == 200); + for (int i = 0; i < 120 && nlohmann::json::parse(client.Get("/status")->body).at("state") != "Idle"; i++) + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + auto res = client.Get("/statistics/data_collection"); + REQUIRE(res->status == 200); + REQUIRE(res->body.find("protected_run") == std::string::npos); + auto stats = client.Get("/statistics"); + REQUIRE(stats->status == 200); + REQUIRE(stats->body.find("protected_run") == std::string::npos); + REQUIRE(client.Get("/rugnux/status")->status == 200); + REQUIRE(client.Get("/result/scan")->status != 401); + } + SECTION("the control plane and the broker status stay open") { REQUIRE(client.Get("/status")->status == 200); REQUIRE(client.Get("/image_buffer/status")->status == 200); @@ -122,11 +136,11 @@ TEST_CASE("JFJochBrokerHttp_BearerTokens", "[broker]") { nlohmann::json body = {{"beam_x_pxl", 1000}, {"beam_y_pxl", 1000}, {"detector_distance_mm", 100}, {"incident_energy_keV", 12.4}}; REQUIRE(client.Post("/start", body.dump(), "application/json")->status == 500); - REQUIRE(client.Post("/initialize")->status == 200); REQUIRE(client.Get("/statistics/data_collection")->status == 401); REQUIRE(client.Get("/statistics/data_collection", good)->status == 200); - // The next start without tokens lifts the protection. + // Back from Inactive; the next start without tokens leaves the dataset open. + REQUIRE(client.Post("/initialize")->status == 200); start(nlohmann::json::object()); REQUIRE(client.Get("/statistics/data_collection")->status == 200); REQUIRE(client.Get("/statistics/data_collection", wrong)->status == 200);