Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-15050] Track vault registration for CXP export in settings #4335

Merged
merged 5 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,21 @@ interface SettingsDiskSource {
* Emits updates that track [getShowImportLoginsSettingBadge] for the given [userId].
*/
fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?>

/**
* Gets whether or not the given [userId] has registered for export via the credential exchange
* protocol.
*/
fun getVaultRegisteredForExport(userId: String): Boolean?

/**
* Stores the given value for whether or not the given [userId] has registered for export via
* the credential exchange protocol.
*/
fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?)

/**
* Emits updates that track [getVaultRegisteredForExport] for the given [userId].
*/
fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
private const val SHOW_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
private const val SHOW_IMPORT_LOGINS_SETTING_BADGE = "showImportLoginsSettingBadge"
private const val LAST_SCHEME_CHANGE_INSTANT = "lastDatabaseSchemeChangeInstant"
private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
david-livefront marked this conversation as resolved.
Show resolved Hide resolved

/**
* Primary implementation of [SettingsDiskSource].
Expand Down Expand Up @@ -80,6 +81,9 @@ class SettingsDiskSourceImpl(
private val mutableScreenCaptureAllowedFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()

private val mutableVaultRegisteredForExportFlow =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()

override var appLanguage: AppLanguage?
get() = getString(key = APP_LANGUAGE_KEY)
?.let { storedValue ->
Expand Down Expand Up @@ -181,6 +185,7 @@ class SettingsDiskSourceImpl(
storeLastSyncTime(userId = userId, lastSyncTime = null)
storeClearClipboardFrequencySeconds(userId = userId, frequency = null)
removeWithPrefix(prefix = ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY.appendIdentifier(userId))
storeVaultRegisteredForExport(userId = userId, isRegistered = null)

// The following are intentionally not cleared so they can be
// restored after logging out and back in:
Expand Down Expand Up @@ -443,6 +448,18 @@ class SettingsDiskSourceImpl(
getMutableShowImportLoginsSettingBadgeFlow(userId)
.onSubscription { emit(getShowImportLoginsSettingBadge(userId)) }

override fun getVaultRegisteredForExport(userId: String): Boolean? =
getBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId))

override fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?) {
putBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId), isRegistered)
getMutableVaultRegisteredForExportFlow(userId).tryEmit(isRegistered)
}

override fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?> =
getMutableVaultRegisteredForExportFlow(userId)
.onSubscription { emit(getVaultRegisteredForExport(userId)) }

private fun getMutableLastSyncFlow(
userId: String,
): MutableSharedFlow<Instant?> =
Expand Down Expand Up @@ -493,4 +510,10 @@ class SettingsDiskSourceImpl(
mutableShowImportLoginsSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}

private fun getMutableVaultRegisteredForExportFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableVaultRegisteredForExportFlow.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,21 @@ interface SettingsRepository {
* Record that a user has logged in on this device.
*/
fun storeUserHasLoggedInValue(userId: String)

/**
* Returns true if the given [userId] has previously registered for export via the credential
* exchange protocol.
*/
fun isVaultRegisteredForExport(userId: String): Boolean

/**
* Stores that the given [userId] has previously registered for export via the credential
* exchange protocol.
*/
fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean)

/**
* Gets updates for the [isVaultRegisteredForExport] value for the given [userId].
*/
fun getVaultRegisteredForExportFlow(userId: String): StateFlow<Boolean>
}
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,27 @@ class SettingsRepositoryImpl(
settingsDiskSource.storeUseHasLoggedInPreviously(userId)
}

override fun isVaultRegisteredForExport(userId: String): Boolean {
return settingsDiskSource.getVaultRegisteredForExport(userId) == true
}

override fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean) {
settingsDiskSource.storeVaultRegisteredForExport(userId, isRegistered)
}

override fun getVaultRegisteredForExportFlow(userId: String): StateFlow<Boolean> {
return settingsDiskSource
.getVaultRegisteredForExportFlow(userId)
.map { it ?: false }
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = settingsDiskSource
.getVaultRegisteredForExport(userId)
?: false,
)
}

/**
* If there isn't already one generated, generate a symmetric sync key that would be used
* for communicating via IPC.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1208,4 +1208,38 @@ class SettingsDiskSourceTest {
assertFalse(awaitItem() ?: true)
}
}

@Test
fun `getVaultRegisteredForExport should pull from SharedPreferences`() {
val mockUserId = "mockUserId"
val vaultRegisteredForExportKey =
"bwPreferencesStorage:isVaultRegisteredForExport_$mockUserId"
fakeSharedPreferences.edit {
putBoolean(vaultRegisteredForExportKey, true)
}
assertTrue(settingsDiskSource.getVaultRegisteredForExport(userId = mockUserId)!!)
}

@Test
fun `storeVaultRegisteredForExport should update SharedPreferences`() {
val mockUserId = "mockUserId"
val vaultRegisteredForExportKey =
"bwPreferencesStorage:isVaultRegisteredForExport_$mockUserId"
settingsDiskSource.storeVaultRegisteredForExport(mockUserId, true)
assertTrue(fakeSharedPreferences.getBoolean(vaultRegisteredForExportKey, false))
}

@Test
fun `storeVaultRegisteredForExport should update the flow value`() = runTest {
val mockUserId = "mockUserId"
settingsDiskSource.getVaultRegisteredForExportFlow(mockUserId).test {
// The initial values of the Flow are in sync
assertFalse(awaitItem() ?: false)
settingsDiskSource.storeVaultRegisteredForExport(mockUserId, true)
assertTrue(awaitItem() ?: false)
// Update the value to false
settingsDiskSource.storeVaultRegisteredForExport(mockUserId, false)
assertFalse(awaitItem() ?: true)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class FakeSettingsDiskSource : SettingsDiskSource {
private val userShowUnlockBadge = mutableMapOf<String, Boolean?>()
private val userShowImportLoginsBadge = mutableMapOf<String, Boolean?>()
private var storedLastDatabaseSchemeChangeInstant: Instant? = null
private val vaultRegisteredForExport = mutableMapOf<String, Boolean?>()

private val mutableShowAutoFillSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
Expand All @@ -78,6 +79,9 @@ class FakeSettingsDiskSource : SettingsDiskSource {
private val mutableShowImportLoginsSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()

private val mutableVaultRegisteredForExportFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()

override var appLanguage: AppLanguage? = null

override var appTheme: AppTheme
Expand Down Expand Up @@ -142,7 +146,9 @@ class FakeSettingsDiskSource : SettingsDiskSource {

override var lastDatabaseSchemeChangeInstant: Instant?
get() = storedLastDatabaseSchemeChangeInstant
set(value) { storedLastDatabaseSchemeChangeInstant = value }
set(value) {
storedLastDatabaseSchemeChangeInstant = value
}

override val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
get() = mutableLastDatabaseSchemeChangeInstant.onSubscription {
Expand Down Expand Up @@ -176,6 +182,7 @@ class FakeSettingsDiskSource : SettingsDiskSource {
mutableVaultTimeoutActionsFlowMap.remove(userId)
mutableVaultTimeoutInMinutesFlowMap.remove(userId)
mutableLastSyncCallFlowMap.remove(userId)
mutableVaultRegisteredForExportFlowMap.remove(userId)
}

override fun getLastSyncTime(userId: String): Instant? = storedLastSyncTime[userId]
Expand Down Expand Up @@ -348,6 +355,19 @@ class FakeSettingsDiskSource : SettingsDiskSource {
emit(getShowImportLoginsSettingBadge(userId = userId))
}

override fun getVaultRegisteredForExport(userId: String): Boolean =
vaultRegisteredForExport[userId] ?: false

override fun storeVaultRegisteredForExport(userId: String, registered: Boolean?) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed the clearData on the fake

vaultRegisteredForExport[userId] = registered
getMutableVaultRegisteredForExportFlow(userId = userId).tryEmit(registered)
}

override fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?> =
getMutableVaultRegisteredForExportFlow(userId = userId).onSubscription {
emit(getVaultRegisteredForExport(userId = userId))
}

//region Private helper functions
private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow<Boolean?> {
return mutableScreenCaptureAllowedFlowMap.getOrPut(userId) {
Expand Down Expand Up @@ -400,5 +420,12 @@ class FakeSettingsDiskSource : SettingsDiskSource {
bufferedMutableSharedFlow(replay = 1)
}

private fun getMutableVaultRegisteredForExportFlow(
userId: String,
): MutableSharedFlow<Boolean?> =
mutableVaultRegisteredForExportFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}

//endregion Private helper functions
}
Original file line number Diff line number Diff line change
Expand Up @@ -1184,18 +1184,59 @@ class SettingsRepositoryTest {
assertFalse(awaitItem())
}
}

@Test
fun `isVaultRegisteredForExport should return false if no value exists`() {
assertFalse(settingsRepository.isVaultRegisteredForExport(userId = "userId"))
}

@Test
fun `isVaultRegisteredForExport should return true if it exists`() {
val userId = "userId"
fakeSettingsDiskSource.storeVaultRegisteredForExport(userId = userId, registered = true)
assertTrue(settingsRepository.isVaultRegisteredForExport(userId = userId))
}

@Test
fun `storeVaultRegisteredForExport should store value of true to disk`() {
val userId = "userId"
settingsRepository.storeVaultRegisteredForExport(userId = userId, isRegistered = true)
assertTrue(fakeSettingsDiskSource.getVaultRegisteredForExport(userId = userId))
}

@Test
fun `getVaultRegisteredForExportFlow should react to changes in SettingsDiskSource`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
settingsRepository
.getVaultRegisteredForExportFlow(userId = USER_ID)
.test {
assertFalse(awaitItem())
fakeSettingsDiskSource.storeVaultRegisteredForExport(
userId = USER_ID,
registered = true,
)
assertTrue(awaitItem())
fakeSettingsDiskSource.storeVaultRegisteredForExport(
userId = USER_ID,
registered = false,
)
assertFalse(awaitItem())
}
}
}

private const val USER_ID: String = "userId"
private const val AUTHENTICATION_SYNC_KEY = "authSyncKey"

private val MOCK_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS = TrustedDeviceUserDecryptionOptionsJson(
encryptedPrivateKey = null,
encryptedUserKey = null,
hasAdminApproval = false,
hasLoginApprovingDevice = false,
hasManageResetPasswordPermission = false,
)
private val MOCK_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS =
TrustedDeviceUserDecryptionOptionsJson(
encryptedPrivateKey = null,
encryptedUserKey = null,
hasAdminApproval = false,
hasLoginApprovingDevice = false,
hasManageResetPasswordPermission = false,
)

private val MOCK_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDecryptionOptionsJson(
hasMasterPassword = false,
Expand Down