feat(v1.1.0): HTTP Client {{$keeper(...)}} integration + Run Keeper Securely run configuration#12
Conversation
Qodana Community for JVMIt seems all right 👌 No new problems were found according to the checks applied 💡 Qodana analysis was run in the pull request mode: only the changed files were checked View the detailed Qodana reportTo be able to view the detailed Qodana report, you can either:
To get - name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2025.1.1
with:
upload-result: trueContact Qodana teamContact us at qodana-support@jetbrains.com
|
- Remove unused imports (WriteCommandAction, ProgressManager, ExperimentalSerializationApi) and redundant @OptIn - Drop redundant casts on RunConfiguration#getOptions(); switch to property-access syntax (s.options, cfg.options, File#isFile) - Drop unnecessary safe calls / .trim() on non-null Strings in KeeperSecureRunConfigurationEditor - Convert HTTP Client snippet builder to a string template - Sentence-case the background task title ("Run Keeper securely") - Suppress UNUSED_PARAMETER on the editor's project ctor arg (kept to satisfy the Java caller's contract) No behavior change. Compile + lints clean.
| trimmedField, | ||
| logger, | ||
| ) | ||
| extracted ?: "[Keeper] Field '$trimmedField' not found in record $trimmedUid" |
There was a problem hiding this comment.
This potentially sends sensitive field and UID information to the remote server on failure. Instead of returning an error string as the variable value, throw so the HTTP Client surfaces it as an error and the request doesn't fire. Something like
extracted ?: throw IllegalStateException("Keeper field '$trimmedField' not found in record $trimmedUid")
| extracted ?: "[Keeper] Field '$trimmedField' not found in record $trimmedUid" | ||
| } catch (e: Exception) { | ||
| logger.warn("Keeper HTTP variable resolution failed: ${e.message}", e) | ||
| "[Keeper] ${e.message ?: e::class.simpleName}" |
There was a problem hiding this comment.
Same issue as line 38 above. I recommend throwing an error rather than logging to the server:
} catch (e: Exception) {
logger.warn("Keeper HTTP variable resolution failed: ${e.message}", e)
throw e
}
| }.queue() | ||
| } | ||
|
|
||
| override fun destroyProcessImpl() { |
There was a problem hiding this comment.
With this empty, when users press the stop button nothing will happen and the user will likely be confused. Consider implementing destroyProcessImpl with a process spawned in the ScriptRunner and passed to this Handler via an onProcessStarted callback. Store the reference as @Volatile since destroyProcessImpl and startNotify run on different threads.
| override fun getProcessInput(): java.io.OutputStream? = null | ||
|
|
||
| override fun startNotify() { | ||
| super.startNotify() |
There was a problem hiding this comment.
startNotify() should signal that an already-running process is ready for output, not launch the work. The process should be started before startNotify() is called, with destroyProcessImpl() holding a reference to it. As written, the run lifecycle timings are misaligned with actual execution and Stop cannot be wired up cleanly.
| logger.info("Found ${keeperRefs.size} Keeper references to process") | ||
| indicator.text = "Fetching ${keeperRefs.size} secrets via persistent shell..." | ||
|
|
||
| keeperRefs.forEachIndexed { index, (key, uid, field) -> |
There was a problem hiding this comment.
Cancellation is never checked in this loop. Add if (indicator.isCanceled) throw ProcessCanceledException() at the top of each iteration so Cancel is honored between secret fetches.
|
|
||
| override fun applyEditorTo(s: KeeperSecureRunConfiguration) { | ||
| val o = s.options | ||
| val env = envFileField.text.trim().ifBlank { ".env" } |
There was a problem hiding this comment.
ifBlank { ".env" } silently overwrites a blank field when the user saves. If they cleared it intentionally, checkConfiguration will still error. Let it do that directly rather than hiding the blank input. Same issue on line 54 in resetEditorFrom.
|
|
||
| private fun isHttpClientRequestFile(file: VirtualFile): Boolean { | ||
| val ext = file.extension ?: return false | ||
| return ext.equals("http", ignoreCase = true) || ext.equals("rest", ignoreCase = true) |
There was a problem hiding this comment.
File type detected by extension string. Consider using the FileType API instead. Compare file.fileType against HttpRequestFileType.INSTANCE (guarded by the optional com.jetbrains.restClient dependency check) to handle custom associations and scratch files correctly.
| } | ||
|
|
||
| private fun buildInsertionSuccessMessage(insertText: String, keeperNotation: String): String { | ||
| val isHttpSnippet = insertText.startsWith("{{") && insertText.contains("keeper(") |
There was a problem hiding this comment.
Detecting the snippet type by pattern-matching the output string is fragile, it breaks if the snippet format changes. Consider passing a Boolean from insertionTextForCurrentFile into buildInsertionSuccessMessage instead.
| @@ -1,5 +1,15 @@ | |||
| # Keeper Security JetBrains Plugin Changelog | |||
|
|
|||
| ## [Unreleased] | |||
There was a problem hiding this comment.
Pin the version number here instead of Unreleased
|
|
||
| pluginGroup = com.keepersecurity.jetbrains | ||
| pluginName = Keeper Security | ||
| pluginRepositoryUrl = https://github.com/[your-username]/keeper-jetbrains-plugin |
There was a problem hiding this comment.
Placeholder URL. Update to https://github.com/Keeper-Security/keeper-jetbrains-plugin before publishing
Throw on HTTP variable resolution failures instead of returning error strings (avoids leaking record UID / field path into outbound requests). Wire the run-config Stop button via @volatile Process + ProgressIndicator references and honor cancellation between secret fetches. Drain stdout on a separate thread and bound process.waitFor with a periodic poll so hung scripts can be force-killed. Use the FileType API (guarded by a com.jetbrains.restClient plugin check) to detect HTTP Client request files; pass an explicit Boolean into the success-message builder instead of pattern-matching the inserted text. Drop the silent ".env" substitution from the run-config editor, drop the duplicate applyDefaults call, and remove the unconditional 5s sleep after the Keeper shell starts in favor of a poll-until-prompt loop. Tighten the readiness check to a liveness probe plus the authoritative KeeperShellService.isReady() flag. Pin CHANGELOG to 1.1.0 and replace the placeholder pluginRepositoryUrl.
|
Hi @stas-schaller — thanks for the thorough review on this. All 14 items are addressed in #13. Two callouts where the implementation deviates slightly from the literal suggestion:
Tagging you on #13 for review. |
…tp-client Address PR #12 review feedback (1.1.0 follow-up)
Summary
Adds two major features for v1.1.0 and refactors the existing "Run Keeper Securely" pipeline into a shared runner.
Run → Edit Configurations → + → Keeper Securely) — configurable.envpath, working directory, command, with output streamed to the Run tool window. Defaults are auto-detected for Python SDK / venv and common entry scripts (main.py,app.py,manage.py) when a new configuration is created..http/.restfiles via the dynamic variable{{ $keeper("recordUid", "field") }}. The existing Get Keeper Secret action now detects file type and inserts the correct snippet (keeper://...for.env/scripts,{{$keeper(...)}}for HTTP Client files).KeeperSecureScriptRunnerpipeline used by both the Tools menu action and the new run configuration. SlimsKeeperSecretAction.ktby ~500 lines.Closes #9, #11.
Changes
New — Run Configuration (
keepersecurity.run)KeeperSecureRunConfiguration.java— run configuration implementationKeeperSecureRunConfigurationType.kt— registers the configuration typeKeeperSecureRunConfigurationEditor.kt— settings UIKeeperSecureRunConfigurationOptions.kt— persisted optionsKeeperSecureRunProfileState.kt— execution stateKeeperSecureProcessHandler.kt— process lifecycle / output streamingKeeperSecureScriptRunner.kt— shared pipeline (env resolution + execution)KeeperSecureRunDefaults.kt— Python SDK / venv / entry-script auto-detectionKeeperSecurePathUtil.kt— path resolution helpersNew — HTTP Client integration (
keepersecurity.http)KeeperHttpClientKeeperFunctionValue.java— implements the$keeper(...)functionKeeperHttpClientDynamicVariablesProvider.kt— registers the dynamic variableKeeperHttpSecretResolver.kt— resolvesrecordUid/fieldagainst the vaultMETA-INF/keeper-http-client.xml— optional plugin descriptor (loaded only whencom.jetbrains.restClientis present)Updated
action/KeeperGetSecretAction.kt— file-type-aware snippet insertionaction/KeeperSecretAction.kt— delegates toKeeperSecureScriptRunnerutil/KeeperRecordFieldExtractor.kt— shared field extraction utilityMETA-INF/plugin.xml— registers run configuration type + optionalcom.jetbrains.restClientdependencyBuild
gradle.properties—pluginVersion 1.0.0 → 1.1.0,platformType IC → IU, addsplatformBundledPlugins=com.jetbrains.restClient(required to compile againstcom.intellij.httpClient.*)build.gradle.kts— JDK 21 toolchain noteDocs & assets
CHANGELOG.md— 1.1.0 entryREADME.md— HTTP Client + run configuration sections, prerequisitesMETA-INF/pluginIcon.svg/pluginIcon_dark.svgCompatibility notes
com.jetbrains.restClientdependency is declared optional inplugin.xml, so the plugin still loads on IDEs without HTTP Client (e.g. IntelliJ IDEA Community). HTTP-Client-specific features simply won't register in those IDEs.platformType=IUbecause the HTTP Client APIs are only bundled with Ultimate-tier IDEs. Community-only verification can be done by overridingplatformType=ICvia Gradle property.IDE → Settings → Build → Gradle → Gradle JVM.Test plan
./gradlew clean buildsucceeds on JDK 21./gradlew runIdelaunches a sandbox IDE with the plugin loadedRun → Edit Configurations → + → Keeper Securelycreates a new configkeeper://references in.envand streams script output to the Run tool window.httpfile,{{ $keeper("<recordUid>", "<field>") }}resolves to the live vault value at request timeTools → Keeper Vault → Get Keeper Secretinside a.httpfile inserts{{$keeper(...)}}.env/ script file insertskeeper://..../gradlew verifyPluginpasses