Make SdkClientManager the single source of the Bitwarden SDK Client (#1242)

This commit is contained in:
David Perez 2024-04-08 16:47:25 -05:00 committed by Álison Fernandes
parent bf26db1d4f
commit de39f76627
9 changed files with 94 additions and 114 deletions

View file

@ -6,53 +6,43 @@ import com.bitwarden.core.MasterPasswordPolicyOptions
import com.bitwarden.core.RegisterKeyResponse
import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientAuth
import com.bitwarden.sdk.ClientPlatform
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
/**
* Primary implementation of [AuthSdkSource] that serves as a convenience wrapper around a
* [ClientAuth].
*/
class AuthSdkSourceImpl(
private val clientAuth: ClientAuth,
private val clientPlatform: ClientPlatform,
dispatcherManager: DispatcherManager,
featureFlagManager: BitwardenFeatureFlagManager,
private val sdkClientManager: SdkClientManager,
) : AuthSdkSource {
private val ioScope = CoroutineScope(dispatcherManager.io)
init {
ioScope.launch {
clientPlatform.loadFlags(featureFlagManager.featureFlags)
}
}
override suspend fun getNewAuthRequest(
email: String,
): Result<AuthRequestResponse> = runCatching {
clientAuth.newAuthRequest(
email = email,
)
getClient()
.auth()
.newAuthRequest(
email = email,
)
}
override suspend fun getUserFingerprint(
email: String,
publicKey: String,
): Result<String> = runCatching {
clientPlatform.fingerprint(
req = FingerprintRequest(
fingerprintMaterial = email,
publicKey = publicKey,
),
)
getClient()
.platform()
.fingerprint(
req = FingerprintRequest(
fingerprintMaterial = email,
publicKey = publicKey,
),
)
}
override suspend fun hashPassword(
@ -61,12 +51,14 @@ class AuthSdkSourceImpl(
kdf: Kdf,
purpose: HashPurpose,
): Result<String> = runCatching {
clientAuth.hashPassword(
email = email,
password = password,
kdfParams = kdf,
purpose = purpose,
)
getClient()
.auth()
.hashPassword(
email = email,
password = password,
kdfParams = kdf,
purpose = purpose,
)
}
override suspend fun makeRegisterKeys(
@ -74,11 +66,13 @@ class AuthSdkSourceImpl(
password: String,
kdf: Kdf,
): Result<RegisterKeyResponse> = runCatching {
clientAuth.makeRegisterKeys(
email = email,
password = password,
kdf = kdf,
)
getClient()
.auth()
.makeRegisterKeys(
email = email,
password = password,
kdf = kdf,
)
}
override suspend fun passwordStrength(
@ -87,7 +81,8 @@ class AuthSdkSourceImpl(
additionalInputs: List<String>,
): Result<PasswordStrength> = runCatching {
@Suppress("UnsafeCallOnNullableType")
clientAuth
getClient()
.auth()
.passwordStrength(
password = password,
email = email,
@ -101,10 +96,16 @@ class AuthSdkSourceImpl(
passwordStrength: PasswordStrength,
policy: MasterPasswordPolicyOptions,
): Result<Boolean> = runCatching {
clientAuth.satisfiesPolicy(
password = password,
strength = passwordStrength.toUByte(),
policy = policy,
)
getClient()
.auth()
.satisfiesPolicy(
password = password,
strength = passwordStrength.toUByte(),
policy = policy,
)
}
private suspend fun getClient(
userId: String? = null,
): Client = sdkClientManager.getOrCreateClient(userId = userId)
}

View file

@ -1,10 +1,8 @@
package com.x8bit.bitwarden.data.auth.datasource.sdk.di
import com.bitwarden.sdk.Client
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSourceImpl
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -21,13 +19,8 @@ object AuthSdkModule {
@Provides
@Singleton
fun provideAuthSdkSource(
client: Client,
featureFlagManager: BitwardenFeatureFlagManager,
dispatcherManager: DispatcherManager,
sdkClientManager: SdkClientManager,
): AuthSdkSource = AuthSdkSourceImpl(
clientAuth = client.auth(),
clientPlatform = client.platform(),
featureFlagManager = featureFlagManager,
dispatcherManager = dispatcherManager,
sdkClientManager = sdkClientManager,
)
}

View file

@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.di
import com.bitwarden.sdk.Client
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Provides dependencies related to encryption / decryption / secure generation.
*/
@Module
@InstallIn(SingletonComponent::class)
object EncryptionModule {
@Provides
@Singleton
fun provideBitwardenClient(): Client {
return Client(null)
}
}

View file

@ -11,11 +11,11 @@ interface SdkClientManager {
* Returns the cached [Client] instance for the given [userId], otherwise creates and caches
* a new one and returns it.
*/
suspend fun getOrCreateClient(userId: String): Client
suspend fun getOrCreateClient(userId: String?): Client
/**
* Clears any resources from the [Client] associated with the given [userId] and removes it
* from the internal cache.
*/
fun destroyClient(userId: String)
fun destroyClient(userId: String?)
}

View file

@ -9,19 +9,19 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
class SdkClientManagerImpl(
private val featureFlagManager: BitwardenFeatureFlagManager,
private val clientProvider: suspend () -> Client = {
Client(null)
.apply { platform().loadFlags(featureFlagManager.featureFlags) }
Client(settings = null).apply {
platform().loadFlags(featureFlagManager.featureFlags)
}
},
) : SdkClientManager {
private val userIdToClientMap = mutableMapOf<String, Client>()
private val userIdToClientMap = mutableMapOf<String?, Client>()
override suspend fun getOrCreateClient(
userId: String,
): Client =
userIdToClientMap.getOrPut(key = userId) { clientProvider() }
userId: String?,
): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider() }
override fun destroyClient(
userId: String,
userId: String?,
) {
userIdToClientMap
.remove(key = userId)

View file

@ -3,50 +3,57 @@ package com.x8bit.bitwarden.data.tools.generator.datasource.sdk
import com.bitwarden.generators.PassphraseGeneratorRequest
import com.bitwarden.generators.PasswordGeneratorRequest
import com.bitwarden.generators.UsernameGeneratorRequest
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientGenerators
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
/**
* Implementation of [GeneratorSdkSource] that delegates password generation.
*
* @property clientGenerator An instance of [ClientGenerators] provided by the Bitwarden SDK.
* @property sdkClientManager The [SdkClientManager] used to retrieve an instance of the
* [ClientGenerators] provided by the Bitwarden SDK.
*/
class GeneratorSdkSourceImpl(
private val clientGenerator: ClientGenerators,
private val sdkClientManager: SdkClientManager,
) : GeneratorSdkSource {
override suspend fun generatePassword(
request: PasswordGeneratorRequest,
): Result<String> = runCatching {
clientGenerator.password(request)
getClient().generators().password(request)
}
override suspend fun generatePassphrase(
request: PassphraseGeneratorRequest,
): Result<String> = runCatching {
clientGenerator.passphrase(request)
getClient().generators().passphrase(request)
}
override suspend fun generatePlusAddressedEmail(
request: UsernameGeneratorRequest.Subaddress,
): Result<String> = runCatching {
clientGenerator.username(request)
getClient().generators().username(request)
}
override suspend fun generateCatchAllEmail(
request: UsernameGeneratorRequest.Catchall,
): Result<String> = runCatching {
clientGenerator.username(request)
getClient().generators().username(request)
}
override suspend fun generateRandomWord(
request: UsernameGeneratorRequest.Word,
): Result<String> = runCatching {
clientGenerator.username(request)
getClient().generators().username(request)
}
override suspend fun generateForwardedServiceEmail(
request: UsernameGeneratorRequest.Forwarded,
): Result<String> = runCatching {
clientGenerator.username(request)
getClient().generators().username(request)
}
private suspend fun getClient(
userId: String? = null,
): Client = sdkClientManager.getOrCreateClient(userId = userId)
}

View file

@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.sdk.di
import com.bitwarden.sdk.Client
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSource
import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSourceImpl
import dagger.Module
@ -19,6 +19,6 @@ object GeneratorSdkModule {
@Provides
@Singleton
fun provideGeneratorSdkSource(
client: Client,
): GeneratorSdkSource = GeneratorSdkSourceImpl(clientGenerator = client.generators())
sdkClientManager: SdkClientManager,
): GeneratorSdkSource = GeneratorSdkSourceImpl(sdkClientManager = sdkClientManager)
}

View file

@ -6,20 +6,20 @@ import com.bitwarden.core.MasterPasswordPolicyOptions
import com.bitwarden.core.RegisterKeyResponse
import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientAuth
import com.bitwarden.sdk.ClientPlatform
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
@ -28,26 +28,18 @@ class AuthSdkSourceTest {
private val clientPlatform = mockk<ClientPlatform> {
coEvery { loadFlags(any()) } just runs
}
private val featureFlagManager = mockk<BitwardenFeatureFlagManager> {
coEvery { featureFlags } returns emptyMap()
private val client = mockk<Client> {
every { auth() } returns clientAuth
every { platform() } returns clientPlatform
}
private val sdkClientManager = mockk<SdkClientManager> {
coEvery { getOrCreateClient(userId = null) } returns client
}
private val dispatcherManager = FakeDispatcherManager()
private val authSkdSource: AuthSdkSource = AuthSdkSourceImpl(
clientAuth = clientAuth,
clientPlatform = clientPlatform,
featureFlagManager = featureFlagManager,
dispatcherManager = dispatcherManager,
sdkClientManager = sdkClientManager,
)
@BeforeEach
fun setup() {
coVerify(exactly = 1) {
featureFlagManager.featureFlags
clientPlatform.loadFlags(any())
}
}
@Test
fun `getNewAuthRequest should call SDK and return a Result with correct data`() = runBlocking {
val email = "test@gmail.com"

View file

@ -5,10 +5,13 @@ import com.bitwarden.generators.ForwarderServiceType
import com.bitwarden.generators.PassphraseGeneratorRequest
import com.bitwarden.generators.PasswordGeneratorRequest
import com.bitwarden.generators.UsernameGeneratorRequest
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientGenerators
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.util.asSuccess
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
@ -16,7 +19,13 @@ import org.junit.Test
class GeneratorSdkSourceTest {
private val clientGenerators = mockk<ClientGenerators>()
private val generatorSdkSource: GeneratorSdkSource = GeneratorSdkSourceImpl(clientGenerators)
private val client = mockk<Client> {
every { generators() } returns clientGenerators
}
private val sdkClientManager = mockk<SdkClientManager> {
coEvery { getOrCreateClient(userId = null) } returns client
}
private val generatorSdkSource: GeneratorSdkSource = GeneratorSdkSourceImpl(sdkClientManager)
@Test
fun `generatePassword should call SDK and return a Result with the generated password`() =