mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-785: Vault timeout policy (#924)
This commit is contained in:
parent
c7f063a306
commit
89dd552908
12 changed files with 498 additions and 37 deletions
|
@ -98,4 +98,16 @@ sealed class PolicyInformation {
|
|||
const val TYPE_PASSPHRASE: String = "passphrase"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a policy enforcing rules on the user's vault timeout settings.
|
||||
*/
|
||||
@Serializable
|
||||
data class VaultTimeout(
|
||||
@SerialName("minutes")
|
||||
val minutes: Int?,
|
||||
|
||||
@SerialName("action")
|
||||
val action: String?,
|
||||
) : PolicyInformation()
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.util
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -29,10 +30,15 @@ val SyncResponseJson.Policy.policyInformation: PolicyInformation?
|
|||
get() = data?.toString()?.let {
|
||||
when (type) {
|
||||
PolicyTypeJson.MASTER_PASSWORD -> {
|
||||
Json.decodeFromString<PolicyInformation.MasterPassword>(it)
|
||||
Json.decodeFromStringOrNull<PolicyInformation.MasterPassword>(it)
|
||||
}
|
||||
|
||||
PolicyTypeJson.PASSWORD_GENERATOR -> {
|
||||
Json.decodeFromString<PolicyInformation.PasswordGenerator>(it)
|
||||
Json.decodeFromStringOrNull<PolicyInformation.PasswordGenerator>(it)
|
||||
}
|
||||
|
||||
PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT -> {
|
||||
Json.decodeFromStringOrNull<PolicyInformation.VaultTimeout>(it)
|
||||
}
|
||||
|
||||
else -> null
|
||||
|
|
|
@ -3,16 +3,21 @@ package com.x8bit.bitwarden.data.platform.repository
|
|||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
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.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
|
@ -24,7 +29,9 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
|
@ -42,6 +49,7 @@ class SettingsRepositoryImpl(
|
|||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
) : SettingsRepository {
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
@ -286,6 +294,13 @@ class SettingsRepositoryImpl(
|
|||
?: DEFAULT_IS_SCREEN_CAPTURE_ALLOWED,
|
||||
)
|
||||
|
||||
init {
|
||||
policyManager
|
||||
.getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
|
||||
.onEach { updateVaultUnlockSettingsIfNecessary(it) }
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
|
||||
override fun disableAutofill() {
|
||||
autofillManager.disableAutofillServices()
|
||||
|
||||
|
@ -451,6 +466,36 @@ class SettingsRepositoryImpl(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the parameters of the vault unlock policy against the user's
|
||||
* settings to determine whether to update the user's settings.
|
||||
*/
|
||||
private fun updateVaultUnlockSettingsIfNecessary(
|
||||
policies: List<SyncResponseJson.Policy>,
|
||||
) {
|
||||
// The vault timeout policy can only be implemented in organizations that have
|
||||
// the single organization policy, meaning that if this is enabled, the user is
|
||||
// only in one organization and hence there is only one result in the list.
|
||||
val vaultUnlockPolicy = policies
|
||||
.firstOrNull()
|
||||
?.policyInformation as? PolicyInformation.VaultTimeout
|
||||
?: return
|
||||
|
||||
// Adjust the user's timeout or method if necessary to meet the policy requirements.
|
||||
vaultUnlockPolicy.minutes?.let { maxMinutes ->
|
||||
if ((vaultTimeout.vaultTimeoutInMinutes ?: Int.MAX_VALUE) > maxMinutes) {
|
||||
vaultTimeout = VaultTimeout.Custom(maxMinutes)
|
||||
}
|
||||
}
|
||||
vaultUnlockPolicy.action?.let {
|
||||
vaultTimeoutAction = if (it == "lock") {
|
||||
VaultTimeoutAction.LOCK
|
||||
} else {
|
||||
VaultTimeoutAction.LOGOUT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,8 +5,8 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
|||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||
|
@ -44,12 +44,12 @@ object PlatformRepositoryModule {
|
|||
fun provideSettingsRepository(
|
||||
autofillManager: AutofillManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
appForegroundManager: AppForegroundManager,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
encryptionManager: BiometricsEncryptionManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
policyManager: PolicyManager,
|
||||
): SettingsRepository =
|
||||
SettingsRepositoryImpl(
|
||||
autofillManager = autofillManager,
|
||||
|
@ -59,5 +59,6 @@ object PlatformRepositoryModule {
|
|||
vaultSdkSource = vaultSdkSource,
|
||||
biometricsEncryptionManager = encryptionManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
policyManager = policyManager,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -37,10 +37,14 @@ import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
|||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLogoutConfirmationDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPolicyWarningText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow
|
||||
|
@ -60,6 +64,7 @@ import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
|
|||
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.util.displayLabel
|
||||
import com.x8bit.bitwarden.ui.platform.util.minutes
|
||||
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
||||
import java.time.LocalTime
|
||||
|
||||
|
@ -210,7 +215,15 @@ fun AccountSecurityScreen(
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
SessionTimeoutPolicyRow(
|
||||
vaultTimeoutPolicyMinutes = state.vaultTimeoutPolicyMinutes,
|
||||
vaultTimeoutPolicyAction = state.vaultTimeoutPolicyAction,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
SessionTimeoutRow(
|
||||
vaultTimeoutPolicyMinutes = state.vaultTimeoutPolicyMinutes,
|
||||
selectedVaultTimeoutType = state.vaultTimeout.type,
|
||||
onVaultTimeoutTypeSelect = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AccountSecurityAction.VaultTimeoutTypeSelect(it)) }
|
||||
|
@ -219,6 +232,7 @@ fun AccountSecurityScreen(
|
|||
)
|
||||
(state.vaultTimeout as? VaultTimeout.Custom)?.let { customTimeout ->
|
||||
SessionCustomTimeoutRow(
|
||||
vaultTimeoutPolicyMinutes = state.vaultTimeoutPolicyMinutes,
|
||||
customVaultTimeout = customTimeout,
|
||||
onCustomVaultTimeoutSelect = remember(viewModel) {
|
||||
{
|
||||
|
@ -231,6 +245,7 @@ fun AccountSecurityScreen(
|
|||
)
|
||||
}
|
||||
SessionTimeoutActionRow(
|
||||
vaultTimeoutPolicyAction = state.vaultTimeoutPolicyAction,
|
||||
selectedVaultTimeoutAction = state.vaultTimeoutAction,
|
||||
onVaultTimeoutActionSelect = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AccountSecurityAction.VaultTimeoutActionSelect(it)) }
|
||||
|
@ -468,9 +483,44 @@ private fun UnlockWithPinRow(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionTimeoutPolicyRow(
|
||||
vaultTimeoutPolicyMinutes: Int?,
|
||||
vaultTimeoutPolicyAction: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// Show the policy warning if applicable.
|
||||
if (vaultTimeoutPolicyMinutes != null || !vaultTimeoutPolicyAction.isNullOrBlank()) {
|
||||
// Calculate the hours and minutes to show in the policy label.
|
||||
val hours = vaultTimeoutPolicyMinutes?.floorDiv(MINUTES_PER_HOUR)
|
||||
val minutes = vaultTimeoutPolicyMinutes?.mod(MINUTES_PER_HOUR)
|
||||
|
||||
// Get the localized version of the action.
|
||||
val action = if (vaultTimeoutPolicyAction == "lock") {
|
||||
R.string.lock.asText()
|
||||
} else {
|
||||
R.string.log_out.asText()
|
||||
}
|
||||
|
||||
val policyText = if (hours == null || minutes == null) {
|
||||
R.string.vault_timeout_action_policy_in_effect.asText(action)
|
||||
} else if (vaultTimeoutPolicyAction.isNullOrBlank()) {
|
||||
R.string.vault_timeout_policy_in_effect.asText(hours, minutes)
|
||||
} else {
|
||||
R.string.vault_timeout_policy_with_action_in_effect.asText(hours, minutes, action)
|
||||
}
|
||||
|
||||
BitwardenPolicyWarningText(
|
||||
text = policyText(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SessionTimeoutRow(
|
||||
vaultTimeoutPolicyMinutes: Int?,
|
||||
selectedVaultTimeoutType: VaultTimeout.Type,
|
||||
onVaultTimeoutTypeSelect: (VaultTimeout.Type) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -492,6 +542,10 @@ private fun SessionTimeoutRow(
|
|||
when {
|
||||
shouldShowSelectionDialog -> {
|
||||
val vaultTimeoutOptions = VaultTimeout.Type.entries
|
||||
.filter {
|
||||
it.minutes <= (vaultTimeoutPolicyMinutes ?: Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
BitwardenSelectionDialog(
|
||||
title = stringResource(id = R.string.session_timeout),
|
||||
onDismissRequest = { shouldShowSelectionDialog = false },
|
||||
|
@ -535,11 +589,13 @@ private fun SessionTimeoutRow(
|
|||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SessionCustomTimeoutRow(
|
||||
vaultTimeoutPolicyMinutes: Int?,
|
||||
customVaultTimeout: VaultTimeout.Custom,
|
||||
onCustomVaultTimeoutSelect: (VaultTimeout.Custom) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var shouldShowTimePickerDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var shouldShowViolatesPoliciesDialog by remember { mutableStateOf(false) }
|
||||
val vaultTimeoutInMinutes = customVaultTimeout.vaultTimeoutInMinutes
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = R.string.custom),
|
||||
|
@ -564,21 +620,49 @@ private fun SessionCustomTimeoutRow(
|
|||
initialMinute = vaultTimeoutInMinutes.mod(MINUTES_PER_HOUR),
|
||||
onTimeSelect = { hour, minute ->
|
||||
shouldShowTimePickerDialog = false
|
||||
onCustomVaultTimeoutSelect(
|
||||
VaultTimeout.Custom(
|
||||
vaultTimeoutInMinutes = hour * MINUTES_PER_HOUR + minute,
|
||||
),
|
||||
)
|
||||
|
||||
val totalMinutes = (hour * MINUTES_PER_HOUR) + minute
|
||||
if (vaultTimeoutPolicyMinutes != null &&
|
||||
totalMinutes > vaultTimeoutPolicyMinutes
|
||||
) {
|
||||
shouldShowViolatesPoliciesDialog = true
|
||||
} else {
|
||||
onCustomVaultTimeoutSelect(
|
||||
VaultTimeout.Custom(
|
||||
vaultTimeoutInMinutes = totalMinutes,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
onDismissRequest = { shouldShowTimePickerDialog = false },
|
||||
is24Hour = true,
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldShowViolatesPoliciesDialog) {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = R.string.warning.asText(),
|
||||
message = R.string.vault_timeout_to_large.asText(),
|
||||
),
|
||||
onDismissRequest = {
|
||||
shouldShowViolatesPoliciesDialog = false
|
||||
vaultTimeoutPolicyMinutes?.let {
|
||||
onCustomVaultTimeoutSelect(
|
||||
VaultTimeout.Custom(
|
||||
vaultTimeoutInMinutes = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SessionTimeoutActionRow(
|
||||
vaultTimeoutPolicyAction: String?,
|
||||
selectedVaultTimeoutAction: VaultTimeoutAction,
|
||||
onVaultTimeoutActionSelect: (VaultTimeoutAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -587,7 +671,11 @@ private fun SessionTimeoutActionRow(
|
|||
var shouldShowLogoutActionConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = R.string.session_timeout_action),
|
||||
onClick = { shouldShowSelectionDialog = true },
|
||||
onClick = {
|
||||
// The option is not selectable if there's a policy in place.
|
||||
if (vaultTimeoutPolicyAction != null) return@BitwardenTextRow
|
||||
shouldShowSelectionDialog = true
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
|
|
|
@ -5,19 +5,24 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import androidx.lifecycle.viewModelScope
|
||||
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.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -36,6 +41,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
private val vaultRepository: VaultRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val policyManager: PolicyManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<AccountSecurityState, AccountSecurityEvent, AccountSecurityAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
|
@ -47,6 +53,8 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
|
||||
vaultTimeout = settingsRepository.vaultTimeout,
|
||||
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
|
||||
vaultTimeoutPolicyMinutes = null,
|
||||
vaultTimeoutPolicyAction = null,
|
||||
),
|
||||
) {
|
||||
private val webSettingsUrl: String
|
||||
|
@ -63,6 +71,18 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
policyManager
|
||||
.getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
|
||||
.map { policies ->
|
||||
AccountSecurityAction.Internal.PolicyUpdateReceive(
|
||||
vaultTimeoutPolicies = policies.mapNotNull {
|
||||
it.policyInformation as? PolicyInformation.VaultTimeout
|
||||
},
|
||||
)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
AccountSecurityAction.Internal.FingerprintResultReceive(
|
||||
|
@ -268,6 +288,10 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
is AccountSecurityAction.Internal.FingerprintResultReceive -> {
|
||||
handleFingerprintResultReceived(action)
|
||||
}
|
||||
|
||||
is AccountSecurityAction.Internal.PolicyUpdateReceive -> {
|
||||
handlePolicyUpdateReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -308,6 +332,20 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePolicyUpdateReceive(
|
||||
action: AccountSecurityAction.Internal.PolicyUpdateReceive,
|
||||
) {
|
||||
// The vault timeout policy can only be implemented in organizations that have
|
||||
// the single organization policy, meaning that if this is enabled, the user is
|
||||
// only in one organization and hence there is only one result in the list.
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultTimeoutPolicyMinutes = action.vaultTimeoutPolicies?.firstOrNull()?.minutes,
|
||||
vaultTimeoutPolicyAction = action.vaultTimeoutPolicies?.firstOrNull()?.action,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -322,6 +360,8 @@ data class AccountSecurityState(
|
|||
val isUnlockWithPinEnabled: Boolean,
|
||||
val vaultTimeout: VaultTimeout,
|
||||
val vaultTimeoutAction: VaultTimeoutAction,
|
||||
val vaultTimeoutPolicyMinutes: Int?,
|
||||
val vaultTimeoutPolicyAction: String?,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
|
@ -349,14 +389,6 @@ sealed class AccountSecurityDialog : Parcelable {
|
|||
) : AccountSecurityDialog()
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the Session timeout action.
|
||||
*/
|
||||
enum class SessionTimeoutAction(val text: Text) {
|
||||
LOCK(text = R.string.lock.asText()),
|
||||
LOG_OUT(text = R.string.log_out.asText()),
|
||||
}
|
||||
|
||||
/**
|
||||
* Models events for the account security screen.
|
||||
*/
|
||||
|
@ -569,5 +601,12 @@ sealed class AccountSecurityAction {
|
|||
data class FingerprintResultReceive(
|
||||
val fingerprintResult: UserFingerprintResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* A policy update has been received.
|
||||
*/
|
||||
data class PolicyUpdateReceive(
|
||||
val vaultTimeoutPolicies: List<PolicyInformation.VaultTimeout>?,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,3 +22,25 @@ val VaultTimeout.Type.displayLabel: Text
|
|||
VaultTimeout.Type.CUSTOM -> R.string.custom
|
||||
}
|
||||
.asText()
|
||||
|
||||
/**
|
||||
* The value in minutes for the given [VaultTimeout.Type], used as a comparison
|
||||
* against the maximum timeout allowed by the organization's policy.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
val VaultTimeout.Type.minutes: Int
|
||||
get() = when (this) {
|
||||
VaultTimeout.Type.IMMEDIATELY -> 0
|
||||
VaultTimeout.Type.ONE_MINUTE -> 1
|
||||
VaultTimeout.Type.FIVE_MINUTES -> 5
|
||||
VaultTimeout.Type.FIFTEEN_MINUTES -> 15
|
||||
VaultTimeout.Type.THIRTY_MINUTES -> 30
|
||||
VaultTimeout.Type.ONE_HOUR -> 60
|
||||
VaultTimeout.Type.FOUR_HOURS -> 240
|
||||
|
||||
VaultTimeout.Type.ON_APP_RESTART,
|
||||
VaultTimeout.Type.NEVER,
|
||||
-> Int.MAX_VALUE
|
||||
|
||||
VaultTimeout.Type.CUSTOM -> 0
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
|||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -46,21 +46,7 @@ class SyncResponseJsonExtensionsTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun `policyInformation converts the Json data to policy information`() {
|
||||
val masterPasswordData = buildJsonObject {
|
||||
put(key = "minLength", value = 10)
|
||||
put(key = "minComplexity", value = 3)
|
||||
put(key = "requireUpper", value = null)
|
||||
put(key = "requireLower", value = null)
|
||||
put(key = "requireNumbers", value = true)
|
||||
put(key = "requireSpecial", value = null)
|
||||
put(key = "enforceOnLogin", value = true)
|
||||
}
|
||||
val masterPasswordPolicy = createMockPolicy(
|
||||
type = PolicyTypeJson.MASTER_PASSWORD,
|
||||
data = masterPasswordData,
|
||||
)
|
||||
fun `policyInformation converts the MasterPassword Json data to policy information`() {
|
||||
val policyInformation = PolicyInformation.MasterPassword(
|
||||
minLength = 10,
|
||||
minComplexity = 3,
|
||||
|
@ -70,10 +56,57 @@ class SyncResponseJsonExtensionsTest {
|
|||
requireSpecial = null,
|
||||
enforceOnLogin = true,
|
||||
)
|
||||
val policy = createMockPolicy(
|
||||
type = PolicyTypeJson.MASTER_PASSWORD,
|
||||
data = Json.encodeToJsonElement(policyInformation).jsonObject,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
policyInformation,
|
||||
masterPasswordPolicy.policyInformation,
|
||||
policy.policyInformation,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `policyInformation converts the PasswordGenerator Json data to policy information`() {
|
||||
val policyInformation = PolicyInformation.PasswordGenerator(
|
||||
defaultType = "password",
|
||||
minLength = null,
|
||||
useUpper = true,
|
||||
useLower = true,
|
||||
useNumbers = null,
|
||||
useSpecial = null,
|
||||
minNumbers = null,
|
||||
minSpecial = null,
|
||||
minNumberWords = 4,
|
||||
capitalize = true,
|
||||
includeNumber = null,
|
||||
)
|
||||
val policy = createMockPolicy(
|
||||
type = PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
data = Json.encodeToJsonElement(policyInformation).jsonObject,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
policyInformation,
|
||||
policy.policyInformation,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `policyInformation converts the VaultTimeout Json data to policy information`() {
|
||||
val policyInformation = PolicyInformation.VaultTimeout(
|
||||
minutes = 10,
|
||||
action = "lock",
|
||||
)
|
||||
val policy = createMockPolicy(
|
||||
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
|
||||
data = Json.encodeToJsonElement(policyInformation).jsonObject,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
policyInformation,
|
||||
policy.policyInformation,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,13 +11,17 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
|
|||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
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.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
|
@ -46,6 +50,12 @@ class SettingsRepositoryTest {
|
|||
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
|
||||
private val vaultSdkSource: VaultSdkSource = mockk()
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk()
|
||||
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
|
||||
private val policyManager: PolicyManager = mockk {
|
||||
every {
|
||||
getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
|
||||
} returns mutableActivePolicyFlow
|
||||
}
|
||||
|
||||
private val settingsRepository = SettingsRepositoryImpl(
|
||||
autofillManager = autofillManager,
|
||||
|
@ -55,6 +65,7 @@ class SettingsRepositoryTest {
|
|||
vaultSdkSource = vaultSdkSource,
|
||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
policyManager = policyManager,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.compose.ui.test.hasAnyAncestor
|
|||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.hasTextExactly
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.isDisplayed
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
|
@ -634,6 +635,35 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithText("Unlock with PIN code").assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `session timeout policy warning should update according to state`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultTimeoutPolicyMinutes = 100,
|
||||
)
|
||||
}
|
||||
val timeOnlyText = "Your organization policies have set your maximum allowed " +
|
||||
"vault timeout to 1 hour(s) and 40 minute(s)."
|
||||
composeTestRule
|
||||
.onNodeWithText(timeOnlyText)
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultTimeoutPolicyMinutes = 100,
|
||||
vaultTimeoutPolicyAction = "lock",
|
||||
)
|
||||
}
|
||||
val bothText = "Your organization policies are affecting your vault timeout. " +
|
||||
"Maximum allowed vault timeout is 1 hour(s) and 40 minute(s). Your vault " +
|
||||
"timeout action is set to Lock."
|
||||
composeTestRule
|
||||
.onNodeWithText(bothText)
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `session timeout should be updated on or off according to state`() {
|
||||
composeTestRule
|
||||
|
@ -708,6 +738,66 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on session timeout click should update according to state`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultTimeoutPolicyMinutes = 100,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Immediately")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("1 minute")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("5 minutes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("15 minutes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("30 minutes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("1 hour")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("4 hours")
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onNodeWithText("On app restart")
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onNodeWithText("Never")
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Custom")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on session timeout selection dialog cancel click should close the dialog`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
@ -960,6 +1050,47 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `custom session timeout dialog Ok click should dismiss the dialog and show an error if value exceeds policy limit`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 123),
|
||||
vaultTimeoutPolicyMinutes = 100,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasTextExactly("Custom", "02:03"))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Your vault timeout exceeds the restrictions set by your organization.")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.isDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.CustomVaultTimeoutSelect(
|
||||
VaultTimeout.Custom(vaultTimeoutInMinutes = 100),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on session timeout action click should show a selection dialog`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
@ -1354,6 +1485,8 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
isUnlockWithPinEnabled = false,
|
||||
vaultTimeout = VaultTimeout.ThirtyMinutes,
|
||||
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
||||
vaultTimeoutPolicyMinutes = null,
|
||||
vaultTimeoutPolicyAction = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import app.cash.turbine.test
|
||||
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.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
|
@ -12,6 +14,10 @@ import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
|||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
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.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -23,6 +29,9 @@ import io.mockk.mockk
|
|||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
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.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
|
@ -41,6 +50,12 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
|
||||
private val policyManager: PolicyManager = mockk {
|
||||
every {
|
||||
getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
|
||||
} returns mutableActivePolicyFlow
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when saved state is set`() {
|
||||
|
@ -60,6 +75,35 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
coVerify { settingsRepository.getUserFingerprint() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state updates when policies change`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
val policyInformation = PolicyInformation.VaultTimeout(
|
||||
minutes = 10,
|
||||
action = "lock",
|
||||
)
|
||||
mutableActivePolicyFlow.emit(
|
||||
listOf(
|
||||
createMockPolicy(
|
||||
isEnabled = true,
|
||||
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
|
||||
data = Json.encodeToJsonElement(policyInformation).jsonObject,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
vaultTimeoutPolicyMinutes = 10,
|
||||
vaultTimeoutPolicyAction = "lock",
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on FingerprintResultReceive should update the fingerprint phrase`() = runTest {
|
||||
val fingerprint = "fingerprint"
|
||||
|
@ -488,17 +532,20 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun createViewModel(
|
||||
initialState: AccountSecurityState? = DEFAULT_STATE,
|
||||
authRepository: AuthRepository = this.authRepository,
|
||||
vaultRepository: VaultRepository = this.vaultRepository,
|
||||
environmentRepository: EnvironmentRepository = this.fakeEnvironmentRepository,
|
||||
settingsRepository: SettingsRepository = this.settingsRepository,
|
||||
policyManager: PolicyManager = this.policyManager,
|
||||
): AccountSecurityViewModel = AccountSecurityViewModel(
|
||||
authRepository = authRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
environmentRepository = environmentRepository,
|
||||
policyManager = policyManager,
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set("state", initialState)
|
||||
},
|
||||
|
@ -515,4 +562,6 @@ private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState(
|
|||
isUnlockWithPinEnabled = false,
|
||||
vaultTimeout = VaultTimeout.ThirtyMinutes,
|
||||
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
||||
vaultTimeoutPolicyMinutes = null,
|
||||
vaultTimeoutPolicyAction = null,
|
||||
)
|
||||
|
|
|
@ -28,4 +28,26 @@ class VaultTimeoutExtensionsTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `minutes should return the correct value for each type`() {
|
||||
mapOf(
|
||||
VaultTimeout.Type.IMMEDIATELY to 0,
|
||||
VaultTimeout.Type.ONE_MINUTE to 1,
|
||||
VaultTimeout.Type.FIVE_MINUTES to 5,
|
||||
VaultTimeout.Type.FIFTEEN_MINUTES to 15,
|
||||
VaultTimeout.Type.THIRTY_MINUTES to 30,
|
||||
VaultTimeout.Type.ONE_HOUR to 60,
|
||||
VaultTimeout.Type.FOUR_HOURS to 240,
|
||||
VaultTimeout.Type.ON_APP_RESTART to Int.MAX_VALUE,
|
||||
VaultTimeout.Type.NEVER to Int.MAX_VALUE,
|
||||
VaultTimeout.Type.CUSTOM to 0,
|
||||
)
|
||||
.forEach { (type, value) ->
|
||||
assertEquals(
|
||||
value,
|
||||
type.minutes,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue