Reading the rule rows a node keeps (classifications, active retention rules, policies) is a query on a secured-data table, and other code runs inside that query: a virtual field value provider or a post-query customizer that another QBit or the host put on the table. When that code runs a query with filter criteria, core validates the operands of classified fields and asks this QBit for the classification of the field, which needs the rows that are being read. The version of the rows was recorded only after the read returned, so the nested question started the read again, and that read asked again: StackOverflowError, and the same on every later request, since nothing was ever recorded. One classification row was enough (core runs no value provider for an empty result). - System reads of the QBit's own tables (SystemWrites.query, the audit key lookup) name the stored fields of the table. Core fills a virtual field only for a query that selects it, so no value provider runs inside a system read. - RuleRows holds one kind of rule rows and is used for all three kinds, which had the same shape. A question asked from inside a read is answered from the rows held when the read began (none before the first read) and never starts another read. The owner's monitor makes that answer private to the reading thread: every other thread waits and gets the rows the read returns. - A read that fails records nothing and clears the reading mark, so the next question reads again. The rows held before are not given to a request in place of rows that could not be read. - RetentionRules.active: a question asked from inside a transaction's read of its own rule edits is answered from the rules every thread shares instead of reading the edits again. Who can be given the nested answer: only code on the reading thread, inside SystemWrites.call, where the session is the system session and the resolver answers FULL without looking at a classification. No field access decision for a person is made from it; the request that started the read, and every request on another thread, gets the rows the read returns. Tests (SecuredDataRuleReloadTest): 9 of the 11 end in StackOverflowError without the fix. They cover a value provider and a post-query customizer on the classification, retention rule and policy tables, the answer a nested question gets in both directions of a change, the session it is asked in, a second thread waiting for the read, and failed reads (an exception and an Error) being read again. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
6.4 KiB
Changelog
All notable changes are documented here, following Keep a Changelog and Semantic Versioning.
[Unreleased]
Fixed: rule reads that ask for their own rows
- Reading the rule rows a node keeps (classifications, active retention rules, policies) no longer recurses when code that runs inside that read asks for the same rows. Another QBit's virtual field value provider, or a post-query customizer, on a secured-data table that ran a query with filter criteria did exactly that: with one classification row present, every request that needed a classification ended in a
StackOverflowError. - The QBit's system reads of its own tables (
SystemWrites.query, the audit key lookup) name the stored fields of the table, so core runs no virtual field value provider inside them. - A question asked from inside a rule read is answered from the rows held when that read began (none before the first read) and starts no other read. Only the reading thread, inside the QBit's system read, can be given that answer; every request gets the rows the read returns. The same holds for a transaction's read of its own retention rule edits.
- A rule read that fails is tried again by the next question instead of failing or recursing from then on.
Fixed: access-log lifecycle
- The access-log worker and JVM shutdown hook hold the logger weakly, so an instance the host drops (tests, reloads) can be collected; its worker stops at the next tick.
withRegisterShutdownHook(false)for hosts that callSecuredDataRuntime.close()themselves;SecuredDataAccessLogger.isWorkerRunning().- Virtual (computed) fields resolve through the table classification instead of throwing (#18).
- Idle access-log ticks open no context, so the worker never validates an unfinished host instance, and a failed tick no longer stops the worker (#18).
Changed: UI API 1.6.0
- UI API extension secured-data 1.1.0 (requires QQQ 4.1.0-RC.15, UI API 1.6.0): operationIds, standard error responses with BasicErrorResponseV1, extension tags, and retained SecuredDataErrorV1 schema deprecated since 1.1.0.
Changed — direct core field access and audit protection
- Remove all #1172/#1182 stand-ins and reflective bridges; use core field-access, classified-value token, audit protection, wipe flag and shredded-event APIs directly.
- Register the resolver, key source and audit protector through core supplemental metadata.
- Run core field-access integration and retention proof tests unconditionally.
- Pin QQQ to
4.1.0-RC.7, which ships field access (#1172) and audit value protection (#1182); all core stand-ins and reflective bridges are removed.
Changed — RC.6 migration
- Pin QQQ to 4.1.0-RC.6; use released classification, client-channel and table-feature APIs directly.
- Fold UI API code and tests into the main build and remove the profile and reflective registration.
- Remove released-API stand-ins and the local contract assertion facade; retain #1172 field-access stand-ins with RC.7 removal markers.
Added: phase (f) retention and wipe
- Retention rules, wipe markers, legal holds, wipe runs and a hidden, locked audit-key table, with the
secured_data.retentionpermission and a "Retention rules" navigation entry. - Actions NULL_WITH_MARKER, OVERWRITE_TEXT, KEYED_HASH, DELETE_RECORD and ARCHIVE_THEN_WIPE (host
ArchiveSinkInterface), with validation and low-entropy refusal. - Atomic wipe batches (fields, markers, key deletions, audits, access log and run row in one rollback-capable transaction), per-record and per-table legal holds, a Review/Result "Run wipe now" process and a daily scheduled run.
- Per-record-field AES-GCM wrapped DEKs (AAD-bound) for crypto-shredding of audit tokens,
CHANGED_ONLYaudit treatment, and the audit-value protector. - App-home warning when known backup retention exceeds a rule's
age_days;withBackupRetentionDaysandwithArchiveSinkoptions; README retention section and the backups residual risk. - Tests: all actions, holds, rollback, Review/Result, schedule, permissions, wrap/unwrap, and the section 15 canary and wipe-proof tests.
Fixed
- The key source returned a DEK array it then zeroed; it now returns copies, with a regression test.
Added
-
Phase (e) configurable reveal-median, distinct-record and exact-match anomaly detection, with system access-log counters, bounded daily median caching, per-user/day alerts, and flags retained without an alert sink.
-
Break-glass start and capped-reveal anomaly flags alongside the existing alerts.
-
One Secured Data app home with six live KPIs, six full-width responsive lists, filtered View all links, theme tokens, compact dates, clamped text/tooltips and explicit empty states.
-
Transaction-aware, value-free policy/classification change audit rows from post-write customizers and two access-log indexes for anomaly/home queries.
-
Boundary, alert, home data, rendering and navigation tests plus a synthetic Next demo harness for coordinator screenshot capture.
-
Phase (c) HTTP-independent reveal options, reason validation, UTC daily limits, grants, audit links, cap alerts and assistant refusal.
-
Five-second per-user grant cache with immediate invalidation and read-time expiration.
-
Start/End break-glass table and record actions, streamed Review/Result summaries, owner/admin authorization, scheduled expiration, alerts and optional admin bell notification.
-
Caller status and the versioned
secured-dataUI API extension behind the temporaryuiapiMaven profile, with committed OpenAPI and contract/HTTP tests. -
Contingent classification field picker support for snake_case and camelCase table selections; unknown-table rejection.
-
Memory and real-core coverage for reveal, break-glass, record locks and expiry. Reason columns support 500 characters; existing RDBMS installations must widen the three
reasoncolumns to VARCHAR(500). -
Phase (b) secured-data classification, policy, reveal-grant, break-glass and insert-only access-log tables, metadata, permissions, pickers and app navigation.
-
Versioned classification and policy resolution with channel caps, table features, break-glass expiry and access fingerprints.
-
Bounded asynchronous value-free logging, HMAC search fingerprints, rate limits, entropy rules and enumeration alerts.
-
RC.5-compatible stand-ins and reflective core adapters; conditional core Query/Get integration coverage.
-
PostgreSQL DDL, H2 persistence/constraint coverage, and configuration/deployment documentation.