Skip to content

fix(security): require signed tenant claims on admin endpoints - #266

Closed
seonghobae wants to merge 47 commits into
fix/pii-logging-16240128950440010639from
fix/admin-endpoint-auth-clean
Closed

fix(security): require signed tenant claims on admin endpoints#266
seonghobae wants to merge 47 commits into
fix/pii-logging-16240128950440010639from
fix/admin-endpoint-auth-clean

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Security objective

Make every administrative conversion-job endpoint fail closed under signed tenant claims, least-privilege permissions, tenant-scoped persistence queries, atomic tenant-scoped mutations, tenant-bound secondary indexes, and privacy-safe audit evidence. This clean replacement supersedes #172 and remains stacked on parent privacy PR #165.

Shipped security boundary

  • admin:read is required for tenant-scoped listing; admin:write is required for delete and retry.
  • Missing, malformed, expired, incorrectly signed, weak-key, and insufficient-permission claims fail before repository access.
  • Administrative listing calls only findAllByTenantId; the controller never materializes the global job list.
  • Delete and retry call atomic tenant-aware repository/state-store methods and never fall back to global lookup followed by a legacy mutation.
  • Missing and cross-tenant objects share the same concealed not-found contract.
  • A missing job UUID now also fails closed across scoped lookup, delete, and retry as empty/false/NOT_FOUND, leaving existing stored state unchanged.
  • Artifact cleanup occurs only after an owned delete succeeds; worker enqueue occurs only after an owned dead-letter transition succeeds.
  • The tenant-plus-content-hash secondary index validates its current primary record, rejects UUID collisions, removes replaced indexes, and updates with the primary map under one shared critical section.
  • Administrative actor and tenant evidence uses separate domain-separated HMAC fingerprints; raw claim, subject, tenant, filename, document, and signature values are excluded.
  • Tenant-claims key material is read from Spring config-tree; direct secret-bearing runtime environment binding is removed.
  • The gateway contract requires stripping untrusted inbound X-Clearfolio-* headers before adding canonical signed claims.

Test-first evidence

The bounded history includes independent RED→GREEN repairs for tenant-scoped listing, fail-closed adapter defaults, atomic delete/retry, stale secondary indexes, UUID collision, delete-versus-replacement races, and deterministic monitor coordination.

The latest review found that tenant-scoped methods threw NullPointerException when an internal or modular caller omitted the job UUID. Test commit 6f522f123465a3fff5de6d47963125097f8da03b added InMemoryConversionJobRepositoryMissingIdentifierTest before production changes. A Java 21 exact-source probe confirmed all three prior paths threw NullPointerException. Commits 3db237778536ba9f9ece7eb076559d45dfbfe118 and 60518a88005b629128fed3bbcc77daf1772b9abf made scoped lookup/delete/retry return the stable concealed outcomes; the same probe then passed and proved the stored owned job remained present. The temporary read-only RED workflow is absent from the final tree. Authoritative security documentation and CHANGELOG.md were updated in 3e519b2e7a937b833bcd25539eda65d47761bde6 and 7737503f44d70f5180f7098554030faba6c73b0b.

Exact-head status

Exact current head is 7737503f44d70f5180f7098554030faba6c73b0b, based on current parent #165 head 8ca895889abfa30e8fd6dbf0f80d6a467011e6fb.

This stacked pull request still has no authoritative exact-head GitHub Actions acceptance evidence because the repository's permanent PR workflows target main. Local Java 21 probes are supporting evidence only. No absent, queued, pending, cancelled, skipped-required, stale-head, local-only, or predecessor-head result is counted as passing.

Merge order and gate

Keep this PR draft. Parent #165 must integrate first. Then reconcile this bounded change onto the resulting protected main and require fresh exact-head Maven verification with zero missed production lines and branches, warning-free public Javadocs, CI, Security Scan, SAST, every fuzz job, CodeRabbit, Strix/OpenCode/Noema review, zero unresolved threads, a counted independent write-authorized approval, and every repository protection. Do not temporarily retarget to manufacture checks, use a legacy mutation fallback, weaken tests, or bypass branch protection.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46644e61-c893-4c80-8700-b0770e6afe1e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

Review clean stacked exact head f4ae8dd695afe1dd41decbc7e6b2a11d0ee5e461 relative to parent #165 exact head 5261356ac34e6545bce947ba0bcf2b1ce9f9be67. Focus on signed tenant-claim verification, least-privilege administrative permissions, tenant-scoped list/delete/retry behavior, cross-tenant concealment, mutation-boundary authorization, audit pseudonymization and key separation, config-tree secret loading, OpenAPI/deployment contracts, and tests. Distinguish this clean replacement from superseded closed PR #172 and ignore its removed stale automated reverts. Keep draft: parent #165 must merge first, then this change must be retargeted/revalidated against protected main with exact-head CI, Security Scan, SAST, fuzz, repository policy, and independent approval.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

@seonghobae: I will review the clean exact head f4ae8dd695afe1dd41decbc7e6b2a11d0ee5e461 relative to parent #165 at 5261356ac34e6545bce947ba0bcf2b1ce9f9be67.

I will focus on signed tenant claims, least-privilege permissions, tenant isolation, mutation-boundary authorization, audit pseudonymization, config-tree secret loading, API and deployment contracts, and tests. I will exclude superseded #172 and its removed stale reverts. I will keep #266 as a draft pending the parent merge and fresh protected-main validation.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent @cwl-noema-review Independently review clean stacked exact head f4ae8dd695afe1dd41decbc7e6b2a11d0ee5e461 relative to parent #165 head 5261356ac34e6545bce947ba0bcf2b1ce9f9be67. Verify signed-claim fail-closed behavior, 32-byte verifier policy, least-privilege permissions, tenant-scoped object and mutation authorization, missing/cross-tenant concealment, privacy-safe domain-separated administrative audit evidence, config-tree secret contract, realistic negative security tests, and compatibility behavior. Treat closed #172 and its removed stale automated reverts as superseded. Keep this draft and do not recommend merge until #165 is integrated and the exact change is revalidated against protected main with all required checks and independent approval.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent Fix the valid tenant-mutation atomicity gap on exact stacked head f4ae8dd695afe1dd41decbc7e6b2a11d0ee5e461 test-first, without weakening authorization, privacy, coverage, Javadoc, or repository gates.

The current DefaultDocumentConversionService.deleteJob(UUID, TenantContext) performs repository.findByTenantAndId(...) and then an unscoped deleteById(...); tenant-aware retry performs findByTenantAndId(...) and then unscoped stateStore.retryDeadLettered(...). This contradicts the PR's documented claim that selection and mutation occur in one persistence boundary and leaves a read-then-mutate interval for future durable/mutable adapters.

Add explicit tenant-scoped mutation contracts at the repository/state-store boundary and implement them atomically in InMemoryConversionJobRepository (for example conditional tenant-scoped delete and tenant-scoped dead-letter retry under the map/job synchronization boundary). Make DefaultDocumentConversionService call only those scoped mutation APIs for administrative delete/retry. Preserve the legacy unscoped compatibility methods for non-administrative callers, but do not let the tenant-aware service methods delegate through an unscoped mutation.

Add deterministic tests that prove: cross-tenant and missing targets never call or apply unscoped mutation; an ownership change/replacement between a preliminary observation and mutation cannot delete or retry a different-tenant job; accepted retry records the actor fingerprint and enqueues exactly once; failed/not-eligible paths do not enqueue; tenant-scoped delete removes the content-hash index only for the actually deleted owned job. Add beginner-readable public Javadocs and update the administrative authorization ADR so its atomicity claim exactly matches the implementation.

Run mvn -B --no-transfer-progress verify and all repository script tests. Keep this PR draft and stacked on #165. Commit only after all tests pass, JaCoCo reports zero missed production lines/branches, public Javadocs are warning-free, and no unrelated parent/privacy/PDF.js/config-tree changes are reverted. Report the exact new head and evidence in the PR conversation.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Manual exact-head review found one valid tenant-isolation layering gap. AdminController.getAllJobs() calls the global DocumentConversionService.getAllJobs() and only then filters ConversionJob objects by tenant in the web layer. Although the response is currently filtered, this materializes every tenant's jobs above the persistence/service authorization boundary and makes a future durable adapter prone to accidental cross-tenant disclosure, excessive reads, and audit ambiguity. Keep this draft stacked on #165 and address the issue test-first: add an explicit tenant-scoped repository/service query contract, make the default production service delegate the authenticated tenant identifier to that boundary, and make AdminController consume only the tenant-scoped result before applying the optional dead-letter filter. Add tests proving the controller never invokes the global list path, the service passes only the authenticated tenant ID, and the in-memory repository cannot return other tenants' jobs. Preserve getAllJobs() only where backward compatibility or non-administrative internals require it; do not weaken signed-claim, concealment, audit, coverage, or parent privacy behavior. Run full local mvn -B --no-transfer-progress verify and report the exact new head.

Copy link
Copy Markdown
Collaborator Author

@opencode-agent Implement the exact-head tenant-scoped administrative list finding in review 4861867976 test-first on current clean stacked head f4ae8dd695afe1dd41decbc7e6b2a11d0ee5e461. Add failing tests first, then the minimal repository/service/controller boundary that prevents the admin web layer from materializing all tenants' jobs. Required proofs: the controller calls only a tenant-scoped service method; the concrete service delegates only the authenticated tenant ID; the in-memory repository returns no other-tenant jobs; null or absent tenant context fails closed; existing global listing remains only for backward-compatible non-admin callers. Preserve parent #165 privacy behavior, signed-claim verification, least-privilege permissions, 404 concealment, audit pseudonymization, config-tree secret loading, complete beginner-readable Javadocs, and zero missed production lines/branches. Run mvn -B --no-transfer-progress verify; commit only after all tests and gates pass. Do not rewrite inherited parent files unrelated to this bounded fix. Report red-test evidence, exact new head, and final command evidence.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

Review stacked exact head c3358629e9361a946a4b7106a5efaf141bf946b2 relative to its current parent branch. Focus on the tenant-scoped repository boundary added after the prior review request: AdminController must never invoke the global job-list path, DocumentConversionService.getJobsForTenant must delegate only to findAllByTenantId, dead-letter filtering must occur after tenant scoping, and delete/retry must remain tenant-authorized at the mutation boundary. Also verify signed-claim fail-closed behavior, privacy-safe administrative audit evidence, complete public Javadocs, and deterministic tests. Keep draft and do not recommend merge until parent #165 is integrated and this slice is restacked and revalidated on protected main.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

@seonghobae: I will review exact head c3358629e9361a946a4b7106a5efaf141bf946b2 against its current parent branch.

I will verify that AdminController uses no global list path, getJobsForTenant uses only findAllByTenantId, and dead-letter filtering occurs after tenant scoping. I will also review tenant-authorized delete and retry mutations, signed-claim fail-closed behavior, privacy-safe audit evidence, config-tree secret loading, public Javadocs, and deterministic tests.

I will keep #266 as a draft. I will not recommend merge before #165 merges and this change is restacked and revalidated on protected main.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Collaborator Author

Manual exact-head review found one still-valid HIGH authorization blocker on c3358629e9361a946a4b7106a5efaf141bf946b2.

DefaultDocumentConversionService.deleteJob(jobId, tenantContext) performs repository.findByTenantAndId(...) and then calls the unscoped deleteJob(jobId). Likewise, tenant retry performs a scoped repository read and then calls stateStore.retryDeadLettered(jobId, operatorId), whose contract contains no tenant predicate. The scoped read and mutation are separate operations, so the current code does not fully satisfy its stated mutation-boundary authorization contract; an adapter or concurrent replacement can change the job associated with the identifier between selection and mutation.

Keep this PR draft. After parent #165 is integrated, fix test-first by adding fail-closed tenant-predicate mutation contracts to the repository/state-store boundary, implementing them atomically in the in-memory adapter, and making durable adapters explicitly implement or reject the operation. Add deterministic concurrency/replacement tests proving that a same-identifier cross-tenant replacement cannot be deleted, retried, transitioned, or enqueued. Re-run the exact restacked head through full CI, zero-missed line/branch coverage, security scans, fuzzing, Strix/OpenCode/Noema, and independent approval before merge.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Please review exact current head 5bf134e606c009584a8fc78a7a45510ad41e65fc as a stacked draft. Focus on the new fail-closed modular boundary: DocumentConversionService tenant-aware delete and retry defaults must not call getJob, global delete, or legacy retry, while DefaultDocumentConversionService must continue to implement repository-scoped tenant selection and mutation. Verify the red→green regression test, consolidated changelog, authoritative security contract, tenant isolation, audit privacy, and absence of unrelated parent reversions. Do not approve or count missing Actions evidence as passing; this head still requires reconciliation onto #165's final integrated head and fresh exact-head gates.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Exact HEAD =='
git rev-parse HEAD
git status --short
git show -s --format='%H%n%P%n%s' HEAD

printf '%s\n' '== Candidate files =='
fd -t f -a . | rg '(DocumentConversionService|ConversionJobRepository|ConversionJobStateStore|InMemoryConversionJobRepository|TenantScopedServiceDefaultsTest|AdminController|CHANGELOG|AGENTS|ADR|Authorization|Security)'

printf '%s\n' '== Symbol outlines =='
for f in $(fd -t f -a . | rg '(DocumentConversionService|DefaultDocumentConversionService|ConversionJobRepository|ConversionJobStateStore|InMemoryConversionJobRepository|TenantScopedServiceDefaultsTest|AdminController)' | head -40); do
  echo "--- $f"
  ast-grep outline "$f" --items all --view compact 2>/dev/null || true
done

printf '%s\n' '== Targeted symbol references =='
rg -n -C 4 \
  '(deleteJob\s*\([^)]*TenantContext|retryDeadLettered\s*\([^)]*TenantContext|getJobsForTenant\s*\(|findByTenantAndId\s*\(|findAllByTenantId\s*\(|deleteByTenant|retryDeadLetteredByTenant|deleteById\s*\(|retryDeadLettered\s*\(|getJob\s*\()' \
  . -g '!**/target/**' -g '!**/.git/**'

printf '%s\n' '== Relevant head commits =='
git log --oneline --decorate -12

printf '%s\n' '== Changed files since likely stack parent =='
git diff --name-status 5261356ac34e6545bce947ba0bcf2b1ce9f9be67..HEAD 2>/dev/null || \
  git diff --name-status HEAD~12..HEAD

printf '%s\n' '== Changelog and security-document references =='
rg -n -C 3 \
  '(tenant-aware|tenant-scoped|fail-closed|administrative|atomic|HMAC|pseudonym|delete|retry)' \
  CHANGELOG.md AGENTS.md . -g '*.md' -g '!**/target/**' -g '!**/.git/**'

Length of output: 200290


🏁 Script executed:

#!/bin/bash
set -euo pipefail

find_one() {
  local name="$1"
  fd -t f -a -E target "$name" . | head -1
}

service_iface="$(find_one 'DocumentConversionService.java')"
service_impl="$(find_one 'DefaultDocumentConversionService.java')"
repo_iface="$(find_one 'ConversionJobRepository.java')"
state_iface="$(find_one 'ConversionJobStateStore.java')"
repo_impl="$(find_one 'InMemoryConversionJobRepository.java')"
defaults_test="$(find_one 'TenantScopedServiceDefaultsTest.java')"
admin_controller="$(find_one 'AdminController.java')"
adr="docs/security/2026-08-05-administrative-authorization.md"

printf '== paths ==\n%s\n' \
  "$service_iface" "$service_impl" "$repo_iface" "$state_iface" "$repo_impl" \
  "$defaults_test" "$admin_controller" "$adr"

printf '%s\n' '== Service interface: tenant-aware methods and defaults =='
rg -n -C 12 \
  '(default .*deleteJob|default .*retryDeadLettered|deleteJob\s*\(|retryDeadLettered\s*\(|getJob\s*\()' \
  "$service_iface"

printf '%s\n' '== Default service: tenant-aware overrides and direct calls =='
rg -n -C 14 \
  '(deleteJob\s*\([^)]*TenantContext|retryDeadLettered\s*\([^)]*TenantContext|findByTenantAndId|deleteById|retryDeadLettered\s*\(|getJob\s*\()' \
  "$service_impl"

printf '%s\n' '== Repository and state-store contracts =='
rg -n -C 10 \
  '(findByTenantAndId|delete|retryDeadLettered|TenantContext|tenant)' \
  "$repo_iface" "$state_iface" "$repo_impl"

printf '%s\n' '== Regression test =='
cat -n "$defaults_test"

printf '%s\n' '== Admin controller list/mutation calls =='
rg -n -C 8 \
  '(getAllJobs|getJobsForTenant|deleteJob\s*\(|retryDeadLettered\s*\()' \
  "$admin_controller"

printf '%s\n' '== Administrative ADR relevant section =='
sed -n '19,66p' "$adr"

printf '%s\n' '== Exact final commits =='
git show --stat --oneline 8fbf2d118b94aff9f0a4657aaf098217ffa4fca9
git diff --check 8fbf2d118b94aff9f0a4657aaf098217ffa4fca9^ 8fbf2d118b94aff9f0a4657aaf098217ffa4fca9
git show --format=fuller --find-renames --find-copies --stat 5bf134e606c009584a8fc78a7a45510ad41e65fc
git diff --check 5bf134e606c009584a8fc78a7a45510ad41e65fc^ 5bf134e606c009584a8fc78a7a45510ad41e65fc

Length of output: 54624


🏁 Script executed:

#!/bin/bash
set -euo pipefail

iface="$(fd -t f -a -E target -p 'DocumentConversionService.java' . | rg '/DocumentConversionService\.java$' | head -1)"
impl="$(fd -t f -a -E target -p 'DefaultDocumentConversionService.java' . | rg '/DefaultDocumentConversionService\.java$' | head -1)"
repo="$(fd -t f -a -E target -p 'ConversionJobRepository.java' . | rg '/ConversionJobRepository\.java$' | head -1)"
state="$(fd -t f -a -E target -p 'ConversionJobStateStore.java' . | rg '/ConversionJobStateStore\.java$' | head -1)"
inmemory="$(fd -t f -a -E target -p 'InMemoryConversionJobRepository.java' . | rg '/InMemoryConversionJobRepository\.java$' | head -1)"

printf '%s\n' '== Exact HEAD and interface =='
git rev-parse HEAD
cat -n "$iface" | sed -n '1,230p'

printf '%s\n' '== Concrete tenant-aware mutation paths =='
cat -n "$impl" | sed -n '214,310p'

printf '%s\n' '== Available mutation contracts =='
cat -n "$repo" | sed -n '60,135p'
cat -n "$state" | sed -n '1,80p'
cat -n "$inmemory" | sed -n '138,152p;232,253p'

printf '%s\n' '== Tenant-aware mutation call graph, excluding tests =='
rg -n --type java -C 2 \
  '(deleteJob\s*\([^,]+,\s*(tenantContext|context)|retryDeadLettered\s*\([^,]+,\s*(tenantContext|context)|findByTenantAndId\s*\(|deleteById\s*\(|stateStore\.retryDeadLettered\s*\()' \
  src/main/java

printf '%s\n' '== Stack file summary and key-file diffs from declared parent =='
git diff --stat 5261356ac34e6545bce947ba0bcf2b1ce9f9be67..HEAD
git diff --name-status 5261356ac34e6545bce947ba0bcf2b1ce9f9be67..HEAD
git diff --numstat 5261356ac34e6545bce947ba0bcf2b1ce9f9be67..HEAD -- \
  src/main/java/com/clearfolio/viewer/service/DocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/test/java/com/clearfolio/viewer/service/TenantScopedServiceDefaultsTest.java \
  docs/security/2026-08-05-administrative-authorization.md \
  CHANGELOG.md

Length of output: 27774


@seonghobae I found one blocking authorization defect on exact head 5bf134e606c009584a8fc78a7a45510ad41e65fc.

HIGH — The concrete tenant-aware mutations are still not atomic.

DocumentConversionService now has correct fail-closed defaults. Lines 78-100 return NOT_FOUND or false without calling getJob, legacy delete, or legacy retry. TenantScopedServiceDefaultsTest verifies this contract.

However, DefaultDocumentConversionService does not meet the same contract:

  • At Lines 229-234, deleteJob(UUID, TenantContext) reads with findByTenantAndId(...) and then calls legacy deleteJob(UUID). That method calls unscoped repository.deleteById(...).
  • At Lines 264-268 and Line 296, retryDeadLettered(UUID, TenantContext, String) reads with findByTenantAndId(...) and then calls unscoped ConversionJobStateStore.retryDeadLettered(UUID, String).
  • ConversionJobRepository and ConversionJobStateStore do not expose a tenant-predicate delete or retry operation.

A replacement of the same jobId after the scoped read can therefore delete or transition a job owned by another tenant. This contradicts the interface Javadocs and the administrative authorization ADR claim that selection and mutation occur within one persistence boundary.

Add explicit tenant-scoped mutation contracts. Implement them atomically in InMemoryConversionJobRepository. Make the tenant-aware service methods use only those contracts. Add deterministic same-ID replacement tests for delete and retry. The tests must also prove that failed or cross-tenant operations do not enqueue work.

The compatibility-default regression test, tenant-scoped list boundary, and fail-closed modular intent are valid. I found no unrelated parent privacy or PDF.js reversion in the inspected stack file changes.

I did not approve this draft. Missing GitHub Actions evidence is not passing evidence. Keep the draft stacked until #165 integrates, then restack and run fresh exact-head required checks.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai @opencode-agent @cwl-noema-review Review exact stacked head 310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0 relative to its current parent branch. A deterministic red test at b47464f3c1a2c75821dba121e090f27b089e178d exposed a cross-map race in which tenant deletion removed the primary UUID record, a concurrent same-UUID replacement installed a valid tenant-content index, and delayed delete cleanup then removed that replacement index. The green implementation at e059824675038fc30e52dc2f356c81b63b6cf3bd serializes save, find-or-store, indexed lookup, tenant-scoped delete, and compatibility delete under one shared primary/index critical section; 310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0 includes authoritative documentation and changelog evidence.

Verify the concurrency test is deterministic and non-flaky; the lock is reentrant for blank-hash find-or-store; no tenant, deduplication, lifecycle, retry, or standalone-adapter contract regresses; public Javadocs and 100% production line/branch coverage remain intact; and no raw identifiers enter audit evidence. Run mvn -B --no-transfer-progress verify and repository script tests in an environment that can execute the stacked head. Keep the PR draft and do not recommend merge before #165 integrates and this slice is restacked and revalidated against protected main with all required checks and independent approval.

seonghobae commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Exact current stacked head 310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0 closes the previously reported administrative authorization interval and cross-map stale-index race:

  • list data crosses the repository boundary only after tenant scoping;
  • delete and retry use fail-closed tenant-scoped repository/state-store operations;
  • same-ID replacement before mutation and stale tenant/hash index paths fail closed;
  • primary UUID and tenant-content-index mutations are serialized under one critical section, including the deterministic delayed-cleanup replacement test.

A separate end-to-end document-lifecycle gap remains and is specified in #263 rather than being hidden by this narrower persistence claim. After deleteByTenantAndId(...) returns, deleteArtifact(jobId) is a best-effort side effect keyed only by UUID; after retryDeadLetteredForTenant(...) returns ACCEPTED, enqueue is also keyed only by UUID. Because the in-memory save(...) contract still permits same-UUID replacement after deletion, a post-mutation replacement or identifier reuse can race artifact cleanup or worker execution unless identity non-reuse, lifecycle generation, and a deletion/dispatch receipt or transactional outbox are added. Artifact cleanup exceptions are logged and swallowed, so the current boolean result can overstate physical cleanup.

Keep #266 draft and stacked on #165. Its atomicity claim is limited to tenant-scoped metadata/state mutation and secondary-index consistency. Implement durable, truthful, idempotent deletion and generation-bound dispatch in #263 after #264/#266 integrate.

@opencode-agent @cwl-noema-review @coderabbitai review exact current head 310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0. Verify that the prior HIGH read-then-unscoped-mutate and cross-map race findings are closed, that the shared critical section does not introduce deadlock or nested-lock regressions, and that the PR does not overclaim atomicity across artifact cleanup or worker enqueue. Do not count absent stacked-head Actions evidence as passing and do not approve before reconciliation onto #165's final integrated head.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected='310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0'
actual="$(git rev-parse HEAD)"
printf '== exact head ==\nexpected=%s\nactual=%s\n' "$expected" "$actual"
git show -s --format='commit=%H%nparents=%P%nsubject=%s' HEAD

printf '\n== commit range since red test ==\n'
git log --oneline b47464f3c1a2c75821dba121e090f27b089e178d^..HEAD

printf '\n== changed paths from declared stack parent ==\n'
git diff --name-status 5261356ac34e6545bce947ba0bcf2b1ce9f9be67..HEAD

printf '\n== candidate files ==\n'
fd -t f -a -E target . | rg '(InMemoryConversionJobRepository|ConversionJobRepository|ConversionJobStateStore|DefaultDocumentConversionService|DocumentConversionService|Atomic|Concurrency|Stale|Collision|Audit|AdministrativeAudit|CHANGELOG|administrative-authorization)' | sort

printf '\n== production outlines ==\n'
for f in \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java \
  src/main/java/com/clearfolio/viewer/security/AuditPseudonymizer.java
do
  if [ -f "$f" ]; then
    echo "--- $f"
    ast-grep outline "$f" --items all --view compact || true
  fi
done

printf '\n== shared-lock/index/mutation implementation ==\n'
rg -n -C 12 --type java \
  '(synchronized|Lock|Reentrant|primary|contentHash|tenantContent|findOrStore|findAllByTenantId|deleteByTenantAndId|deleteById|retryDeadLetteredForTenant|retryDeadLettered)' \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

printf '\n== exact concurrency and relevant regression tests ==\n'
fd -t f -a -E target . | rg '(Atomic|Concurrency|Stale|Collision|TenantScope|InMemoryConversionJobRepository).*Test\.java$' | sort | while read -r f; do
  echo "--- $f"
  rg -n -C 8 '(CountDownLatch|CyclicBarrier|await\(|join\(|Executor|replacement|sameUuid|sameUuid|deleteByTenantAndId|findOrStore|blank|contentHash|retryDeadLettered)' "$f" || true
done

printf '\n== public Javadoc on modified API symbols ==\n'
git diff --unified=8 e059824675038fc30e52dc2f356c81b63b6cf3bd^..HEAD -- \
  src/main/java \
  src/test/java \
  docs/security/2026-08-05-administrative-authorization.md \
  CHANGELOG.md

printf '\n== audit identifier flow ==\n'
rg -n -C 4 --type java \
  '(AdministrativeAudit|actorFingerprint|fingerprint|tenantId\(\)|subject\(\)|jobId|contentHash|filename|message)' \
  src/main/java/com/clearfolio/viewer/audit \
  src/main/java/com/clearfolio/viewer/security \
  src/main/java/com/clearfolio/viewer/controller

printf '\n== whitespace errors ==\n'
git diff --check 5261356ac34e6545bce947ba0bcf2b1ce9f9be67..HEAD

Length of output: 188142


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== exact concurrency test =='
cat -n src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java

printf '%s\n' '== complete repository critical-section and index helpers =='
sed -n '1,360p' src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java

printf '%s\n' '== complete administrative audit logger and tests =='
cat -n src/main/java/com/clearfolio/viewer/audit/AdministrativeAuditLogger.java
cat -n src/test/java/com/clearfolio/viewer/audit/AdministrativeAuditLoggerTest.java
cat -n src/test/java/com/clearfolio/viewer/security/AuditPseudonymizerTest.java

printf '%s\n' '== audit contract wording =='
rg -n -C 8 -i \
  '(audit evidence|audit output|raw|identifier|jobId|fingerprint|pseudonym)' \
  docs/security/2026-08-05-administrative-authorization.md \
  AGENTS.md CHANGELOG.md

printf '%s\n' '== retry and lifecycle synchronization paths =='
sed -n '180,325p' src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java
sed -n '1,240p' src/main/java/com/clearfolio/viewer/repository/RepositoryBackedConversionJobStateStore.java
cat -n src/test/java/com/clearfolio/viewer/repository/RepositoryBackedConversionJobStateStoreTest.java

printf '%s\n' '== exact parent-relative diff for concurrency test and repository =='
git diff --unified=20 e059824675038fc30e52dc2f356c81b63b6cf3bd^..310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0 -- \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
  docs/security/2026-08-05-administrative-authorization.md \
  CHANGELOG.md

printf '%s\n' '== direct structured audit logging calls outside the administrative logger =='
rg -n -C 3 --type java \
  '(LOGGER\.(info|warn|error|debug|trace)\(|logger\.(info|warn|error|debug|trace)\()' \
  src/main/java/com/clearfolio/viewer/audit \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java \
  src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java

Length of output: 103781


@seonghobae I found one issue on exact head 310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0.

MEDIUM — The replacement-race regression test is not deterministic.

InMemoryConversionJobRepositoryConcurrencyTest.tenantDeleteCannotRemoveTheIndexOfAConcurrentReplacement starts the replacement save at Line 65. It then waits one second at Lines 66-69. If the replacement does not start in that interval, the test continues at Line 74 and releases deletion.

In the pre-fix implementation, the replacement can start only after Line 74. Delete cleanup then completes before the replacement index is written. The final lookup passes. The test therefore does not always reproduce the old primary-map/secondary-index interleaving.

Make the test require the replacement operation to reach its index-write point before delete cleanup continues. The fixed implementation blocks that operation on jobIndexLock. Therefore, use a second deterministic coordination point that can observe the blocked save without allowing the test to continue solely on a timeout. Alternatively, test the lock boundary directly with a controlled repository hook. Keep a bounded timeout for deadlock detection.

I verified these static properties:

  • jobIndexLock covers save, findOrStoreByContentHash, findByTenantAndContentHash, deleteByTenantAndId, and deleteById.
  • findOrStoreByContentHash can call save for a blank hash while holding jobIndexLock. Java intrinsic monitors are reentrant.
  • Tenant-scoped delete removes the primary record and its index while it holds the same lock.
  • Retry remains tenant-scoped through ConcurrentHashMap.computeIfPresent. It cannot mutate a same-UUID replacement concurrently with the map operation.
  • The audit logger emits raw job UUIDs. This matches the explicit contract in docs/security/2026-08-05-administrative-authorization.md, which permits an opaque job UUID. I do not treat this as a finding.
  • I found no inspected regression in tenant scoping, deduplication, lifecycle behavior, compatibility defaults, or audit pseudonymization.
  • Public Javadoc and documentation updates are present. git diff --check reported no whitespace errors.

I could not run Maven, repository scripts, JaCoCo, or GitHub Actions in this environment. The claimed 100% coverage and runtime test results are unverified. Keep this PR draft. After #165 integrates, restack on protected main and collect fresh exact-head required checks before approval or merge.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected='d4eb774aed8cf897cdf7c8d4fac5211c82d71d9d'
echo '== Head =='
git rev-parse HEAD
git show -s --format='%H%n%P%n%s' HEAD
if [ "$(git rev-parse HEAD)" != "$expected" ]; then
  echo "WARNING: checked-out head differs from requested $expected"
fi

echo '== Candidate files =='
fd -t f -a -E target . | rg \
'(AdminController|DocumentConversionService|DefaultDocumentConversionService|ConversionJobRepository|ConversionJobStateStore|InMemoryConversionJobRepository|TenantScoped|Atomic|Stale|Collision|administrative-authorization|CHANGELOG)' | sort

echo '== Production outlines =='
for f in \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/controller/AdminController.java; do
  if [ -f "$f" ]; then
    echo "--- $f"
    ast-grep outline "$f" --items all --view compact || true
  fi
done

echo '== Scoped mutations and side effects =='
rg -n --type java -C 10 \
'(deleteByTenantAndId\s*\(|retryDeadLetteredForTenant\s*\(|findByTenantAndId\s*\(|deleteArtifact\s*\(|enqueue\s*\(|deleteById\s*\(|retryDeadLettered\s*\()' \
src/main/java

echo '== In-memory shared synchronization and index handling =='
rg -n -C 12 \
'(synchronized|lock|contentHash|tenantContent|findOrStore|deleteByTenantAndId|deleteById|save\s*\()' \
src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java

echo '== Focused tests =='
fd -t f -a -E target . | rg \
'(TenantScopedAtomic|Stale|Collision|Concurrent|InMemoryConversionJobRepository.*Test|AdminController.*Test).*\.java$' \
| sort | while IFS= read -r f; do
  echo "--- $f"
  rg -n -C 4 \
  '(deleteByTenantAndId|retryDeadLetteredForTenant|same.UUID|replacement|stale|artifact|enqueue|latch|await|assert)' "$f" || true
done

echo '== Documentation atomicity wording =='
rg -n -C 5 \
'(atomic|artifact|enqueue|worker|lifecycle|tenant-scoped|deleteByTenantAndId|retryDeadLetteredForTenant)' \
docs/security CHANGELOG.md AGENTS.md 2>/dev/null || true

echo '== Diff integrity and commits =='
git diff --check HEAD^ HEAD
git show --stat --oneline HEAD
git log --oneline -12

Length of output: 192532


@seonghobae The requested exact-head verification is inconclusive.

The sandbox checked out 310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0, not d4eb774aed8cf897cdf7c8d4fac5211c82d71d9d. The requested commit is an ancestor. I cannot treat a descendant inspection as exact-head verification.

The inspected descendant closes the prior HIGH finding:

  • DefaultDocumentConversionService.deleteJob(UUID, TenantContext) calls deleteByTenantAndId(...). It calls deleteArtifact(jobId) only after the scoped deletion succeeds.
  • DefaultDocumentConversionService.retryDeadLettered(UUID, TenantContext, String) calls retryDeadLetteredForTenant(...). It enqueues only for ACCEPTED.
  • The tenant-aware default contracts fail closed. They do not invoke global lookup or legacy mutations.
  • InMemoryConversionJobRepository applies primary-map and tenant-content-index updates under jobIndexLock.
  • The stale-observation tests reject delete and retry against a same-UUID replacement owned by another tenant.
  • The concurrency test uses latches, bounded waits, and a final index assertion. It verifies that deletion cleanup cannot remove the index for a concurrent same-UUID replacement.

The documentation also limits the atomicity claim to tenant-scoped persistence mutation. It states that artifact cleanup occurs after deletion. It states that worker enqueue occurs after the atomic retry transition. This matches the lifecycle limitation described for #263.

I found no evidence in the inspected descendant that the administrative contract claims atomic artifact cleanup or atomic worker dispatch. The remaining UUID-reuse lifecycle risk remains out of scope for this PR and belongs in #263.

I did not run Maven or repository script tests. The environment cannot execute repository code. Exact-head Actions evidence remains absent. Keep #266 as a draft and stacked on #165. Retry this review after the requested head is checked out, then restack on protected main after #165 integrates and collect fresh exact-head evidence before approval.


✏️ Learnings added
Learnt from: seonghobae
URL: https://github.com/ContextualWisdomLab/clearfolio/pull/266

Timestamp: 2026-08-05T11:14:15.150Z
Learning: For pull request `#266`, the atomicity claim is limited to tenant-scoped metadata deletion and retry state transition. Post-mutation `deleteArtifact(jobId)` and `ConversionWorker.enqueue(jobId)` are UUID-only side effects and are not atomic with the persistence mutation. The durable identity-non-reuse, lifecycle-generation, deletion receipt or outbox, and truthful idempotent cleanup work is tracked in GitHub issue `#263`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected='310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0'
actual="$(git rev-parse HEAD)"
printf '== exact head ==\nexpected=%s\nactual=%s\n' "$expected" "$actual"
git show -s --format='commit=%H%nparents=%P%nsubject=%s' HEAD

printf '\n== focused file map ==\n'
fd -t f -a -E target . | rg \
'(InMemoryConversionJobRepository|ConversionJobRepository|ConversionJobStateStore|DefaultDocumentConversionService|DocumentConversionService|InMemoryConversionJobRepositoryConcurrencyTest|TenantScopedAtomic|administrative-authorization|CHANGELOG)' \
| sort

printf '\n== production outlines ==\n'
for f in \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java; do
  test -f "$f" || continue
  echo "--- $f"
  ast-grep outline "$f" --items all --view compact
done

printf '\n== shared critical section and scoped mutations ==\n'
rg -n -C 12 --type java \
  '(jobIndexLock|synchronized|findOrStoreByContentHash|findByTenantAndContentHash|deleteByTenantAndId|deleteById|retryDeadLetteredForTenant|retryDeadLettered|save\s*\()' \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

printf '\n== concurrency test ==\n'
cat -n src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java

printf '\n== atomic mutation tests ==\n'
for f in \
 src/test/java/com/clearfolio/viewer/service/TenantScopedAtomicMutationBoundaryTest.java \
 src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java; do
  test -f "$f" || continue
  echo "--- $f"
  rg -n -C 5 '(replacement|CountDownLatch|await|deleteByTenantAndId|retryDeadLetteredForTenant|enqueue|artifact|assert)' "$f"
done

printf '\n== atomicity documentation ==\n'
rg -n -C 6 -i \
  '(atomic|artifact|enqueue|worker|UUID|lifecycle|tenant-scoped|deleteByTenantAndId|retryDeadLetteredForTenant)' \
  docs/security/2026-08-05-administrative-authorization.md CHANGELOG.md AGENTS.md

printf '\n== post-fix diff and whitespace ==\n'
git diff --unified=12 e059824675038fc30e52dc2f356c81b63b6cf3bd^..HEAD -- \
 src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
 src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
 docs/security/2026-08-05-administrative-authorization.md CHANGELOG.md
git diff --check 5261356ac34e6545bce947ba0bcf2b1ce9f9be67..HEAD

Length of output: 5101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== production outlines =='
for f in \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobRepository.java \
  src/main/java/com/clearfolio/viewer/repository/ConversionJobStateStore.java \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java; do
  echo "--- $f"
  ast-grep outline "$f" --items all --view signatures
done

printf '%s\n' '== repository: lock, index, delete, retry paths =='
rg -n -C 10 --type java \
  '(jobIndexLock|synchronized|findOrStoreByContentHash|findByTenantAndContentHash|deleteByTenantAndId|deleteById|retryDeadLetteredForTenant|retryDeadLettered|save\s*\()' \
  src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java
sed -n '1,380p' src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java

printf '%s\n' '== service: scoped mutation and side-effect ordering =='
rg -n -C 12 --type java \
  '(deleteJob\s*\([^)]*TenantContext|retryDeadLettered\s*\([^)]*TenantContext|deleteByTenantAndId|retryDeadLetteredForTenant|deleteArtifact|enqueue)' \
  src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java

printf '%s\n' '== deterministic concurrency test =='
cat -n src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java

printf '%s\n' '== atomic mutation boundary tests =='
for f in \
 src/test/java/com/clearfolio/viewer/service/TenantScopedAtomicMutationBoundaryTest.java \
 src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryTenantScopeTest.java; do
  echo "--- $f"
  rg -n -C 6 '(replacement|CountDownLatch|await|deleteByTenantAndId|retryDeadLetteredForTenant|enqueue|artifact|assert)' "$f"
done

printf '%s\n' '== atomicity documentation =='
rg -n -C 6 -i \
  '(atomic|artifact|enqueue|worker|UUID|lifecycle|tenant-scoped|deleteByTenantAndId|retryDeadLetteredForTenant)' \
  docs/security/2026-08-05-administrative-authorization.md CHANGELOG.md AGENTS.md

printf '%s\n' '== post-fix diff and whitespace =='
git diff --unified=12 e059824675038fc30e52dc2f356c81b63b6cf3bd^..HEAD -- \
 src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java \
 src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java \
 docs/security/2026-08-05-administrative-authorization.md CHANGELOG.md
git diff --check 5261356ac34e6545bce947ba0bcf2b1ce9f9be67..HEAD

Length of output: 92842


@seonghobae I verified exact head 310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0.

The prior HIGH read-then-unscoped-mutate finding is closed.

  • Tenant-aware delete calls ConversionJobRepository.deleteByTenantAndId(...).
  • Tenant-aware retry calls ConversionJobStateStore.retryDeadLetteredForTenant(...).
  • The service performs artifact cleanup only after a successful scoped deletion.
  • The service enqueues work only after ACCEPTED.
  • The compatibility paths remain separate from tenant-aware administrative paths.

The shared jobIndexLock covers save, findOrStoreByContentHash, tenant-content indexed lookup, deleteByTenantAndId, and deleteById. This closes the identified primary-map and secondary-index cleanup interval. findOrStoreByContentHash can call save while it holds this monitor. Java intrinsic monitors are reentrant, so this nested call does not cause a self-deadlock.

The authorization ADR and changelog do not claim atomic artifact cleanup or worker dispatch. They state that cleanup and enqueue occur after the scoped persistence mutation. This matches the lifecycle limitation assigned to #263.

However, one MEDIUM test-quality finding remains.

InMemoryConversionJobRepositoryConcurrencyTest.tenantDeleteCannotRemoveTheIndexOfAConcurrentReplacement does not require the replacement save to reach its index operation before it releases delete cleanup.

At Lines 65-72, the test waits one second for replacementIndexReached, but treats a timeout as valid. At Line 74, it releases deletion regardless. In the pre-fix implementation, thread scheduling can delay the replacement until after deletion cleanup. The replacement index then remains valid, and the test passes without exercising the vulnerable interleaving.

Make the test establish a mandatory coordination point for the replacement operation before deletion continues. A controlled repository hook at the lock boundary is suitable. Keep bounded timeouts to detect deadlock. Do not use an optional timeout as proof that the replacement raced with cleanup.

I found no static regression in the tenant-scoped mutation boundary, index synchronization, or the stated atomicity scope. I could not run Maven, repository scripts, JaCoCo, or GitHub Actions in this environment. Absent exact-head CI remains unverified. Keep #266 draft and stacked on #165.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@opencode-agent @cwl-noema-review Independently review exact stacked head d6103aa9fafb7476216593fa4026500f099336a1 relative to its current parent branch. The current CodeRabbit MEDIUM finding on 310b2ee67dff58ef44b4c10c8b7b07ce878e3dc0 was valid: the replacement-race test could release delete cleanup after a timeout without proving replacement save had reached the intended contention point.

The exact-head fix removes timeout-as-success. It requires a replacement-task start handshake, verifies through Java 21 ThreadMXBean that the replacement thread is BLOCKED on a monitor owned by the paused delete thread, asserts replacement index work has not executed before release, and asserts it executes after release. A completed pre-release save is an immediate failure. The outer bounded timeout remains deadlock detection only. No production code, coverage threshold, security policy, or stack order changed.

Verify the test is deterministic and fails the pre-lock implementation rather than passing through scheduler delay; inspect monitor-owner evidence, cleanup in finally, interruption behavior, and absence of flaky timing acceptance. Keep this PR draft. Do not count absent stacked-head Actions evidence as passing and do not approve before #165 integrates, this branch is reconciled onto protected main, and every exact-head repository gate and independent approval succeeds.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected='d6103aa9fafb7476216593fa4026500f099336a1'
actual="$(git rev-parse HEAD)"
printf '== exact head ==\nexpected=%s\nactual=%s\n' "$expected" "$actual"
git show -s --format='commit=%H%nparents=%P%nsubject=%s' HEAD

printf '\n== target file map ==\n'
fd -t f -a -E target . | rg \
'(InMemoryConversionJobRepositoryConcurrencyTest|InMemoryConversionJobRepository|administrative-authorization|CHANGELOG)' | sort

test_file='src/test/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepositoryConcurrencyTest.java'
repo_file='src/main/java/com/clearfolio/viewer/repository/InMemoryConversionJobRepository.java'

printf '\n== test outline ==\n'
ast-grep outline "$test_file" --items all --view expanded

printf '\n== complete replacement-race test ==\n'
cat -n "$test_file"

printf '\n== repository synchronization boundary ==\n'
rg -n -C 14 --type java \
  '(jobIndexLock|synchronized|save\s*\(|deleteByTenantAndId\s*\(|remove.*index|contentHash|tenantContent)' \
  "$repo_file"

printf '\n== test change at exact head ==\n'
git diff --unified=30 HEAD^..HEAD -- "$test_file" "$repo_file" \
  docs/security/2026-08-05-administrative-authorization.md CHANGELOG.md

printf '\n== pre-lock and pre-fix test history ==\n'
git show --format=fuller --stat b47464f3c1a2c75821dba121e090f27b089e178d -- "$test_file"
git show b47464f3c1a2c75821dba121e090f27b089e178d:"$test_file" | cat -n
printf '\n== shared-lock introduction ==\n'
git show --unified=25 e059824675038fc30e52dc2f356c81b63b6cf3bd -- "$repo_file" "$test_file"

printf '\n== Java version and test dependencies ==\n'
rg -n -C 3 '(maven.compiler|<java.version>|source>|target>|ThreadMXBean|ThreadInfo)' \
  pom.xml .mvn src/test/java -g '!**/target/**' || true

printf '\n== cleanup and interruption APIs in test ==\n'
rg -n -C 8 '(finally|interrupt|InterruptedException|shutdown|awaitTermination|Future|ExecutionException|TimeoutException|ThreadMXBean)' \
  "$test_file"

printf '\n== whitespace ==\n'
git diff --check HEAD^..HEAD

Length of output: 57991


@seonghobae I verified exact head d6103aa9fafb7476216593fa4026500f099336a1.

I found no remaining static issue in the replacement-race regression test.

The test now establishes the required interleaving before it releases delete cleanup:

  • It waits for the delete operation to pause during secondary-index cleanup.
  • It waits for the replacement task to start.
  • It requires the replacement save to remain incomplete.
  • It requires the replacement thread to be BLOCKED.
  • It verifies that ThreadMXBean reports the paused delete thread as the blocking monitor owner.
  • It verifies that replacement index work has not started before release.
  • After release, it requires both deletion completion and replacement index work.

This closes the prior timeout-as-success defect. The two-second bound in assertBlockedByDeleteCriticalSection now fails when the replacement does not reach the required contention state. It does not accept scheduler delay as evidence.

The test also fails with the pre-lock implementation. In that implementation, replacement save(...) can complete while delete cleanup is paused. assertBlockedByDeleteCriticalSection(...) rejects the completed saveResult before cleanup is released. The delayed cleanup then cannot produce a false pass.

The monitor-owner check is correctly connected to the repository implementation. InMemoryConversionJobRepository.deleteByTenantAndId(...) holds jobIndexLock while it removes the primary record and its tenant-content index. InMemoryConversionJobRepository.save(...) uses the same monitor.

Cleanup is adequate:

  • The finally block always releases the delete latch.
  • The executor is interrupted and must terminate.
  • The latch helper restores interrupt status and fails the test on interruption.
  • A completed replacement save before release is an immediate failure.

No production code changed in this exact-head commit. git diff --check reported no whitespace errors. The project declares Java 21, which supports the Thread.threadId() and monitor-inspection APIs used by this test.

I did not run Maven, repository scripts, coverage, or GitHub Actions. Exact-head CI remains absent and unverified. Keep #266 as a draft and stacked on #165. Do not approve or merge until #165 integrates, this branch is reconciled with protected main, and fresh exact-head repository gates and independent approval succeed.


✏️ Learnings added
Learnt from: seonghobae
URL: https://github.com/ContextualWisdomLab/clearfolio/pull/266

Timestamp: 2026-08-05T11:26:55.906Z
Learning: For pull request `#266`, `InMemoryConversionJobRepositoryConcurrencyTest.tenantDeleteCannotRemoveTheIndexOfAConcurrentReplacement` must prove that the replacement `save(...)` is blocked on `InMemoryConversionJobRepository.jobIndexLock` owned by the paused tenant-scoped delete operation before cleanup is released. A bounded wait detects missing progress or deadlock; it must not count as proof that the required replacement/delete interleaving occurred.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

Supporting deterministic evidence for exact head d6103aa9fafb7476216593fa4026500f099336a1:

  • Java 21 syntax compilation of the changed test surface succeeded.
  • A bounded standalone probe reproduced the relevant old and fixed primary/index algorithms with the exact replacement-task handshake and monitor-owner assertion used by the test.
  • Observed output:
old=FAIL_AS_EXPECTED: replacement save completed before delete cleanup was released
fixed=PASS

The old no-lock path now fails immediately rather than being accepted after scheduler delay. The fixed shared-monitor path reaches Thread.State.BLOCKED, reports the paused delete thread as monitor owner through ThreadMXBean, and completes index work only after release. This is local supporting evidence only; repository Actions, coverage, Javadoc, security, fuzz, and protected-main review remain mandatory after the stack is reconciled.

Copy link
Copy Markdown
Collaborator Author

Superseded by #268. The implementation head remains d6103aa9fafb7476216593fa4026500f099336a1; only the stacked parent changed from contaminated #165 to clean replacement #267. Closing this duplicate preserves dependency order and prevents review or merge against the retired parent branch. No findings, tests, or security gates are discarded.

@seonghobae seonghobae closed this Aug 5, 2026
@seonghobae seonghobae reopened this Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as superseded by clean stacked replacement #268. The old stack remained attached to superseded parent #165 and therefore inherited that branch's unrelated Netty/SBOM workflow history. #268 targets clean privacy parent #267 and carries the tenant-scoped authorization implementation on the new dependency line. No review, check, or approval from this closed PR is reused as current evidence; #268 remains draft until #267 integrates, the stack is reconciled onto protected main, and all exact-head CI, coverage, Javadoc, security, fuzz, CodeRabbit, Strix/OpenCode/Noema, unresolved-thread, independent-approval, and branch-protection gates pass.

@seonghobae seonghobae closed this Aug 5, 2026
@seonghobae seonghobae reopened this Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as superseded by clean stacked replacement #268. The implementation head is the same, but #268 is based on clean privacy parent #267 rather than closed, polluted parent #165. #268 must remain Draft until Netty prerequisite #269 and privacy parent #267 integrate in order, then reconcile onto protected main and rerun every exact-head gate and independent approval. No evidence from this closed stack is reused.

@seonghobae seonghobae closed this Aug 5, 2026
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