mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 08:35:05 -06:00
[PM-15056] Add exportVaultDataToCxf function to VaultRepository (#5847)
This commit is contained in:
parent
808d57edc5
commit
ba7ee04281
@ -24,6 +24,14 @@ interface VaultDiskSource {
|
||||
*/
|
||||
suspend fun getCiphers(userId: String): List<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers with the given [cipherIds] from the data source for a given [userId].
|
||||
*/
|
||||
suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the data source for a given [userId] that contain TOTP codes.
|
||||
*/
|
||||
|
||||
@ -97,6 +97,24 @@ class VaultDiskSourceImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<SyncResponseJson.Cipher> {
|
||||
val entities = ciphersDao.getSelectedCiphers(userId = userId, cipherIds = cipherIds)
|
||||
return withContext(context = dispatcherManager.default) {
|
||||
entities
|
||||
.map { entity ->
|
||||
async {
|
||||
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
|
||||
string = entity.cipherJson,
|
||||
) { Timber.e(it, "Failed to deserialize Cipher in Vault") }
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTotpCiphers(userId: String): List<SyncResponseJson.Cipher> {
|
||||
val entities = ciphersDao.getAllTotpCiphers(userId = userId)
|
||||
return withContext(context = dispatcherManager.default) {
|
||||
|
||||
@ -37,6 +37,15 @@ interface CiphersDao {
|
||||
userId: String,
|
||||
): List<CipherEntity>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the database with the given [cipherIds] for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM ciphers WHERE user_id = :userId AND id IN (:cipherIds)")
|
||||
suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<CipherEntity>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the database for a given [userId].
|
||||
*/
|
||||
|
||||
@ -266,6 +266,13 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
*/
|
||||
suspend fun importCxfPayload(payload: String): ImportCxfPayloadResult
|
||||
|
||||
/**
|
||||
* Attempt to export the vault data to a CXF file.
|
||||
*
|
||||
* @param ciphers Ciphers selected for export.
|
||||
*/
|
||||
suspend fun exportVaultDataToCxf(ciphers: List<CipherListView>): Result<String>
|
||||
|
||||
/**
|
||||
* Flow that represents the data for a specific vault list item as found by ID. This may emit
|
||||
* `null` if the item cannot be found.
|
||||
|
||||
@ -94,6 +94,7 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toSdkAccount
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@ -986,6 +987,28 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun exportVaultDataToCxf(
|
||||
ciphers: List<CipherListView>,
|
||||
): Result<String> {
|
||||
val userId = activeUserId
|
||||
?: return NoActiveUserException().asFailure()
|
||||
val account = authDiskSource.userState
|
||||
?.activeAccount
|
||||
?.toSdkAccount()
|
||||
?: return NoActiveUserException().asFailure()
|
||||
|
||||
val ciphers = vaultDiskSource
|
||||
.getSelectedCiphers(userId = userId, cipherIds = ciphers.mapNotNull { it.id })
|
||||
.map { it.toEncryptedSdkCipher() }
|
||||
|
||||
return vaultSdkSource
|
||||
.exportVaultDataToCxf(
|
||||
userId = userId,
|
||||
account = account,
|
||||
ciphers = ciphers,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
|
||||
* key. This indicates a scenario in which a user has requested PIN unlocking but requires
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.util
|
||||
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
|
||||
/**
|
||||
* Converts a [AccountJson] to a [Account] for use in the SDK.
|
||||
*/
|
||||
fun AccountJson.toSdkAccount(): Account = Account(
|
||||
id = profile.userId,
|
||||
email = profile.email,
|
||||
name = profile.name,
|
||||
)
|
||||
@ -105,6 +105,22 @@ class VaultDiskSourceTest {
|
||||
assertEquals(ciphers, result2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSelectedCiphers should return selected CiphersDao ciphers`() = runTest {
|
||||
val cipherEntities = listOf(
|
||||
CIPHER_ENTITY,
|
||||
CIPHER_ENTITY.copy(id = "otherCipherId"),
|
||||
)
|
||||
val ciphers = listOf(CIPHER_1)
|
||||
val cipherIds = listOf("mockId-1")
|
||||
|
||||
val result1 = vaultDiskSource.getSelectedCiphers(USER_ID, cipherIds)
|
||||
assertEquals(emptyList<SyncResponseJson.Cipher>(), result1)
|
||||
ciphersDao.insertCiphers(cipherEntities)
|
||||
val result2 = vaultDiskSource.getSelectedCiphers(USER_ID, cipherIds)
|
||||
assertEquals(ciphers, result2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTotpCiphers should return all CiphersDao totp ciphers`() = runTest {
|
||||
val cipherEntities = listOf(
|
||||
|
||||
@ -41,6 +41,12 @@ class FakeCiphersDao : CiphersDao {
|
||||
override suspend fun getAllCiphers(userId: String): List<CipherEntity> =
|
||||
storedCiphers.filter { it.userId == userId }
|
||||
|
||||
override suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<CipherEntity> =
|
||||
storedCiphers.filter { it.userId == userId && it.id in cipherIds }
|
||||
|
||||
override suspend fun getAllTotpCiphers(userId: String): List<CipherEntity> =
|
||||
storedCiphers.filter { it.userId == userId && it.hasTotp }
|
||||
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||
|
||||
import com.bitwarden.exporters.Account
|
||||
|
||||
/**
|
||||
* Creates a mock [com.bitwarden.exporters.Account] for testing purposes
|
||||
*/
|
||||
fun createMockAccount(
|
||||
number: Int,
|
||||
email: String = "mockEmail-$number",
|
||||
name: String? = "mockName-$number",
|
||||
): Account = Account(
|
||||
id = "mockId-$number",
|
||||
email = email,
|
||||
name = name,
|
||||
)
|
||||
@ -71,6 +71,8 @@ import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAccount
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockDecryptCipherListResult
|
||||
@ -4298,6 +4300,62 @@ class VaultRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exportVaultDataToCxf should return success result`() = runTest {
|
||||
val userId = "mockId-1"
|
||||
val account = createMockAccount(number = 1, email = "email", name = null)
|
||||
val cipherListViews = listOf(createMockCipherListView(number = 1))
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.exportVaultDataToCxf(
|
||||
userId = userId,
|
||||
account = account,
|
||||
ciphers = any(),
|
||||
)
|
||||
} returns "TestResult".asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.getSelectedCiphers(
|
||||
userId = userId,
|
||||
cipherIds = cipherListViews.mapNotNull { it.id },
|
||||
)
|
||||
} returns listOf(createMockCipher(number = 1))
|
||||
|
||||
val result = vaultRepository.exportVaultDataToCxf(ciphers = cipherListViews)
|
||||
|
||||
assertEquals("TestResult".asSuccess(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exportVaultDataToCxf should return error result when exportVaultDataToCxf fails`() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
val account = createMockAccount(number = 1, email = "email", name = null)
|
||||
val cipherListViews = listOf(createMockCipherListView(number = 1))
|
||||
val throwable = Throwable()
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.exportVaultDataToCxf(
|
||||
userId = userId,
|
||||
account = account,
|
||||
ciphers = any(),
|
||||
)
|
||||
} returns throwable.asFailure()
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.getSelectedCiphers(
|
||||
userId = userId,
|
||||
cipherIds = cipherListViews.mapNotNull { it.id },
|
||||
)
|
||||
} returns listOf(createMockCipher(number = 1))
|
||||
|
||||
val result = vaultRepository.exportVaultDataToCxf(ciphers = cipherListViews)
|
||||
|
||||
assertEquals(throwable.asFailure(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `silentlyDiscoverCredentials should return result`() = runTest {
|
||||
val userId = "userId"
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Creates a mock [AccountJson.Profile] for testing purposes.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun createMockAccountJsonProfile(
|
||||
number: Int,
|
||||
userId: String = "mockId-$number",
|
||||
email: String = "mockEmail-$number",
|
||||
isEmailVerified: Boolean = true,
|
||||
name: String? = "mockName-$number",
|
||||
stamp: String = "mockSecurityStamp-$number",
|
||||
organizationId: String? = "mockOrganizationId-$number",
|
||||
avatarColorHex: String? = "mockAvatarColorHex-$number",
|
||||
hasPremium: Boolean = false,
|
||||
forcePasswordResetReason: ForcePasswordResetReason? = null,
|
||||
kdfType: KdfTypeJson? = null,
|
||||
kdfIterations: Int? = null,
|
||||
kdfMemory: Int? = null,
|
||||
kdfParallelism: Int? = null,
|
||||
userDecryptionOptions: UserDecryptionOptionsJson? = null,
|
||||
isTwoFactorEnabled: Boolean = false,
|
||||
creationDate: ZonedDateTime = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
): AccountJson.Profile = AccountJson.Profile(
|
||||
userId = userId,
|
||||
email = email,
|
||||
isEmailVerified = isEmailVerified,
|
||||
name = name,
|
||||
stamp = stamp,
|
||||
organizationId = organizationId,
|
||||
avatarColorHex = avatarColorHex,
|
||||
hasPremium = hasPremium,
|
||||
forcePasswordResetReason = forcePasswordResetReason,
|
||||
kdfType = kdfType,
|
||||
kdfIterations = kdfIterations,
|
||||
kdfMemory = kdfMemory,
|
||||
kdfParallelism = kdfParallelism,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
isTwoFactorEnabled = isTwoFactorEnabled,
|
||||
creationDate = creationDate,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a mock [AccountJson] for testing purposes.
|
||||
*/
|
||||
fun createMockAccountJson(
|
||||
number: Int,
|
||||
profile: AccountJson.Profile = createMockAccountJsonProfile(number),
|
||||
tokens: AccountTokensJson = AccountTokensJson(
|
||||
accessToken = "accessToken-$number",
|
||||
refreshToken = "refreshToken-$number",
|
||||
),
|
||||
settings: AccountJson.Settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
): AccountJson = AccountJson(
|
||||
profile = profile,
|
||||
tokens = tokens,
|
||||
settings = settings,
|
||||
)
|
||||
@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.util
|
||||
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.createMockAccountJson
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AccountJsonExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `toAccountData returns populated AccountData when account is non-null`() {
|
||||
val account = createMockAccountJson(number = 1)
|
||||
assertEquals(
|
||||
Account(
|
||||
id = "mockId-1",
|
||||
email = "mockEmail-1",
|
||||
name = "mockName-1",
|
||||
),
|
||||
account.toSdkAccount(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user