[PM-9844] Android - Non-Premium Users Can Copy TOTP Code From Item Menu (#3539)

This commit is contained in:
Dave Severns 2024-07-17 14:06:18 -04:00 committed by GitHub
parent f1c486bf9a
commit 3d584c84f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 120 additions and 36 deletions

View file

@ -101,6 +101,7 @@ class SearchViewModel @Inject constructor(
isIconLoadingDisabled = settingsRepo.isIconLoadingDisabled,
autofillSelectionData = autofillSelectionData,
hasMasterPassword = userState.activeAccount.hasMasterPassword,
isPremium = userState.activeAccount.isPremium,
)
},
) {
@ -657,6 +658,7 @@ class SearchViewModel @Inject constructor(
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
isAutofill = state.isAutofill,
isPremiumUser = state.isPremium,
)
}
@ -701,6 +703,7 @@ data class SearchState(
// Internal
val autofillSelectionData: AutofillSelectionData? = null,
val hasMasterPassword: Boolean,
val isPremium: Boolean,
) : Parcelable {
/**

View file

@ -140,12 +140,14 @@ private fun CipherView.matchedSearch(searchTerm: String): SortPriority? {
/**
* Transforms a list of [CipherView] into [SearchState.ViewState].
*/
@Suppress("LongParameterList")
fun List<CipherView>.toViewState(
searchTerm: String,
baseIconUrl: String,
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
isAutofill: Boolean,
isPremiumUser: Boolean,
): SearchState.ViewState =
when {
searchTerm.isEmpty() -> SearchState.ViewState.Empty(message = null)
@ -156,6 +158,7 @@ fun List<CipherView>.toViewState(
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
isAutofill = isAutofill,
isPremiumUser = isPremiumUser,
),
)
}
@ -172,6 +175,7 @@ private fun List<CipherView>.toDisplayItemList(
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
isAutofill: Boolean,
isPremiumUser: Boolean,
): List<SearchState.DisplayItem> =
this.map {
it.toDisplayItem(
@ -179,6 +183,7 @@ private fun List<CipherView>.toDisplayItemList(
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
isAutofill = isAutofill,
isPremiumUser = isPremiumUser,
)
}
@ -187,6 +192,7 @@ private fun CipherView.toDisplayItem(
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
isAutofill: Boolean,
isPremiumUser: Boolean,
): SearchState.DisplayItem =
SearchState.DisplayItem(
id = id.orEmpty(),
@ -199,7 +205,10 @@ private fun CipherView.toDisplayItem(
isIconLoadingDisabled = isIconLoadingDisabled,
),
extraIconList = toLabelIcons(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
overflowOptions = toOverflowActions(
hasMasterPassword = hasMasterPassword,
isPremiumUser = isPremiumUser,
),
overflowTestTag = "CipherOptionsButton",
totpCode = login?.totp,
autofillSelectionOptions = AutofillSelectionOption

View file

@ -123,6 +123,7 @@ class VaultItemListingViewModel @Inject constructor(
shouldFinishOnComplete = shouldFinishOnComplete,
hasMasterPassword = userState.activeAccount.hasMasterPassword,
fido2CredentialRequest = fido2CreationData?.fido2CredentialRequest,
isPremium = userState.activeAccount.isPremium,
)
},
) {
@ -1008,6 +1009,7 @@ class VaultItemListingViewModel @Inject constructor(
fido2CreationData = state.fido2CredentialRequest,
fido2CredentialAutofillViews = vaultData
.fido2CredentialAutofillViewList,
isPremiumUser = state.isPremium,
)
}
@ -1127,6 +1129,7 @@ data class VaultItemListingState(
val fido2CredentialRequest: Fido2CredentialRequest? = null,
val shouldFinishOnComplete: Boolean = false,
val hasMasterPassword: Boolean,
val isPremium: Boolean,
) {
/**
* Whether or not this represents a listing screen for autofill.

View file

@ -104,6 +104,7 @@ fun VaultData.toViewState(
autofillSelectionData: AutofillSelectionData?,
fido2CreationData: Fido2CredentialRequest?,
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
isPremiumUser: Boolean,
): VaultItemListingState.ViewState {
val filteredCipherViewList = cipherViewList
.filter { cipherView ->
@ -133,6 +134,7 @@ fun VaultData.toViewState(
isAutofill = autofillSelectionData != null,
isFido2Creation = fido2CreationData != null,
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
isPremiumUser = isPremiumUser,
),
displayFolderList = folderList.map { folderView ->
VaultItemListingState.FolderDisplayItem(
@ -271,6 +273,7 @@ private fun List<CipherView>.toDisplayItemList(
isAutofill: Boolean,
isFido2Creation: Boolean,
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
isPremiumUser: Boolean,
): List<VaultItemListingState.DisplayItem> =
this.map {
it.toDisplayItem(
@ -283,6 +286,7 @@ private fun List<CipherView>.toDisplayItemList(
?.firstOrNull { fido2CredentialAutofillView ->
fido2CredentialAutofillView.cipherId == it.id
},
isPremiumUser = isPremiumUser,
)
}
@ -305,6 +309,7 @@ private fun CipherView.toDisplayItem(
isAutofill: Boolean,
isFido2Creation: Boolean,
fido2CredentialAutofillView: Fido2CredentialAutofillView?,
isPremiumUser: Boolean,
): VaultItemListingState.DisplayItem =
VaultItemListingState.DisplayItem(
id = id.orEmpty(),
@ -325,7 +330,10 @@ private fun CipherView.toDisplayItem(
),
iconTestTag = this.toIconTestTag(),
extraIconList = this.toLabelIcons(),
overflowOptions = this.toOverflowActions(hasMasterPassword = hasMasterPassword),
overflowOptions = this.toOverflowActions(
hasMasterPassword = hasMasterPassword,
isPremiumUser = isPremiumUser,
),
optionsTestTag = "CipherOptionsButton",
isAutofill = isAutofill,
isFido2Creation = isFido2Creation,

View file

@ -11,6 +11,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultTrailingIcon
*/
fun CipherView.toOverflowActions(
hasMasterPassword: Boolean,
isPremiumUser: Boolean,
): List<ListingItemOverflowAction.VaultAction> =
this
.id
@ -36,7 +37,10 @@ fun CipherView.toOverflowActions(
.takeIf { this.viewPassword },
this.login?.totp
?.let { ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode = it) }
.takeIf { this.type == CipherType.LOGIN },
.takeIf {
this.type == CipherType.LOGIN &&
(this.organizationUseTotp || isPremiumUser)
},
this.card?.number?.let {
ListingItemOverflowAction.VaultAction.CopyNumberClick(
number = it,

View file

@ -83,6 +83,7 @@ fun VaultData.toViewState(
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
isPremiumUser = isPremium,
)
},
folderItems = filteredFolderViewList
@ -116,6 +117,7 @@ fun VaultData.toViewState(
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
isPremiumUser = isPremium,
)
}
.takeIf { it.size < NO_FOLDER_ITEM_THRESHOLD }
@ -189,11 +191,12 @@ fun List<LoginUriView>?.toLoginIconData(
/**
* Transforms a [CipherView] into a [VaultState.ViewState.VaultItem].
*/
@Suppress("MagicNumber")
@Suppress("MagicNumber", "LongMethod")
private fun CipherView.toVaultItemOrNull(
hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean,
baseIconUrl: String,
isPremiumUser: Boolean,
): VaultState.ViewState.VaultItem? {
val id = this.id ?: return null
return when (type) {
@ -206,7 +209,10 @@ private fun CipherView.toVaultItemOrNull(
baseIconUrl = baseIconUrl,
usePasskeyDefaultIcon = false,
),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
overflowOptions = toOverflowActions(
hasMasterPassword = hasMasterPassword,
isPremiumUser = isPremiumUser,
),
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)
@ -214,7 +220,10 @@ private fun CipherView.toVaultItemOrNull(
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(
id = id,
name = name.asText(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
overflowOptions = toOverflowActions(
hasMasterPassword = hasMasterPassword,
isPremiumUser = isPremiumUser,
),
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)
@ -226,7 +235,10 @@ private fun CipherView.toVaultItemOrNull(
lastFourDigits = card?.number
?.takeLast(4)
?.asText(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
overflowOptions = toOverflowActions(
hasMasterPassword = hasMasterPassword,
isPremiumUser = isPremiumUser,
),
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)
@ -240,7 +252,10 @@ private fun CipherView.toVaultItemOrNull(
else -> "${identity?.firstName} ${identity?.lastName}"
}
?.asText(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
overflowOptions = toOverflowActions(
hasMasterPassword = hasMasterPassword,
isPremiumUser = isPremiumUser,
),
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)

View file

@ -887,6 +887,7 @@ private val DEFAULT_STATE: SearchState = SearchState(
baseIconUrl = "www.test.com",
isIconLoadingDisabled = false,
hasMasterPassword = true,
isPremium = true,
)
private fun createStateForAutofill(

View file

@ -861,6 +861,7 @@ class SearchViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = false,
isAutofill = false,
hasMasterPassword = true,
isPremiumUser = true,
)
} returns expectedViewState
val dataState = DataState.Loaded(
@ -962,6 +963,7 @@ class SearchViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = false,
isAutofill = false,
hasMasterPassword = true,
isPremiumUser = true,
)
} returns expectedViewState
mutableVaultDataStateFlow.tryEmit(
@ -1073,6 +1075,7 @@ class SearchViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = false,
isAutofill = false,
hasMasterPassword = true,
isPremiumUser = true,
)
} returns expectedViewState
val dataState = DataState.Error(
@ -1187,6 +1190,7 @@ class SearchViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = false,
isAutofill = false,
hasMasterPassword = true,
isPremiumUser = true,
)
} returns expectedViewState
val dataState = DataState.NoNetwork(
@ -1355,6 +1359,7 @@ class SearchViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = false,
isAutofill = true,
hasMasterPassword = true,
isPremiumUser = true,
)
} returns expectedViewState
val dataState = DataState.Loaded(
@ -1387,6 +1392,7 @@ private val DEFAULT_STATE: SearchState = SearchState(
baseIconUrl = "https://vault.bitwarden.com/icons",
isIconLoadingDisabled = false,
hasMasterPassword = true,
isPremium = true,
)
private val DEFAULT_USER_STATE = UserState(

View file

@ -298,6 +298,7 @@ class SearchTypeDataExtensionsTest {
isIconLoadingDisabled = false,
isAutofill = false,
hasMasterPassword = true,
isPremiumUser = true,
)
assertEquals(SearchState.ViewState.Empty(message = null), result)
@ -322,6 +323,7 @@ class SearchTypeDataExtensionsTest {
isIconLoadingDisabled = false,
isAutofill = false,
hasMasterPassword = true,
isPremiumUser = true,
)
assertEquals(
@ -361,6 +363,7 @@ class SearchTypeDataExtensionsTest {
isIconLoadingDisabled = false,
isAutofill = true,
hasMasterPassword = true,
isPremiumUser = true,
)
assertEquals(
@ -410,6 +413,7 @@ class SearchTypeDataExtensionsTest {
isIconLoadingDisabled = false,
isAutofill = false,
hasMasterPassword = true,
isPremiumUser = true,
)
assertEquals(

View file

@ -1713,6 +1713,7 @@ private val DEFAULT_STATE = VaultItemListingState(
dialogState = null,
policyDisablesSend = false,
hasMasterPassword = true,
isPremium = false,
)
private val STATE_FOR_AUTOFILL = DEFAULT_STATE.copy(

View file

@ -2374,6 +2374,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
policyDisablesSend = false,
hasMasterPassword = true,
fido2CredentialRequest = null,
isPremium = true,
)
}

View file

@ -400,6 +400,7 @@ class VaultItemListingDataExtensionsTest {
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
)
assertEquals(
@ -488,6 +489,7 @@ class VaultItemListingDataExtensionsTest {
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
isPremiumUser = true,
)
assertEquals(
@ -553,6 +555,7 @@ class VaultItemListingDataExtensionsTest {
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
)
@ -574,6 +577,7 @@ class VaultItemListingDataExtensionsTest {
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
)
@ -593,6 +597,7 @@ class VaultItemListingDataExtensionsTest {
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
)
@ -615,6 +620,7 @@ class VaultItemListingDataExtensionsTest {
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
)
@ -640,6 +646,7 @@ class VaultItemListingDataExtensionsTest {
),
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
)
}
@ -781,6 +788,7 @@ class VaultItemListingDataExtensionsTest {
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
)
assertEquals(
@ -823,6 +831,7 @@ class VaultItemListingDataExtensionsTest {
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
)
assertEquals(

View file

@ -16,12 +16,7 @@ import org.junit.jupiter.api.Test
class CipherViewExtensionsTest {
@Test
fun `toOverflowActions should return all actions for a login cipher`() {
val id = "mockId-1"
val username = "Bitwarden"
val password = "password"
val totpCode = "mockTotp-1"
val uri = "www.test.com"
fun `toOverflowActions should return all actions for a login cipher when a user has premium`() {
val cipher = createMockCipherView(number = 1, cipherType = CipherType.LOGIN).copy(
id = id,
login = createMockLoginView(number = 1).copy(
@ -31,7 +26,7 @@ class CipherViewExtensionsTest {
),
)
val result = cipher.toOverflowActions(hasMasterPassword = false)
val result = cipher.toOverflowActions(hasMasterPassword = false, isPremiumUser = true)
assertEquals(
listOf(
@ -53,14 +48,41 @@ class CipherViewExtensionsTest {
)
}
@Test
fun `toOverflowActions should not return TOTP action when a user does not have premium`() {
val cipher = createMockCipherView(number = 1, cipherType = CipherType.LOGIN).copy(
id = id,
login = createMockLoginView(number = 1).copy(
username = username,
password = password,
uris = listOf(createMockUriView(number = 1).copy(uri = uri)),
),
)
val result = cipher.toOverflowActions(hasMasterPassword = false, isPremiumUser = false)
assertEquals(
listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id),
ListingItemOverflowAction.VaultAction.EditClick(
cipherId = id,
requiresPasswordReprompt = false,
),
ListingItemOverflowAction.VaultAction.CopyUsernameClick(username = username),
ListingItemOverflowAction.VaultAction.CopyPasswordClick(
password = password,
requiresPasswordReprompt = false,
cipherId = id,
),
ListingItemOverflowAction.VaultAction.LaunchClick(url = uri),
),
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `toOverflowActions should return the correct actions when viewPassword is false for a login cipher`() {
val id = "mockId-1"
val username = "Bitwarden"
val password = "password"
val totpCode = "mockTotp-1"
val uri = "www.test.com"
val cipher = createMockCipherView(number = 1, cipherType = CipherType.LOGIN).copy(
id = id,
login = createMockLoginView(number = 1).copy(
@ -70,7 +92,7 @@ class CipherViewExtensionsTest {
),
viewPassword = false,
)
val result = cipher.toOverflowActions(hasMasterPassword = true)
val result = cipher.toOverflowActions(hasMasterPassword = true, isPremiumUser = false)
assertEquals(
listOf(
@ -80,7 +102,6 @@ class CipherViewExtensionsTest {
requiresPasswordReprompt = true,
),
ListingItemOverflowAction.VaultAction.CopyUsernameClick(username = username),
ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode = totpCode),
ListingItemOverflowAction.VaultAction.LaunchClick(url = uri),
),
result,
@ -89,7 +110,6 @@ class CipherViewExtensionsTest {
@Test
fun `toOverflowActions should return minimum actions for a login cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(
number = 1,
isDeleted = true,
@ -105,7 +125,7 @@ class CipherViewExtensionsTest {
),
)
val result = cipher.toOverflowActions(hasMasterPassword = true)
val result = cipher.toOverflowActions(hasMasterPassword = true, isPremiumUser = false)
assertEquals(
listOf(ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id)),
@ -115,7 +135,6 @@ class CipherViewExtensionsTest {
@Test
fun `toOverflowActions should return all actions for a card cipher`() {
val id = "mockId-1"
val number = "1322-2414-7634-2354"
val securityCode = "123"
val cipher = createMockCipherView(number = 1, cipherType = CipherType.CARD).copy(
@ -126,7 +145,7 @@ class CipherViewExtensionsTest {
),
)
val result = cipher.toOverflowActions(hasMasterPassword = true)
val result = cipher.toOverflowActions(hasMasterPassword = true, isPremiumUser = false)
assertEquals(
listOf(
@ -151,7 +170,6 @@ class CipherViewExtensionsTest {
@Test
fun `toOverflowActions should return minimum actions for a card cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(
number = 1,
isDeleted = true,
@ -165,7 +183,7 @@ class CipherViewExtensionsTest {
),
)
val result = cipher.toOverflowActions(hasMasterPassword = false)
val result = cipher.toOverflowActions(hasMasterPassword = false, isPremiumUser = false)
assertEquals(
listOf(ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id)),
@ -175,13 +193,12 @@ class CipherViewExtensionsTest {
@Test
fun `toOverflowActions should return all actions for a identity cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(number = 1, cipherType = CipherType.IDENTITY).copy(
id = id,
identity = createMockIdentityView(number = 1),
)
val result = cipher.toOverflowActions(hasMasterPassword = false)
val result = cipher.toOverflowActions(hasMasterPassword = false, isPremiumUser = false)
assertEquals(
listOf(
@ -197,7 +214,6 @@ class CipherViewExtensionsTest {
@Test
fun `toOverflowActions should return minimum actions for a identity cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(
number = 1,
isDeleted = true,
@ -208,7 +224,7 @@ class CipherViewExtensionsTest {
identity = createMockIdentityView(number = 1),
)
val result = cipher.toOverflowActions(hasMasterPassword = true)
val result = cipher.toOverflowActions(hasMasterPassword = true, false)
assertEquals(
listOf(ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id)),
@ -218,7 +234,6 @@ class CipherViewExtensionsTest {
@Test
fun `toOverflowActions should return all actions for a secure note cipher`() {
val id = "mockId-1"
val notes = "so secure"
val cipher = createMockCipherView(number = 1, cipherType = CipherType.SECURE_NOTE).copy(
id = id,
@ -226,7 +241,7 @@ class CipherViewExtensionsTest {
notes = notes,
)
val result = cipher.toOverflowActions(hasMasterPassword = true)
val result = cipher.toOverflowActions(hasMasterPassword = true, isPremiumUser = false)
assertEquals(
listOf(
@ -243,7 +258,6 @@ class CipherViewExtensionsTest {
@Test
fun `toOverflowActions should return minimum actions for a secure note cipher`() {
val id = "mockId-1"
val cipher = createMockCipherView(
number = 1,
isDeleted = true,
@ -255,7 +269,7 @@ class CipherViewExtensionsTest {
notes = null,
)
val result = cipher.toOverflowActions(hasMasterPassword = false)
val result = cipher.toOverflowActions(hasMasterPassword = false, isPremiumUser = false)
assertEquals(
listOf(ListingItemOverflowAction.VaultAction.ViewClick(cipherId = id)),
@ -358,3 +372,9 @@ class CipherViewExtensionsTest {
assertEquals(expected, result)
}
}
private const val id = "mockId-1"
private const val username = "Bitwarden"
private const val password = "password"
private const val totpCode = "mockTotp-1"
private const val uri = "www.test.com"