BIT-784: Enforce send options policy (#933)

This commit is contained in:
Caleb Derosier 2024-02-01 00:46:44 -07:00 committed by Álison Fernandes
parent c9eca38e08
commit 267fd8d077
10 changed files with 151 additions and 7 deletions

View file

@ -99,6 +99,18 @@ sealed class PolicyInformation {
}
}
/**
* Represents a policy enforcing rules on the user's add & edit send options.
*
* @property shouldDisableHideEmail Indicates whether the user should have the ability to hide
* their email address from send recipients.
*/
@Serializable
data class SendOptions(
@SerialName("disableHideEmail")
val shouldDisableHideEmail: Boolean?,
) : PolicyInformation()
/**
* Represents a policy enforcing rules on the user's vault timeout settings.
*/

View file

@ -40,6 +40,9 @@ val SyncResponseJson.Policy.policyInformation: PolicyInformation?
PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT -> {
Json.decodeFromStringOrNull<PolicyInformation.VaultTimeout>(it)
}
PolicyTypeJson.SEND_OPTIONS -> {
Json.decodeFromStringOrNull<PolicyInformation.SendOptions>(it)
}
else -> null
}

View file

@ -12,6 +12,8 @@ inline fun <reified T : PolicyInformation> PolicyManager.getActivePolicies(): Li
val type = when (T::class.java) {
PolicyInformation.MasterPassword::class.java -> PolicyTypeJson.MASTER_PASSWORD
PolicyInformation.PasswordGenerator::class.java -> PolicyTypeJson.PASSWORD_GENERATOR
PolicyInformation.SendOptions::class.java -> PolicyTypeJson.SEND_OPTIONS
PolicyInformation.VaultTimeout::class.java -> PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT
else -> {
throw IllegalStateException(

View file

@ -475,6 +475,7 @@ private fun AddSendOptions(
isChecked = state.common.isHideEmailChecked,
onCheckedChange = addSendHandlers.onHideEmailToggle,
readOnly = sendRestrictionPolicy,
enabled = state.common.isHideEmailChecked || state.common.isHideEmailAddressEnabled,
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenWideSwitch(

View file

@ -7,10 +7,12 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.core.SendView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
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.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
@ -88,6 +90,9 @@ class AddSendViewModel @Inject constructor(
noteInput = "",
isHideEmailChecked = false,
isDeactivateChecked = false,
isHideEmailAddressEnabled = !policyManager
.getActivePolicies<PolicyInformation.SendOptions>()
.any { it.shouldDisableHideEmail ?: false },
deletionDate = ZonedDateTime
.now(clock)
// We want the default time to be midnight, so we remove all values
@ -319,6 +324,7 @@ class AddSendViewModel @Inject constructor(
.environment
.environmentUrlData
.baseWebSendUrl,
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
)
?: AddSendState.ViewState.Error(
message = R.string.generic_error_message.asText(),
@ -356,6 +362,7 @@ class AddSendViewModel @Inject constructor(
.environment
.environmentUrlData
.baseWebSendUrl,
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
)
?: AddSendState.ViewState.Error(
message = R.string.generic_error_message.asText(),
@ -625,6 +632,11 @@ class AddSendViewModel @Inject constructor(
)
}
private val isHideEmailAddressEnabled: Boolean
get() = !policyManager
.getActivePolicies<PolicyInformation.SendOptions>()
.any { it.shouldDisableHideEmail ?: false }
private inline fun onContent(
crossinline block: (AddSendState.ViewState.Content) -> Unit,
) {
@ -764,6 +776,7 @@ data class AddSendState(
val noteInput: String,
val isHideEmailChecked: Boolean,
val isDeactivateChecked: Boolean,
val isHideEmailAddressEnabled: Boolean,
val deletionDate: ZonedDateTime,
val expirationDate: ZonedDateTime?,
val sendUrl: String?,

View file

@ -13,6 +13,7 @@ import java.time.ZonedDateTime
fun SendView.toViewState(
clock: Clock,
baseWebSendUrl: String,
isHideEmailAddressEnabled: Boolean,
): AddSendState.ViewState.Content =
AddSendState.ViewState.Content(
common = AddSendState.ViewState.Content.Common(
@ -30,6 +31,7 @@ fun SendView.toViewState(
expirationDate = this.expirationDate?.let { ZonedDateTime.ofInstant(it, clock.zone) },
sendUrl = this.toSendUrl(baseWebSendUrl),
hasPassword = this.hasPassword,
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
),
selectedType = when (type) {
SendType.TEXT -> {

View file

@ -762,6 +762,49 @@ class AddSendScreenTest : BaseComposeTest() {
.assertIsOn()
}
@Test
fun `hide email toggle should be disabled according to state`() = runTest {
// Expand options section:
composeTestRule
.onNodeWithText("Options")
.performScrollTo()
.performClick()
mutableStateFlow.update {
it.copy(
viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(
isHideEmailAddressEnabled = false,
),
),
)
}
// Toggle should be disabled
composeTestRule
.onNodeWithText("Hide my email address", substring = true)
.performScrollTo()
.assertIsDisplayed()
.assertIsNotEnabled()
mutableStateFlow.update {
it.copy(
viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(
isHideEmailChecked = true,
),
),
)
}
// Toggle should be enabled
composeTestRule
.onNodeWithText("Hide my email address", substring = true)
.performScrollTo()
.assertIsDisplayed()
.assertIsEnabled()
}
@Test
fun `on deactivate this send toggle should send DeactivateThisSendToggle`() = runTest {
// Expand options section:
@ -957,6 +1000,7 @@ class AddSendScreenTest : BaseComposeTest() {
expirationDate = null,
sendUrl = null,
hasPassword = true,
isHideEmailAddressEnabled = true,
)
private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text(

View file

@ -6,6 +6,7 @@ import app.cash.turbine.test
import com.bitwarden.core.SendView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
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
@ -14,6 +15,7 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@ -39,6 +41,9 @@ import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonObject
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
@ -75,6 +80,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
}
private val policyManager: PolicyManager = mockk {
every { getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) } returns emptyList()
every { getActivePolicies(type = PolicyTypeJson.SEND_OPTIONS) } returns emptyList()
}
@BeforeEach
@ -101,6 +107,30 @@ class AddSendViewModelTest : BaseViewModelTest() {
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
}
@Test
fun `initial state should be correct when a sendOption includes shouldDisableHideEmail`() {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.SEND_OPTIONS)
} returns listOf(
SyncResponseJson.Policy(
id = "123",
type = PolicyTypeJson.SEND_OPTIONS,
isEnabled = true,
data = Json.encodeToJsonElement(
PolicyInformation.SendOptions(shouldDisableHideEmail = true),
).jsonObject,
organizationId = "id2",
),
)
val viewModel = createViewModel()
val viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(
isHideEmailAddressEnabled = false,
),
)
assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value)
}
@Test
fun `initial state should read from saved state when present`() {
val savedState = DEFAULT_STATE.copy(
@ -270,7 +300,13 @@ class AddSendViewModelTest : BaseViewModelTest() {
viewState = viewState,
)
val mockSendView = createMockSendView(number = 1)
every { mockSendView.toViewState(clock, DEFAULT_ENVIRONMENT_URL) } returns viewState
every {
mockSendView.toViewState(
clock = clock,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
isHideEmailAddressEnabled = true,
)
} returns viewState
every { viewState.toSendView(clock) } returns mockSendView
val sendUrl = "www.test.com/send/test"
val resultSendView = mockk<SendView> {
@ -308,7 +344,13 @@ class AddSendViewModelTest : BaseViewModelTest() {
every { id } returns sendId
}
val errorMessage = "Failure"
every { mockSendView.toViewState(clock, DEFAULT_ENVIRONMENT_URL) } returns viewState
every {
mockSendView.toViewState(
clock = clock,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
isHideEmailAddressEnabled = true,
)
} returns viewState
every { viewState.toSendView(clock) } returns mockSendView
coEvery {
vaultRepository.updateSend(sendId = sendId, sendView = mockSendView)
@ -426,7 +468,11 @@ class AddSendViewModelTest : BaseViewModelTest() {
)
val mockSendView = createMockSendView(number = 1)
every {
mockSendView.toViewState(clock = clock, baseWebSendUrl = DEFAULT_ENVIRONMENT_URL)
mockSendView.toViewState(
clock = clock,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
isHideEmailAddressEnabled = true,
)
} returns viewState
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
val viewModel = createViewModel(
@ -467,7 +513,11 @@ class AddSendViewModelTest : BaseViewModelTest() {
)
val mockSendView = createMockSendView(number = 1)
every {
mockSendView.toViewState(clock, DEFAULT_ENVIRONMENT_URL)
mockSendView.toViewState(
clock = clock,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
isHideEmailAddressEnabled = true,
)
} returns DEFAULT_VIEW_STATE
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
val viewModel = createViewModel(
@ -509,7 +559,11 @@ class AddSendViewModelTest : BaseViewModelTest() {
} returns RemovePasswordSendResult.Error(errorMessage = errorMessage)
val mockSendView = createMockSendView(number = 1)
every {
mockSendView.toViewState(clock, DEFAULT_ENVIRONMENT_URL)
mockSendView.toViewState(
clock = clock,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
isHideEmailAddressEnabled = true,
)
} returns DEFAULT_VIEW_STATE
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
val initialState = DEFAULT_STATE.copy(
@ -575,7 +629,11 @@ class AddSendViewModelTest : BaseViewModelTest() {
)
val mockSendView = createMockSendView(number = 1)
every {
mockSendView.toViewState(clock, DEFAULT_ENVIRONMENT_URL)
mockSendView.toViewState(
clock = clock,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
isHideEmailAddressEnabled = true,
)
} returns DEFAULT_VIEW_STATE
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
val viewModel = createViewModel(
@ -630,7 +688,11 @@ class AddSendViewModelTest : BaseViewModelTest() {
)
val mockSendView = createMockSendView(number = 1)
every {
mockSendView.toViewState(clock = clock, baseWebSendUrl = DEFAULT_ENVIRONMENT_URL)
mockSendView.toViewState(
clock = clock,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
isHideEmailAddressEnabled = true,
)
} returns viewState
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
val viewModel = createViewModel(
@ -997,6 +1059,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
expirationDate = null,
sendUrl = null,
hasPassword = false,
isHideEmailAddressEnabled = true,
)
private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text(

View file

@ -77,6 +77,7 @@ private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common(
expirationDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
sendUrl = null,
hasPassword = false,
isHideEmailAddressEnabled = true,
)
private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text(

View file

@ -19,6 +19,7 @@ class SendViewExtensionsTest {
val result = sendView.toViewState(
clock = FIXED_CLOCK,
baseWebSendUrl = "www.test.com/",
isHideEmailAddressEnabled = true,
)
assertEquals(
@ -36,6 +37,7 @@ class SendViewExtensionsTest {
val result = sendView.toViewState(
clock = FIXED_CLOCK,
baseWebSendUrl = "www.test.com/",
isHideEmailAddressEnabled = true,
)
assertEquals(
@ -72,6 +74,7 @@ private val DEFAULT_COMMON: AddSendState.ViewState.Content.Common =
),
sendUrl = "www.test.com/mockAccessId-1/mockKey-1",
hasPassword = true,
isHideEmailAddressEnabled = true,
)
private val DEFAULT_TEXT_TYPE: AddSendState.ViewState.Content.SendType.Text =