docs(java): document the 17.4.0 breaking changes, and three review follow-ups - #1120
Merged
stas-schaller merged 4 commits intoAug 19, 2026
Merged
Conversation
The 17.4.0 section listed only features and fixes. Enumerating every class in the compiled jar and diffing javap output against master shows exactly three public members change relative to 17.3.0, plus the two synthetic constructors Kotlin emits for default arguments: deleteFolder() return type changed KeeperRecord.copy() arity changed SecretsManagerOptions.copy() arity changed deleteFolder() was already called out in the release PR description but never reached the changelog, which is what users actually read on upgrade. The second entry covers a case easy to state too narrowly. Adding a constructor parameter changes copy(), but it also changes the synthetic $default constructor, so Kotlin code that merely builds a KeeperRecord while omitting a defaulted argument breaks on a jar swap too. Verified: a caller compiled against 17.3.0 and run against 17.4.0 throws NoSuchMethodError: KeeperRecord.<init>(..., int, DefaultConstructorMarker) Recompiling resolves it. Java call sites are unaffected either way, since both types carry @jvmoverloads. Matches the Breaking Changes block the 17.3.0 section already uses.
…(KSM-1262) Both temp-file write paths caught IOException and threw a SecretsManagerException that discarded the original exception, so the stack trace and errno were lost. The message also asserted a cause it could not know: createTempFile fails on a full or read-only volume, a missing parent directory, or an fd limit, none of which are a permission problem, and the operator was told the directory was not writable. SecretsManagerException gains an optional cause. @jvmoverloads keeps the single-argument constructor, so Java callers and the existing subclasses that call super(message) are unaffected; javap confirms no member was removed from the class.
The field carries a backend permission decision, and the only description of it lived in the changelog. Records the three things a caller needs: the meaning of each value, that the SDK does not enforce it before an update, and that the `false` default is reachable only through direct construction because the response envelope requires the field.
…-1176) The test indexed records[0] directly. fetchAndDecryptSecrets swallows per-record failures to stderr, so a decrypt regression would have surfaced as IndexOutOfBoundsException rather than a readable assertion; both neighbouring tests already guard on the record count first. Folds the two cases into a loop over true/false. Verified the assertion still discriminates: hardcoding isEditable = true in decryptRecord fails the test, and reverting passes.
mgallego-keeper
added a commit
that referenced
this pull request
Aug 19, 2026
PR #1120 changed both write paths to report what the file system returned and to retain the original IOException as the cause, but shipped no test for it, so a later refactor can drop the cause without anything failing. Points LocalConfigStorage at a path whose parent does not exist and asserts the cause survives, that a missing directory is not reported as a permissions problem, and that the message names the directory it could not use. Verified against the implementation now on the release branch.
mgallego-keeper
added a commit
that referenced
this pull request
Aug 19, 2026
PR #1120 changed both write paths to report what the file system returned and to retain the original IOException as the cause, but shipped no test for it, so a later refactor can drop the cause without anything failing. Points LocalConfigStorage at a path whose parent does not exist and asserts the cause survives, that a missing directory is not reported as a permissions problem, and that the message names the directory it could not use. Verified against the implementation now on the release branch.
mgallego-keeper
added a commit
that referenced
this pull request
Aug 19, 2026
…cle guard Brings in #1117 (Java API compatibility), #1119 (CI on release-branch PRs) and #1120 (17.4.0 docs). Two files conflicted; SecretsManager.kt merged cleanly and the cycle guard is unchanged. README.md: both sides appended a changelog entry at the same position. Kept both, with KSM-1269 ahead of KSM-1270 in landing order. SecretsManagerTest.kt: the conflict bundled the new cycle test with testSecretsManagerOptionsAcceptsCustomTimeouts, whose incoming side was a deletion. That test was not dropped by #1117, it was relocated to the new TimeoutTest.kt as timeoutOptions_acceptCustomValues with identical assertions. Resolving in favour of our side would have left the same assertions duplicated across two files, so the deletion is accepted here and only the cycle test is kept. Verified on Zulu JDK 8: 71 tests, 0 failures. That is 69 from the PR head, plus 3 from TimeoutTest.kt, minus the 1 relocated test. The net diff against the base branch is the KSM-1270 change alone.
mgallego-keeper
added a commit
that referenced
this pull request
Aug 19, 2026
…-531-java-proxy Resolves the three conflicts against the base commits that landed after the rebase (#1117, #1119, #1120): - SecretsManager.kt: keep the proxy-aware uploadFile call and private helper, which are supersets of base's timeout threading (proxyUrl, allowUnverifiedCertificate, connectTimeoutMillis, readTimeoutMillis all carried). Give proxyUrl a null default in the full postFunction form so TimeoutTest's postFunction(url, tk, payload, true, readTimeoutMillis = X) call binds; the explicit 4-arg overload is kept for the published Java descriptor. - SecretsManagerExceptions.kt: keep base's @jvmoverloads cause constructor and KDoc, and restore the serialVersionUID pin (5401507264959279624) so exceptions round-trip with jars built from released 16.6.6/17.2.0/17.3.0. Without the pin the computed SUID changes to 1054703023149159532 and cross-version deserialization throws InvalidClassException. - README.md: take base's 17.4.0 Breaking Changes block, corrected for this merge: KeeperRecord gained one constructor parameter (isEditable) and SecretsManagerOptions gained three (connectTimeoutMillis, readTimeoutMillis, proxyUrl). All eight base changelog entries and the KSM-531 entry survive. Merged tree verified on JDK 8: 95 tests, 0 failures; the released 4-arg postFunction descriptor and the pinned SUID are present in the built jar.
mgallego-keeper
added a commit
that referenced
this pull request
Aug 19, 2026
PR #1120 changed both write paths to report what the file system returned and to retain the original IOException as the cause, but shipped no test for it, so a later refactor can drop the cause without anything failing. Points LocalConfigStorage at a path whose parent does not exist and asserts the cause survives, that a missing directory is not reported as a permissions problem, and that the message names the directory it could not use. Verified against the implementation now on the release branch.
stas-schaller
pushed a commit
that referenced
this pull request
Aug 19, 2026
…d gate stderr diagnostics (#1122) * test(java): cover the mixed-category password shuffle (KSM-1203) Every existing testGeneratePassword case requests a single character category, so all 32 characters come from one charset and the final shuffle cannot be observed. Those assertions pass whether the shuffle uses SecureRandom, uses Random.Default, or is deleted outright, which left the KSM-1203 fix with no coverage at all. randomSample() emits characters grouped by category, so a mixed request is the only configuration where the shuffle is visible: without one, position 0 is always lowercase. The new test asserts that the category of the first character varies across 200 passwords, and that each category keeps exactly the requested count. Verified to fail when the shuffle is removed from generatePassword. It does not distinguish a CSPRNG from a weak PRNG, since both produce a uniform permutation; the test comment says so rather than implying coverage it does not have. * test(java): cover the config write failure cause (KSM-1262) PR #1120 changed both write paths to report what the file system returned and to retain the original IOException as the cause, but shipped no test for it, so a later refactor can drop the cause without anything failing. Points LocalConfigStorage at a path whose parent does not exist and asserts the cause survives, that a missing directory is not reported as a permissions problem, and that the message names the directory it could not use. Verified against the implementation now on the release branch. * test(java): prove the config write swaps the file instead of rewriting it (KSM-1262) testConfigFileAtomicWriteIsOwnerOnly asserted only that the finished config file is 0600. The implementation it replaced also ended at 0600, by calling chmod after writing the private key through an 0644 handle, so the test passed against the vulnerable code and could not detect the fix being reverted. What KSM-1262 actually changed is that secrets are never written to the visible path: they go to a 0600 temp file that is renamed into place. A rename installs a different inode, so BasicFileAttributes.fileKey() changes across a write while an in-place rewrite keeps the same value. Comparing fileKey() across two writes makes that deterministic to assert, with no race to lose. Also asserts the permissions survive the swap and that no ksm_*.tmp staging file is left behind on success. Verified to fail against an in-place write plus chmod-after implementation, reporting the unchanged inode. * fix(java): gate delete and folder-skip diagnostics on loggingEnabled Three stderr writes added for KSM-1081 and KSM-1086 ignored the loggingEnabled option that the throttle and key-rotation diagnostics in the same file already respect, so a consumer that had explicitly turned logging off still got stderr output from deleteSecret, deleteFolder and the getFolders skip path. The folder-skip message also diverged from the four equivalent handlers in fetchAndDecryptSecrets, which all include e.javaClass.simpleName. One of those logs the identical "Folder <uid> skipped due to error:" prefix, so the file emitted near-identical lines in two formats. The class name is not decoration here: the causes this path exists for include a null data field, whose KotlinNullPointerException carries no message, so the line read "skipped due to error: null" and told an operator nothing. Tests drive getFolders through an injected queryFunction with one undecryptable folder, capturing stderr to assert nothing is written when loggingEnabled is false and that the exception type appears when it is true. Both verified to fail against the previous handler. * docs(java): correct the 17.4.0 changelog The section had drifted out of the ascending ticket order the file uses, and it documented no breaking change even though deleteFolder's return type changed in KSM-1086. Callers read the changelog, not the release PR description. Changes: - Restores ascending ticket order (1081, 1086, 1176, 1203, 1207, 1248, 1262). - Adds the missing Breaking Changes block for deleteFolder, noting that no working code can be affected: the old return type required a "records" field while the backend sends "folders", so every previous call threw MissingFieldException rather than returning a value. - KSM-1203 gains a statement that existing passwords need no rotation. Character selection always drew from SecureRandom, so only the arrangement was affected: for generatePassword(32, 8, 8, 8, 8) roughly 137 of the 193 bits were never at risk, and the default generatePassword() was unaffected because all of its characters come from a single set. Without this, the entry reads as though every password the SDK has generated is suspect. - KSM-1207 notes that downloadFile and downloadThumbnail take only a KeeperFile, so they use the built-in defaults rather than the values configured on SecretsManagerOptions. - KSM-1262 documents the config file now being read as UTF-8 explicitly, where it previously used the JVM default charset while always being written as UTF-8. - KSM-1081 and KSM-1086 note that the per-item diagnostics are gated on loggingEnabled.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-ups from the review pass over the v17.4.0 release branch, picking up what #1117 did not cover. Four fixes, one commit each; every commit builds and tests green on its own (70 tests, 0 failures).
Nothing here changes runtime behaviour except the exception message in item 2.
1. The changelog never documented the 17.4.0 breaking changes
The 17.4.0 section listed only features and fixes. #1110's description calls out the
deleteFolder()return type change under a Breaking Changes heading, but that never reachedREADME.md, which is what users actually read on upgrade. The 17.3.0 section right below it already uses a Breaking Changes block, so the convention exists.To make the list defensible rather than a judgment call, I enumerated every class in the compiled jar and diffed
javapoutput against master. Exactly three public members change, plus two synthetic constructors:deleteFolder()SecretsManagerDeleteResponsetoSecretsManagerDeleteFolderResponseKeeperRecord.copy()SecretsManagerOptions.copy()Nothing else is removed anywhere in the SDK.
The second entry is easy to state too narrowly, and my first draft did. Adding a constructor parameter changes
copy(), but it also changes the synthetic$defaultconstructor, which is the real call target whenever Kotlin source omits a defaulted argument. So Kotlin code that merely builds aKeeperRecordbreaks on a jar swap too, not justcopy()callers. Verified by compiling a caller against 17.3.0 and running it against 17.4.0:Recompiling resolves it, and Java call sites are unaffected either way thanks to the
@JvmOverloadsadded in #1117. The changelog entry now says exactly that.2. A failed config write threw away the underlying IOException (KSM-1262)
Both temp-file write paths in
LocalConfigStoragecaughtIOExceptionand threw aSecretsManagerExceptionconstructed from a message alone, so the stack trace and the errno were gone. The message also asserted a cause it could not know:Files.createTempFilealso fails on a full or read-only volume, a missing parent directory, and an fd limit. In each of those cases the operator was told, confidently, that it was a permission problem.SecretsManagerExceptiongains an optionalcause.@JvmOverloadskeeps the single-argument constructor, so Java callers and the two existing subclasses that callsuper(message)are untouched;javapconfirms no member was removed from the class.3.
KeeperRecord.isEditablehad no KDocThe field carries a backend permission decision and its only description lived in the changelog. The doc records the three things a caller needs: what each value means, that the SDK does not enforce it before an update (per KSM-1173), and that the
falsedefault is reachable only through direct construction, because the response envelope requires the field.Worth knowing for the last point:
SecretsManagerResponseRecord.isEditablehas no default, so a server that omitted the field would fail the wholegetSecrets()call rather than yieldingfalse:That is pre-existing and not changed here, but it is why the default is documented as construction-only.
4. The isEditable test could fail as IndexOutOfBounds
testIsEditableForwardedToKeeperRecordindexedrecords[0]directly.fetchAndDecryptSecretsswallows per-record failures to stderr, so a decrypt regression would have surfaced asIndexOutOfBoundsExceptioninstead of a readable assertion. Both neighbouring tests already guard on the record count first.Also folds the two cases into a loop. The assertion still discriminates: hardcoding
isEditable = trueindecryptRecordfails the test, reverting passes.Verification
javapsweep over every class in the jar: the three members above are the only public removals between master and this branch.javaponSecretsManagerException: two additions, nothing removed.NoSuchMethodErrordescribed in item 1.Not addressed here
Found during the same pass, left out as pre-existing and outside the release-blocker scope. Happy to open tickets:
fsyncbefore the rename, so a power cut can leave a truncated config. Outside the permissions threat model KSM-1262 states, but "atomic write" invites the expectation.keyIdatSecretsManager.kt:1738is a public mutable top-levelvar, mutated as a side effect of thekeeperPublicKeysinitializer and exported asgetKeyId()/setKeyId().testConfigFileAtomicWriteIsOwnerOnlyasserts permissions but not atomicity despite the name, and does not coversaveCachedValue, the other half of KSM-1262.LocalConfigStorageswitched fromFileReadertoreadText(Charsets.UTF_8)in fix(java): validate server key_id range and eliminate config write race window #1109, which pairs correctly with the UTF-8 write side and fixes a latent platform-charset mismatch. Harmless for ASCII configs, but it went unremarked in the changelog.