-
Notifications
You must be signed in to change notification settings - Fork 8
fix: exclude cluster/replication topology operations from ambient user context #1727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -59,6 +59,34 @@ const GLOBAL_SCHEMA_UPDATE_OPERATIONS_ENUM = { | |||||||||||||
| [terms.OPERATIONS_ENUM.DROP_SCHEMA]: true, | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| // Cluster/replication topology operations must NOT run under the request's ambient user context | ||||||||||||||
| // (the #1591 attribution wrap below). Two reasons: | ||||||||||||||
| // | ||||||||||||||
| // 1. Their writes are internal node-registry bookkeeping (hdb_nodes, certificates), not | ||||||||||||||
| // user-authored data — they should not be stamped with a user principal. | ||||||||||||||
| // 2. Critically, they start long-lived, non-awaited replication subscription work (a peer | ||||||||||||||
| // connection's connect/handshake/reconnect lifecycle) that outlives the handler. Because that | ||||||||||||||
| // work is constructed while this handler's AsyncLocalStorage scope is active, it keeps | ||||||||||||||
| // observing the ambient { user } for its ENTIRE life, not just the request — which corrupts | ||||||||||||||
| // the subscription's identity and produces a WebSocket 1006 reconnect storm that never meshes | ||||||||||||||
| // (regression from #1591/#1592; the pre-#1591 world gave this background work no ambient user). | ||||||||||||||
| // | ||||||||||||||
| // The durable fix is to detach that background work from the ambient context where it is spawned | ||||||||||||||
| // (harper-pro's replication component); this core-side exclusion resolves the regression for the | ||||||||||||||
| // operations that trigger it without that background work ever inheriting a user. The names span | ||||||||||||||
| // core and harper-pro's replication component (registered via server.registerOperation), so this | ||||||||||||||
| // set must be kept in sync if new topology-management operations are added. | ||||||||||||||
| const OPERATIONS_EXCLUDED_FROM_AMBIENT_USER = new Set<string>([ | ||||||||||||||
| terms.OPERATIONS_ENUM.ADD_NODE, // 'add_node' | ||||||||||||||
| terms.OPERATIONS_ENUM.UPDATE_NODE, // 'update_node' | ||||||||||||||
| terms.OPERATIONS_ENUM.SET_NODE_REPLICATION, // 'set_node_replication' | ||||||||||||||
| 'add_node_back', | ||||||||||||||
| 'set_node', | ||||||||||||||
| 'remove_node', | ||||||||||||||
| 'remove_node_back', | ||||||||||||||
| 'configure_cluster', | ||||||||||||||
| ]); | ||||||||||||||
|
|
||||||||||||||
| import { OperationFunctionObject } from './OperationFunctionObject.ts'; | ||||||||||||||
|
|
||||||||||||||
| type ValueOf<T> = T[keyof T]; | ||||||||||||||
|
|
@@ -97,14 +125,29 @@ export async function processLocalTransaction(req: OperationRequest, operationFu | |||||||||||||
| // differs (or is absent), the ambient context is merged rather than replaced: other ambient | ||||||||||||||
| // properties (open transaction, signal, caches) are preserved so atomicity is unaffected and | ||||||||||||||
| // only the user is swapped for attribution; the outer context object itself is never mutated. | ||||||||||||||
| // Cluster/replication topology operations are excluded (see OPERATIONS_EXCLUDED_FROM_AMBIENT_USER): | ||||||||||||||
| // they spawn long-lived background work that would otherwise inherit this ambient user for its | ||||||||||||||
| // whole life. | ||||||||||||||
| const hdbUser = req.body.hdb_user; | ||||||||||||||
| const currentStore = contextStorage.getStore(); | ||||||||||||||
| const callOperationFunction = () => | ||||||||||||||
| operationFunctionCaller.callOperationFunctionAsAwait(operationFunction, req.body, null); | ||||||||||||||
| let data = | ||||||||||||||
| hdbUser && currentStore?.user !== hdbUser | ||||||||||||||
| ? await contextStorage.run({ ...currentStore, user: hdbUser }, callOperationFunction) | ||||||||||||||
| let data; | ||||||||||||||
| if (OPERATIONS_EXCLUDED_FROM_AMBIENT_USER.has(req.body.operation)) { | ||||||||||||||
| // Excluded operations must run with no ambient user principal. Strip any ambient user — | ||||||||||||||
| // whether it would come from this request's hdb_user or from an enclosing ambient context | ||||||||||||||
| // (e.g. a nested server.operation() call from within an authenticated handler) — so the | ||||||||||||||
| // long-lived background work they spawn never inherits a user. Other ambient state is | ||||||||||||||
| // preserved, and the outer context object is never mutated. | ||||||||||||||
| data = currentStore | ||||||||||||||
| ? await contextStorage.run({ ...currentStore, user: undefined }, callOperationFunction) | ||||||||||||||
| : await callOperationFunction(); | ||||||||||||||
|
Comment on lines
+142
to
144
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can optimize this check and avoid an unnecessary
Suggested change
References
|
||||||||||||||
| } else { | ||||||||||||||
| data = | ||||||||||||||
| hdbUser && currentStore?.user !== hdbUser | ||||||||||||||
| ? await contextStorage.run({ ...currentStore, user: hdbUser }, callOperationFunction) | ||||||||||||||
| : await callOperationFunction(); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (typeof data !== 'object') { | ||||||||||||||
| data = { message: data }; | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
remove_node_backwon't match the op as registered in harper-proharper-pro registers this operation with a trailing-semicolon typo in the name —
name: 'remove_node_back;'(replication/setNode.ts:331). So this exclusion entry (remove_node_back, no semicolon) never matchesreq.body.operationfor that handler, and the entry is currently a no-op.Low impact in practice:
removeNodeBackonly deletes anhdb_nodesrecord and spawns no long-lived subscription work, so no 1006 storm results — the exclusion here is attribution-only. But it's a live instance of exactly the cross-layer name-drift this PR's own comment warns about. The real fix is dropping the stray;in harper-pro's registration; keep this list entry as-is (it's the correct intended name).—
Generated by Barber AI