From 1f213460ad1db4cd2d487b15c6cc649f340ba77a Mon Sep 17 00:00:00 2001 From: stroblme Date: Thu, 15 Oct 2026 09:50:04 +0200 Subject: [PATCH] Audit shared-library source writes A shared source goes live in every flow using it the moment it is saved, so the write is recorded in the audit trail like a publish. Private source saves stay drafts and are not audited. --- backend/fluksio/api/routes/flows.py | 5 +++- backend/tests/api/routes/test_flows.py | 34 ++++++++++++++++++++++++++ docs/code/api.md | 3 ++- frontend/src/client/sdk.gen.ts | 3 ++- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/backend/fluksio/api/routes/flows.py b/backend/fluksio/api/routes/flows.py index 69094f6..bc352af 100644 --- a/backend/fluksio/api/routes/flows.py +++ b/backend/fluksio/api/routes/flows.py @@ -625,6 +625,7 @@ async def save_node_source( node_id: str, source: NodeSource, controller: FlowControllerDep, + user: CurrentUser, ) -> Any: """Save a node's source as an unpublished edit and report whether it loads. @@ -634,7 +635,8 @@ async def save_node_source( A shared node writes to the library, so the fix reaches every flow using it — and that one is live immediately rather than waiting for a publish, - because the copy is not any single flow's to hold back. + because the copy is not any single flow's to hold back. Being live, it is + audited like a publish. """ definition = _read_flow(controller, name) ref = _source_ref(definition, node_id) @@ -643,6 +645,7 @@ async def save_node_source( controller.store.write_lib_source, ref, source.code ) if changed: + _audit(f"edited shared node '{ref}'", name, user) await controller.reload_lib_users(ref) else: await run_in_threadpool( diff --git a/backend/tests/api/routes/test_flows.py b/backend/tests/api/routes/test_flows.py index 80fbfe1..d28ada2 100644 --- a/backend/tests/api/routes/test_flows.py +++ b/backend/tests/api/routes/test_flows.py @@ -581,3 +581,37 @@ def test_a_shared_node_in_use_is_retired_only_with_detach( assert source.json()["code"] == WORKING_NODE client.delete(f"{PREFIX}/shared", headers=superuser_token_headers) + + +def test_a_shared_source_write_is_audited( + client: TestClient, + superuser_token_headers: dict[str, str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """It goes live at once, so it is recorded like a publish.""" + client.put( + f"{PREFIX}/audited", headers=superuser_token_headers, json=a_flow("audited") + ) + client.post( + f"{PREFIX}/audited/nodes/sensor/share", + headers=superuser_token_headers, + json={"lib_name": "audited_reading"}, + ) + + published: list[dict] = [] + monkeypatch.setattr(event_bus, "publish", published.append) + client.put( + f"{PREFIX}/audited/nodes/sensor/source", + headers=superuser_token_headers, + json={"code": WORKING_NODE}, + ) + + (audit,) = [e for e in published if e["type"] == "audit"] + assert audit["action"] == "edited shared node 'audited_reading'" + assert audit["flow"] == "audited" + + client.delete( + f"{PREFIX}/library/audited_reading?detach=true", + headers=superuser_token_headers, + ) + client.delete(f"{PREFIX}/audited", headers=superuser_token_headers) diff --git a/docs/code/api.md b/docs/code/api.md index 5c790b0..3d6df91 100644 --- a/docs/code/api.md +++ b/docs/code/api.md @@ -80,7 +80,8 @@ Function and Command nodes, changing Command settings, or changing `resources.env`. Other users can edit wiring, titles, and ordinary settings. Ordinary source edits stay in the draft until publishing. Shared library -source changes affect its users immediately. +source changes affect its users immediately, and the audit trail records them +like a publish. `POST /flows/{name}/nodes/{id}/share` promotes a source to the library; `/unshare` makes it private again. `GET /flows/library` lists shared sources. `DELETE /flows/library/{name}` retires one, and answers 409 while nodes still diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts index 9fcd840..fbad4f3 100644 --- a/frontend/src/client/sdk.gen.ts +++ b/frontend/src/client/sdk.gen.ts @@ -1017,7 +1017,8 @@ export class FlowsService { * * A shared node writes to the library, so the fix reaches every flow using * it — and that one is live immediately rather than waiting for a publish, - * because the copy is not any single flow's to hold back. + * because the copy is not any single flow's to hold back. Being live, it is + * audited like a publish. * @param data The data for the request. * @param data.name * @param data.nodeId