IJPL-199758 Asynchronous credential store API

IJ-CR-177617 IJ-MR-178816

(cherry picked from commit e000745417425417326e229462f66d208a39db9a)

GitOrigin-RevId: 9659a0132ca707c9b955f46abf51b9dbeb044323
This commit is contained in:
Vyacheslav Karpukhin
2025-09-29 17:25:42 +02:00
committed by intellij-monorepo-bot
parent 7c5ca137c9
commit cc613702a4
8 changed files with 88 additions and 32 deletions

View File

@@ -9,24 +9,19 @@ import com.intellij.credentialStore.keePass.getDefaultDbFile
import com.intellij.ide.passwordSafe.PasswordSafe
import com.intellij.notification.NotificationAction
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ComponentManagerEx
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.util.NlsContexts
import com.intellij.serviceContainer.NonInjectable
import com.intellij.util.Ephemeral
import com.intellij.util.SlowOperations
import com.intellij.util.concurrency.SynchronizedClearableLazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.TestOnly
import org.jetbrains.concurrency.Promise
import org.jetbrains.concurrency.asPromise
import java.io.Closeable
import java.nio.file.Path
import java.nio.file.Paths
@@ -35,7 +30,7 @@ private val LOG: Logger
get() = logger<CredentialStore>()
@Internal
abstract class BasePasswordSafe(private val coroutineScope: CoroutineScope) : PasswordSafe() {
abstract class BasePasswordSafe : PasswordSafe() {
protected abstract val settings: PasswordSafeSettings
override var isRememberPasswordByDefault: Boolean
@@ -128,12 +123,8 @@ abstract class BasePasswordSafe(private val coroutineScope: CoroutineScope) : Pa
}
}
// maybe in the future we will use native async; this method added here instead "if needed, just use runAsync in your code"
override fun getAsync(attributes: CredentialAttributes): Promise<Credentials?> {
return coroutineScope.async(Dispatchers.IO) {
get(attributes)
}.asCompletableFuture().asPromise()
}
override suspend fun getAsync(attributes: CredentialAttributes): Ephemeral<Credentials> =
currentProvider.getAsync(attributes)
suspend fun save() {
val keePassCredentialStore = currentProviderIfComputed as? KeePassCredentialStore ?: return
@@ -161,7 +152,7 @@ abstract class BasePasswordSafe(private val coroutineScope: CoroutineScope) : Pa
@Internal
class TestPasswordSafeImpl @NonInjectable constructor(
override val settings: PasswordSafeSettings
) : BasePasswordSafe(coroutineScope = (ApplicationManager.getApplication() as ComponentManagerEx).getCoroutineScope()) {
) : BasePasswordSafe() {
@TestOnly
constructor() : this(service<PasswordSafeSettings>())
@@ -173,7 +164,7 @@ class TestPasswordSafeImpl @NonInjectable constructor(
}
@Internal
class PasswordSafeImpl(coroutineScope: CoroutineScope) : BasePasswordSafe(coroutineScope), SettingsSavingComponent {
class PasswordSafeImpl : BasePasswordSafe(), SettingsSavingComponent {
override val settings: PasswordSafeSettings
get() = service<PasswordSafeSettings>()
}

View File

@@ -12,6 +12,7 @@ jvm_library(
"//platform/util",
"@lib//:kotlin-stdlib",
"//platform/core-api:core",
"//platform/util/concurrency",
]
)
### auto-generated section `build intellij.platform.credentialStore` end

View File

@@ -0,0 +1,7 @@
com.intellij.credentialStore.CredentialStore
- *:ephemeral(java.lang.Object,kotlin.coroutines.Continuation):java.lang.Object
- *:getAsync(com.intellij.credentialStore.CredentialAttributes,kotlin.coroutines.Continuation):java.lang.Object
*:com.intellij.util.Ephemeral
- a:asFlow():kotlinx.coroutines.flow.Flow
- a:derive(kotlin.jvm.functions.Function1):com.intellij.util.Ephemeral
- a:unwrap(kotlin.coroutines.Continuation):java.lang.Object

View File

@@ -76,7 +76,6 @@ a:com.intellij.ide.passwordSafe.PasswordSafe
- com.intellij.ide.passwordSafe.PasswordStorage
- sf:Companion:com.intellij.ide.passwordSafe.PasswordSafe$Companion
- <init>():V
- a:getAsync(com.intellij.credentialStore.CredentialAttributes):org.jetbrains.concurrency.Promise
- sf:getInstance():com.intellij.ide.passwordSafe.PasswordSafe
- a:isMemoryOnly():Z
- a:isPasswordStoredOnlyInMemory(com.intellij.credentialStore.CredentialAttributes,com.intellij.credentialStore.Credentials):Z

View File

@@ -12,5 +12,6 @@
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.concurrency" />
</component>
</module>

View File

@@ -1,23 +1,34 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.credentialStore;
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.credentialStore
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.intellij.util.Ephemeral
import com.intellij.util.StaticEphemeral
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.concurrency.await
import org.jetbrains.concurrency.runAsync
/**
* Please see <a href="https://plugins.jetbrains.com/docs/intellij/persisting-sensitive-data.html">Storing Sensitive Data</a>.
* Please see [Storing Sensitive Data](https://plugins.jetbrains.com/docs/intellij/persisting-sensitive-data.html).
*/
public interface CredentialStore {
@Nullable Credentials get(@NotNull CredentialAttributes attributes);
interface CredentialStore {
operator fun get(attributes: CredentialAttributes): Credentials?
default @Nullable String getPassword(@NotNull CredentialAttributes attributes) {
var credentials = get(attributes);
return credentials == null ? null : credentials.getPasswordAsString();
fun getPassword(attributes: CredentialAttributes): String? {
val credentials = get(attributes)
return credentials?.getPasswordAsString()
}
void set(@NotNull CredentialAttributes attributes, @Nullable Credentials credentials);
@ApiStatus.Experimental
suspend fun getAsync(attributes: CredentialAttributes): Ephemeral<Credentials> =
ephemeral(runAsync { get(attributes) }.await() )
default void setPassword(@NotNull CredentialAttributes attributes, @Nullable String password) {
set(attributes, password == null ? null : new Credentials(attributes.getUserName(), password));
@ApiStatus.Experimental
suspend fun <T : Any> ephemeral(value: T?): Ephemeral<T> =
StaticEphemeral(value)
operator fun set(attributes: CredentialAttributes, credentials: Credentials?)
fun setPassword(attributes: CredentialAttributes, password: String?) {
set(attributes, password?.let { Credentials(attributes.userName, it) })
}
}

View File

@@ -5,7 +5,6 @@ import com.intellij.credentialStore.CredentialAttributes
import com.intellij.credentialStore.CredentialStore
import com.intellij.credentialStore.Credentials
import com.intellij.openapi.application.ApplicationManager
import org.jetbrains.concurrency.Promise
/**
* [See the documentation](https://plugins.jetbrains.com/docs/intellij/persisting-sensitive-data.html).
@@ -27,7 +26,5 @@ abstract class PasswordSafe : CredentialStore, PasswordStorage {
abstract operator fun set(attributes: CredentialAttributes, credentials: Credentials?, memoryOnly: Boolean)
abstract fun getAsync(attributes: CredentialAttributes): Promise<Credentials?>
abstract fun isPasswordStoredOnlyInMemory(attributes: CredentialAttributes, credentials: Credentials): Boolean
}

View File

@@ -0,0 +1,49 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.util
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.jetbrains.annotations.ApiStatus
/**
* Represents a wrapper for an ephemeral value with a limited lifetime.
*/
@ApiStatus.Experimental
interface Ephemeral<out T : Any> {
/**
* Suspends until the ephemeral value held by this instance is available and returns it, or `null` if the value has expired.
*/
suspend fun unwrap(): T?
/**
* Returns a long-living `Flow` that tracks the ephemeral value lifecycle.
*
* Flow behavior:
* - Starts with `null`
* - Emits the value when available
* - Emits `null` when expired
* - Can re-emit the value if it becomes available again
*
* The flow can outlive the original `Ephemeral` instance. Exceptions are logged and treated as `null`.
*
* @return A `Flow<T?>` where `null` indicates unavailable value
*/
fun asFlow(): Flow<T?>
/**
* Creates a new `Ephemeral` instance by transforming the value held by the current instance
* using the provided mapping function. The resulting value inherits the original lifetime.
*/
fun <P : Any> derive(map: (T) -> P?): Ephemeral<P>
}
internal class StaticEphemeral<T : Any>(private val data: T?) : Ephemeral<T> {
override suspend fun unwrap(): T? =
data
override fun asFlow(): Flow<T?> =
flowOf(data)
override fun <P : Any> derive(map: (T) -> P?): Ephemeral<P> =
StaticEphemeral(data?.let(map))
}