BIT-786: Export vault policy (#875)

This commit is contained in:
Shannon Draeker 2024-01-30 14:26:36 -07:00 committed by Álison Fernandes
parent 7b6f9491b3
commit 96401aba79
9 changed files with 107 additions and 2 deletions

View file

@ -84,6 +84,11 @@ interface AuthRepository : AuthenticatorProvider {
*/
val passwordPolicies: List<PolicyInformation.MasterPassword>
/**
* Return whether there are any export vault policies enabled for the current user.
*/
val hasExportVaultPoliciesEnabled: Boolean
/**
* Clears the pending deletion state that occurs when the an account is successfully deleted.
*/

View file

@ -229,6 +229,14 @@ class AuthRepositoryImpl(
.orEmpty()
} ?: emptyList()
override val hasExportVaultPoliciesEnabled: Boolean
get() = activeUserId?.let { userId ->
authDiskSource
.getPolicies(userId)
?.any { it.type == PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT && it.isEnabled }
?: false
} ?: false
init {
pushManager
.syncOrgKeysFlow

View file

@ -20,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
*
* @param label The text to be displayed on the button.
* @param onClick A lambda which will be invoked when the button is clicked.
* @param isEnabled Whether or not the button is enabled.
* @param modifier A [Modifier] for this composable, allowing for adjustments to its appearance
* or behavior. This can be used to apply padding, layout, and other Modifiers.
*/
@ -27,6 +28,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
fun BitwardenFilledTonalButton(
label: String,
onClick: () -> Unit,
isEnabled: Boolean = true,
modifier: Modifier,
) {
Button(
@ -35,6 +37,7 @@ fun BitwardenFilledTonalButton(
vertical = 10.dp,
horizontal = 24.dp,
),
enabled = isEnabled,
colors = ButtonDefaults.filledTonalButtonColors(),
modifier = modifier,
) {

View file

@ -49,6 +49,7 @@ import kotlinx.collections.immutable.persistentListOf
* (or `null` if no option is selected).
* @param onOptionSelected A lambda that is invoked when an option
* is selected from the dropdown menu.
* @param isEnabled Whether or not the button is enabled.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param supportingText A optional supporting text that will appear below the text field.
* @param tooltip A nullable [TooltipData], representing the tooltip icon.
@ -60,6 +61,7 @@ fun BitwardenMultiSelectButton(
options: ImmutableList<String>,
selectedOption: String?,
onOptionSelected: (String) -> Unit,
isEnabled: Boolean = true,
modifier: Modifier = Modifier,
supportingText: String? = null,
tooltip: TooltipData? = null,
@ -80,6 +82,7 @@ fun BitwardenMultiSelectButton(
.fillMaxWidth()
.clickable(
indication = null,
enabled = isEnabled,
interactionSource = remember { MutableInteractionSource() },
) {
shouldShowDialog = !shouldShowDialog
@ -93,6 +96,7 @@ fun BitwardenMultiSelectButton(
Spacer(modifier = Modifier.width(3.dp))
IconButton(
onClick = it.onClick,
enabled = isEnabled,
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.primary,
),

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.exportvault
import android.widget.Toast
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
@ -154,6 +156,7 @@ fun ExportVaultScreen(
}
@Composable
@Suppress("LongMethod")
private fun ExportVaultScreenContent(
state: ExportVaultState,
onExportFormatOptionSelected: (ExportVaultFormat) -> Unit,
@ -168,6 +171,26 @@ private fun ExportVaultScreenContent(
.verticalScroll(rememberScrollState()),
) {
if (state.policyPreventsExport) {
Text(
text = stringResource(id = R.string.disable_personal_vault_export_policy_in_effect),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(horizontal = 16.dp)
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(4.dp),
)
.padding(8.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
}
BitwardenMultiSelectButton(
label = stringResource(id = R.string.file_format),
options = ExportVaultFormat.entries.map { it.displayLabel }.toImmutableList(),
@ -178,6 +201,7 @@ private fun ExportVaultScreenContent(
.first { it.displayLabel == selectedOptionLabel }
onExportFormatOptionSelected(selectedOption)
},
isEnabled = !state.policyPreventsExport,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
@ -188,6 +212,7 @@ private fun ExportVaultScreenContent(
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
value = state.passwordInput,
readOnly = state.policyPreventsExport,
onValueChange = onPasswordInputChanged,
modifier = Modifier
.padding(horizontal = 16.dp)
@ -211,6 +236,7 @@ private fun ExportVaultScreenContent(
BitwardenFilledTonalButton(
label = stringResource(id = R.string.export_vault),
onClick = onExportVaultClick,
isEnabled = !state.policyPreventsExport,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),

View file

@ -33,6 +33,7 @@ class ExportVaultViewModel @Inject constructor(
dialogState = null,
exportFormat = ExportVaultFormat.JSON,
passwordInput = "",
policyPreventsExport = authRepository.hasExportVaultPoliciesEnabled,
),
) {
init {
@ -165,6 +166,7 @@ data class ExportVaultState(
val dialogState: DialogState?,
val exportFormat: ExportVaultFormat,
val passwordInput: String,
val policyPreventsExport: Boolean,
) : Parcelable {
/**
* Represents the current state of any dialogs on the screen.

View file

@ -499,6 +499,38 @@ class AuthRepositoryTest {
assertNull(repository.rememberedOrgIdentifier)
}
@Test
fun `hasExportVaultPoliciesEnabled checks if any export vault policies are enabled`() {
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
// No stored policies returns false.
assertFalse(repository.hasExportVaultPoliciesEnabled)
// Stored but disabled policies returns false.
fakeAuthDiskSource.storePolicies(
userId = USER_ID_1,
policies = listOf(
createMockPolicy(
type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
isEnabled = false,
),
),
)
assertFalse(repository.hasExportVaultPoliciesEnabled)
// Stored enabled policies returns true.
fakeAuthDiskSource.storePolicies(
userId = USER_ID_1,
policies = listOf(
createMockPolicy(
type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
isEnabled = true,
),
),
)
assertTrue(repository.hasExportVaultPoliciesEnabled)
}
@Test
fun `clear Pending Account Deletion should unblock userState updates`() = runTest {
val masterPassword = "hello world"

View file

@ -116,6 +116,19 @@ class ExportVaultScreenTest : BaseComposeTest() {
}
}
@Test
fun `policy text should update according to state`() {
val text =
"One or more organization policies prevents your from exporting your individual vault."
composeTestRule.onNodeWithText(text).assertDoesNotExist()
mutableStateFlow.update {
it.copy(policyPreventsExport = true)
}
composeTestRule.onNodeWithText(text).assertIsDisplayed()
}
@Test
fun `file format selection button should send ExportFormatOptionSelect action`() {
// Open the menu.
@ -186,4 +199,5 @@ private val DEFAULT_STATE = ExportVaultState(
dialogState = null,
exportFormat = ExportVaultFormat.JSON,
passwordInput = "",
policyPreventsExport = false,
)

View file

@ -9,21 +9,31 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.exportvault.model.ExportVaultFormat
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class ExportVaultViewModelTest : BaseViewModelTest() {
private val authRepository: AuthRepository = mockk()
private val authRepository: AuthRepository = mockk {
every { hasExportVaultPoliciesEnabled } returns false
}
private val savedStateHandle = SavedStateHandle()
@Test
fun `initial state should be correct`() = runTest {
every { authRepository.hasExportVaultPoliciesEnabled } returns true
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
assertEquals(
DEFAULT_STATE.copy(
policyPreventsExport = true,
),
awaitItem(),
)
}
}
@ -183,4 +193,5 @@ private val DEFAULT_STATE = ExportVaultState(
dialogState = null,
exportFormat = ExportVaultFormat.JSON,
passwordInput = "",
policyPreventsExport = false,
)