mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
Add logout manager (#604)
This commit is contained in:
parent
f1b9ded3e3
commit
3def25366b
10 changed files with 494 additions and 279 deletions
|
@ -0,0 +1,17 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
/**
|
||||
* Manages the logging out of users and clearing of their data.
|
||||
*/
|
||||
interface UserLogoutManager {
|
||||
/**
|
||||
* Completely logs out the given [userId], removing all data.
|
||||
*/
|
||||
fun logout(userId: String)
|
||||
|
||||
/**
|
||||
* Partially logs out the given [userId]. All data for the given [userId] will be removed with
|
||||
* the exception of basic account data.
|
||||
*/
|
||||
fun softLogout(userId: String)
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Primary implementation of [UserLogoutManager].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class UserLogoutManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val generatorDiskSource: GeneratorDiskSource,
|
||||
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
|
||||
private val pushDiskSource: PushDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
) : UserLogoutManager {
|
||||
private val scope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
override fun logout(userId: String) {
|
||||
val currentUserState = authDiskSource.userState ?: return
|
||||
|
||||
// Remove the active user from the accounts map
|
||||
val updatedAccounts = currentUserState
|
||||
.accounts
|
||||
.filterKeys { it != userId }
|
||||
|
||||
// Check if there is a new active user
|
||||
if (updatedAccounts.isNotEmpty()) {
|
||||
// If we logged out a non-active user, we want to leave the active user unchanged.
|
||||
// If we logged out the active user, we want to set the active user to the first one
|
||||
// in the list.
|
||||
val updatedActiveUserId = currentUserState
|
||||
.activeUserId
|
||||
.takeUnless { it == userId }
|
||||
?: updatedAccounts.entries.first().key
|
||||
|
||||
// Update the user information and emit an updated token
|
||||
authDiskSource.userState = currentUserState.copy(
|
||||
activeUserId = updatedActiveUserId,
|
||||
accounts = updatedAccounts,
|
||||
)
|
||||
} else {
|
||||
// Update the user information and log out
|
||||
authDiskSource.userState = null
|
||||
}
|
||||
|
||||
clearData(userId = userId)
|
||||
}
|
||||
|
||||
override fun softLogout(userId: String) {
|
||||
val userState = authDiskSource.userState ?: return
|
||||
val updatedAccount = userState
|
||||
.accounts[userId]
|
||||
// Clear the tokens for the current user if present
|
||||
?.copy(
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = null,
|
||||
refreshToken = null,
|
||||
),
|
||||
)
|
||||
authDiskSource.userState = userState
|
||||
.copy(
|
||||
accounts = userState
|
||||
.accounts
|
||||
.toMutableMap()
|
||||
.apply {
|
||||
updatedAccount?.let { set(userId, updatedAccount) }
|
||||
},
|
||||
)
|
||||
|
||||
// Save any data that will still need to be retained after otherwise clearing all dat
|
||||
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||
|
||||
clearData(userId = userId)
|
||||
|
||||
// Restore data that is still required
|
||||
settingsDiskSource.apply {
|
||||
storeVaultTimeoutInMinutes(
|
||||
userId = userId,
|
||||
vaultTimeoutInMinutes = vaultTimeoutInMinutes,
|
||||
)
|
||||
storeVaultTimeoutAction(
|
||||
userId = userId,
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearData(userId: String) {
|
||||
authDiskSource.clearData(userId = userId)
|
||||
generatorDiskSource.clearData(userId = userId)
|
||||
pushDiskSource.clearData(userId = userId)
|
||||
settingsDiskSource.clearData(userId = userId)
|
||||
scope.launch {
|
||||
passwordHistoryDiskSource.clearPasswordHistories(userId = userId)
|
||||
vaultDiskSource.deleteVaultData(userId = userId)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager.di
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides managers in the auth package.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AuthManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserLogoutManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
generatorDiskSource: GeneratorDiskSource,
|
||||
passwordHistoryDiskSource: PasswordHistoryDiskSource,
|
||||
pushDiskSource: PushDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): UserLogoutManager =
|
||||
UserLogoutManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
generatorDiskSource = generatorDiskSource,
|
||||
passwordHistoryDiskSource = passwordHistoryDiskSource,
|
||||
pushDiskSource = pushDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
}
|
|
@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
|||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
|
@ -65,6 +66,7 @@ class AuthRepositoryImpl constructor(
|
|||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository {
|
||||
private val mutableSpecialCircumstanceStateFlow =
|
||||
|
@ -254,49 +256,9 @@ class AuthRepositoryImpl constructor(
|
|||
}
|
||||
|
||||
override fun logout(userId: String) {
|
||||
val currentUserState = authDiskSource.userState ?: return
|
||||
val wasActiveUser = userId == activeUserId
|
||||
|
||||
// Remove the active user from the accounts map
|
||||
val updatedAccounts = currentUserState
|
||||
.accounts
|
||||
.filterKeys { it != userId }
|
||||
authDiskSource.apply {
|
||||
storeUserKey(userId = userId, userKey = null)
|
||||
storePrivateKey(userId = userId, privateKey = null)
|
||||
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = null)
|
||||
storeOrganizationKeys(userId = userId, organizationKeys = null)
|
||||
storeOrganizations(userId = userId, organizations = null)
|
||||
}
|
||||
|
||||
// Check if there is a new active user
|
||||
if (updatedAccounts.isNotEmpty()) {
|
||||
// If we logged out a non-active user, we want to leave the active user unchanged.
|
||||
// If we logged out the active user, we want to set the active user to the first one
|
||||
// in the list.
|
||||
val updatedActiveUserId = currentUserState
|
||||
.activeUserId
|
||||
.takeUnless { it == userId }
|
||||
?: updatedAccounts.entries.first().key
|
||||
|
||||
// Update the user information and emit an updated token
|
||||
authDiskSource.userState = currentUserState.copy(
|
||||
activeUserId = updatedActiveUserId,
|
||||
accounts = updatedAccounts,
|
||||
)
|
||||
} else {
|
||||
// Update the user information and log out
|
||||
authDiskSource.userState = null
|
||||
}
|
||||
|
||||
// Clear settings
|
||||
settingsRepository.clearData(userId)
|
||||
|
||||
// Delete all the vault data
|
||||
vaultRepository.deleteVaultData(userId)
|
||||
|
||||
// Lock the vault for the logged out user
|
||||
vaultRepository.lockVaultIfNecessary(userId)
|
||||
userLogoutManager.logout(userId = userId)
|
||||
|
||||
// Clear the current vault data if the logged out user was the active one.
|
||||
if (wasActiveUser) vaultRepository.clearUnlockedData()
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
|
@ -36,6 +37,7 @@ object AuthRepositoryModule {
|
|||
environmentRepository: EnvironmentRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
identityService = identityService,
|
||||
|
@ -46,5 +48,6 @@ object AuthRepositoryModule {
|
|||
environmentRepository = environmentRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
userLogoutManager = userLogoutManager,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,11 @@ interface PushDiskSource {
|
|||
*/
|
||||
var registeredPushToken: String?
|
||||
|
||||
/**
|
||||
* Clears all the data for the given user.
|
||||
*/
|
||||
fun clearData(userId: String)
|
||||
|
||||
/**
|
||||
* Retrieves the last stored token for a user.
|
||||
*/
|
||||
|
|
|
@ -26,6 +26,11 @@ class PushDiskSourceImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeCurrentPushToken(userId = userId, pushToken = null)
|
||||
storeLastPushTokenRegistrationDate(userId = userId, registrationDate = null)
|
||||
}
|
||||
|
||||
override fun getCurrentPushToken(userId: String): String? {
|
||||
return getString("${CURRENT_PUSH_TOKEN_KEY}_$userId")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
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 io.mockk.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class UserLogoutManagerTest {
|
||||
private val authDiskSource: AuthDiskSource = mockk {
|
||||
every { userState = any() } just runs
|
||||
every { clearData(any()) } just runs
|
||||
}
|
||||
private val generatorDiskSource: GeneratorDiskSource = mockk {
|
||||
every { clearData(any()) } just runs
|
||||
}
|
||||
private val settingsDiskSource: SettingsDiskSource = mockk {
|
||||
every { clearData(any()) } just runs
|
||||
every { storeVaultTimeoutInMinutes(any(), any()) } just runs
|
||||
every { storeVaultTimeoutAction(any(), any()) } just runs
|
||||
}
|
||||
private val pushDiskSource: PushDiskSource = mockk {
|
||||
coEvery { clearData(any()) } just runs
|
||||
}
|
||||
private val passwordHistoryDiskSource: PasswordHistoryDiskSource = mockk {
|
||||
coEvery { clearPasswordHistories(any()) } just runs
|
||||
}
|
||||
private val vaultDiskSource: VaultDiskSource = mockk {
|
||||
coEvery { deleteVaultData(any()) } just runs
|
||||
}
|
||||
|
||||
private val userLogoutManager: UserLogoutManager =
|
||||
UserLogoutManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
generatorDiskSource = generatorDiskSource,
|
||||
passwordHistoryDiskSource = passwordHistoryDiskSource,
|
||||
pushDiskSource = pushDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
)
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `logout for single account should clear data associated with the given user and null out the user state`() {
|
||||
val userId = USER_ID_1
|
||||
every { authDiskSource.userState } returns SINGLE_USER_STATE_1
|
||||
|
||||
userLogoutManager.logout(userId = USER_ID_1)
|
||||
|
||||
verify { authDiskSource.userState = null }
|
||||
assertDataCleared(userId = userId)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `logout for multiple accounts should clear data associated with the given user and change to the new active user`() {
|
||||
val userId = USER_ID_1
|
||||
every { authDiskSource.userState } returns MULTI_USER_STATE
|
||||
|
||||
userLogoutManager.logout(userId = USER_ID_1)
|
||||
|
||||
verify { authDiskSource.userState = SINGLE_USER_STATE_2 }
|
||||
assertDataCleared(userId = userId)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `logout for non-active accounts should clear data associated with the given user and leave the active user unchanged`() {
|
||||
val userId = USER_ID_2
|
||||
every { authDiskSource.userState } returns MULTI_USER_STATE
|
||||
|
||||
userLogoutManager.logout(userId = USER_ID_2)
|
||||
|
||||
verify { authDiskSource.userState = SINGLE_USER_STATE_1 }
|
||||
assertDataCleared(userId = userId)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `softLogout should clear most data associated with the given user and remove token data from the account in the user state`() {
|
||||
val userId = USER_ID_1
|
||||
val vaultTimeoutInMinutes = 360
|
||||
val vaultTimeoutAction = VaultTimeoutAction.LOCK
|
||||
every { authDiskSource.userState } returns SINGLE_USER_STATE_1
|
||||
every {
|
||||
settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||
} returns vaultTimeoutInMinutes
|
||||
every {
|
||||
settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||
} returns vaultTimeoutAction
|
||||
|
||||
userLogoutManager.softLogout(userId = userId)
|
||||
|
||||
val updatedAccount = ACCOUNT_1
|
||||
.copy(
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = null,
|
||||
refreshToken = null,
|
||||
),
|
||||
)
|
||||
val updatedUserState = SINGLE_USER_STATE_1
|
||||
.copy(
|
||||
accounts = SINGLE_USER_STATE_1
|
||||
.accounts
|
||||
.toMutableMap().apply {
|
||||
set(userId, updatedAccount)
|
||||
},
|
||||
)
|
||||
verify { authDiskSource.userState = updatedUserState }
|
||||
assertDataCleared(userId = userId)
|
||||
|
||||
verify {
|
||||
settingsDiskSource.storeVaultTimeoutInMinutes(
|
||||
userId = userId,
|
||||
vaultTimeoutInMinutes = vaultTimeoutInMinutes,
|
||||
)
|
||||
}
|
||||
verify {
|
||||
settingsDiskSource.storeVaultTimeoutAction(
|
||||
userId = userId,
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertDataCleared(userId: String) {
|
||||
verify { authDiskSource.clearData(userId = userId) }
|
||||
verify { generatorDiskSource.clearData(userId = userId) }
|
||||
verify { pushDiskSource.clearData(userId = userId) }
|
||||
verify { settingsDiskSource.clearData(userId = userId) }
|
||||
coVerify { passwordHistoryDiskSource.clearPasswordHistories(userId = userId) }
|
||||
coVerify {
|
||||
vaultDiskSource.deleteVaultData(userId = userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val EMAIL_2 = "test2@bitwarden.com"
|
||||
private const val ACCESS_TOKEN = "accessToken"
|
||||
private const val ACCESS_TOKEN_2 = "accessToken2"
|
||||
private const val REFRESH_TOKEN = "refreshToken"
|
||||
private const val USER_ID_1 = "2a135b23-e1fb-42c9-bec3-573857bc8181"
|
||||
private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02"
|
||||
private val ACCOUNT_1 = AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = USER_ID_1,
|
||||
email = "test@bitwarden.com",
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = null,
|
||||
),
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = ACCESS_TOKEN,
|
||||
refreshToken = REFRESH_TOKEN,
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
)
|
||||
private val ACCOUNT_2 = AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = USER_ID_2,
|
||||
email = EMAIL_2,
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester 2",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = 400000,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
userDecryptionOptions = null,
|
||||
),
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = ACCESS_TOKEN_2,
|
||||
refreshToken = "refreshToken",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
)
|
||||
private val SINGLE_USER_STATE_1 = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1,
|
||||
),
|
||||
)
|
||||
private val SINGLE_USER_STATE_2 = UserStateJson(
|
||||
activeUserId = USER_ID_2,
|
||||
accounts = mapOf(
|
||||
USER_ID_2 to ACCOUNT_2,
|
||||
),
|
||||
)
|
||||
private val MULTI_USER_STATE = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1,
|
||||
USER_ID_2 to ACCOUNT_2,
|
||||
),
|
||||
)
|
|
@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL
|
|||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_2
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_3
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_4
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
|
@ -120,6 +121,9 @@ class AuthRepositoryTest {
|
|||
),
|
||||
)
|
||||
}
|
||||
private val userLogoutManager: UserLogoutManager = mockk {
|
||||
every { logout(any()) } just runs
|
||||
}
|
||||
|
||||
private val repository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
|
@ -130,6 +134,7 @@ class AuthRepositoryTest {
|
|||
environmentRepository = fakeEnvironmentRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
userLogoutManager = userLogoutManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
|
@ -150,6 +155,49 @@ class AuthRepositoryTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authStateFlow should react to user state changes`() {
|
||||
assertEquals(
|
||||
AuthState.Unauthenticated,
|
||||
repository.authStateFlow.value,
|
||||
)
|
||||
|
||||
// Update the active user updates the state
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
assertEquals(
|
||||
AuthState.Authenticated(ACCESS_TOKEN),
|
||||
repository.authStateFlow.value,
|
||||
)
|
||||
|
||||
// Updating the non-active user does not update the state
|
||||
fakeAuthDiskSource.userState = MULTI_USER_STATE
|
||||
assertEquals(
|
||||
AuthState.Authenticated(ACCESS_TOKEN),
|
||||
repository.authStateFlow.value,
|
||||
)
|
||||
|
||||
// Clearing the tokens of the active state results in the Unauthenticated state
|
||||
val updatedAccount = ACCOUNT_1.copy(
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = null,
|
||||
refreshToken = null,
|
||||
),
|
||||
)
|
||||
val updatedState = MULTI_USER_STATE.copy(
|
||||
accounts = MULTI_USER_STATE
|
||||
.accounts
|
||||
.toMutableMap()
|
||||
.apply {
|
||||
set(USER_ID_1, updatedAccount)
|
||||
},
|
||||
)
|
||||
fakeAuthDiskSource.userState = updatedState
|
||||
assertEquals(
|
||||
AuthState.Unauthenticated,
|
||||
repository.authStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userStateFlow should update according to changes in its underyling data sources`() {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
@ -1043,251 +1091,26 @@ class AuthRepositoryTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `logout for single account should clear the access token and stored data`() = runTest {
|
||||
// First login:
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
|
||||
coEvery {
|
||||
accountsService.preLogin(email = EMAIL)
|
||||
} returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
} returns Result.success(successResponse)
|
||||
coEvery {
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||
userKey = successResponse.key,
|
||||
privateKey = successResponse.privateKey,
|
||||
organizationKeys = ORGANIZATION_KEYS,
|
||||
masterPassword = PASSWORD,
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
coEvery { vaultRepository.sync() } just runs
|
||||
every {
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(
|
||||
previousUserState = null,
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
)
|
||||
} returns SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.apply {
|
||||
storeUserKey(
|
||||
userId = USER_ID_1,
|
||||
userKey = PUBLIC_KEY,
|
||||
)
|
||||
storePrivateKey(
|
||||
userId = USER_ID_1,
|
||||
privateKey = PRIVATE_KEY,
|
||||
)
|
||||
storeUserAutoUnlockKey(
|
||||
userId = USER_ID_1,
|
||||
userAutoUnlockKey = USER_AUTO_UNLOCK_KEY,
|
||||
)
|
||||
storeOrganizationKeys(
|
||||
userId = USER_ID_1,
|
||||
organizationKeys = ORGANIZATION_KEYS,
|
||||
)
|
||||
storeOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = ORGANIZATIONS,
|
||||
)
|
||||
}
|
||||
fun `logout for the active account should call logout on the UserLogoutManager and clear the user's in memory vault data`() {
|
||||
val userId = USER_ID_1
|
||||
fakeAuthDiskSource.userState = MULTI_USER_STATE
|
||||
|
||||
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
repository.logout(userId = userId)
|
||||
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||
|
||||
// Then call logout:
|
||||
repository.authStateFlow.test {
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), awaitItem())
|
||||
|
||||
repository.logout()
|
||||
|
||||
assertEquals(AuthState.Unauthenticated, awaitItem())
|
||||
assertNull(fakeAuthDiskSource.userState)
|
||||
fakeAuthDiskSource.assertPrivateKey(
|
||||
userId = USER_ID_1,
|
||||
privateKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertUserKey(
|
||||
userId = USER_ID_1,
|
||||
userKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertUserAutoUnlockKey(
|
||||
userId = USER_ID_1,
|
||||
userAutoUnlockKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizationKeys(
|
||||
userId = USER_ID_1,
|
||||
organizationKeys = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = null,
|
||||
)
|
||||
verify { settingsRepository.clearData(userId = USER_ID_1) }
|
||||
verify { vaultRepository.deleteVaultData(userId = USER_ID_1) }
|
||||
verify { vaultRepository.clearUnlockedData() }
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_1) }
|
||||
}
|
||||
verify { userLogoutManager.logout(userId = userId) }
|
||||
verify { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `logout for multiple accounts should update current access token and stored keys`() =
|
||||
runTest {
|
||||
// First populate multiple user accounts
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
|
||||
fun `logout for an inactive account should call logout on the UserLogoutManager`() {
|
||||
val userId = USER_ID_2
|
||||
fakeAuthDiskSource.userState = MULTI_USER_STATE
|
||||
|
||||
// Then login:
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
|
||||
coEvery {
|
||||
accountsService.preLogin(email = EMAIL)
|
||||
} returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
} returns Result.success(successResponse)
|
||||
coEvery {
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||
userKey = successResponse.key,
|
||||
privateKey = successResponse.privateKey,
|
||||
organizationKeys = null,
|
||||
masterPassword = PASSWORD,
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
coEvery { vaultRepository.sync() } just runs
|
||||
every {
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(
|
||||
previousUserState = SINGLE_USER_STATE_2,
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
)
|
||||
} returns MULTI_USER_STATE
|
||||
fakeAuthDiskSource.apply {
|
||||
storeUserKey(
|
||||
userId = USER_ID_2,
|
||||
userKey = PUBLIC_KEY,
|
||||
)
|
||||
storePrivateKey(
|
||||
userId = USER_ID_2,
|
||||
privateKey = PRIVATE_KEY,
|
||||
)
|
||||
storeUserAutoUnlockKey(
|
||||
userId = USER_ID_2,
|
||||
userAutoUnlockKey = USER_AUTO_UNLOCK_KEY,
|
||||
)
|
||||
storeOrganizationKeys(
|
||||
userId = USER_ID_2,
|
||||
organizationKeys = ORGANIZATION_KEYS,
|
||||
)
|
||||
}
|
||||
repository.logout(userId = userId)
|
||||
|
||||
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||
assertEquals(MULTI_USER_STATE, fakeAuthDiskSource.userState)
|
||||
|
||||
// Then call logout:
|
||||
repository.authStateFlow.test {
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), awaitItem())
|
||||
|
||||
repository.logout()
|
||||
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN_2), awaitItem())
|
||||
assertEquals(SINGLE_USER_STATE_2, fakeAuthDiskSource.userState)
|
||||
fakeAuthDiskSource.assertPrivateKey(
|
||||
userId = USER_ID_1,
|
||||
privateKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertUserKey(
|
||||
userId = USER_ID_1,
|
||||
userKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertUserAutoUnlockKey(
|
||||
userId = USER_ID_1,
|
||||
userAutoUnlockKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizationKeys(
|
||||
userId = USER_ID_1,
|
||||
organizationKeys = null,
|
||||
)
|
||||
verify { settingsRepository.clearData(userId = USER_ID_1) }
|
||||
verify { vaultRepository.deleteVaultData(userId = USER_ID_1) }
|
||||
verify { vaultRepository.clearUnlockedData() }
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_1) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logout for non-active accounts should leave the active user unchanged`() = runTest {
|
||||
// First populate multiple user accounts and active user is #3
|
||||
val initialUserState = MULTI_USER_STATE_2
|
||||
val finalUserState = initialUserState.copy(
|
||||
accounts = initialUserState.accounts.filter { it.key != USER_ID_2 },
|
||||
)
|
||||
fakeAuthDiskSource.userState = initialUserState
|
||||
fakeAuthDiskSource.apply {
|
||||
storeUserKey(
|
||||
userId = USER_ID_2,
|
||||
userKey = PUBLIC_KEY,
|
||||
)
|
||||
storePrivateKey(
|
||||
userId = USER_ID_2,
|
||||
privateKey = PRIVATE_KEY,
|
||||
)
|
||||
storeUserAutoUnlockKey(
|
||||
userId = USER_ID_2,
|
||||
userAutoUnlockKey = USER_AUTO_UNLOCK_KEY,
|
||||
)
|
||||
storeOrganizationKeys(
|
||||
userId = USER_ID_2,
|
||||
organizationKeys = ORGANIZATION_KEYS,
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(initialUserState, fakeAuthDiskSource.userState)
|
||||
|
||||
repository.authStateFlow.test {
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN_3), awaitItem())
|
||||
|
||||
repository.logout(USER_ID_2)
|
||||
|
||||
// The auth state does not actually change
|
||||
expectNoEvents()
|
||||
assertEquals(finalUserState, fakeAuthDiskSource.userState)
|
||||
fakeAuthDiskSource.assertPrivateKey(
|
||||
userId = USER_ID_2,
|
||||
privateKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertUserKey(
|
||||
userId = USER_ID_2,
|
||||
userKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertUserAutoUnlockKey(
|
||||
userId = USER_ID_2,
|
||||
userAutoUnlockKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizationKeys(
|
||||
userId = USER_ID_2,
|
||||
organizationKeys = null,
|
||||
)
|
||||
verify { settingsRepository.clearData(userId = USER_ID_2) }
|
||||
verify { vaultRepository.deleteVaultData(userId = USER_ID_2) }
|
||||
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_2) }
|
||||
}
|
||||
verify { userLogoutManager.logout(userId = userId) }
|
||||
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -38,6 +38,24 @@ class PushDiskSourceTest {
|
|||
assertNull(pushDiskSource.registeredPushToken)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearData should clear all necessary data for the given user`() {
|
||||
val userId = "userId"
|
||||
pushDiskSource.storeCurrentPushToken(
|
||||
userId = userId,
|
||||
pushToken = "pushToken",
|
||||
)
|
||||
pushDiskSource.storeLastPushTokenRegistrationDate(
|
||||
userId = userId,
|
||||
registrationDate = ZonedDateTime.now(),
|
||||
)
|
||||
|
||||
pushDiskSource.clearData(userId = userId)
|
||||
|
||||
assertNull(pushDiskSource.getCurrentPushToken(userId = userId))
|
||||
assertNull(pushDiskSource.getLastPushTokenRegistrationDate(userId = userId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentPushToken should pull from SharedPreferences`() {
|
||||
val currentPushTokenBaseKey = "bwPreferencesStorage:pushCurrentToken"
|
||||
|
|
Loading…
Add table
Reference in a new issue