mirror of
https://github.com/bitwarden/android.git
synced 2026-06-01 18:26:31 -05:00
[PM-32802] fix: 400 error when archiving/unarchiving org-owned ciphers (#6592)
This commit is contained in:
@@ -8,10 +8,12 @@ import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.manager.file.FileManager
|
||||
import com.bitwarden.data.manager.model.DownloadResult
|
||||
import com.bitwarden.network.model.ArchiveCipherResponseJson
|
||||
import com.bitwarden.network.model.AttachmentJsonResponse
|
||||
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.bitwarden.network.model.CreateCipherResponseJson
|
||||
import com.bitwarden.network.model.ShareCipherJsonRequest
|
||||
import com.bitwarden.network.model.UnarchiveCipherResponseJson
|
||||
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
||||
import com.bitwarden.network.model.UpdateCipherResponseJson
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
@@ -168,33 +170,29 @@ class CipherManagerImpl(
|
||||
cipherView: CipherView,
|
||||
): ArchiveCipherResult {
|
||||
val userId = activeUserId ?: return ArchiveCipherResult.Error(NoActiveUserException())
|
||||
return cipherView
|
||||
.encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId)
|
||||
.flatMap { encryptionContext ->
|
||||
ciphersService
|
||||
.archiveCipher(cipherId = cipherId)
|
||||
.flatMap {
|
||||
vaultSdkSource.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = encryptionContext.cipher,
|
||||
)
|
||||
return ciphersService
|
||||
.archiveCipher(cipherId = cipherId)
|
||||
.flatMap { response ->
|
||||
when (response) {
|
||||
is ArchiveCipherResponseJson.Invalid -> {
|
||||
IllegalStateException(response.firstValidationErrorMessage)
|
||||
.asFailure()
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
vaultSdkSource.encryptCipher(
|
||||
userId = userId,
|
||||
cipherView = it.copy(archivedDate = clock.instant()),
|
||||
)
|
||||
}
|
||||
.onSuccess {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedNetworkCipherResponse(),
|
||||
)
|
||||
settingsDiskSource.storeIntroducingArchiveActionCardDismissed(
|
||||
userId = userId,
|
||||
isDismissed = true,
|
||||
)
|
||||
|
||||
is ArchiveCipherResponseJson.Success -> {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = response.cipher.copy(
|
||||
collectionIds = cipherView.collectionIds,
|
||||
),
|
||||
)
|
||||
settingsDiskSource.storeIntroducingArchiveActionCardDismissed(
|
||||
userId = userId,
|
||||
isDismissed = true,
|
||||
)
|
||||
response.asSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { ArchiveCipherResult.Success },
|
||||
@@ -207,29 +205,25 @@ class CipherManagerImpl(
|
||||
cipherView: CipherView,
|
||||
): UnarchiveCipherResult {
|
||||
val userId = activeUserId ?: return UnarchiveCipherResult.Error(NoActiveUserException())
|
||||
return cipherView
|
||||
.encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId)
|
||||
.flatMap { encryptionContext ->
|
||||
ciphersService
|
||||
.unarchiveCipher(cipherId = cipherId)
|
||||
.flatMap {
|
||||
vaultSdkSource.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = encryptionContext.cipher,
|
||||
)
|
||||
return ciphersService
|
||||
.unarchiveCipher(cipherId = cipherId)
|
||||
.flatMap { response ->
|
||||
when (response) {
|
||||
is UnarchiveCipherResponseJson.Invalid -> {
|
||||
IllegalStateException(response.firstValidationErrorMessage)
|
||||
.asFailure()
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
vaultSdkSource.encryptCipher(
|
||||
userId = userId,
|
||||
cipherView = it.copy(archivedDate = null),
|
||||
)
|
||||
}
|
||||
.onSuccess {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedNetworkCipherResponse(),
|
||||
)
|
||||
|
||||
is UnarchiveCipherResponseJson.Success -> {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = response.cipher.copy(
|
||||
collectionIds = cipherView.collectionIds,
|
||||
),
|
||||
)
|
||||
response.asSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { UnarchiveCipherResult.Success },
|
||||
@@ -255,6 +249,9 @@ class CipherManagerImpl(
|
||||
): DeleteCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return DeleteCipherResult.Error(error = NoActiveUserException())
|
||||
// Unlike archive/unarchive, soft delete requires edit permissions, so the
|
||||
// migration check is intentional here to ensure the cipher is up-to-date
|
||||
// before deletion.
|
||||
return cipherView
|
||||
.encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId)
|
||||
.flatMap { encryptionContext ->
|
||||
|
||||
@@ -8,11 +8,13 @@ import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.data.manager.file.FileManager
|
||||
import com.bitwarden.data.manager.model.DownloadResult
|
||||
import com.bitwarden.network.model.ArchiveCipherResponseJson
|
||||
import com.bitwarden.network.model.AttachmentJsonRequest
|
||||
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.bitwarden.network.model.CreateCipherResponseJson
|
||||
import com.bitwarden.network.model.ShareCipherJsonRequest
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.UnarchiveCipherResponseJson
|
||||
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
||||
import com.bitwarden.network.model.UpdateCipherResponseJson
|
||||
import com.bitwarden.network.model.createMockAttachment
|
||||
@@ -74,6 +76,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.HttpException
|
||||
@@ -711,60 +714,63 @@ class CipherManagerTest {
|
||||
fun `archiveCipher with ciphersService archiveCipher failure should return ArchiveCipherResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-1"
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
val encryptionContext = createMockEncryptionContext(number = 1)
|
||||
val error = Throwable("Fail")
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
ciphersService.archiveCipher(cipherId = cipherId)
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = cipherManager.archiveCipher(
|
||||
cipherId = cipherId,
|
||||
cipherView = cipherView,
|
||||
cipherView = createMockCipherView(number = 1),
|
||||
)
|
||||
|
||||
assertEquals(ArchiveCipherResult.Error(error = error), result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `archiveCipher with ciphersService archiveCipher invalid should return ArchiveCipherResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val cipherId = "mockId-1"
|
||||
coEvery {
|
||||
ciphersService.archiveCipher(cipherId = cipherId)
|
||||
} returns ArchiveCipherResponseJson.Invalid(
|
||||
message = "You do not have permission to edit this.",
|
||||
validationErrors = null,
|
||||
).asSuccess()
|
||||
|
||||
val result = cipherManager.archiveCipher(
|
||||
cipherId = cipherId,
|
||||
cipherView = createMockCipherView(number = 1),
|
||||
)
|
||||
|
||||
assertTrue(result is ArchiveCipherResult.Error)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `archiveCipher with ciphersService archiveCipher success should return ArchiveCipherResult success`() =
|
||||
runTest {
|
||||
val fixedInstant = Instant.parse("2023-10-27T12:00:00Z")
|
||||
val userId = "mockId-1"
|
||||
val cipherId = "mockId-1"
|
||||
val encryptionContext = createMockEncryptionContext(
|
||||
number = 1,
|
||||
cipher = createMockSdkCipher(number = 1, clock = clock),
|
||||
)
|
||||
val cipher = createMockCipher(number = 1)
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
fakeSettingsDiskSource.storeIntroducingArchiveActionCardDismissed(
|
||||
userId = userId,
|
||||
isDismissed = null,
|
||||
)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(
|
||||
userId = userId,
|
||||
cipherView = cipherView.copy(archivedDate = fixedInstant),
|
||||
)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(userId = userId, cipher = encryptionContext.cipher)
|
||||
} returns cipherView.asSuccess()
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { ciphersService.archiveCipher(cipherId = cipherId) } returns Unit.asSuccess()
|
||||
coEvery {
|
||||
ciphersService.archiveCipher(cipherId = cipherId)
|
||||
} returns ArchiveCipherResponseJson.Success(cipher = cipher).asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = encryptionContext.toEncryptedNetworkCipherResponse(),
|
||||
cipher = cipher.copy(
|
||||
collectionIds = cipherView.collectionIds,
|
||||
),
|
||||
)
|
||||
} just runs
|
||||
|
||||
@@ -780,70 +786,6 @@ class CipherManagerTest {
|
||||
assertEquals(ArchiveCipherResult.Success, result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `archiveCipher with cipher migration success should return ArchiveCipherResult success`() =
|
||||
runTest {
|
||||
val fixedInstant = Instant.parse("2023-10-27T12:00:00Z")
|
||||
val userId = "mockId-1"
|
||||
val cipherId = "mockId-1"
|
||||
val encryptionContext = createMockEncryptionContext(
|
||||
number = 1,
|
||||
cipher = createMockSdkCipher(number = 1, clock = clock),
|
||||
)
|
||||
val cipherView = createMockCipherView(number = 1).copy(key = null)
|
||||
val networkCipher = createMockCipher(number = 1).copy(key = null)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
ciphersService.updateCipher(
|
||||
cipherId = cipherId,
|
||||
body = encryptionContext.toEncryptedNetworkCipher(),
|
||||
)
|
||||
} returns UpdateCipherResponseJson.Success(networkCipher).asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = networkCipher)
|
||||
} just runs
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = networkCipher.toEncryptedSdkCipher(),
|
||||
)
|
||||
} returns cipherView.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(
|
||||
userId = userId,
|
||||
cipherView = cipherView.copy(archivedDate = fixedInstant),
|
||||
)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(userId = userId, cipher = encryptionContext.cipher)
|
||||
} returns cipherView.asSuccess()
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { ciphersService.archiveCipher(cipherId = cipherId) } returns Unit.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = encryptionContext.toEncryptedNetworkCipherResponse(),
|
||||
)
|
||||
} just runs
|
||||
|
||||
val result = cipherManager.archiveCipher(
|
||||
cipherId = cipherId,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
|
||||
assertEquals(ArchiveCipherResult.Success, result)
|
||||
coVerify(exactly = 1) {
|
||||
ciphersService.updateCipher(
|
||||
cipherId = cipherId,
|
||||
body = encryptionContext.toEncryptedNetworkCipher(),
|
||||
)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = networkCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unarchiveCipher with no active user should return UnarchiveCipherResult Error`() =
|
||||
runTest {
|
||||
@@ -862,55 +804,59 @@ class CipherManagerTest {
|
||||
fun `unarchiveCipher with ciphersService unarchiveCipher failure should return UnarchiveCipherResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-1"
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
val encryptionContext = createMockEncryptionContext(number = 1)
|
||||
val error = Throwable("Fail")
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
ciphersService.unarchiveCipher(cipherId = cipherId)
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = cipherManager.unarchiveCipher(
|
||||
cipherId = cipherId,
|
||||
cipherView = cipherView,
|
||||
cipherView = createMockCipherView(number = 1),
|
||||
)
|
||||
|
||||
assertEquals(UnarchiveCipherResult.Error(error = error), result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unarchiveCipher with ciphersService unarchiveCipher invalid should return UnarchiveCipherResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val cipherId = "mockId-1"
|
||||
coEvery {
|
||||
ciphersService.unarchiveCipher(cipherId = cipherId)
|
||||
} returns UnarchiveCipherResponseJson.Invalid(
|
||||
message = "You do not have permission to edit this.",
|
||||
validationErrors = null,
|
||||
).asSuccess()
|
||||
|
||||
val result = cipherManager.unarchiveCipher(
|
||||
cipherId = cipherId,
|
||||
cipherView = createMockCipherView(number = 1),
|
||||
)
|
||||
|
||||
assertTrue(result is UnarchiveCipherResult.Error)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unarchiveCipher with ciphersService unarchiveCipher success should return UnarchiveCipherResult success`() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
val cipherId = "mockId-1"
|
||||
val encryptionContext = createMockEncryptionContext(
|
||||
number = 1,
|
||||
cipher = createMockSdkCipher(number = 1, clock = clock),
|
||||
)
|
||||
val cipher = createMockCipher(number = 1)
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(
|
||||
userId = userId,
|
||||
cipherView = cipherView.copy(archivedDate = null),
|
||||
)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(userId = userId, cipher = encryptionContext.cipher)
|
||||
} returns cipherView.asSuccess()
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { ciphersService.unarchiveCipher(cipherId = cipherId) } returns Unit.asSuccess()
|
||||
coEvery {
|
||||
ciphersService.unarchiveCipher(cipherId = cipherId)
|
||||
} returns UnarchiveCipherResponseJson.Success(cipher = cipher).asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = encryptionContext.toEncryptedNetworkCipherResponse(),
|
||||
cipher = cipher.copy(
|
||||
collectionIds = cipherView.collectionIds,
|
||||
),
|
||||
)
|
||||
} just runs
|
||||
|
||||
@@ -922,69 +868,6 @@ class CipherManagerTest {
|
||||
assertEquals(UnarchiveCipherResult.Success, result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unarchiveCipher with cipher migration success should return UnarchiveCipherResult success`() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
val cipherId = "mockId-1"
|
||||
val encryptionContext = createMockEncryptionContext(
|
||||
number = 1,
|
||||
cipher = createMockSdkCipher(number = 1, clock = clock),
|
||||
)
|
||||
val cipherView = createMockCipherView(number = 1).copy(key = null)
|
||||
val networkCipher = createMockCipher(number = 1).copy(key = null)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
ciphersService.updateCipher(
|
||||
cipherId = cipherId,
|
||||
body = encryptionContext.toEncryptedNetworkCipher(),
|
||||
)
|
||||
} returns UpdateCipherResponseJson.Success(networkCipher).asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = networkCipher)
|
||||
} just runs
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = networkCipher.toEncryptedSdkCipher(),
|
||||
)
|
||||
} returns cipherView.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptCipher(
|
||||
userId = userId,
|
||||
cipherView = cipherView.copy(archivedDate = null),
|
||||
)
|
||||
} returns encryptionContext.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(userId = userId, cipher = encryptionContext.cipher)
|
||||
} returns cipherView.asSuccess()
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { ciphersService.unarchiveCipher(cipherId = cipherId) } returns Unit.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = encryptionContext.toEncryptedNetworkCipherResponse(),
|
||||
)
|
||||
} just runs
|
||||
|
||||
val result = cipherManager.unarchiveCipher(
|
||||
cipherId = cipherId,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
|
||||
assertEquals(UnarchiveCipherResult.Success, result)
|
||||
coVerify(exactly = 1) {
|
||||
ciphersService.updateCipher(
|
||||
cipherId = cipherId,
|
||||
body = encryptionContext.toEncryptedNetworkCipher(),
|
||||
)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = networkCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hardDeleteCipher with no active user should return DeleteCipherResult Error`() = runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
Reference in New Issue
Block a user