diff --git a/.github/workflows/test.java.yml b/.github/workflows/test.java.yml index 76fcbe458..d653d365f 100644 --- a/.github/workflows/test.java.yml +++ b/.github/workflows/test.java.yml @@ -3,6 +3,12 @@ name: Test-Java on: pull_request: branches: [ master ] + paths: + - 'sdk/java/core/**' + - '.github/workflows/test.java.yml' + +permissions: + contents: read jobs: test-java: @@ -10,24 +16,25 @@ jobs: strategy: max-parallel: 1 matrix: - java-version: [ '8', '11', '16', '17', '18' ] + java-version: [ '8', '11', '17', '21' ] name: KSM test with Java ${{ matrix.java-version }} defaults: run: working-directory: ./sdk/java/core steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Setup Java ${{ matrix.java-version }} - uses: actions/setup-java@v4 + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4 with: distribution: 'zulu' java-version: ${{ matrix.java-version }} - - name: Setup, Build and Test - uses: gradle/actions/setup-gradle@v3 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 with: gradle-version: '8.14' - arguments: build test - build-root-directory: ./sdk/java/core - + - name: Build and Test + run: gradle build test diff --git a/sdk/java/core/README.md b/sdk/java/core/README.md index 4312552da..9c19877b6 100644 --- a/sdk/java/core/README.md +++ b/sdk/java/core/README.md @@ -4,6 +4,16 @@ For more information see our official documentation page https://docs.keeper.io/ # Change Log +## 17.2.1 +- KSM-902 - Add IL5 (DoD Impact Level 5) region mapping (`IL5` → `il5.keepersecurity.us`) +- KSM-823 - Fix `custom` field omitted from record create payload when no custom fields are set + - `KeeperRecordData.custom` now defaults to `mutableListOf()` instead of `null` — `kotlinx-serialization` previously skipped null fields, causing `"custom"` to be absent from the V3 API payload + - Consistent with Commander and Vault which always include `"custom": []` +- KSM-854 - Fix `KeeperFileData` crash when `lastModified` field is absent from file metadata + - Files uploaded by non-SDK Keeper clients (iOS, Android, Web Vault) may omit `lastModified` + - Previously threw `MissingFieldException` and silently skipped the file attachment + - Now defaults to `0` when the field is absent, consistent with .NET SDK behavior (KSM-674) + ## 17.2.0 - **SECURITY (KSM-699)** - Fix file permissions for config.json and cache.dat - Config and cache files now created with 0600 permissions (owner read/write only) diff --git a/sdk/java/core/build.gradle.kts b/sdk/java/core/build.gradle.kts index fea5a2cdf..773950ab6 100644 --- a/sdk/java/core/build.gradle.kts +++ b/sdk/java/core/build.gradle.kts @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "com.keepersecurity.secrets-manager" // During publishing, If version ends with '-SNAPSHOT' then it will be published to Maven snapshot repository -version = "17.2.0" +version = "17.2.1" plugins { `java-library` diff --git a/sdk/java/core/src/main/kotlin/com/keepersecurity/secretsManager/core/RecordData.kt b/sdk/java/core/src/main/kotlin/com/keepersecurity/secretsManager/core/RecordData.kt index 2b7d63d77..69ed2afb3 100644 --- a/sdk/java/core/src/main/kotlin/com/keepersecurity/secretsManager/core/RecordData.kt +++ b/sdk/java/core/src/main/kotlin/com/keepersecurity/secretsManager/core/RecordData.kt @@ -3,6 +3,8 @@ package com.keepersecurity.secretsManager.core +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -12,20 +14,21 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +@OptIn(ExperimentalSerializationApi::class) @Serializable data class KeeperRecordData @JvmOverloads constructor( var title: String, val type: String, val fields: MutableList, - var custom: MutableList? = null, + @EncodeDefault var custom: MutableList = mutableListOf(), var notes: String? = null ) { inline fun getField(): T? { - return (fields + (custom ?: listOf())).find { x -> x is T } as? T + return (fields + custom).find { x -> x is T } as? T } fun getField(clazz: Class): KeeperRecordField? { - return (fields + (custom ?: listOf())).find { x -> x.javaClass == clazz } + return (fields + custom).find { x -> x.javaClass == clazz } } } @@ -57,7 +60,7 @@ data class KeeperFileData( val type: String? = null, val size: Long, @Serializable(with = FlexibleLongSerializer::class) - val lastModified: Long + val lastModified: Long = 0 ) @Serializable diff --git a/sdk/java/core/src/main/kotlin/com/keepersecurity/secretsManager/core/SecretsManager.kt b/sdk/java/core/src/main/kotlin/com/keepersecurity/secretsManager/core/SecretsManager.kt index 6618e61eb..6cf737466 100644 --- a/sdk/java/core/src/main/kotlin/com/keepersecurity/secretsManager/core/SecretsManager.kt +++ b/sdk/java/core/src/main/kotlin/com/keepersecurity/secretsManager/core/SecretsManager.kt @@ -19,7 +19,7 @@ import java.util.* import java.util.concurrent.* import javax.net.ssl.* -const val KEEPER_CLIENT_VERSION = "mj17.2.0" +const val KEEPER_CLIENT_VERSION = "mj17.2.1" const val KEY_HOSTNAME = "hostname" // base url for the Secrets Manager service const val KEY_SERVER_PUBIC_KEY_ID = "serverPublicKeyId" @@ -706,6 +706,7 @@ fun initializeStorage(storage: KeyValueStorage, oneTimeToken: String, hostName: "GOV" -> "govcloud.keepersecurity.us" "JP" -> "keepersecurity.jp" "CA" -> "keepersecurity.ca" + "IL5" -> "il5.keepersecurity.us" else -> tokenParts[0] } clientKey = tokenParts[1] @@ -920,7 +921,7 @@ fun getNotationResults(options: SecretsManagerOptions, notation: String): List record.data.fields - "custom_field" -> record.data.custom ?: mutableListOf() + "custom_field" -> record.data.custom else -> throw Exception("Notation error - Expected /field or /custom_field but found /$selector") } @@ -991,9 +992,7 @@ fun completeTransaction(options: SecretsManagerOptions, recordUid: String, rollb @ExperimentalSerializationApi fun addCustomField(record: KeeperRecord, field: KeeperRecordField) { if (field.javaClass.superclass == KeeperRecordField::class.java) { - if (record.data.custom == null) - record.data.custom = mutableListOf() - record.data.custom!!.add(field) + record.data.custom.add(field) } } diff --git a/sdk/java/core/src/test/kotlin/com/keepersecurity/secretsManager/core/SecretsManagerTest.kt b/sdk/java/core/src/test/kotlin/com/keepersecurity/secretsManager/core/SecretsManagerTest.kt index 14e842ff7..b4c516c63 100644 --- a/sdk/java/core/src/test/kotlin/com/keepersecurity/secretsManager/core/SecretsManagerTest.kt +++ b/sdk/java/core/src/test/kotlin/com/keepersecurity/secretsManager/core/SecretsManagerTest.kt @@ -109,6 +109,9 @@ internal class SecretsManagerTest { initializeStorage(storage, "eu:ONE_TIME_TOKEN") assertEquals("keepersecurity.eu", storage.getString("hostname")) storage = InMemoryStorage() + initializeStorage(storage, "IL5:ONE_TIME_TOKEN") + assertEquals("il5.keepersecurity.us", storage.getString("hostname")) + storage = InMemoryStorage() initializeStorage(storage, "fake.keepersecurity.com:ONE_TIME_TOKEN") assertEquals("fake.keepersecurity.com", storage.getString("hostname")) } @@ -122,6 +125,44 @@ internal class SecretsManagerTest { assertEquals("fake.keepersecurity.com", storage.getString("hostname")) } + @Test + fun testRecordCreateEmptyCustomSerialized() { + // KSM-823: RecordCreate with no custom fields must include "custom": [] in JSON payload + val recordData = KeeperRecordData( + title = "Test Record", + type = "login", + fields = mutableListOf() + ) + val json = Json.encodeToString(recordData) + assertTrue(json.contains("\"custom\":[]") || json.contains("\"custom\": []"), + "Serialized payload must include custom:[] even when no custom fields are set. Got: $json") + } + + @Test + fun testKeeperFileDataMissingLastModified() { + // GH-973 / KSM-854: lastModified entirely absent — must deserialize without throwing + val json = """{"title":"test.txt","name":"test.txt","type":"text/plain","size":1024}""" + val result = Json.decodeFromString(json) + assertEquals(0L, result.lastModified) + assertEquals("test.txt", result.name) + } + + @Test + fun testKeeperFileDataIntegerLastModified() { + // Regression guard: normal integer lastModified + val json = """{"title":"test.txt","name":"test.txt","size":1024,"lastModified":1700000000000}""" + val result = Json.decodeFromString(json) + assertEquals(1700000000000L, result.lastModified) + } + + @Test + fun testKeeperFileDataFractionalLastModified() { + // Regression guard for KSM-673: fractional lastModified (iOS client format) + val json = """{"title":"test.txt","name":"test.txt","size":1024,"lastModified":1760646182.790214}""" + val result = Json.decodeFromString(json) + assertEquals(1760646182L, result.lastModified) + } + // @Test // uncomment to debug the integration test fun integrationTest() { val trustAllPostFunction: (