mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-784: Enforce send options policy (#933)
This commit is contained in:
parent
c9eca38e08
commit
267fd8d077
10 changed files with 151 additions and 7 deletions
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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?,
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Add table
Reference in a new issue