mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
[PM-14222] Managed user account deletion prevention (#5114)
Co-authored-by: Matt Portune <mportune@macbook-work.lan>
This commit is contained in:
parent
186bea2d1d
commit
639ca02739
@ -20,4 +20,5 @@ data class Organization(
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val role: OrganizationType,
|
||||
val keyConnectorUrl: String?,
|
||||
val userIsClaimedByOrganization: Boolean,
|
||||
)
|
||||
|
||||
@ -23,6 +23,7 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
|
||||
role = this.type,
|
||||
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
|
||||
keyConnectorUrl = this.keyConnectorUrl,
|
||||
userIsClaimedByOrganization = this.userIsClaimedByOrganization,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@ -2,12 +2,14 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deletea
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@ -20,22 +22,27 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledErrorButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedErrorButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
@ -115,61 +122,52 @@ fun DeleteAccountScreen(
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = R.drawable.ic_warning),
|
||||
contentDescription = null,
|
||||
tint = BitwardenTheme.colorScheme.status.error,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.deleting_your_account_is_permanent),
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
color = BitwardenTheme.colorScheme.status.error,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.delete_account_explanation),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
DeleteAccountButton(
|
||||
onDeleteAccountConfirmDialogClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
DeleteAccountAction.DeleteAccountConfirmDialogClick(it),
|
||||
)
|
||||
}
|
||||
},
|
||||
onDeleteAccountClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick) }
|
||||
},
|
||||
isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled,
|
||||
modifier = Modifier
|
||||
.testTag("DELETE ACCOUNT")
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenOutlinedErrorButton(
|
||||
label = stringResource(id = R.string.cancel),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(DeleteAccountAction.CancelClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.testTag("CANCEL")
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
if (state.isUserManagedByOrganization) {
|
||||
WarningMessageCard(
|
||||
headerText = stringResource(id = R.string.cannot_delete_your_account),
|
||||
subtitleText = stringResource(
|
||||
id = R.string.cannot_delete_your_account_explanation,
|
||||
),
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
} else {
|
||||
WarningMessageCard(
|
||||
headerText = stringResource(id = R.string.deleting_your_account_is_permanent),
|
||||
subtitleText = stringResource(id = R.string.delete_account_explanation),
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
DeleteAccountButton(
|
||||
onDeleteAccountConfirmDialogClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
DeleteAccountAction.DeleteAccountConfirmDialogClick(it),
|
||||
)
|
||||
}
|
||||
},
|
||||
onDeleteAccountClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick) }
|
||||
},
|
||||
isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled,
|
||||
modifier = Modifier
|
||||
.testTag("DELETE ACCOUNT")
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenOutlinedErrorButton(
|
||||
label = stringResource(id = R.string.cancel),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(DeleteAccountAction.CancelClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.testTag("CANCEL")
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -204,3 +202,50 @@ private fun DeleteAccountButton(
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningMessageCard(
|
||||
headerText: String,
|
||||
subtitleText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.cardStyle(
|
||||
cardStyle = CardStyle.Full,
|
||||
paddingHorizontal = 12.dp,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = R.drawable.ic_warning),
|
||||
contentDescription = null,
|
||||
tint = BitwardenTheme.colorScheme.status.weak1,
|
||||
)
|
||||
Spacer(Modifier.width(width = 12.dp))
|
||||
Column(modifier = Modifier.weight(weight = 1f)) {
|
||||
Text(
|
||||
text = headerText,
|
||||
style = BitwardenTheme.typography.titleSmall,
|
||||
color = BitwardenTheme.colorScheme.status.weak1,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 4.dp))
|
||||
Text(
|
||||
text = subtitleText,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(width = 4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WarningMessageCard_preview() {
|
||||
WarningMessageCard(
|
||||
headerText = stringResource(id = R.string.cannot_delete_your_account),
|
||||
subtitleText = stringResource(id = R.string.cannot_delete_your_account_explanation),
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,13 +3,13 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deletea
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -32,12 +32,16 @@ class DeleteAccountViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<DeleteAccountState, DeleteAccountEvent, DeleteAccountAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: DeleteAccountState(
|
||||
dialog = null,
|
||||
isUnlockWithPasswordEnabled = requireNotNull(authRepository.userStateFlow.value)
|
||||
.activeAccount
|
||||
.hasMasterPassword,
|
||||
),
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val account = requireNotNull(authRepository.userStateFlow.value).activeAccount
|
||||
DeleteAccountState(
|
||||
dialog = null,
|
||||
isUnlockWithPasswordEnabled = account.hasMasterPassword,
|
||||
isUserManagedByOrganization = account
|
||||
.organizations
|
||||
.any { it.userIsClaimedByOrganization } == true,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
init {
|
||||
@ -160,11 +164,13 @@ class DeleteAccountViewModel @Inject constructor(
|
||||
* @param dialog The dialog for the [DeleteAccountScreen].
|
||||
* @param isUnlockWithPasswordEnabled Whether or not the user is able to unlock the vault with
|
||||
* their master password.
|
||||
* @param isUserManagedByOrganization Whether or not the user is managed by an organization.
|
||||
*/
|
||||
@Parcelize
|
||||
data class DeleteAccountState(
|
||||
val dialog: DeleteAccountDialog?,
|
||||
val isUnlockWithPasswordEnabled: Boolean,
|
||||
val isUserManagedByOrganization: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
|
||||
@ -1,20 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportHeight="28"
|
||||
android:viewportWidth="28">
|
||||
<group>
|
||||
<clip-path android:pathData="M0.667,0.667h26.667v26.667h-26.667z" />
|
||||
<path
|
||||
android:fillColor="#BA1A1A"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M16.036,3.434L26.957,20.361C28.069,22.083 26.646,24 24.921,24H3.079C1.354,24 -0.069,22.083 1.043,20.361L11.964,3.434C12.91,1.967 15.09,1.967 16.036,3.434ZM14.635,4.337C14.345,3.888 13.655,3.888 13.365,4.337L2.443,21.264C2.141,21.733 2.498,22.333 3.079,22.333H24.921C25.502,22.333 25.859,21.733 25.557,21.264L14.635,4.337Z" />
|
||||
<path
|
||||
android:fillColor="#BA1A1A"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M14.001,9.499C14.461,9.499 14.834,9.872 14.834,10.332V15.528C14.834,15.989 14.461,16.362 14.001,16.362C13.541,16.362 13.168,15.989 13.168,15.528V10.332C13.168,9.872 13.541,9.499 14.001,9.499Z" />
|
||||
<path
|
||||
android:fillColor="#BA1A1A"
|
||||
android:pathData="M15.126,19.785C15.126,20.406 14.622,20.91 14.001,20.91C13.38,20.91 12.876,20.406 12.876,19.785C12.876,19.163 13.38,18.66 14.001,18.66C14.622,18.66 15.126,19.163 15.126,19.785Z" />
|
||||
</group>
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13.25,16.587C13.25,17.284 12.69,17.848 12,17.848C11.31,17.848 10.75,17.284 10.75,16.587C10.75,15.891 11.31,15.327 12,15.327C12.69,15.327 13.25,15.891 13.25,16.587Z"
|
||||
android:fillColor="#CB263A"/>
|
||||
<path
|
||||
android:pathData="M11.003,9.023C10.703,9.023 10.47,9.286 10.507,9.584L11.008,13.627C11.039,13.878 11.252,14.066 11.504,14.066H12.496C12.748,14.066 12.961,13.878 12.992,13.627L13.493,9.584C13.53,9.286 13.297,9.023 12.997,9.023H11.003Z"
|
||||
android:fillColor="#CB263A"/>
|
||||
<path
|
||||
android:pathData="M13.717,4C12.954,2.667 11.046,2.667 10.283,4L2.269,18C1.505,19.333 2.459,21 3.986,21H20.014C21.541,21 22.495,19.333 21.731,18L13.717,4ZM20.43,18.745L12.415,4.745C12.228,4.418 11.772,4.418 11.585,4.745L3.57,18.745C3.369,19.098 3.634,19.5 3.986,19.5H20.014C20.366,19.5 20.631,19.098 20.43,18.745Z"
|
||||
android:fillColor="#CB263A"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
||||
@ -1263,4 +1263,6 @@ Do you want to switch to this account?</string>
|
||||
<string name="a_certificate_with_the_alias_x_already_exists_do_you_want_to_replace_it">A certificate with this alias \"%s\" already exists. Do you want to replace it?\nReplace the certificate may impact your connection to any environments currently using it.</string>
|
||||
<string name="replace_certificate">Replace certificate</string>
|
||||
<string name="unable_to_read_certificate">Unable to read certificate.</string>
|
||||
<string name="cannot_delete_your_account">Cannot delete your account</string>
|
||||
<string name="cannot_delete_your_account_explanation">This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details.</string>
|
||||
</resources>
|
||||
|
||||
@ -4679,6 +4679,7 @@ class AuthRepositoryTest {
|
||||
every { shouldUseKeyConnector } returns true
|
||||
every { type } returns OrganizationType.USER
|
||||
every { keyConnectorUrl } returns null
|
||||
every { userIsClaimedByOrganization } returns false
|
||||
},
|
||||
)
|
||||
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)
|
||||
@ -4708,6 +4709,7 @@ class AuthRepositoryTest {
|
||||
every { shouldUseKeyConnector } returns true
|
||||
every { type } returns OrganizationType.USER
|
||||
every { keyConnectorUrl } returns url
|
||||
every { userIsClaimedByOrganization } returns false
|
||||
},
|
||||
)
|
||||
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)
|
||||
@ -4745,6 +4747,7 @@ class AuthRepositoryTest {
|
||||
every { shouldUseKeyConnector } returns true
|
||||
every { type } returns OrganizationType.USER
|
||||
every { keyConnectorUrl } returns url
|
||||
every { userIsClaimedByOrganization } returns false
|
||||
},
|
||||
)
|
||||
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)
|
||||
@ -4785,6 +4788,7 @@ class AuthRepositoryTest {
|
||||
every { shouldUseKeyConnector } returns true
|
||||
every { type } returns OrganizationType.USER
|
||||
every { keyConnectorUrl } returns url
|
||||
every { userIsClaimedByOrganization } returns false
|
||||
},
|
||||
)
|
||||
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)
|
||||
@ -4824,6 +4828,7 @@ class AuthRepositoryTest {
|
||||
every { shouldUseKeyConnector } returns true
|
||||
every { type } returns OrganizationType.USER
|
||||
every { keyConnectorUrl } returns url
|
||||
every { userIsClaimedByOrganization } returns false
|
||||
},
|
||||
)
|
||||
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)
|
||||
@ -6887,6 +6892,42 @@ class AuthRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isUserManagedByOrganization should return true if any org userIsClaimedByOrganization is true`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.storeUserKey(userId = USER_ID_1, userKey = ENCRYPTED_USER_KEY)
|
||||
val organizations = listOf(
|
||||
createMockOrganization(number = 0)
|
||||
.copy(
|
||||
userIsClaimedByOrganization = true,
|
||||
),
|
||||
createMockOrganization(number = 1),
|
||||
)
|
||||
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)
|
||||
assertEquals(
|
||||
SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = listOf(
|
||||
UserOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = organizations.toOrganizations(),
|
||||
),
|
||||
),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
onboardingStatus = null,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UNIQUE_APP_ID = "testUniqueAppId"
|
||||
private const val NAME = "Example Name"
|
||||
|
||||
@ -195,6 +195,7 @@ class AuthDiskSourceExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-1",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -208,6 +209,7 @@ class AuthDiskSourceExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-2",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -221,6 +223,7 @@ class AuthDiskSourceExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-3",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -369,6 +372,7 @@ class AuthDiskSourceExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-1",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -401,6 +405,7 @@ class AuthDiskSourceExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-1",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -414,6 +419,7 @@ class AuthDiskSourceExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-2",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -24,6 +24,7 @@ class SyncResponseJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-1",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
createMockOrganization(number = 1).toOrganization(),
|
||||
)
|
||||
@ -40,6 +41,7 @@ class SyncResponseJsonExtensionsTest {
|
||||
shouldUseKeyConnector = true,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-1",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "mockId-2",
|
||||
@ -48,6 +50,7 @@ class SyncResponseJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.USER,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-2",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
listOf(
|
||||
|
||||
@ -367,6 +367,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = false,
|
||||
@ -432,6 +433,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -477,6 +479,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = true,
|
||||
@ -538,6 +541,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -584,6 +588,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = false,
|
||||
@ -653,6 +658,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -699,6 +705,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = false,
|
||||
@ -768,6 +775,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -814,6 +822,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = false,
|
||||
@ -883,6 +892,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -930,6 +940,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.USER,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = false,
|
||||
@ -1002,6 +1013,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.USER,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1207,6 +1219,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.USER,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = false,
|
||||
@ -1278,6 +1291,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.USER,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1324,6 +1338,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = false,
|
||||
@ -1395,6 +1410,7 @@ class UserStateJsonExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -295,6 +295,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
|
||||
shouldUseKeyConnector = true,
|
||||
role = OrganizationType.USER,
|
||||
keyConnectorUrl = KEY_CONNECTOR_URL,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
needsMasterPassword = false,
|
||||
|
||||
@ -387,6 +387,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||
shouldUseKeyConnector = true,
|
||||
role = OrganizationType.USER,
|
||||
keyConnectorUrl = "bitwarden.com",
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
needsMasterPassword = false,
|
||||
|
||||
@ -998,6 +998,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
shouldManageResetPassword = false,
|
||||
role = OrganizationType.USER,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationAdmin",
|
||||
@ -1006,6 +1007,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
shouldManageResetPassword = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationOwner",
|
||||
@ -1014,6 +1016,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
shouldManageResetPassword = false,
|
||||
role = OrganizationType.OWNER,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationCustom",
|
||||
@ -1022,6 +1025,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
shouldManageResetPassword = false,
|
||||
role = OrganizationType.CUSTOM,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
needsMasterPassword = false,
|
||||
|
||||
@ -214,9 +214,48 @@ class DeleteAccountScreenTest : BaseComposeTest() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `if isUserManagedByOrganization should display cannot delete message and hide delete button`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Cannot delete your account")
|
||||
.assertDoesNotExist()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("This action cannot be completed because your account " +
|
||||
"is owned by an organization. " +
|
||||
"Contact your organization administrator for additional details.")
|
||||
.assertDoesNotExist()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Delete account")
|
||||
.filterToOne(hasClickAction())
|
||||
.assertExists()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(isUserManagedByOrganization = true)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Cannot delete your account")
|
||||
.assertExists()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("This action cannot be completed because your account " +
|
||||
"is owned by an organization. " +
|
||||
"Contact your organization administrator for additional details.")
|
||||
.assertExists()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Delete account")
|
||||
.filterToOne(hasClickAction())
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE: DeleteAccountState = DeleteAccountState(
|
||||
dialog = null,
|
||||
isUnlockWithPasswordEnabled = true,
|
||||
isUserManagedByOrganization = false,
|
||||
)
|
||||
|
||||
@ -255,4 +255,5 @@ private val DEFAULT_USER_STATE: UserState = UserState(
|
||||
private val DEFAULT_STATE: DeleteAccountState = DeleteAccountState(
|
||||
dialog = null,
|
||||
isUnlockWithPasswordEnabled = true,
|
||||
isUserManagedByOrganization = false,
|
||||
)
|
||||
|
||||
@ -4619,6 +4619,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = true,
|
||||
|
||||
@ -565,6 +565,7 @@ class CipherViewExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = true,
|
||||
|
||||
@ -3350,6 +3350,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.OWNER,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -499,6 +499,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "mockOrganizationId-2",
|
||||
@ -507,6 +508,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "mockOrganizationId-3",
|
||||
@ -515,6 +517,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
||||
@ -109,6 +109,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "mockOrganizationId-2",
|
||||
@ -117,6 +118,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "mockOrganizationId-3",
|
||||
@ -125,6 +127,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
||||
@ -259,6 +259,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -346,6 +347,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -634,6 +636,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -83,6 +83,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -111,6 +112,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -143,6 +145,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -175,6 +178,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -222,6 +226,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -267,6 +272,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -316,6 +322,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -395,6 +402,7 @@ class UserStateExtensionsTest {
|
||||
shouldManageResetPassword = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationId-A",
|
||||
@ -403,6 +411,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
@ -455,6 +464,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationId-A",
|
||||
@ -463,6 +473,7 @@ class UserStateExtensionsTest {
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
||||
@ -344,6 +344,9 @@ data class SyncResponseJson(
|
||||
|
||||
@SerialName("status")
|
||||
val status: OrganizationStatusType,
|
||||
|
||||
@SerialName("userIsClaimedByOrganization")
|
||||
val userIsClaimedByOrganization: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@ -101,7 +101,8 @@ private const val SYNC_SUCCESS_JSON = """
|
||||
"name": "mockName-1",
|
||||
"useApi": false,
|
||||
"familySponsorshipValidUntil": "2023-10-27T12:00:00.00Z",
|
||||
"status": 1
|
||||
"status": 1,
|
||||
"userIsClaimedByOrganization": false
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
@ -176,7 +177,8 @@ private const val SYNC_SUCCESS_JSON = """
|
||||
"name": "mockName-1",
|
||||
"useApi": false,
|
||||
"familySponsorshipValidUntil": "2023-10-27T12:00:00.00Z",
|
||||
"status": 1
|
||||
"status": 1,
|
||||
"userIsClaimedByOrganization": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -69,6 +69,7 @@ fun createMockOrganization(
|
||||
shouldUseApi = false,
|
||||
familySponsorshipValidUntil = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
|
||||
status = OrganizationStatusType.ACCEPTED,
|
||||
userIsClaimedByOrganization = false,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user