Skip to content
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 @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.manager

import com.bitwarden.policies.PolicyType
import com.bitwarden.policies.PolicyView
import com.x8bit.bitwarden.data.platform.manager.model.EffectiveSendPolicy
import kotlinx.coroutines.flow.Flow

/**
Expand All @@ -18,6 +19,19 @@ interface PolicyManager {
*/
fun getActivePolicies(type: PolicyType): List<PolicyView>

/**
* Returns the current, precedence-resolved [EffectiveSendPolicy] for the active user. When
* the `pm-31885-send-controls` feature flag is disabled, or no organization has an active
* SendControls policy, this is equivalent to today's DisableSend/SendOptions logic.
*/
fun getEffectiveSendPolicy(): EffectiveSendPolicy

/**
* Returns a flow that emits the current [EffectiveSendPolicy] for the active user whenever
* the underlying policies or the `pm-31885-send-controls` feature flag change.
*/
fun getEffectiveSendPolicyFlow(): Flow<EffectiveSendPolicy>

/**
* Get all the policies of the given [type] that are enabled and applicable to the [userId].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import com.bitwarden.policies.PolicyType
import com.bitwarden.policies.PolicyView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.platform.manager.model.EffectiveSendPolicy
import com.x8bit.bitwarden.data.vault.repository.util.toSdkOrganizationPolicyContext
import com.x8bit.bitwarden.data.vault.repository.util.toSdkPolicyViews
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand Down Expand Up @@ -47,6 +50,30 @@ class PolicyManagerImpl(
?.let { userId -> getUserPolicies(userId = userId, type = type) }
.orEmpty()

override fun getEffectiveSendPolicy(): EffectiveSendPolicy =
resolveEffectiveSendPolicy(
disableSendPolicies = getActivePolicies(type = PolicyType.DISABLE_SEND),
isSendControlsEnabled = featureFlagManager.getFeatureFlag(key = FlagKey.SendControls),
sendControlsPolicies = getActivePolicies(type = PolicyType.SEND_CONTROLS),
sendOptionsPolicies = getActivePolicies(type = PolicyType.SEND_OPTIONS),
)

override fun getEffectiveSendPolicyFlow(): Flow<EffectiveSendPolicy> =
combine(
featureFlagManager.getFeatureFlagFlow(key = FlagKey.SendControls),
getActivePoliciesFlow(type = PolicyType.SEND_CONTROLS),
getActivePoliciesFlow(type = PolicyType.DISABLE_SEND),
getActivePoliciesFlow(type = PolicyType.SEND_OPTIONS),
) { isSendControlsEnabled, sendControlsPolicies, disableSendPolicies, sendOptionsPolicies ->
resolveEffectiveSendPolicy(
disableSendPolicies = disableSendPolicies,
isSendControlsEnabled = isSendControlsEnabled,
sendControlsPolicies = sendControlsPolicies,
sendOptionsPolicies = sendOptionsPolicies,
)
}
.distinctUntilChanged()

override fun getUserPolicies(
userId: String,
type: PolicyType,
Expand Down Expand Up @@ -170,6 +197,62 @@ class PolicyManagerImpl(
this.organizationShouldUsePolicies
}
}

/**
* Resolves the [EffectiveSendPolicy] from the raw, already-filtered active policies of each
* relevant type. When [isSendControlsEnabled] is `false`, this is equivalent to the legacy
* DisableSend/SendOptions logic. When `true`, an organization's active SendControls policy
* (if any) takes precedence over that same organization's legacy DisableSend/SendOptions
* policies, while other organizations' legacy policies remain in effect.
*/
private fun resolveEffectiveSendPolicy(
disableSendPolicies: List<PolicyView>,
isSendControlsEnabled: Boolean,
sendControlsPolicies: List<PolicyView>,
sendOptionsPolicies: List<PolicyView>,
): EffectiveSendPolicy {
if (!isSendControlsEnabled) {
return EffectiveSendPolicy(
allowedDomains = null,
allowedSendTypes = null,
deletionHours = null,
disableHideEmail = sendOptionsPolicies
.mapNotNull { it.policyInformation as? PolicyInformation.SendOptions }
.any { it.shouldDisableHideEmail ?: false },
disableSend = disableSendPolicies.any(),
whoCanAccess = null,
)
}

val decodedSendControls = sendControlsPolicies
// Sorted so that the passthrough fields below are sourced deterministically when
// multiple organizations have an active SendControls policy.
.sortedBy { it.revisionDate }
.mapNotNull { policy ->
(policy.policyInformation as? PolicyInformation.SendControls)
?.let { policy.organizationId to it }
}
val organizationIdsWithSendControls = decodedSendControls.map { it.first }.toSet()

val remainingDisableSendPolicies = disableSendPolicies
.filterNot { organizationIdsWithSendControls.contains(it.organizationId) }
val remainingSendOptions = sendOptionsPolicies
.filterNot { organizationIdsWithSendControls.contains(it.organizationId) }
.mapNotNull { it.policyInformation as? PolicyInformation.SendOptions }

val firstSendControls = decodedSendControls.firstOrNull()?.second

Comment on lines +227 to +244
return EffectiveSendPolicy(
allowedDomains = firstSendControls?.allowedDomains,
allowedSendTypes = firstSendControls?.allowedSendTypes,
deletionHours = firstSendControls?.deletionHours,
disableHideEmail = decodedSendControls.any { it.second.disableHideEmail == true } ||
remainingSendOptions.any { it.shouldDisableHideEmail ?: false },
disableSend = decodedSendControls.any { it.second.disableSend == true } ||
remainingDisableSendPolicies.any(),
whoCanAccess = firstSendControls?.whoCanAccess,
)
}
}

private data class OrganizationPolicyData(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.x8bit.bitwarden.data.platform.manager.model

import com.bitwarden.network.model.SendAccessTypeJson
import com.bitwarden.network.model.SendTypeJson

/**
* The effective, precedence-resolved Send policy for the active user, combining the legacy
* DisableSend/SendOptions policies with the newer SendControls policy (type 21) per the
* `pm-31885-send-controls` feature flag.
*
* When an organization has an active SendControls policy, that organization's legacy
* DisableSend/SendOptions policies are ignored in favor of the SendControls values; other
* organizations' legacy policies remain in effect.
*
* @property allowedDomains The allowed recipient email domains, sourced from whichever
* organization's active SendControls policy has the earliest revision date, if any. Currently
* unused by the UI.
* @property allowedSendTypes The types of Sends that are allowed to be created, sourced the same
* way as [allowedDomains]. Currently unused by the UI.
* @property deletionHours The enforced number of hours until a Send is deleted, sourced the same
* way as [allowedDomains]. Currently unused by the UI.
* @property disableHideEmail Whether the ability to hide one's email address on a Send should be
* disabled.
* @property disableSend Whether the ability to create and edit Sends should be disabled.
* @property whoCanAccess The access type Sends are restricted to, sourced the same way as
* [allowedDomains]. Currently unused by the UI.
*/
data class EffectiveSendPolicy(
val allowedDomains: String?,
val allowedSendTypes: List<SendTypeJson>?,
val deletionHours: Int?,
val disableHideEmail: Boolean,
val disableSend: Boolean,
val whoCanAccess: SendAccessTypeJson?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar

import androidx.annotation.StringRes
import androidx.lifecycle.viewModelScope
import com.bitwarden.policies.PolicyType
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.base.DeferredBackgroundEvent
import com.bitwarden.ui.platform.resource.BitwardenString
Expand Down Expand Up @@ -35,9 +34,7 @@ class VaultUnlockedNavBarViewModel @Inject constructor(
notificationState = VaultUnlockedNavBarNotificationState(
settingsTabNotificationCount = firstTimeActionManager.allSettingsBadgeCountFlow.value,
),
areSendsDisabled = policyManager
.getActivePolicies(type = PolicyType.DISABLE_SEND)
.any(),
areSendsDisabled = policyManager.getEffectiveSendPolicy().disableSend,
),
) {
init {
Expand All @@ -56,8 +53,8 @@ class VaultUnlockedNavBarViewModel @Inject constructor(
.launchIn(viewModelScope)

policyManager
.getActivePoliciesFlow(type = PolicyType.DISABLE_SEND)
.map { VaultUnlockedNavBarAction.Internal.SendPolicyUpdateReceive(it.any()) }
.getEffectiveSendPolicyFlow()
.map { VaultUnlockedNavBarAction.Internal.SendPolicyUpdateReceive(it.disableSend) }
.onEach(::sendAction)
.launchIn(viewModelScope)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.data.repository.util.baseWebSendUrl
import com.bitwarden.policies.PolicyType
import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.icon.model.IconData
Expand Down Expand Up @@ -69,9 +68,7 @@ class SendViewModel @Inject constructor(
viewState = SendState.ViewState.Loading,
dialogState = null,
isPullToRefreshSettingEnabled = settingsRepo.getPullToRefreshEnabledFlow().value,
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyType.DISABLE_SEND)
.any(),
policyDisablesSend = policyManager.getEffectiveSendPolicy().disableSend,
isRefreshing = false,
isPremiumUser = authRepo.userStateFlow.value?.activeAccount?.isPremium == true,
isUpgradedToPremiumCardEligible = false,
Expand All @@ -85,8 +82,8 @@ class SendViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)
policyManager
.getActivePoliciesFlow(type = PolicyType.DISABLE_SEND)
.map { SendAction.Internal.PolicyUpdateReceive(it.any()) }
.getEffectiveSendPolicyFlow()
.map { SendAction.Internal.PolicyUpdateReceive(it.disableSend) }
.onEach(::sendAction)
.launchIn(viewModelScope)
vaultRepo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.takeUntilLoaded
import com.bitwarden.data.repository.util.baseWebSendUrl
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
import com.bitwarden.policies.PolicyType
import com.bitwarden.network.model.SendAccessTypeJson
import com.bitwarden.network.model.SendTypeJson
import com.bitwarden.send.SendView
import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel
Expand All @@ -21,13 +22,12 @@ import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.concat
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.billing.manager.PremiumStateManager
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.model.EffectiveSendPolicy
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
Expand Down Expand Up @@ -98,6 +98,7 @@ class AddEditSendViewModel @Inject constructor(
val args = savedStateHandle.toAddEditSendArgs()
val sendType = args.sendType
val addEditSendType = args.addEditSendType
val effectiveSendPolicy = policyManager.getEffectiveSendPolicy()

AddEditSendState(
sendType = sendType,
Expand All @@ -114,9 +115,7 @@ class AddEditSendViewModel @Inject constructor(
noteInput = "",
isHideEmailChecked = false,
isDeactivateChecked = false,
isHideEmailAddressEnabled = !policyManager
.getActivePolicies<PolicyInformation.SendOptions>()
.any { it.shouldDisableHideEmail ?: false },
isHideEmailAddressEnabled = !effectiveSendPolicy.disableHideEmail,
deletionDate = clock
.instant()
.plus(@Suppress("MagicNumber") 7, ChronoUnit.DAYS),
Expand Down Expand Up @@ -148,9 +147,11 @@ class AddEditSendViewModel @Inject constructor(
},
dialogState = null,
baseWebSendUrl = environmentRepo.environment.baseWebSendUrl,
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyType.DISABLE_SEND)
.any(),
policyDisablesSend = effectiveSendPolicy.disableSend,
allowedDomains = effectiveSendPolicy.allowedDomains,
allowedSendTypes = effectiveSendPolicy.allowedSendTypes,
deletionHours = effectiveSendPolicy.deletionHours,
whoCanAccess = effectiveSendPolicy.whoCanAccess,
isPremium = authRepo.userStateFlow.value?.activeAccount?.isPremium == true,
)
},
Expand Down Expand Up @@ -825,9 +826,7 @@ class AddEditSendViewModel @Inject constructor(
}

private val isHideEmailAddressEnabled: Boolean
get() = !policyManager
.getActivePolicies<PolicyInformation.SendOptions>()
.any { it.shouldDisableHideEmail ?: false }
get() = !policyManager.getEffectiveSendPolicy().disableHideEmail

private inline fun onContent(
crossinline block: (AddEditSendState.ViewState.Content) -> Unit,
Expand Down Expand Up @@ -886,6 +885,15 @@ class AddEditSendViewModel @Inject constructor(

/**
* Models state for the add/edit send screen.
*
* @property allowedDomains The allowed recipient email domains, sourced from
* [EffectiveSendPolicy.allowedDomains]. Currently unused by the UI.
* @property allowedSendTypes The types of Sends that are allowed to be created, sourced from
* [EffectiveSendPolicy.allowedSendTypes]. Currently unused by the UI.
* @property deletionHours The enforced Send deletion window in hours, sourced from
* [EffectiveSendPolicy.deletionHours]. Currently unused by the UI.
* @property whoCanAccess The access type Sends are restricted to, sourced from
* [EffectiveSendPolicy.whoCanAccess]. Currently unused by the UI.
*/
@Parcelize
data class AddEditSendState(
Expand All @@ -897,6 +905,10 @@ data class AddEditSendState(
val isShared: Boolean,
val baseWebSendUrl: String,
val policyDisablesSend: Boolean,
val allowedDomains: String?,
val allowedSendTypes: List<SendTypeJson>?,
val deletionHours: Int?,
val whoCanAccess: SendAccessTypeJson?,
val isPremium: Boolean,
) : Parcelable {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ class VaultItemListingViewModel @Inject constructor(
?.let {
VaultItemListingState.DialogState.Loading(BitwardenString.loading.asText())
},
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyType.DISABLE_SEND)
.any(),
policyDisablesSend = policyManager.getEffectiveSendPolicy().disableSend,
restrictItemTypesPolicyOrgIds = persistentListOf(),
autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull(),
hasMasterPassword = userState.activeAccount.hasMasterPassword,
Expand Down Expand Up @@ -214,8 +212,8 @@ class VaultItemListingViewModel @Inject constructor(
.launchIn(viewModelScope)

policyManager
.getActivePoliciesFlow(type = PolicyType.DISABLE_SEND)
.map { VaultItemListingsAction.Internal.PolicyUpdateReceive(it.any()) }
.getEffectiveSendPolicyFlow()
.map { VaultItemListingsAction.Internal.PolicyUpdateReceive(it.disableSend) }
.onEach(::sendAction)
.launchIn(viewModelScope)

Expand Down
Loading
Loading