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.
This commit is contained in:
2026-10-15 09:50:04 +02:00
parent 0004e4b9bd
commit 1f213460ad
4 changed files with 42 additions and 3 deletions
+4 -1
View File
@@ -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(
+34
View File
@@ -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)
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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