Add dedicated writer for certificate validation - #1517
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a dedicated CertificateValidationWriter to persist certificate validation results (and an OCSP/CRL-driven ISSUED→REVOKED transition) via targeted database updates, aiming to avoid clobbering concurrently updated certificate columns (notably state).
Changes:
- Added
CertificateValidationWriterservice with transactional writer methods for validation results and conditional revocation transition. - Refactored
X509CertificateValidator(and an exception path inCertificateServiceImpl) to use targeted UPDATEs instead of entity saves for validation persistence and revocation transitions. - Added/updated tests covering concurrency-oriented behavior and transactional proxying.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/com/czertainly/core/service/writer/CertificateValidationWriter.java | New transactional writer service for targeted certificate validation/state updates. |
| src/main/java/com/czertainly/core/validation/certificate/X509CertificateValidator.java | Uses writer for validation persistence and conditional revocation; adds zero-row outcome classification/logging. |
| src/main/java/com/czertainly/core/service/impl/CertificateServiceImpl.java | Uses writer on validation failure path instead of saving the entity. |
| src/main/java/com/czertainly/core/dao/repository/CertificateRepository.java | Adds JPQL bulk update methods for validation result writes, conditional state transition, and state read-back. |
| src/test/java/com/czertainly/core/validation/certificate/X509CertificateValidatorZeroRowOutcomeTest.java | New unit test for zero-row outcome classification. |
| src/test/java/com/czertainly/core/service/writer/ValidationResultVsRevokeTest.java | New integration test simulating validate-vs-revoke race and ensuring state isn’t clobbered. |
| src/test/java/com/czertainly/core/service/writer/CertificateValidationWriterTxTest.java | New integration test verifying transactional proxying and writer behavior. |
| src/test/java/com/czertainly/core/service/writer/CertificateChainWriterTxTest.java | Test method renames and minor assertion message change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Add clearAutomatically/flushAutomatically to @Modifying on updateValidationResult and transitionIssuedToRevoked -- prevents stale managed entity from re-flushing a full UPDATE after a JPQL bulk UPDATE, which would overwrite concurrently changed columns - Fix misleading comment in X509CertificateValidator: state read-back runs within the same transaction (REQUIRED propagation), not outside the writer's tx - Add ValidationResultVsRevokeTest case covering the ambient-tx entity-dirtying scenario Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sync in-memory certificate.certificateValidationResult to null on the exception path to match the DB write from applyValidationResult - Fix inaccurate "within the same transaction" comment in X509CertificateValidator: the read-back after markRevokedIfStillIssued is a separate read, not part of a shared transaction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lubomirw
left a comment
There was a problem hiding this comment.
Code review: 3 advisory finding(s). All non-blocking — well-structured PR with thorough tests.
|



What the old code got wrong
Before this PR, both
X509CertificateValidator.finalizeValidationand the catch-path ofCertificateService.validatewrote validation results by callingrepository.save(certificate)on an in-memory entity snapshot.savedelegates toem.merge, which writes every column back to the database — includingstate. If a concurrent operator-driven revoke had already committedstate = PENDING_REVOKEbetween the time the validate path loaded the entity and the time it calledsave, the merge would silently overwrite it with the staleISSUEDsnapshot the validator was holding.The same structural problem appeared in the OCSP/CRL-driven revocation path inside
finalizeValidation: after detecting a REVOKED status from OCSP or CRL, the validator would directly writestate = REVOKEDviasave, again overwriting the row wholesale instead of doing a conditional update.What this PR does, and where it sits in the path to the fix
This PR is PR 2 of a 5-PR sequence that converges on a clean, race-free revoke fix. It introduces
CertificateValidationWriter— the writer bean for the validation path — and the three targeted@Modifying @Querymethods it needs inCertificateRepository.applyValidationResultis a targeted UPDATE that touches only the three validation columns plus the audit timestamp — it never reads or writesstate. A concurrent revoke'sstate = PENDING_REVOKEsurvives intact regardless of when the validate path commits.markRevokedIfStillIssuedis a compare-and-swap: it only transitions the row fromISSUED → REVOKEDif the row is still in that exact state. A 0-row return means something else got there first;classifyZeroRowOutcomereads back the current state and decides whether the intent was already fulfilled by a concurrent path (REVOKED/PENDING_REVOKE) or whether the observed state is genuinely diverged and needs a reconciliation warning.PR sequence to the revoke fix:
Tests
CertificateValidationWriterTxTest— AOP proxy guard (validates the writer is Spring-proxied so@Transactionaladvice fires),applyValidationResultpersists all three columns and refreshesupdated,markRevokedIfStillIssuedtransitionsISSUED → REVOKED(1 row) and is a no-op forPENDING_REVOKE(0 rows).ValidationResultVsRevokeTest— deterministic simulation of the race: insertISSUED, JDBC-UPDATEstatetoPENDING_REVOKE(models concurrent revoke commit), callapplyValidationResultwith a stale view, assertstateremainsPENDING_REVOKEandvalidationStatuswas written correctly.X509CertificateValidatorZeroRowOutcomeTest— parametrized unit tests forclassifyZeroRowOutcome:REVOKED/PENDING_REVOKE→INTENT_ALREADY_SATISFIED; all other states (includingnullfor a deleted row) →STATE_DIVERGENCE.