mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-786: Export vault policy (#875)
This commit is contained in:
parent
7b6f9491b3
commit
96401aba79
9 changed files with 107 additions and 2 deletions
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue