diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/restriction/RestrictionManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/restriction/RestrictionManagerImpl.kt index 4631523ef38..b72c4d1974d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/restriction/RestrictionManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/restriction/RestrictionManagerImpl.kt @@ -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)) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensions.kt index a27c9daf825..79c0f0f45fc 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensions.kt @@ -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", diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentViewModel.kt index 49ef9b980cc..4436c32e236 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentViewModel.kt @@ -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 diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt index a2a418abf7b..ca1a29a4959 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt @@ -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 @@ -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, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModel.kt index 71b9628bf70..a3510be7ff9 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModel.kt @@ -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 @@ -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 @@ -43,6 +45,7 @@ class LandingViewModel @Inject constructor( private val environmentRepository: EnvironmentRepository, snackbarRelayManager: SnackbarRelayManager, savedStateHandle: SavedStateHandle, + featureFlagManager: FeatureFlagManager, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: LandingState( @@ -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() { @@ -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) { @@ -136,6 +146,7 @@ class LandingViewModel @Inject constructor( } is LandingAction.Internal.SnackbarDataReceived -> handleSnackbarDataReceived(action) + is LandingAction.Internal.FedRampFeatureUpdated -> handleFedRampFeatureUpdated(action) } } @@ -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) @@ -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. @@ -284,12 +300,22 @@ data class LandingState( val selectedEnvironmentLabel: String, val dialog: DialogState?, val accountSummaries: List, + val isFedRampEnabled: Boolean, ) : Parcelable { /** * The selectable environments. */ val environmentTypeOptions: ImmutableList - 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. @@ -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() } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationViewModel.kt index 6815721bd7e..00763f1e847 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationViewModel.kt @@ -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 @@ -296,7 +301,10 @@ data class StartRegistrationState( * The selectable environments. */ val environmentTypeOptions: ImmutableList - get() = Environment.Type.entries.toImmutableList() + get() = Environment.Type + .entries + .filterNot { it == Environment.Type.FED_RAMP } + .toImmutableList() } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/util/EnvironmentExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/util/EnvironmentExtensions.kt index 87a78fcfe0b..9ba8e882cb5 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/util/EnvironmentExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/util/EnvironmentExtensions.kt @@ -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() } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/restriction/RestrictionManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/restriction/RestrictionManagerTest.kt index ec24401a6a5..7ebe7f5216d 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/restriction/RestrictionManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/restriction/RestrictionManagerTest.kt @@ -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`() { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensionsTest.kt index 4a146d2b636..6eb954d94d8 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensionsTest.kt @@ -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", @@ -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", @@ -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", diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt index ccc0ba6c13a..6772619d74f 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt @@ -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 @@ -506,4 +523,5 @@ private val DEFAULT_STATE = LandingState( selectedEnvironmentLabel = Environment.Us.label, dialog = null, accountSummaries = emptyList(), + isFedRampEnabled = true, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt index 40b8395c91b..0aaf99cf72d 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt @@ -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 @@ -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 @@ -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 { @@ -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" @@ -623,6 +645,7 @@ class LandingViewModelTest : BaseViewModelTest() { vaultRepository = vaultRepository, environmentRepository = fakeEnvironmentRepository, snackbarRelayManager = snackbarRelayManager, + featureFlagManager = featureFlagManager, savedStateHandle = savedStateHandle, ) @@ -637,4 +660,5 @@ private val DEFAULT_STATE = LandingState( selectedEnvironmentLabel = Environment.Us.label, dialog = null, accountSummaries = emptyList(), + isFedRampEnabled = true, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationViewModelTest.kt index 06355ddabd5..7c4f89173cb 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationViewModelTest.kt @@ -251,22 +251,34 @@ class StartRegistrationViewModelTest : BaseViewModelTest() { } @Test - fun `EnvironmentTypeSelect should update value of selected region for US or EU`() = runTest { - val inputEnvironmentType = Environment.Type.EU - val viewModel = createViewModel() - viewModel.stateFlow.test { - awaitItem() - viewModel.trySendAction( - EnvironmentTypeSelect( - inputEnvironmentType, - ), - ) - assertEquals( - DEFAULT_STATE.copy(selectedEnvironmentType = Environment.Type.EU), - awaitItem(), - ) + fun `EnvironmentTypeSelect should update value of selected region for UE, US, and FedRAMP`() = + runTest { + val viewModel = createViewModel() + viewModel.stateFlow.test { + skipItems(count = 1) + + viewModel.trySendAction( + EnvironmentTypeSelect(environmentType = Environment.Type.EU), + ) + assertEquals( + DEFAULT_STATE.copy(selectedEnvironmentType = Environment.Type.EU), + awaitItem(), + ) + + viewModel.trySendAction( + EnvironmentTypeSelect(environmentType = Environment.Type.US), + ) + assertEquals( + DEFAULT_STATE.copy(selectedEnvironmentType = Environment.Type.US), + awaitItem(), + ) + + viewModel.trySendAction( + EnvironmentTypeSelect(environmentType = Environment.Type.FED_RAMP), + ) + expectNoEvents() + } } - } @Test fun `EnvironmentTypeSelect should emit NavigateToEnvironment for self-hosted`() = runTest { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/util/EnvironmentExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/util/EnvironmentExtensionsTest.kt index 9633ddad38d..cc434e5c00f 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/util/EnvironmentExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/util/EnvironmentExtensionsTest.kt @@ -23,6 +23,14 @@ class EnvironmentExtensionsTest { ) } + @Test + fun `displayLabel for FED_RAMP type should return the correct value`() { + assertEquals( + "bitwarden-gov.com".asText(), + Environment.Type.FED_RAMP.displayLabel, + ) + } + @Test fun `displayLabel for SELF_HOSTED type should return the correct value`() { assertEquals( diff --git a/data/src/main/kotlin/com/bitwarden/data/datasource/disk/model/EnvironmentUrlDataJson.kt b/data/src/main/kotlin/com/bitwarden/data/datasource/disk/model/EnvironmentUrlDataJson.kt index 9ee4f12a235..6688f9e3543 100644 --- a/data/src/main/kotlin/com/bitwarden/data/datasource/disk/model/EnvironmentUrlDataJson.kt +++ b/data/src/main/kotlin/com/bitwarden/data/datasource/disk/model/EnvironmentUrlDataJson.kt @@ -49,6 +49,7 @@ data class EnvironmentUrlDataJson( get() = when (base) { DEFAULT_US.base -> EnvironmentRegion.UNITED_STATES DEFAULT_EU.base -> EnvironmentRegion.EUROPEAN_UNION + DEFAULT_FED_RAMP.base -> EnvironmentRegion.FED_RAMP else -> { if (base.contains(BITWARDEN_INTERNAL_DOMAIN)) { EnvironmentRegion.INTERNAL @@ -106,5 +107,11 @@ data class EnvironmentUrlDataJson( webVault = "https://vault.bitwarden.eu", events = "https://events.bitwarden.eu", ) + + /** + * Default [EnvironmentUrlDataJson] for the FedRAMP environment. + */ + val DEFAULT_FED_RAMP: EnvironmentUrlDataJson = + EnvironmentUrlDataJson(base = "https://vault.bitwarden-gov.com") } } diff --git a/data/src/main/kotlin/com/bitwarden/data/repository/model/Environment.kt b/data/src/main/kotlin/com/bitwarden/data/repository/model/Environment.kt index ef7bbf435b1..0eea51e7389 100644 --- a/data/src/main/kotlin/com/bitwarden/data/repository/model/Environment.kt +++ b/data/src/main/kotlin/com/bitwarden/data/repository/model/Environment.kt @@ -45,6 +45,17 @@ sealed class Environment { get() = "bitwarden.eu" } + /** + * The default FedRAMP environment. + */ + data object FedRamp : Environment() { + override val type: Type get() = Type.FED_RAMP + override val environmentUrlData: EnvironmentUrlDataJson + get() = EnvironmentUrlDataJson.DEFAULT_FED_RAMP + override val label: String + get() = "bitwarden-gov.com" + } + /** * A custom self-hosted environment with a fully configurable [environmentUrlData]. */ @@ -63,6 +74,7 @@ sealed class Environment { enum class Type { US, EU, + FED_RAMP, SELF_HOSTED, } } diff --git a/data/src/main/kotlin/com/bitwarden/data/repository/model/EnvironmentRegion.kt b/data/src/main/kotlin/com/bitwarden/data/repository/model/EnvironmentRegion.kt index a24f6d64c6e..5ee4994b930 100644 --- a/data/src/main/kotlin/com/bitwarden/data/repository/model/EnvironmentRegion.kt +++ b/data/src/main/kotlin/com/bitwarden/data/repository/model/EnvironmentRegion.kt @@ -6,6 +6,7 @@ package com.bitwarden.data.repository.model enum class EnvironmentRegion { UNITED_STATES, EUROPEAN_UNION, + FED_RAMP, INTERNAL, SELF_HOSTED, } diff --git a/data/src/main/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlDataJsonExtensions.kt b/data/src/main/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlDataJsonExtensions.kt index ecb47ad9253..fb2e0d133de 100644 --- a/data/src/main/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlDataJsonExtensions.kt +++ b/data/src/main/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlDataJsonExtensions.kt @@ -7,15 +7,21 @@ import java.net.URI private const val DEFAULT_US_API_URL: String = "https://api.bitwarden.com" private const val DEFAULT_EU_API_URL: String = "https://api.bitwarden.eu" +private const val DEFAULT_FED_RAMP_API_URL: String = "https://api.bitwarden-gov.com" private const val DEFAULT_US_EVENTS_URL: String = "https://events.bitwarden.com" private const val DEFAULT_EU_EVENTS_URL: String = "https://events.bitwarden.eu" +private const val DEFAULT_FED_RAMP_EVENTS_URL: String = "https://events.bitwarden-gov.com" private const val DEFAULT_US_IDENTITY_URL: String = "https://identity.bitwarden.com" private const val DEFAULT_EU_IDENTITY_URL: String = "https://identity.bitwarden.eu" +private const val DEFAULT_FED_RAMP_IDENTITY_URL: String = "https://identity.bitwarden-gov.com" private const val DEFAULT_US_WEB_VAULT_URL: String = "https://vault.bitwarden.com" private const val DEFAULT_EU_WEB_VAULT_URL: String = "https://vault.bitwarden.eu" +private const val DEFAULT_FED_RAMP_WEB_VAULT_URL: String = "https://vault.bitwarden-gov.com" private const val DEFAULT_US_WEB_SEND_URL: String = "https://send.bitwarden.com/#" +private const val DEFAULT_FED_RAMP_WEB_SEND_URL: String = "https://send.bitwarden-gov.com/#" private const val DEFAULT_US_ICON_URL: String = "https://icons.bitwarden.net" private const val DEFAULT_EU_ICON_URL: String = "https://icons.bitwarden.eu" +private const val DEFAULT_FED_RAMP_ICON_URL: String = "https://icons.bitwarden-gov.com" /** * Returns the base api URL or the default value if one is not present. @@ -24,6 +30,7 @@ val EnvironmentUrlDataJson.baseApiUrl: String get() = when (this.environmentRegion) { EnvironmentRegion.UNITED_STATES -> DEFAULT_US_API_URL EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_API_URL + EnvironmentRegion.FED_RAMP -> DEFAULT_FED_RAMP_API_URL EnvironmentRegion.INTERNAL, EnvironmentRegion.SELF_HOSTED, -> { @@ -40,6 +47,7 @@ val EnvironmentUrlDataJson.appLinksScheme: String get() = when (this.environmentRegion) { EnvironmentRegion.UNITED_STATES, EnvironmentRegion.EUROPEAN_UNION, + EnvironmentRegion.FED_RAMP, EnvironmentRegion.INTERNAL, -> "https" @@ -53,6 +61,7 @@ val EnvironmentUrlDataJson.baseEventsUrl: String get() = when (this.environmentRegion) { EnvironmentRegion.UNITED_STATES -> DEFAULT_US_EVENTS_URL EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_EVENTS_URL + EnvironmentRegion.FED_RAMP -> DEFAULT_FED_RAMP_EVENTS_URL EnvironmentRegion.INTERNAL, EnvironmentRegion.SELF_HOSTED, -> { @@ -69,6 +78,7 @@ val EnvironmentUrlDataJson.baseIdentityUrl: String get() = when (this.environmentRegion) { EnvironmentRegion.UNITED_STATES -> DEFAULT_US_IDENTITY_URL EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_IDENTITY_URL + EnvironmentRegion.FED_RAMP -> DEFAULT_FED_RAMP_IDENTITY_URL EnvironmentRegion.INTERNAL, EnvironmentRegion.SELF_HOSTED, -> { @@ -87,6 +97,7 @@ val EnvironmentUrlDataJson.baseWebVaultUrlOrNull: String? get() = when (this.environmentRegion) { EnvironmentRegion.UNITED_STATES -> DEFAULT_US_WEB_VAULT_URL EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_WEB_VAULT_URL + EnvironmentRegion.FED_RAMP -> DEFAULT_FED_RAMP_WEB_VAULT_URL EnvironmentRegion.INTERNAL, EnvironmentRegion.SELF_HOSTED, -> this.webVault.sanitizeUrl ?: this.base.sanitizeUrl @@ -106,6 +117,7 @@ val EnvironmentUrlDataJson.baseWebVaultUrlOrDefault: String val EnvironmentUrlDataJson.baseWebSendUrl: String get() = when (this.environmentRegion) { EnvironmentRegion.UNITED_STATES -> DEFAULT_US_WEB_SEND_URL + EnvironmentRegion.FED_RAMP -> DEFAULT_FED_RAMP_WEB_SEND_URL EnvironmentRegion.EUROPEAN_UNION, EnvironmentRegion.INTERNAL, EnvironmentRegion.SELF_HOSTED, @@ -128,6 +140,7 @@ val EnvironmentUrlDataJson.baseIconUrl: String get() = when (this.environmentRegion) { EnvironmentRegion.UNITED_STATES -> DEFAULT_US_ICON_URL EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_ICON_URL + EnvironmentRegion.FED_RAMP -> DEFAULT_FED_RAMP_ICON_URL EnvironmentRegion.INTERNAL, EnvironmentRegion.SELF_HOSTED, -> { @@ -139,7 +152,7 @@ val EnvironmentUrlDataJson.baseIconUrl: String /** * Returns the appropriate pre-defined labels for environments matching the known US/EU values. - * Otherwise returns the host of the custom base URL. + * Otherwise, returns the host of the custom base URL. * * @see getSelfHostedUrlOrNull */ @@ -147,6 +160,7 @@ val EnvironmentUrlDataJson.labelOrBaseUrlHost: String get() = when (this) { EnvironmentUrlDataJson.DEFAULT_US -> Environment.Us.label EnvironmentUrlDataJson.DEFAULT_EU -> Environment.Eu.label + EnvironmentUrlDataJson.DEFAULT_FED_RAMP -> Environment.FedRamp.label else -> { // Grab the domain // Ex: @@ -189,6 +203,7 @@ fun EnvironmentUrlDataJson.toEnvironmentUrls(): Environment = EnvironmentUrlDataJson.DEFAULT_LEGACY_EU, -> Environment.Eu + EnvironmentUrlDataJson.DEFAULT_FED_RAMP -> Environment.FedRamp else -> Environment.SelfHosted(environmentUrlData = this) } diff --git a/data/src/test/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt b/data/src/test/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt index 9a171732af9..e97ab7b69e1 100644 --- a/data/src/test/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt +++ b/data/src/test/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt @@ -193,6 +193,15 @@ class EnvironmentUrlsDataJsonExtensionsTest { ) } + @Test + fun `labelOrBaseUrlHost should correctly convert FedRAMP environment to the correct label`() { + val environment = EnvironmentUrlDataJson.DEFAULT_FED_RAMP + assertEquals( + Environment.FedRamp.label, + environment.labelOrBaseUrlHost, + ) + } + @Suppress("MaxLineLength") @Test fun `labelOrBaseUrlHost should correctly convert self hosted environment to the correct label`() { @@ -219,6 +228,14 @@ class EnvironmentUrlsDataJsonExtensionsTest { ) } + @Test + fun `toEnvironmentUrls should correctly convert FedRAMP urls to the expected type`() { + assertEquals( + Environment.FedRamp, + EnvironmentUrlDataJson.DEFAULT_FED_RAMP.toEnvironmentUrls(), + ) + } + @Test fun `toEnvironmentUrls should correctly convert custom urls to the expected type`() { assertEquals( @@ -254,6 +271,14 @@ class EnvironmentUrlsDataJsonExtensionsTest { ) } + @Test + fun `toEnvironmentUrlsOrDefault should correctly convert FedRAMP urls to the expected type`() { + assertEquals( + Environment.FedRamp, + EnvironmentUrlDataJson.DEFAULT_FED_RAMP.toEnvironmentUrlsOrDefault(), + ) + } + @Suppress("MaxLineLength") @Test fun `toEnvironmentUrlsOrDefault should correctly convert legacy EU urls to the expected type`() { @@ -357,6 +382,16 @@ class EnvironmentUrlsDataJsonExtensionsTest { ) } + @Test + fun `appLinksScheme should return the correct scheme for FedRAMP environment`() { + val expectedScheme = "https" + + assertEquals( + expectedScheme, + EnvironmentUrlDataJson.DEFAULT_FED_RAMP.appLinksScheme, + ) + } + @Test fun `appLinksScheme should return the correct scheme for internal environment`() { val expectedScheme = "https"