BIT-782: Implement Personal Ownership policy support (#920)

This commit is contained in:
Caleb Derosier
2024-01-31 20:54:08 -07:00
committed by Álison Fernandes
parent f380e21600
commit debfbc04b0
17 changed files with 427 additions and 29 deletions

View File

@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
@@ -17,6 +18,7 @@ import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
@@ -61,6 +63,7 @@ class SearchViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager,
private val policyManager: PolicyManager,
private val autofillSelectionManager: AutofillSelectionManager,
private val vaultRepo: VaultRepository,
private val authRepo: AuthRepository,
@@ -84,7 +87,11 @@ class SearchViewModel @Inject constructor(
dialogState = null,
vaultFilterData = when (searchType) {
is SearchType.Sends -> null
is SearchType.Vault -> userState.activeAccount.toVaultFilterData()
is SearchType.Vault -> userState.activeAccount.toVaultFilterData(
isIndividualVaultDisabled = policyManager
.getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
.any(),
)
},
baseWebSendUrl = environmentRepo.environment.environmentUrlData.baseWebSendUrl,
baseIconUrl = environmentRepo.environment.environmentUrlData.baseIconUrl,

View File

@@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenPolicyWarningText
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
@@ -58,6 +59,17 @@ fun VaultAddEditContent(
modifier = modifier
.semantics { testTagsAsResourceId = true },
) {
item {
if (state.isIndividualVaultDisabled && isAddItemMode) {
BitwardenPolicyWarningText(
text = stringResource(R.string.personal_ownership_policy_in_effect),
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
}
}
item {
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),

View File

@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull
@@ -17,6 +18,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
@@ -74,6 +76,7 @@ class VaultAddEditViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val authRepository: AuthRepository,
private val clipboardManager: BitwardenClipboardManager,
private val policyManager: PolicyManager,
private val vaultRepository: VaultRepository,
private val generatorRepository: GeneratorRepository,
private val settingsRepository: SettingsRepository,
@@ -84,6 +87,9 @@ class VaultAddEditViewModel @Inject constructor(
initialState = savedStateHandle[KEY_STATE]
?: run {
val vaultAddEditType = VaultAddEditArgs(savedStateHandle).vaultAddEditType
val isIndividualVaultDisabled = policyManager
.getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
.any()
// Check for autofill data to pre-populate
val autofillSaveItem = specialCircumstanceManager
@@ -104,11 +110,12 @@ class VaultAddEditViewModel @Inject constructor(
}
val defaultAddTypeContent = autofillSelectionData
?.toDefaultAddTypeContent()
?.toDefaultAddTypeContent(isIndividualVaultDisabled)
?: autofillSaveItem
?.toDefaultAddTypeContent()
?.toDefaultAddTypeContent(isIndividualVaultDisabled)
?: VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(),
isIndividualVaultDisabled = isIndividualVaultDisabled,
type = VaultAddEditState.ViewState.Content.ItemType.Login(),
)
@@ -1140,8 +1147,11 @@ class VaultAddEditViewModel @Inject constructor(
private fun VaultAddEditState.determineContentState(
vaultData: VaultData,
userData: UserState?,
): VaultAddEditState =
copy(
): VaultAddEditState {
val isIndividualVaultDisabled = policyManager
.getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
.any()
return copy(
viewState = vaultData.cipherViewList
.find { it.id == vaultAddEditType.vaultItemId }
.validateCipherOrReturnErrorState(
@@ -1152,6 +1162,7 @@ class VaultAddEditViewModel @Inject constructor(
// or use the current state for Add
(cipherView?.toViewState(
isClone = isCloneMode,
isIndividualVaultDisabled = isIndividualVaultDisabled,
resourceManager = resourceManager,
) ?: viewState)
.appendFolderAndOwnerData(
@@ -1159,10 +1170,12 @@ class VaultAddEditViewModel @Inject constructor(
collectionViewList = vaultData.collectionViewList
.filter { !it.readOnly },
activeAccount = currentAccount,
isIndividualVaultDisabled = isIndividualVaultDisabled,
resourceManager = resourceManager,
)
},
)
}
private fun handleVaultTotpCodeReceive(action: VaultAddEditAction.Internal.TotpCodeReceive) {
when (action.totpResult) {
@@ -1473,6 +1486,7 @@ data class VaultAddEditState(
data class Content(
val common: Common,
val type: ItemType,
val isIndividualVaultDisabled: Boolean,
val previousItemTypes: Map<ItemTypeOption, ItemType> = emptyMap(),
) : ViewState() {
@@ -1512,6 +1526,7 @@ data class VaultAddEditState(
*/
val selectedOwner: Owner?
get() = availableOwners.find { it.id == selectedOwnerId }
?: availableOwners.firstOrNull()
/**
* Helper to provide the currently selected folder.

View File

@@ -11,11 +11,14 @@ import java.util.UUID
* Returns pre-filled content that may be used for an "add" type
* [VaultAddEditState.ViewState.Content].
*/
fun AutofillSaveItem.toDefaultAddTypeContent(): VaultAddEditState.ViewState.Content =
fun AutofillSaveItem.toDefaultAddTypeContent(
isIndividualVaultDisabled: Boolean,
): VaultAddEditState.ViewState.Content =
when (this) {
is AutofillSaveItem.Card -> {
VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(),
isIndividualVaultDisabled = isIndividualVaultDisabled,
type = VaultAddEditState.ViewState.Content.ItemType.Card(
number = this.number.orEmpty(),
expirationMonth = VaultCardExpirationMonth
@@ -35,6 +38,7 @@ fun AutofillSaveItem.toDefaultAddTypeContent(): VaultAddEditState.ViewState.Cont
common = VaultAddEditState.ViewState.Content.Common(
name = simpleUri.orEmpty(),
),
isIndividualVaultDisabled = isIndividualVaultDisabled,
type = VaultAddEditState.ViewState.Content.ItemType.Login(
username = this.username.orEmpty(),
password = this.password.orEmpty(),

View File

@@ -10,7 +10,9 @@ import java.util.UUID
* Returns pre-filled content that may be used for an "add" type
* [VaultAddEditState.ViewState.Content].
*/
fun AutofillSelectionData.toDefaultAddTypeContent(): VaultAddEditState.ViewState.Content {
fun AutofillSelectionData.toDefaultAddTypeContent(
isIndividualVaultDisabled: Boolean,
): VaultAddEditState.ViewState.Content {
val uri = this.uri
val simpleUri = uri?.toHostOrPathOrNull()
val defaultAddType = when (this.type) {
@@ -34,6 +36,7 @@ fun AutofillSelectionData.toDefaultAddTypeContent(): VaultAddEditState.ViewState
common = VaultAddEditState.ViewState.Content.Common(
name = simpleUri.orEmpty(),
),
isIndividualVaultDisabled = isIndividualVaultDisabled,
type = defaultAddType,
)
}

View File

@@ -30,6 +30,7 @@ import java.util.UUID
*/
fun CipherView.toViewState(
isClone: Boolean,
isIndividualVaultDisabled: Boolean,
resourceManager: ResourceManager,
): VaultAddEditState.ViewState =
VaultAddEditState.ViewState.Content(
@@ -86,6 +87,7 @@ fun CipherView.toViewState(
availableOwners = emptyList(),
customFieldData = this.fields.orEmpty().map { it.toCustomField() },
),
isIndividualVaultDisabled = isIndividualVaultDisabled,
)
/**
@@ -95,6 +97,7 @@ fun VaultAddEditState.ViewState.appendFolderAndOwnerData(
folderViewList: List<FolderView>,
collectionViewList: List<CollectionView>,
activeAccount: UserState.Account,
isIndividualVaultDisabled: Boolean,
resourceManager: ResourceManager,
): VaultAddEditState.ViewState {
return (this as? VaultAddEditState.ViewState.Content)?.let { currentContentState ->
@@ -111,6 +114,7 @@ fun VaultAddEditState.ViewState.appendFolderAndOwnerData(
),
availableOwners = activeAccount.toAvailableOwners(
collectionViewList = collectionViewList,
isIndividualVaultDisabled = isIndividualVaultDisabled,
),
),
)
@@ -161,10 +165,17 @@ private fun UserState.Account.toSelectedOwnerId(cipherView: CipherView?): String
private fun UserState.Account.toAvailableOwners(
collectionViewList: List<CollectionView>,
isIndividualVaultDisabled: Boolean,
): List<VaultAddEditState.Owner> =
listOf(VaultAddEditState.Owner(name = email, id = null, collections = emptyList()))
.plus(
organizations.map {
listOfNotNull(
VaultAddEditState.Owner(
name = email,
id = null,
collections = emptyList(),
)
.takeUnless { isIndividualVaultDisabled },
*organizations
.map {
VaultAddEditState.Owner(
name = it.name.orEmpty(),
id = it.id,
@@ -181,8 +192,9 @@ private fun UserState.Account.toAvailableOwners(
)
},
)
},
)
}
.toTypedArray(),
)
private fun FieldView.toCustomField() =
when (this.type) {

View File

@@ -7,10 +7,12 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
@@ -52,6 +54,7 @@ class VaultViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val clipboardManager: BitwardenClipboardManager,
private val clock: Clock,
private val policyManager: PolicyManager,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
@@ -59,7 +62,11 @@ class VaultViewModel @Inject constructor(
val userState = requireNotNull(authRepository.userStateFlow.value)
val accountSummaries = userState.toAccountSummaries()
val activeAccountSummary = userState.toActiveAccountSummary()
val vaultFilterData = userState.activeAccount.toVaultFilterData()
val vaultFilterData = userState.activeAccount.toVaultFilterData(
isIndividualVaultDisabled = policyManager
.getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
.any(),
)
val appBarTitle = vaultFilterData.toAppBarTitle()
VaultState(
appBarTitle = appBarTitle,
@@ -414,7 +421,11 @@ class VaultViewModel @Inject constructor(
// navigating.
if (state.isSwitchingAccounts) return
val vaultFilterData = userState.activeAccount.toVaultFilterData()
val vaultFilterData = userState.activeAccount.toVaultFilterData(
isIndividualVaultDisabled = policyManager
.getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
.any(),
)
val appBarTitle = vaultFilterData.toAppBarTitle()
mutableStateFlow.update {
val accountSummaries = userState.toAccountSummaries()

View File

@@ -47,16 +47,18 @@ fun UserState.Account.toAccountSummary(
* Converts the given [UserState.Account] to a [VaultFilterData] (if applicable). Filter data is
* only relevant when the given account is associated with one or more organizations.
*/
fun UserState.Account.toVaultFilterData(): VaultFilterData? =
fun UserState.Account.toVaultFilterData(
isIndividualVaultDisabled: Boolean,
): VaultFilterData? =
this
.organizations
.takeIf { it.isNotEmpty() }
?.let { organizations ->
VaultFilterData(
selectedVaultFilterType = VaultFilterType.AllVaults,
vaultFilterTypes = listOf(
vaultFilterTypes = listOfNotNull(
VaultFilterType.AllVaults,
VaultFilterType.MyVault,
VaultFilterType.MyVault.takeUnless { isIndividualVaultDisabled },
*organizations
.sortedBy { it.name }
.map { organization ->