PM-11642: Security stamp soft logout (#3859)

This commit is contained in:
David Perez 2024-09-04 11:54:31 -05:00 committed by GitHub
parent e3a4a7b153
commit c017c1b10c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 61 additions and 21 deletions

View file

@ -14,15 +14,15 @@ interface UserLogoutManager {
val logoutEventFlow: SharedFlow<LogoutEvent> val logoutEventFlow: SharedFlow<LogoutEvent>
/** /**
* Completely logs out the given [userId], removing all data. * Completely logs out the given [userId], removing all data. If [isExpired] is true, a toast
* If [isExpired] is true, a toast will be displayed * will be displayed letting the user know the session has expired.
* letting the user know the session has expired.
*/ */
fun logout(userId: String, isExpired: Boolean = false) fun logout(userId: String, isExpired: Boolean = false)
/** /**
* Partially logs out the given [userId]. All data for the given [userId] will be removed with * Partially logs out the given [userId]. All data for the given [userId] will be removed with
* the exception of basic account data. * the exception of basic account data. If [isExpired] is true, a toast will be displayed
* letting the user know the session has expired.
*/ */
fun softLogout(userId: String) fun softLogout(userId: String, isExpired: Boolean = false)
} }

View file

@ -64,7 +64,10 @@ class UserLogoutManagerImpl(
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId)) mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
} }
override fun softLogout(userId: String) { override fun softLogout(userId: String, isExpired: Boolean) {
if (isExpired) {
showToast(message = R.string.login_expired)
}
authDiskSource.storeAccountTokens( authDiskSource.storeAccountTokens(
userId = userId, userId = userId,
accountTokens = null, accountTokens = null,
@ -74,7 +77,11 @@ class UserLogoutManagerImpl(
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId) val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId) val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
switchUserIfAvailable(currentUserId = userId, removeCurrentUserFromAccounts = false) switchUserIfAvailable(
currentUserId = userId,
removeCurrentUserFromAccounts = false,
isExpired = isExpired,
)
clearData(userId = userId) clearData(userId = userId)
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId)) mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))

View file

@ -358,7 +358,7 @@ class VaultRepositoryImpl(
// Log the user out if the stamps do not match // Log the user out if the stamps do not match
localSecurityStamp?.let { localSecurityStamp?.let {
if (serverSecurityStamp != localSecurityStamp) { if (serverSecurityStamp != localSecurityStamp) {
userLogoutManager.logout(userId = userId, isExpired = true) userLogoutManager.softLogout(userId = userId, isExpired = true)
return@launch return@launch
} }
} }

View file

@ -138,21 +138,21 @@ class UserLogoutManagerTest {
verify { authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null) } verify { authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null) }
assertDataCleared(userId = userId) assertDataCleared(userId = userId)
verify { verify(exactly = 1) {
settingsDiskSource.storeVaultTimeoutInMinutes( settingsDiskSource.storeVaultTimeoutInMinutes(
userId = userId, userId = userId,
vaultTimeoutInMinutes = vaultTimeoutInMinutes, vaultTimeoutInMinutes = vaultTimeoutInMinutes,
) )
}
verify {
settingsDiskSource.storeVaultTimeoutAction( settingsDiskSource.storeVaultTimeoutAction(
userId = userId, userId = userId,
vaultTimeoutAction = vaultTimeoutAction, vaultTimeoutAction = vaultTimeoutAction,
) )
Toast
.makeText(context, R.string.account_switched_automatically, Toast.LENGTH_SHORT)
.show()
} }
} }
@Suppress("MaxLineLength")
@Test @Test
fun `softLogout should switch active user but keep previous user in accounts list`() { fun `softLogout should switch active user but keep previous user in accounts list`() {
val userId = USER_ID_1 val userId = USER_ID_1
@ -171,10 +171,44 @@ class UserLogoutManagerTest {
userLogoutManager.softLogout(userId = userId) userLogoutManager.softLogout(userId = userId)
verify { authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null) } verify(exactly = 1) {
verify { authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null)
authDiskSource.userState = authDiskSource.userState = UserStateJson(
UserStateJson(activeUserId = USER_ID_2, accounts = MULTI_USER_STATE.accounts) activeUserId = USER_ID_2,
accounts = MULTI_USER_STATE.accounts,
)
Toast
.makeText(context, R.string.account_switched_automatically, Toast.LENGTH_SHORT)
.show()
}
}
@Suppress("MaxLineLength")
@Test
fun `softLogout with isExpired true should switch active user and keep previous user in accounts list but display the login expired toast`() {
val userId = USER_ID_1
val vaultTimeoutInMinutes = 360
val vaultTimeoutAction = VaultTimeoutAction.LOGOUT
mockToast(R.string.login_expired)
every { authDiskSource.userState } returns MULTI_USER_STATE
every {
settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
} returns vaultTimeoutInMinutes
every {
settingsDiskSource.getVaultTimeoutAction(userId = userId)
} returns vaultTimeoutAction
userLogoutManager.softLogout(userId = userId, isExpired = true)
verify(exactly = 1) {
authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null)
authDiskSource.userState = UserStateJson(
activeUserId = USER_ID_2,
accounts = MULTI_USER_STATE.accounts,
)
Toast.makeText(context, R.string.login_expired, Toast.LENGTH_SHORT).show()
} }
} }

View file

@ -140,7 +140,7 @@ class VaultRepositoryTest {
) )
private val dispatcherManager: DispatcherManager = FakeDispatcherManager() private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
private val userLogoutManager: UserLogoutManager = mockk { private val userLogoutManager: UserLogoutManager = mockk {
every { logout(any(), any()) } just runs every { softLogout(any(), any()) } just runs
} }
private val fileManager: FileManager = mockk { private val fileManager: FileManager = mockk {
coEvery { delete(*anyVararg()) } just runs coEvery { delete(*anyVararg()) } just runs
@ -851,9 +851,8 @@ class VaultRepositoryTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1" val userId = "mockId-1"
val mockSyncResponse = createMockSyncResponse(number = 1) val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.copy( coEvery { syncService.sync() } returns mockSyncResponse
profile = createMockProfile(number = 1).copy(securityStamp = "newStamp"), .copy(profile = createMockProfile(number = 1).copy(securityStamp = "newStamp"))
)
.asSuccess() .asSuccess()
coEvery { coEvery {
@ -868,7 +867,7 @@ class VaultRepositoryTest {
vaultRepository.sync() vaultRepository.sync()
coVerify { coVerify {
userLogoutManager.logout(userId = userId, isExpired = true) userLogoutManager.softLogout(userId = userId, isExpired = true)
} }
coVerify(exactly = 0) { coVerify(exactly = 0) {