mirror of
https://github.com/bitwarden/android.git
synced 2026-03-11 20:54:58 -05:00
[PM-33227] feat: Add Clear SSO Cookies button to debug menu (#6620)
This commit is contained in:
@@ -22,4 +22,9 @@ interface CookieDiskSource {
|
||||
* @param config The [CookieConfigurationData] to persist, or `null` to delete.
|
||||
*/
|
||||
fun storeCookieConfig(hostname: String, config: CookieConfigurationData?)
|
||||
|
||||
/**
|
||||
* Clears all stored cookie configurations across all hostnames.
|
||||
*/
|
||||
fun clearCookies()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.CookieConfigurationData
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val CONFIG_PREFIX = "elb_cookie_config"
|
||||
private const val ENCRYPTED_PREFIX = "bwSecureStorage:$CONFIG_PREFIX"
|
||||
|
||||
/**
|
||||
* Implementation of [CookieDiskSource] using encrypted SharedPreferences.
|
||||
@@ -15,7 +17,7 @@ private const val CONFIG_PREFIX = "elb_cookie_config"
|
||||
*/
|
||||
class CookieDiskSourceImpl(
|
||||
sharedPreferences: SharedPreferences,
|
||||
encryptedSharedPreferences: SharedPreferences,
|
||||
private val encryptedSharedPreferences: SharedPreferences,
|
||||
private val json: Json,
|
||||
) : CookieDiskSource,
|
||||
BaseEncryptedDiskSource(
|
||||
@@ -33,4 +35,14 @@ class CookieDiskSourceImpl(
|
||||
val key = CONFIG_PREFIX.appendIdentifier(hostname)
|
||||
putEncryptedString(key, config?.let { json.encodeToString(it) })
|
||||
}
|
||||
|
||||
override fun clearCookies() {
|
||||
val keysToRemove = encryptedSharedPreferences
|
||||
.all
|
||||
.keys
|
||||
.filter { it.startsWith(ENCRYPTED_PREFIX) }
|
||||
encryptedSharedPreferences.edit {
|
||||
keysToRemove.forEach { key -> remove(key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,4 +49,9 @@ interface DebugMenuRepository {
|
||||
* @param userStateUpdateTrigger A passable lambda to trigger a user state update.
|
||||
*/
|
||||
fun modifyStateToShowOnboardingCarousel(userStateUpdateTrigger: () -> Unit)
|
||||
|
||||
/**
|
||||
* Clears all stored SSO cookie configurations.
|
||||
*/
|
||||
fun clearSsoCookies()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.getFlagValueOrDefault
|
||||
@@ -20,6 +21,7 @@ class DebugMenuRepositoryImpl(
|
||||
private val serverConfigRepository: ServerConfigRepository,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val cookieDiskSource: CookieDiskSource,
|
||||
) : DebugMenuRepository {
|
||||
|
||||
private val mutableOverridesUpdatedFlow = bufferedMutableSharedFlow<Unit>(replay = 1)
|
||||
@@ -68,4 +70,8 @@ class DebugMenuRepositoryImpl(
|
||||
settingsDiskSource.hasUserLoggedInOrCreatedAccount = false
|
||||
userStateUpdateTrigger.invoke()
|
||||
}
|
||||
|
||||
override fun clearSsoCookies() {
|
||||
cookieDiskSource.clearCookies()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
@@ -92,10 +93,12 @@ object PlatformRepositoryModule {
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
cookieDiskSource: CookieDiskSource,
|
||||
): DebugMenuRepository = DebugMenuRepositoryImpl(
|
||||
featureFlagOverrideDiskSource = featureFlagOverrideDiskSource,
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
cookieDiskSource = cookieDiskSource,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,6 +119,19 @@ fun DebugMenuScreen(
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.clear_sso_cookies),
|
||||
onClick = {
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.ClearSsoCookies,
|
||||
)
|
||||
},
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
|
||||
@@ -64,6 +64,7 @@ class DebugMenuViewModel @Inject constructor(
|
||||
DebugMenuAction.GenerateCrashClick -> handleCrashClick()
|
||||
DebugMenuAction.GenerateErrorReportClick -> handleErrorReportClick()
|
||||
DebugMenuAction.TriggerCookieAcquisition -> handleTriggerCookieAcquisition()
|
||||
DebugMenuAction.ClearSsoCookies -> handleClearSsoCookies()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +101,10 @@ class DebugMenuViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClearSsoCookies() {
|
||||
debugMenuRepository.clearSsoCookies()
|
||||
}
|
||||
|
||||
private fun handleTriggerCookieAcquisition() {
|
||||
cookieAcquisitionRequestManager.setPendingCookieAcquisition(
|
||||
data = CookieAcquisitionRequest(
|
||||
@@ -196,6 +201,11 @@ sealed class DebugMenuAction {
|
||||
*/
|
||||
data object TriggerCookieAcquisition : DebugMenuAction()
|
||||
|
||||
/**
|
||||
* The user has clicked clear SSO cookies button.
|
||||
*/
|
||||
data object ClearSsoCookies : DebugMenuAction()
|
||||
|
||||
/**
|
||||
* Internal actions not triggered from the UI.
|
||||
*/
|
||||
|
||||
@@ -138,6 +138,38 @@ class CookieDiskSourceTest {
|
||||
assertEquals(config2, cookieDiskSource.getCookieConfig(hostname2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearCookies should remove all stored cookie configs`() {
|
||||
val hostname1 = "vault.bitwarden.com"
|
||||
val hostname2 = "other.bitwarden.com"
|
||||
val config1 = CookieConfigurationData(
|
||||
hostname = hostname1,
|
||||
cookies = listOf(
|
||||
CookieConfigurationData.Cookie(name = "A", value = "1"),
|
||||
),
|
||||
)
|
||||
val config2 = CookieConfigurationData(
|
||||
hostname = hostname2,
|
||||
cookies = listOf(
|
||||
CookieConfigurationData.Cookie(name = "B", value = "2"),
|
||||
),
|
||||
)
|
||||
|
||||
cookieDiskSource.storeCookieConfig(hostname1, config1)
|
||||
cookieDiskSource.storeCookieConfig(hostname2, config2)
|
||||
|
||||
cookieDiskSource.clearCookies()
|
||||
|
||||
assertNull(cookieDiskSource.getCookieConfig(hostname1))
|
||||
assertNull(cookieDiskSource.getCookieConfig(hostname2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearCookies should be safe to call when no cookies are stored`() {
|
||||
cookieDiskSource.clearCookies()
|
||||
assertNull(cookieDiskSource.getCookieConfig("vault.bitwarden.com"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storage should isolate configs by hostname`() {
|
||||
val hostname1 = "vault.bitwarden.com"
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import io.mockk.every
|
||||
@@ -42,11 +43,16 @@ class DebugMenuRepositoryTest {
|
||||
every { hasUserLoggedInOrCreatedAccount = any() } just runs
|
||||
}
|
||||
|
||||
private val mockCookieDiskSource = mockk<CookieDiskSource> {
|
||||
every { clearCookies() } just runs
|
||||
}
|
||||
|
||||
private val debugMenuRepository = DebugMenuRepositoryImpl(
|
||||
featureFlagOverrideDiskSource = mockFeatureFlagOverrideDiskSource,
|
||||
serverConfigRepository = mockServerConfigRepository,
|
||||
settingsDiskSource = mockSettingsDiskSource,
|
||||
authDiskSource = mockAuthDiskSource,
|
||||
cookieDiskSource = mockCookieDiskSource,
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -169,6 +175,13 @@ class DebugMenuRepositoryTest {
|
||||
mockSettingsDiskSource.storeShouldShowAddLoginCoachMark(shouldShow = null)
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun `clearSsoCookies should call clearCookies on CookieDiskSource`() {
|
||||
debugMenuRepository.clearSsoCookies()
|
||||
verify(exactly = 1) {
|
||||
mockCookieDiskSource.clearCookies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TEST_STRING_VALUE = "test"
|
||||
|
||||
@@ -149,6 +149,16 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
verify(exactly = 1) { viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clear SSO cookies should send ClearSsoCookies action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Clear SSO cookies")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) { viewModel.trySendAction(DebugMenuAction.ClearSsoCookies) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reset all coach mark tours should send ResetCoachMarkTourStatuses action`() {
|
||||
composeTestRule
|
||||
|
||||
@@ -143,6 +143,15 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ClearSsoCookies should call clearSsoCookies on DebugMenuRepository`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(DebugMenuAction.ClearSsoCookies)
|
||||
verify(exactly = 1) {
|
||||
mockDebugMenuRepository.clearSsoCookies()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TriggerCookieAcquisition should set pending cookie acquisition`() =
|
||||
runTest {
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<string name="archive_items">Archive Items</string>
|
||||
<string name="send_email_verification">Send Email Verification</string>
|
||||
<string name="trigger_cookie_acquisition">Trigger cookie acquisition</string>
|
||||
<string name="clear_sso_cookies">Clear SSO cookies</string>
|
||||
|
||||
<!-- endregion Debug Menu -->
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user