Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class RestrictionManagerImpl(
// If the baseEnvironmentUrl matches the predefined EU environment, assume it is the
// default EU environment.
Environment.Eu.environmentUrlData.base -> Environment.Eu
// If the baseEnvironmentUrl matches the predefined FedRAMP environment, assume it is
// the default FedRAMP environment.
Environment.FedRamp.environmentUrlData.base -> Environment.FedRamp
// Otherwise make a custom self-host environment.
else -> Environment.SelfHosted(EnvironmentUrlDataJson(baseEnvironmentUrl))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ private fun EnvironmentUrlDataJson.authTabData(
)
}

EnvironmentRegion.FED_RAMP -> {
AuthTabData.HttpsScheme(
host = "bitwarden-gov.com",
path = "$kind-callback",
)
}

EnvironmentRegion.SELF_HOSTED -> {
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://$kind-callback",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class EnvironmentViewModel @Inject constructor(
val environmentUrlData = when (val environment = environmentRepository.environment) {
Environment.Us,
Environment.Eu,
Environment.FedRamp,
-> EnvironmentUrlDataJson(base = "")

is Environment.SelfHosted -> environment.environmentUrlData
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.auth.feature.landing

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -274,28 +275,35 @@ private fun LandingScreenContent(

Spacer(modifier = Modifier.height(height = 24.dp))

Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.wrapContentHeight(),
AnimatedVisibility(
visible = state.allowCreateAccount,
label = "CreateAccountAnimatedVisibility",
) {
Text(
text = stringResource(id = BitwardenString.new_to_bitwarden),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
)
Column {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.wrapContentHeight(),
) {
Text(
text = stringResource(id = BitwardenString.new_to_bitwarden),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
)

BitwardenTextButton(
label = stringResource(id = BitwardenString.create_an_account),
onClick = onCreateAccountClick,
modifier = Modifier
.testTag("CreateAccountLabel"),
)
BitwardenTextButton(
label = stringResource(id = BitwardenString.create_an_account),
onClick = onCreateAccountClick,
modifier = Modifier
.testTag("CreateAccountLabel"),
)
}
Spacer(modifier = Modifier.height(height = 8.dp))
}
}
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextButton(
label = stringResource(id = BitwardenString.app_settings),
onClick = onAppSettingsClick,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.landing
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel
Expand All @@ -16,6 +17,7 @@ import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
Expand Down Expand Up @@ -43,6 +45,7 @@ class LandingViewModel @Inject constructor(
private val environmentRepository: EnvironmentRepository,
snackbarRelayManager: SnackbarRelayManager<SnackbarRelay>,
savedStateHandle: SavedStateHandle,
featureFlagManager: FeatureFlagManager,
) : BaseViewModel<LandingState, LandingEvent, LandingAction>(
initialState = savedStateHandle[KEY_STATE]
?: LandingState(
Expand All @@ -53,12 +56,13 @@ class LandingViewModel @Inject constructor(
selectedEnvironmentLabel = environmentRepository.environment.label,
dialog = null,
accountSummaries = authRepository.userStateFlow.value?.toAccountSummaries().orEmpty(),
isFedRampEnabled = featureFlagManager.getFeatureFlag(FlagKey.FedRamp),
),
) {

/**
* Returns the [AccountSummary] from the current state that matches the current email input and
* the the current environment, or `null` if there is no match.
* the current environment, or `null` if there is no match.
*/
private val matchingAccountSummary: AccountSummary?
get() {
Expand Down Expand Up @@ -106,6 +110,12 @@ class LandingViewModel @Inject constructor(
.map { LandingAction.Internal.SnackbarDataReceived(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)

featureFlagManager
.getFeatureFlagFlow(FlagKey.FedRamp)
.map { LandingAction.Internal.FedRampFeatureUpdated(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}

override fun handleAction(action: LandingAction) {
Expand Down Expand Up @@ -136,6 +146,7 @@ class LandingViewModel @Inject constructor(
}

is LandingAction.Internal.SnackbarDataReceived -> handleSnackbarDataReceived(action)
is LandingAction.Internal.FedRampFeatureUpdated -> handleFedRampFeatureUpdated(action)
}
}

Expand Down Expand Up @@ -231,6 +242,7 @@ class LandingViewModel @Inject constructor(
val environment = when (action.environmentType) {
Environment.Type.US -> Environment.Us
Environment.Type.EU -> Environment.Eu
Environment.Type.FED_RAMP -> Environment.FedRamp
Environment.Type.SELF_HOSTED -> {
// Launch the self-hosted screen and select the full environment details there.
sendEvent(LandingEvent.NavigateToEnvironment)
Expand Down Expand Up @@ -258,6 +270,10 @@ class LandingViewModel @Inject constructor(
sendEvent(LandingEvent.ShowSnackbar(action.data))
}

private fun handleFedRampFeatureUpdated(action: LandingAction.Internal.FedRampFeatureUpdated) {
mutableStateFlow.update { it.copy(isFedRampEnabled = action.isEnabled) }
}

/**
* If the user state account is changed to an active but not "logged in" account we can
* pre-populate the email field with this account.
Expand All @@ -284,12 +300,22 @@ data class LandingState(
val selectedEnvironmentLabel: String,
val dialog: DialogState?,
val accountSummaries: List<AccountSummary>,
val isFedRampEnabled: Boolean,
) : Parcelable {
/**
* The selectable environments.
*/
val environmentTypeOptions: ImmutableList<Environment.Type>
get() = Environment.Type.entries.toImmutableList()
get() = Environment.Type
.entries
.filterNot { it == Environment.Type.FED_RAMP && !isFedRampEnabled }
.toImmutableList()

/**
* Determines if the user should be allowed to create a new account.
*/
val allowCreateAccount: Boolean
get() = selectedEnvironmentType != Environment.Type.FED_RAMP

/**
* Determines whether the app bar should be visible based on the presence of account summaries.
Expand Down Expand Up @@ -453,5 +479,12 @@ sealed class LandingAction {
* Internal action to update the email input state from a non-user action
*/
data class UpdateEmailState(val emailInput: String) : Internal()

/**
* Indicates that FedRamp feature has been enabled or disabled.
*/
data class FedRampFeatureUpdated(
val isEnabled: Boolean,
) : Internal()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ class StartRegistrationViewModel @Inject constructor(
sendEvent(StartRegistrationEvent.NavigateToEnvironment)
return
}

Environment.Type.FED_RAMP -> {
// New account creation is not supported with FedRAMP.
return
}
}

// Update the environment in the repo; the VM state will update accordingly because it is
Expand Down Expand Up @@ -296,7 +301,10 @@ data class StartRegistrationState(
* The selectable environments.
*/
val environmentTypeOptions: ImmutableList<Environment.Type>
get() = Environment.Type.entries.toImmutableList()
get() = Environment.Type
.entries
.filterNot { it == Environment.Type.FED_RAMP }
.toImmutableList()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ val Environment.Type.displayLabel: Text
Environment.Type.US -> Environment.Us.label.asText()
Environment.Type.EU -> Environment.Eu.label.asText()
Environment.Type.SELF_HOSTED -> BitwardenString.self_hosted.asText()
Environment.Type.FED_RAMP -> Environment.FedRamp.label.asText()
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,21 @@ class RestrictionManagerTest {
)
}

@Suppress("MaxLineLength")
@Test
fun `initialize with baseEnvironmentUrl bundle data matching the current FedRAMP environment should set the environment to FedRAMP`() {
every {
restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to "https://vault.bitwarden-gov.com")

restrictionManager.initialize()

verify(exactly = 1) {
restrictionsManager.applicationRestrictions
}
assertEquals(Environment.FedRamp, fakeEnvironmentRepository.environment)
}

@Suppress("MaxLineLength")
@Test
fun `initialize with baseEnvironmentUrl bundle data matching the current self-hosted environment should set the environment to self-hosted`() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ class EnvironmentUrlDataJsonExtensionsTest {
),
EnvironmentUrlDataJson.DEFAULT_EU.duoAuthTabData,
)
assertEquals(
AuthTabData.HttpsScheme(
host = "bitwarden-gov.com",
path = "duo-callback",
),
EnvironmentUrlDataJson.DEFAULT_FED_RAMP.duoAuthTabData,
)
assertEquals(
AuthTabData.HttpsScheme(
host = "bitwarden.pw",
Expand Down Expand Up @@ -55,6 +62,13 @@ class EnvironmentUrlDataJsonExtensionsTest {
),
EnvironmentUrlDataJson.DEFAULT_EU.webAuthnAuthTabData,
)
assertEquals(
AuthTabData.HttpsScheme(
host = "bitwarden-gov.com",
path = "webauthn-callback",
),
EnvironmentUrlDataJson.DEFAULT_FED_RAMP.webAuthnAuthTabData,
)
assertEquals(
AuthTabData.HttpsScheme(
host = "bitwarden.pw",
Expand Down Expand Up @@ -87,6 +101,13 @@ class EnvironmentUrlDataJsonExtensionsTest {
),
EnvironmentUrlDataJson.DEFAULT_EU.ssoAuthTabData,
)
assertEquals(
AuthTabData.HttpsScheme(
host = "bitwarden-gov.com",
path = "sso-callback",
),
EnvironmentUrlDataJson.DEFAULT_FED_RAMP.ssoAuthTabData,
)
assertEquals(
AuthTabData.HttpsScheme(
host = "bitwarden.pw",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,23 @@ class LandingScreenTest : BitwardenComposeTest() {
assertTrue(onNavigateToPreAuthSettingsCalled)
}

@Test
fun `allowCreateAccount state should show and hide CreateAccount UI`() {
composeTestRule
.onNodeWithText(text = "Create an account")
.performScrollTo()
.assertIsDisplayed()

mutableStateFlow.update { it.copy(selectedEnvironmentType = Environment.Type.FED_RAMP) }
composeTestRule.onNodeWithText(text = "Create an account").assertDoesNotExist()

mutableStateFlow.update { it.copy(selectedEnvironmentType = Environment.Type.EU) }
composeTestRule
.onNodeWithText(text = "Create an account")
.performScrollTo()
.assertIsDisplayed()
}

@Test
fun `selecting environment should send EnvironmentOptionSelect action`() {
val selectedEnvironment = Environment.Eu
Expand Down Expand Up @@ -506,4 +523,5 @@ private val DEFAULT_STATE = LandingState(
selectedEnvironmentLabel = Environment.Us.label,
dialog = null,
accountSummaries = emptyList(),
isFedRampEnabled = true,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.auth.feature.landing

import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.ui.platform.base.BaseViewModelTest
Expand All @@ -15,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
Expand Down Expand Up @@ -47,6 +49,13 @@ class LandingViewModelTest : BaseViewModelTest() {
getSnackbarDataFlow(SnackbarRelay.ENVIRONMENT_SAVED)
} returns mutableSnackbarSharedFlow
}
private val mutableFedRampFlagStateFlow = MutableStateFlow(true)
private val featureFlagManager: FeatureFlagManager = mockk {
every { getFeatureFlagFlow(key = FlagKey.FedRamp) } returns mutableFedRampFlagStateFlow
every {
getFeatureFlag(key = FlagKey.FedRamp)
} answers { mutableFedRampFlagStateFlow.value }
}

@Test
fun `initial state should be correct when there is no remembered email`() = runTest {
Expand Down Expand Up @@ -124,6 +133,19 @@ class LandingViewModelTest : BaseViewModelTest() {
}
}

@Test
fun `FedRAMP flag changes should update state`() = runTest {
val viewModel = createViewModel()

viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
mutableFedRampFlagStateFlow.value = false
assertEquals(DEFAULT_STATE.copy(isFedRampEnabled = false), awaitItem())
mutableFedRampFlagStateFlow.value = true
assertEquals(DEFAULT_STATE.copy(isFedRampEnabled = true), awaitItem())
}
}

@Test
fun `LockAccountClick should call lockVault for the given account`() {
val accountUserId = "userId"
Expand Down Expand Up @@ -623,6 +645,7 @@ class LandingViewModelTest : BaseViewModelTest() {
vaultRepository = vaultRepository,
environmentRepository = fakeEnvironmentRepository,
snackbarRelayManager = snackbarRelayManager,
featureFlagManager = featureFlagManager,
savedStateHandle = savedStateHandle,
)

Expand All @@ -637,4 +660,5 @@ private val DEFAULT_STATE = LandingState(
selectedEnvironmentLabel = Environment.Us.label,
dialog = null,
accountSummaries = emptyList(),
isFedRampEnabled = true,
)
Loading
Loading