Skip to content

Commit

Permalink
[PM-15054] Add API for importing ciphers (#4339)
Browse files Browse the repository at this point in the history
  • Loading branch information
SaintPatrck authored Nov 22, 2024
1 parent 050b3b3 commit 89935ac
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonReq
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
Expand Down Expand Up @@ -149,4 +150,9 @@ interface CiphersApi {
*/
@GET("ciphers/has-unassigned-ciphers")
suspend fun hasUnassignedCiphers(): NetworkResult<Boolean>

@POST("ciphers/import")
suspend fun importCiphers(
@Body body: ImportCiphersJsonRequest,
): NetworkResult<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Represents an import ciphers request.
*
* @property folders A list of folders to import.
* @property ciphers A list of ciphers to import.
* @property folderRelationships A map of cipher folder relationships to import. Key correlates to
* the index of the cipher in the ciphers list. Value correlates to the index of the folder in the
* folders list.
*/
@Serializable
data class ImportCiphersJsonRequest(
@SerialName("folders")
val folders: List<FolderWithIdJsonRequest>,
@SerialName("ciphers")
val ciphers: List<CipherJsonRequest>,
@SerialName("folderRelationships")
val folderRelationships: Map<Int, Int>,
) {
/**
* Represents a folder request with an optional [id] if the folder already exists.
*
* @property name The name of the folder.
* @property id The ID of the folder, if it already exists. Null otherwise.
**/
@Serializable
data class FolderWithIdJsonRequest(
@SerialName("name")
val name: String?,
@SerialName("id")
val id: String?,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* The response body for importing ciphers.
*/
@Serializable
sealed class ImportCiphersResponseJson {

/**
* Models a successful json response.
*/
@Serializable
object Success : ImportCiphersResponseJson()

/**
* Represents the json body of an invalid request.
*
* @param validationErrors a map where each value is a list of error messages for each key.
* The values in the array should be used for display to the user, since the keys tend to come
* back as nonsense. (eg: empty string key)
*/
@Serializable
data class Invalid(
@SerialName("message")
private val invalidMessage: String? = null,

@SerialName("Message")
private val errorMessage: String? = null,

@SerialName("validationErrors")
val validationErrors: Map<String, List<String>>?,
) : ImportCiphersResponseJson() {
/**
* A generic error message.
*/
val message: String? get() = invalidMessage ?: errorMessage
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonReq
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
Expand Down Expand Up @@ -118,4 +120,9 @@ interface CiphersService {
* Returns a boolean indicating if the active user has unassigned ciphers.
*/
suspend fun hasUnassignedCiphers(): Result<Boolean>

/**
* Attempt to import ciphers.
*/
suspend fun importCiphers(request: ImportCiphersJsonRequest): Result<ImportCiphersResponseJson>
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRes
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
Expand Down Expand Up @@ -216,6 +218,23 @@ class CiphersServiceImpl(
.hasUnassignedCiphers()
.toResult()

override suspend fun importCiphers(
request: ImportCiphersJsonRequest,
): Result<ImportCiphersResponseJson> =
ciphersApi
.importCiphers(body = request)
.toResult()
.map { ImportCiphersResponseJson.Success }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<ImportCiphersResponseJson.Invalid>(
code = 400,
json = json,
)
?: throw throwable
}

private fun createMultipartBodyBuilder(
encryptedFile: File,
filename: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi
import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
Expand Down Expand Up @@ -321,6 +323,32 @@ class CiphersServiceTest : BaseServiceTest() {
val result = ciphersService.hasUnassignedCiphers()
assertTrue(result.getOrThrow())
}

@Test
fun `importCiphers should return the correct response`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val result = ciphersService.importCiphers(
request = ImportCiphersJsonRequest(
ciphers = listOf(createMockCipherJsonRequest(number = 1)),
folders = emptyList(),
folderRelationships = emptyMap(),
),
)
assertEquals(ImportCiphersResponseJson.Success, result.getOrThrow())
}

@Test
fun `importCiphers should return an error when the response is an error`() = runTest {
server.enqueue(MockResponse().setResponseCode(400))
val result = ciphersService.importCiphers(
request = ImportCiphersJsonRequest(
ciphers = listOf(createMockCipherJsonRequest(number = 1)),
folders = emptyList(),
folderRelationships = emptyMap(),
),
)
assertTrue(result.isFailure)
}
}

private fun setupMockUri(
Expand Down

0 comments on commit 89935ac

Please sign in to comment.