From 9ec08aab96f6d95b478721ff6a58a23a6c3cbf7f Mon Sep 17 00:00:00 2001 From: Katsiaryna Tsytsenia Date: Tue, 13 Aug 2024 11:06:31 +0300 Subject: [PATCH] IJMP-1814 Fix for connection host/port validation Signed-off-by: Katsiaryna Tsytsenia --- build.gradle.kts | 2 +- .../ui/zosmf/ZOSMFConnectionConfigurable.kt | 31 ++- .../explorer/utils/validationFunctions.kt | 4 +- .../zowe/service/ZoweConfigServiceImpl.kt | 16 +- .../zosmf/ZOSMFConnectionConfigurableTest.kt | 222 ++++++++++++++++++ 5 files changed, 255 insertions(+), 20 deletions(-) create mode 100644 src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 980c3dfd6..9e95f6a40 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,7 +47,7 @@ val junitVersion = "5.10.2" val mockkVersion = "1.13.10" val ibmMqVersion = "9.3.5.0" val jGraphTVersion = "1.5.2" -val zoweKotlinSdkVersion = "0.5.0-rc.9" +val zoweKotlinSdkVersion = "0.5.0-rc.11" val javaKeytarVersion = "1.0.0" repositories { diff --git a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt index af684b615..02f279bad 100644 --- a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt +++ b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt @@ -64,11 +64,18 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti showAndTestConnection()?.let { connectionsTableModel?.addRow(it) } } + /**Unable to save invalid URL + * Updates the selected profilef or current connection. + * If update is not possible(brocken URL), throws IllegalStateException exception + */ + @Throws(IllegalStateException::class) private fun ZoweConfig.updateFromState(state: ConnectionDialogState) { - setProfile(ZoweConfigServiceImpl.getProfileNameFromConnName(state.connectionName)) val uri = URI(state.connectionUrl) + if (uri.host.isNullOrEmpty()) + throw IllegalStateException("Unable to save invalid URL: ${state.connectionUrl}") + setProfile(ZoweConfigServiceImpl.getProfileNameFromConnName(state.connectionName)) host = uri.host - port = uri.port.toLong() + port = if (uri.port==-1) 10443 else uri.port.toLong() protocol = state.connectionUrl.split("://")[0] user = state.username password = state.password @@ -100,13 +107,21 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti val zoweConfig = parseConfigJson(configFile.inputStream) zoweConfig.extractSecureProperties(configFile.path.split("/").toTypedArray()) - zoweConfig.updateFromState(state) - runWriteActionOnWriteThread { - zoweConfig.setProfile(ZoweConfigServiceImpl.getProfileNameFromConnName(state.connectionName)) - zoweConfig.saveSecureProperties(configFile.path.split("/").toTypedArray()) - zoweConfig.restoreProfile() - configFile.setBinaryContent(zoweConfig.toJson().toByteArray(configFile.charset)) + kotlin.runCatching { + zoweConfig.updateFromState(state) } + .onSuccess { + runWriteActionOnWriteThread { + zoweConfig.setProfile(ZoweConfigServiceImpl.getProfileNameFromConnName(state.connectionName)) + zoweConfig.saveSecureProperties(configFile.path.split("/").toTypedArray()) + zoweConfig.restoreProfile() + configFile.setBinaryContent(zoweConfig.toJson().toByteArray(configFile.charset)) + } + } + .onFailure { + Messages.showErrorDialog("Unable to save invalid URL: ${state.connectionUrl}", "Invalid URL") + return + } } } diff --git a/src/main/kotlin/org/zowe/explorer/utils/validationFunctions.kt b/src/main/kotlin/org/zowe/explorer/utils/validationFunctions.kt index dcd4e0489..66750a674 100644 --- a/src/main/kotlin/org/zowe/explorer/utils/validationFunctions.kt +++ b/src/main/kotlin/org/zowe/explorer/utils/validationFunctions.kt @@ -21,12 +21,12 @@ import org.zowe.explorer.explorer.ui.UssDirNode import org.zowe.explorer.explorer.ui.UssFileNode import org.zowe.explorer.utils.crudable.Crudable import org.zowe.explorer.utils.crudable.find -import org.zowe.kotlinsdk.DatasetOrganization import javax.swing.JComponent import javax.swing.JPasswordField import javax.swing.JTextField -private val urlRegex = Regex("^(https?|http)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]") +private val urlRegex = + Regex("^(https?|http)://[-a-zA-Z0-9+&@#/%?=~_|!,.;]*(:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{1,5})|([0-9]{1,4})))?") private val maskRegex = Regex("^[A-Za-z\\$\\*%@#][A-Za-z0-9\\-\\$\\*%@#]{0,7}") private val ussPathRegex = Regex("^/$|^(/[^/]+)+$") private val forbiddenSymbol = "/" diff --git a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt index 15339cd64..810e24546 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt @@ -22,13 +22,9 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.VirtualFileManager import org.zowe.explorer.config.ConfigService -import org.zowe.explorer.config.connect.ConnectionConfig -import org.zowe.explorer.config.connect.CredentialService -import org.zowe.explorer.config.connect.getPassword -import org.zowe.explorer.config.connect.getUsername +import org.zowe.explorer.config.connect.* import org.zowe.explorer.config.connect.ui.zosmf.ConnectionDialogState import org.zowe.explorer.config.connect.ui.zosmf.ZOSMFConnectionConfigurable.Companion.warningMessageForDeleteConfig -import org.zowe.explorer.config.connect.whoAmI import org.zowe.explorer.config.ws.FilesWorkingSetConfig import org.zowe.explorer.config.ws.JesWorkingSetConfig import org.zowe.explorer.dataops.DataOpsManager @@ -391,15 +387,17 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService createZoweSchemaJsonIfNotExists() val urlRegex = - "(https?:\\/\\/)(www\\.)?([-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})\b?([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)" + "^(https?|http)://([-a-zA-Z0-9+&@#/%?=~_|!,.;]*)(:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{1,5})|([0-9]{1,4})))?" val pattern: Pattern = Pattern.compile(urlRegex) val matcher: Matcher = pattern.matcher(state.connectionUrl) var host = "localhost" - var port = "443" + var port = "10443" if (matcher.matches()) { - host = matcher.group(3) - port = matcher.group(4).substring(1) + if (matcher.group(2) != null) + host = matcher.group(2) + if (matcher.group(3) != null) + port = matcher.group(3).substring(1) } val content = getResourceStream("files/${ZOWE_CONFIG_NAME}") diff --git a/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt b/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt new file mode 100644 index 000000000..38f70957f --- /dev/null +++ b/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt @@ -0,0 +1,222 @@ +package org.zowe.explorer.config.connect.ui.zosmf + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.showOkCancelDialog +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.mockk.* +import org.zowe.explorer.testutils.WithApplicationShouldSpec +import org.zowe.kotlinsdk.zowe.config.DefaultKeytarWrapper +import org.zowe.kotlinsdk.zowe.config.KeytarWrapper +import org.zowe.kotlinsdk.zowe.config.ZoweConfig +import java.nio.file.Path +import javax.swing.Icon +import kotlin.reflect.KFunction +import kotlin.reflect.full.declaredMemberFunctions +import kotlin.reflect.jvm.isAccessible + +class ZOSMFConnectionConfigurableTest : WithApplicationShouldSpec({ + + val zOSMFConnectionConfigurableMock = spyk() + var isShowOkCancelDialogCalled = false + var isFindFileByNioPathCalled = false + var isInputStreamCalled = false + + afterSpec { + clearAllMocks() + unmockkAll() + } + + beforeEach { + isShowOkCancelDialogCalled = false + isFindFileByNioPathCalled = false + isInputStreamCalled = false + } + + context("ZOSMFConnectionConfigurable:") { + + val state = ConnectionDialogState( + connectionUuid = "0000", + connectionUrl = "https://111.111.111.111:111", + connectionName = "zowe-local-zosmf/testProj", + zoweConfigPath = "/zowe/conf/path" + ) + + val ret = mutableListOf(Messages.OK) + val showOkCancelDialogMock: (String, String, String, String, Icon?, DialogWrapper.DoNotAskOption?, Project?) -> Int = + ::showOkCancelDialog + mockkStatic(showOkCancelDialogMock as KFunction<*>) + every { + showOkCancelDialogMock(any(), any(), any(), any(), null, null, null) + } answers { + isShowOkCancelDialogCalled = true + ret[0] + } + + mockkConstructor(DefaultKeytarWrapper::class) + every { anyConstructed().setPassword(any(), any(), any()) } just Runs + every { anyConstructed().deletePassword(any(), any()) } returns true + + should("updateZoweConfigIfNeeded null state and Ok") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + try { + it.call(zOSMFConnectionConfigurableMock, null) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Zowe config file not found") + } + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe false + } + + should("updateZoweConfigIfNeeded null zoweConfigPath and Ok") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + try { + state.zoweConfigPath = null + it.call(zOSMFConnectionConfigurableMock, state) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Zowe config file not found") + } + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe false + } + + ret[0] = Messages.CANCEL + + should("updateZoweConfigIfNeeded null state and Cancel") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + try { + state.zoweConfigPath = null + it.call(zOSMFConnectionConfigurableMock, null) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Zowe config file not found") + } + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe false + } + + state.zoweConfigPath = "/zowe/conf/path" + ret[0] = Messages.OK + + should("updateZoweConfigIfNeeded throw Zowe config file not found") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + try { + it.call(zOSMFConnectionConfigurableMock, state) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Zowe config file not found") + } + } + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe false + } + + val vfMock = mockk() + val vfmMock: VirtualFileManager = mockk() + mockkStatic(VirtualFileManager::class) + every { VirtualFileManager.getInstance() } returns vfmMock + every { vfmMock.findFileByNioPath(any()) } answers { + isFindFileByNioPathCalled = true + vfMock + } + every { vfMock.inputStream } answers { + isInputStreamCalled = true + val fileCont = "{\n" + + " \"\$schema\": \"./zowe.schema.json\",\n" + + " \"profiles\": {\n" + + " \"zosmf\": {\n" + + " \"type\": \"zosmf\",\n" + + " \"properties\": {\n" + + " \"port\": 443\n" + + " },\n" + + " \"secure\": []\n" + + " },\n" + + " \"tso\": {\n" + + " \"type\": \"tso\",\n" + + " \"properties\": {\n" + + " \"account\": \"\",\n" + + " \"codePage\": \"1047\",\n" + + " \"logonProcedure\": \"IZUFPROC\"\n" + + " },\n" + + " \"secure\": []\n" + + " },\n" + + " \"ssh\": {\n" + + " \"type\": \"ssh\",\n" + + " \"properties\": {\n" + + " \"port\": 22\n" + + " },\n" + + " \"secure\": []\n" + + " },\n" + + " \"base\": {\n" + + " \"type\": \"base\",\n" + + " \"properties\": {\n" + + " \"host\": \"example.host\",\n" + + " \"rejectUnauthorized\": true\n" + + " },\n" + + " \"secure\": [\n" + + " \"user\",\n" + + " \"password\"\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"defaults\": {\n" + + " \"zosmf\": \"zosmf\",\n" + + " \"tso\": \"tso\",\n" + + " \"ssh\": \"ssh\",\n" + + " \"base\": \"base\"\n" + + " }\n" + + "}" + fileCont.toByteArray().inputStream() + } + every { vfMock.path } returns "/zowe/file/path/zowe.config.json" + every { vfMock.charset } returns Charsets.UTF_8 + every { vfMock.setBinaryContent(any()) } just Runs + + mockkObject(ZoweConfig) + val confMap = mutableMapOf>() + val configCredentialsMap = mutableMapOf() + configCredentialsMap["profiles.base.properties.user"] = "testUser" + configCredentialsMap["profiles.base.properties.password"] = "testPass" + confMap.clear() + confMap["/zowe/file/path/zowe.config.json"] = configCredentialsMap + every { ZoweConfig.Companion["readZoweCredentialsFromStorage"](any()) } returns confMap + + should("updateZoweConfigIfNeeded success") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + it.call(zOSMFConnectionConfigurableMock, state) + isShowOkCancelDialogCalled shouldBe true + isFindFileByNioPathCalled shouldBe true + isInputStreamCalled shouldBe true + } + } + + should("updateZoweConfigIfNeeded failed") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + state.connectionUrl = "111@@@:8080" + try { + it.call(zOSMFConnectionConfigurableMock, state) + } catch (t: Throwable) { + t.cause.toString().shouldContain("Unable to save invalid URL") + } + } + } + } +} +) \ No newline at end of file