From 3ac20d42127a7b0f2d2fe4f3dcb392cf8f05e4fb Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 26 Sep 2025 00:14:05 +0300 Subject: [PATCH 1/5] impl: store last used URL in Toolbox Settings Store Context: Toolbox can store key/value pairs in two places: - a settings store which is backed by a clear text json file per each plugin - native keystore for sensitive data At the same time some of Coder's clients (ex: Netflix) would like to deploy at scale preconfigured settings for Toolbox. Most of the needed settings are part of json backed store except the last used URL. This PR reworks the code around the last used URL/token and moves the URL in the json backed store, making it easy to configure. At the same time we still support the pair stored in the native keystore for backward compatibility reasons. --- CHANGELOG.md | 4 ++++ .../kotlin/com/coder/toolbox/CoderRemoteProvider.kt | 13 +++++++------ .../kotlin/com/coder/toolbox/CoderToolboxContext.kt | 11 ++++++----- .../coder/toolbox/settings/ReadOnlyCoderSettings.kt | 6 ++++++ .../com/coder/toolbox/store/CoderSecretsStore.kt | 5 +---- .../com/coder/toolbox/store/CoderSettingsStore.kt | 5 +++++ .../kotlin/com/coder/toolbox/store/StoreKeys.kt | 2 +- .../com/coder/toolbox/views/DeploymentUrlStep.kt | 4 ++-- 8 files changed, 32 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fae586..e4eaf18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Changed + +- simplified storage for last used url and token + ## 0.6.6 - 2025-09-24 ### Changed diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index d65484c..0ce9ae0 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -7,7 +7,6 @@ import com.coder.toolbox.sdk.ex.APIResponseException import com.coder.toolbox.sdk.v2.models.WorkspaceStatus import com.coder.toolbox.util.CoderProtocolHandler import com.coder.toolbox.util.DialogUi -import com.coder.toolbox.util.toURL import com.coder.toolbox.util.waitForTrue import com.coder.toolbox.util.withPath import com.coder.toolbox.views.Action @@ -364,8 +363,8 @@ class CoderRemoteProvider( if (shouldDoAutoSetup()) { try { CoderCliSetupContext.apply { - url = context.secrets.lastDeploymentURL.toURL() - token = context.secrets.lastToken + url = context.deploymentUrl + token = context.secrets.tokenFor(context.deploymentUrl) } CoderCliSetupWizardState.goToStep(WizardStep.CONNECT) return CoderCliSetupWizardPage( @@ -399,14 +398,16 @@ class CoderRemoteProvider( * Auto-login only on first the firs run if there is a url & token configured or the auth * should be done via certificates. */ - private fun shouldDoAutoSetup(): Boolean = firstRun && (context.secrets.canAutoLogin || !settings.requireTokenAuth) + private fun shouldDoAutoSetup(): Boolean = firstRun && (canAutoLogin() || !settings.requireTokenAuth) + + fun canAutoLogin(): Boolean = !context.secrets.tokenFor(context.deploymentUrl).isNullOrBlank() private fun onConnect(client: CoderRestClient, cli: CoderCLIManager) { // Store the URL and token for use next time. - context.secrets.lastDeploymentURL = client.url.toString() + context.settingsStore.updateLastUsedUrl(client.url) if (context.settingsStore.requireTokenAuth) { context.secrets.lastToken = client.token ?: "" - context.secrets.storeTokenFor(client.url, context.secrets.lastToken) + context.secrets.storeTokenFor(client.url, client.token ?: "") context.logger.info("Deployment URL and token were stored and will be available for automatic connection") } else { context.logger.info("Deployment URL was stored and will be available for automatic connection") diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt index baac820..3176caf 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt @@ -36,14 +36,15 @@ data class CoderToolboxContext( * * In order of preference: * - * 1. Last used URL. - * 2. URL in settings. - * 3. CODER_URL. - * 4. URL in global cli config. + * 1. Last used URL from the settings. + * 2. Last used URL from the secrets store. + * 3. Default URL */ val deploymentUrl: URL get() { - if (this.secrets.lastDeploymentURL.isNotBlank()) { + if (!this.settingsStore.lastDeploymentURL.isNullOrBlank()) { + return this.settingsStore.lastDeploymentURL!!.toURL() + } else if (this.secrets.lastDeploymentURL.isNotBlank()) { return this.secrets.lastDeploymentURL.toURL() } return this.settingsStore.defaultURL.toURL() diff --git a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt index 0000ea6..9ac6438 100644 --- a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt +++ b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt @@ -8,6 +8,12 @@ import java.util.Locale.getDefault * Read-only interface for accessing Coder settings */ interface ReadOnlyCoderSettings { + + /** + * The last used deployment URL. + */ + val lastDeploymentURL: String? + /** * The default URL to show in the connection window. */ diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt index a807b69..20fe234 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt @@ -18,14 +18,11 @@ class CoderSecretsStore(private val store: PluginSecretStore) { } } - var lastDeploymentURL: String + val lastDeploymentURL: String get() = get("last-deployment-url") - set(value) = set("last-deployment-url", value) var lastToken: String get() = get("last-token") set(value) = set("last-token", value) - val canAutoLogin: Boolean - get() = lastDeploymentURL.isNotBlank() && lastToken.isNotBlank() fun tokenFor(url: URL): String? = store[url.host] diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt index f770da8..66706ca 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt @@ -36,6 +36,7 @@ class CoderSettingsStore( ) : ReadOnlyTLSSettings // Properties implementation + override val lastDeploymentURL: String? get() = store[LAST_USED_URL] override val defaultURL: String get() = store[DEFAULT_URL] ?: "https://dev.coder.com" override val binarySource: String? get() = store[BINARY_SOURCE] override val binaryDirectory: String? get() = store[BINARY_DIRECTORY] @@ -155,6 +156,10 @@ class CoderSettingsStore( fun readOnly(): ReadOnlyCoderSettings = this // Write operations + fun updateLastUsedUrl(url: URL) { + store[LAST_USED_URL] = url.toString() + } + fun updateBinarySource(source: String) { store[BINARY_SOURCE] = source } diff --git a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt index 5f8f5af..555c6b5 100644 --- a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt +++ b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt @@ -2,7 +2,7 @@ package com.coder.toolbox.store internal const val CODER_SSH_CONFIG_OPTIONS = "CODER_SSH_CONFIG_OPTIONS" -internal const val CODER_URL = "CODER_URL" +internal const val LAST_USED_URL = "lastDeploymentURL" internal const val DEFAULT_URL = "defaultURL" diff --git a/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt b/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt index be3d4d0..27e53f9 100644 --- a/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt @@ -63,8 +63,8 @@ class DeploymentUrlStep( errorField.textState.update { context.i18n.pnotr("") } - urlField.textState.update { - context.secrets.lastDeploymentURL + urlField.contentState.update { + context.deploymentUrl.toString() } signatureFallbackStrategyField.checkedState.update { From 5399431d96d11b96c9ce2c0f37b70b24d6f28365 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 26 Sep 2025 00:19:38 +0300 Subject: [PATCH 2/5] chore: last token should be stored only per URL After a successful connection to a deployment URL we used to save the same token associated with two keys: - last-token - and associated with the last url But it does not make sense, once we have to URL we can easily resolve the token associated with the URL. In other words this commit removes the ability to read/save a token associated with `last-token` key. --- .../com/coder/toolbox/CoderRemoteProvider.kt | 1 - .../com/coder/toolbox/store/CoderSecretsStore.kt | 16 +--------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 0ce9ae0..ed4854c 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -406,7 +406,6 @@ class CoderRemoteProvider( // Store the URL and token for use next time. context.settingsStore.updateLastUsedUrl(client.url) if (context.settingsStore.requireTokenAuth) { - context.secrets.lastToken = client.token ?: "" context.secrets.storeTokenFor(client.url, client.token ?: "") context.logger.info("Deployment URL and token were stored and will be available for automatic connection") } else { diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt index 20fe234..6db18dd 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt @@ -8,21 +8,7 @@ import java.net.URL * Provides Coder secrets backed by the secrets store service. */ class CoderSecretsStore(private val store: PluginSecretStore) { - private fun get(key: String): String = store[key] ?: "" - - private fun set(key: String, value: String) { - if (value.isBlank()) { - store.clear(key) - } else { - store[key] = value - } - } - - val lastDeploymentURL: String - get() = get("last-deployment-url") - var lastToken: String - get() = get("last-token") - set(value) = set("last-token", value) + val lastDeploymentURL: String = store["last-deployment-url"] ?: "" fun tokenFor(url: URL): String? = store[url.host] From 99e4fad4d9fe75743f351ff2a50200aa3ec6cd2f Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 26 Sep 2025 00:22:27 +0300 Subject: [PATCH 3/5] chore: next version is 0.7.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index da96b92..dc031f5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.6.6 +version=0.7.0 group=com.coder.toolbox name=coder-toolbox \ No newline at end of file From b6c6e7c5a3056f707ccc2bb0f9eef085c3d38631 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Sat, 27 Sep 2025 11:04:04 +0300 Subject: [PATCH 4/5] chore: mark property as deprecated URL stored in the native OS secrets store is now superseded by the URL stored in the JSON backed settings store. --- src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt index 6db18dd..a5466b4 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt @@ -8,6 +8,10 @@ import java.net.URL * Provides Coder secrets backed by the secrets store service. */ class CoderSecretsStore(private val store: PluginSecretStore) { + @Deprecated( + message = "The URL is now stored the JSON backed settings store. Use CoderSettingsStore#lastDeploymentURL", + replaceWith = ReplaceWith("context.settingsStore.lastDeploymentURL") + ) val lastDeploymentURL: String = store["last-deployment-url"] ?: "" fun tokenFor(url: URL): String? = store[url.host] From 5de589f36009e5a72be3c9a73f3d73b970d29850 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Sat, 27 Sep 2025 11:25:31 +0300 Subject: [PATCH 5/5] fix: replace unsafe call of non-nullable symbol With a takIf/let pattern. Theoretically you can have two threads racing - one changing the value of lastDeploymentURL and another \one reading. In our code that should be pretty much impossible. But the compiler can't be sure of that so it throws and error. --- src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt index 3176caf..ac3cbcc 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt @@ -42,12 +42,9 @@ data class CoderToolboxContext( */ val deploymentUrl: URL get() { - if (!this.settingsStore.lastDeploymentURL.isNullOrBlank()) { - return this.settingsStore.lastDeploymentURL!!.toURL() - } else if (this.secrets.lastDeploymentURL.isNotBlank()) { - return this.secrets.lastDeploymentURL.toURL() - } - return this.settingsStore.defaultURL.toURL() + return settingsStore.lastDeploymentURL?.takeIf { it.isNotBlank() }?.toURL() + ?: secrets.lastDeploymentURL.takeIf { it.isNotBlank() }?.toURL() + ?: settingsStore.defaultURL.toURL() } suspend fun logAndShowError(title: String, error: String) {