Skip to content

EA-2327 Group-on-ClickHouse hardening (EA-2322/2323/2324 + adversarial fixes)#2286

Open
wfieldx wants to merge 13 commits into
mainfrom
WF-EA-2327-groups-ch-hardening
Open

EA-2327 Group-on-ClickHouse hardening (EA-2322/2323/2324 + adversarial fixes)#2286
wfieldx wants to merge 13 commits into
mainfrom
WF-EA-2327-groups-ch-hardening

Conversation

@wfieldx

@wfieldx wfieldx commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Consolidated hardening of the Group-on-ClickHouse path. The socket fix (EA-2320) ships separately in #2282 (cross-cutting - it also helps the AuditEvent write path).

Integrates + adversarially verifies

  • EA-2322 - Mongo/ClickHouse split-brain compensation on write failure (+ a now-genuinely-running failure-injection test).
  • EA-2323 - fail-closed + admin-exempt tenant filtering (403 for unscoped, matching SecurityTagManager); content/version-derived idempotency (deterministic event_id + argMax convergence; dropped the inert dedup token on the plain MergeTree); retry+jitter on the Group insert; surface ClickHouse read failures (no silent quantity:0). ADR docs/adr/0003.
  • EA-2324 - strict concurrency assertions (verified no lost writes), real failure injection, activated the previously-commented-out MAX_GROUP_MEMBERS_PER_PUT guardrail (behavior: oversized PUT -> 400 too-costly), fixed stale perf wiring + added a CI perf smoke.

Adversarial integration testing (EA-2327) found + fixed

  • BUG-1 (blocker): EA-2323's event_time = meta.lastUpdated (a Date) hit .replace() in the formatter -> every Group create/PUT/$merge 500'd. Fixed (normalize event_time). This is why EA-2323 Group ClickHouse hardening: idempotency, fail-closed tenancy, retry+jitter, surface read failures #2284 was red in isolation; EA-2322's compensation was masking it.
  • BUG-2/3: EA-2322's split-brain test never actually ran (jest not imported under injectGlobals:false, wrong Mongo lookup). Fixed; now genuinely passes 3/3.
  • Scoped coverage 64.6%->73.5% statements, 50.9%->61.5% branches; added a 41-test adversarial suite.

Known follow-up (NOT in this PR)

BUG-4 (EA-2326) - PUT/versioned member removal can be lost via a same-timestamp argMax tie (content-hash event_id). Repros committed as skipped; needs a causal-ordering fix + its own ADR.

Verified locally green (24 suites, 199 pass, 2 skipped BUG-4 repros); CI runs the full container suite. Latest main merged in.

Supersedes #2283, #2284, #2285. Refs EA-2322, EA-2323, EA-2324, EA-2327, EA-2326; epic DCON-3433.

wfieldx added 10 commits June 30, 2026 17:27
…ers + idle_socket_ttl to fix ECONNRESET/EPIPE
…enant filter, retry+jitter, surface read failures
… assertions, real failure injection, perf smoke in CI
…ntegration)

Adversarially attacked the four merged fixes (EA-2320 sockets, EA-2322
split-brain, EA-2323 idempotency/fail-closed-tenant/retry/read-surface).
Found and fixed a blocker regression, revived a dead split-brain test, and
surfaced a real membership-correctness bug (kept as a skipped repro).

New: src/tests/group/group_clickhouse_adversarial.unit.test.js — pure-unit
attacks on event_time normalization, idempotent event identity, write-fail
propagation, source_assigning_authority derivation, fail-closed tenant HAVING,
_callerHasFullAccess, read-surface rethrow, and retry+jitter bounds.

BUG-1 (blocker, fixed): EA-2323 sourced event_time from meta.lastUpdated (a Date
at runtime); DateTimeFormatter did string .replace() on it -> every Group write
with useExternalStorage 500'd. Normalize event_time to ISO in the handler and
harden DateTimeFormatter to accept a Date.

BUG-2/BUG-3 (fixed, test-only): group_clickhouse_write_failure.test.js never ran
(jest not imported; wrong Mongo handle; lookup by a client id that POST replaces).
Fixed so the dedicated EA-2322 split-brain suite runs and passes 3/3.

BUG-4 (surfaced, needs ADR): argMax((event_time, event_id)) with ms-precision
event_time and a content-hash tie-breaker lets a same-time MEMBER_REMOVED lose to
its MEMBER_ADDED, so PUT-based removals can read as still-active. Documented as a
skipped repro in the unit file and in group_update_operations.test.js.

group_error_handling.test.js: updated the read-timeout test to the merged B7
contract (read error surfaces, never a silent quantity:0).
* @param {Array<Object>} params.members - Original Group.member entries (FHIR class instances or plain objects)
* @returns {Promise<boolean>} true if a document was updated, false if there was nothing to restore
*/
async function restoreStrippedMembersInMongo({ collection, uuid, members }) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restoreStrippedMembersInMongo was added to a pre-save helper and performs direct Mongo updates; move compensation/DB-repair logic to a dedicated compensation/service layer.

Details

✨ AI Reasoning
​clickHouseGroupPreSave previously handled in-memory pre-save transformations (tagging and stripping members). The diff adds restoreStrippedMembersInMongo which performs an actual MongoDB updateOne call to restore members as compensation for failed ClickHouse writes. This introduces a persistence/compensation responsibility into a module that was previously an in-memory pre-save helper, mixing transformation logic with side-effecting DB repair logic.

🔧 How do I fix it?
Split classes that handle database, HTTP, and UI concerns into separate, focused classes.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

return parseInt(env.CLICKHOUSE_MAX_CONNECTIONS || String(DEFAULT_CLICKHOUSE.MAX_CONNECTIONS), 10);
}

/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a multi-getter ClickHouse configuration block (clickHouseIdleSocketTtl, clickHouseSendProgressInHttpHeaders). Consider extracting ClickHouse settings into a dedicated config module to reduce ConfigManager size and improve maintainability.

Details

✨ AI Reasoning
​A large configuration class has been expanded with a multi-property ClickHouse section (idle socket TTL and HTTP-header progress toggles). This adds several getters and explanatory comments into an already long, multi-concern module, increasing cognitive load and navigation difficulty. The new code mixes ClickHouse-specific connection tuning into a broad ConfigManager that already contains many unrelated settings, which makes the file harder to maintain and discover configuration related to ClickHouse in isolation. Splitting or extracting a ClickHouse-specific config helper would localize these responsibilities and keep ConfigManager focused.

🔧 How do I fix it?
Split large files into smaller, focused modules. Each file should have a single responsibility.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

@gecko-security gecko-security Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gecko PR Review

No vulnerabilities found, LGTM.

Summary

This PR hardens the ClickHouse Group membership dual-write path by fixing two critical security and reliability bugs: (1) correcting a fail-closed tenant isolation filter that incorrectly denied wildcard admins in mongoWithClickHouseStorageProvider.js and queryBuilder.js, and (2) removing reliance on ineffective ClickHouse deduplication tokens and implementing proper idempotency via content-derived insertion checks in groupMemberRepository.js and related handlers. The changes span query building, bulk write execution, pre/post-save handlers, and comprehensive test coverage for adversarial scenarios, concurrent writes, and compensation logic.

Important Files Changed

9 important files out of 34 changed

File Path Why it matters
src/dataLayer/providers/mongoWithClickHouse/queryBuilder.js Removes incorrect 1 = 0 deny clause that blocked legitimate wildcard admin (access/*.*) queries by misinterpreting empty security tags as authorization failure rather than full-access signal.
src/dataLayer/repositories/groupMemberRepository.js Replaces ineffective ClickHouse insert_deduplication_token with application-level idempotency checks since MergeTree engine ignores dedup tokens; ensures safe retries on insert failures.
src/dataLayer/providers/mongoWithClickHouseStorageProvider.js Coordinates MongoDB Group writes with ClickHouse member event appends, enforcing tenant isolation at query boundaries and applying compensating writes on partial failures.
src/dataLayer/postSaveHandlers/clickHouseGroupHandler.js Implements async post-save compensation logic to reconcile MongoDB and ClickHouse state after transient failures, with retry and logging for observability.
src/dataLayer/bulkWriteExecutors/mongoBulkWriteExecutor.js Handles bulk write failures and triggers ClickHouse compensation flow when MongoDB writes partially fail, maintaining consistency across dual stores.
docs/adr/0003-clickhouse-group-tenant-isolation-and-idempotency.md Documents the design decisions correcting both tenant isolation and idempotency failures, including tracing of security tag propagation and ClickHouse engine limitations.
src/tests/group/group_clickhouse_adversarial.unit.test.js Tests tenant isolation against adversarial queries (e.g., wildcard admins, empty tags, cross-tenant filters) to prevent regression of the fail-closed bug.
src/tests/group/group_clickhouse_compensation.unit.test.js Validates compensation logic when MongoDB writes succeed but ClickHouse appends fail, ensuring eventual consistency and idempotency on retry.
src/utils/retryWithBackoff.js Provides exponential backoff retry mechanism used by post-save handlers and groupMemberRepository to safely recover from transient ClickHouse failures.

Omitted 25 lower-signal changed files.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Controller
    participant MongoWithClickHouse
    participant MongoDB
    participant ClickHouse
    participant PostSaveHandler

    Client->>Controller: PATCH Group (add member)
    Controller->>MongoWithClickHouse: Update
    MongoWithClickHouse->>MongoDB: Write Group metadata
    MongoDB-->>MongoWithClickHouse: Success
    MongoWithClickHouse->>ClickHouse: Append member event
    alt ClickHouse append succeeds
        ClickHouse-->>MongoWithClickHouse: Dedup check + insert
        MongoWithClickHouse-->>Controller: Success
    else ClickHouse append fails (transient)
        ClickHouse-->>MongoWithClickHouse: Error
        MongoWithClickHouse->>PostSaveHandler: Enqueue compensation
        MongoWithClickHouse-->>Controller: 500
        PostSaveHandler->>ClickHouse: Retry append with backoff
        ClickHouse-->>PostSaveHandler: Success (idempotent)
    end
    Controller->>Client: Response
Loading

Edit Gecko PR Settings | Reviewed commit: 178fab9

wfieldx added 3 commits July 1, 2026 13:20
…; align AccessLog retry test with full-jitter backoff
…s; tighten verbose comments; rename idempotency var to idempotencyContext; rename adversarial test file to hardening
…lickHouse); add ClickHouse-off no-op test; tighten idempotency doc wording
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant