[PM-12406] Introduce new endpoint and replace SSO details response flow (#4177)

This commit is contained in:
André Bispo 2024-11-04 10:53:57 +00:00 committed by GitHub
parent c2537f329d
commit e5e0464929
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 490 additions and 12 deletions

View file

@ -2,6 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import retrofit2.http.Body
import retrofit2.http.POST
@ -16,4 +18,12 @@ interface UnauthenticatedOrganizationApi {
suspend fun getClaimedDomainOrganizationDetails(
@Body body: OrganizationDomainSsoDetailsRequestJson,
): Result<OrganizationDomainSsoDetailsResponseJson>
/**
* Checks for the verfied organization domains of an email for SSO purposes.
*/
@POST("/organizations/domain/sso/verified")
suspend fun getVerifiedOrganizationDomainsByEmail(
@Body body: VerifiedOrganizationDomainSsoDetailsRequest,
): Result<VerifiedOrganizationDomainSsoDetailsResponse>
}

View file

@ -1,16 +1,26 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
/**
* Response object returned when requesting organization domain SSO details.
*
* @property isSsoAvailable Whether or not SSO is available for this domain.
* @property organizationIdentifier The organization's identifier.
* @property verifiedDate The date the domain was verified.
*/
@Serializable
data class OrganizationDomainSsoDetailsResponseJson(
@SerialName("ssoAvailable") val isSsoAvailable: Boolean,
@SerialName("organizationIdentifier") val organizationIdentifier: String,
@SerialName("ssoAvailable")
val isSsoAvailable: Boolean,
@SerialName("organizationIdentifier")
val organizationIdentifier: String,
@SerialName("verifiedDate")
@Contextual
val verifiedDate: ZonedDateTime?,
)

View file

@ -0,0 +1,14 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Request body object when retrieving organization verified domain SSO info.
*
* @param email The email address to check against.
*/
@Serializable
data class VerifiedOrganizationDomainSsoDetailsRequest(
@SerialName("email") val email: String,
)

View file

@ -0,0 +1,35 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Response object returned when requesting organization verified domain SSO details.
*
* @property verifiedOrganizationDomainSsoDetails The list of verified organization domain SSO
* details.
*/
@Serializable
data class VerifiedOrganizationDomainSsoDetailsResponse(
@SerialName("data")
val verifiedOrganizationDomainSsoDetails: List<VerifiedOrganizationDomainSsoDetail>,
) {
/**
* Response body for an organization verified domain SSO details.
*
* @property organizationName The name of the organization.
* @property organizationIdentifier The identifier of the organization.
* @property domainName The name of the domain.
*/
@Serializable
data class VerifiedOrganizationDomainSsoDetail(
@SerialName("organizationName")
val organizationName: String,
@SerialName("organizationIdentifier")
val organizationIdentifier: String,
@SerialName("domainName")
val domainName: String,
)
}

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
/**
* Provides an API for querying organization endpoints.
@ -38,4 +39,12 @@ interface OrganizationService {
suspend fun getOrganizationKeys(
organizationId: String,
): Result<OrganizationKeysResponseJson>
/**
* Request organization verified domain details for an [email] needed for SSO
* requests.
*/
suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): Result<VerifiedOrganizationDomainSsoDetailsResponse>
}

View file

@ -7,6 +7,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomain
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
/**
* Default implementation of [OrganizationService].
@ -52,4 +54,13 @@ class OrganizationServiceImpl(
.getOrganizationKeys(
organizationId = organizationId,
)
override suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): Result<VerifiedOrganizationDomainSsoDetailsResponse> = unauthenticatedOrganizationApi
.getVerifiedOrganizationDomainsByEmail(
body = VerifiedOrganizationDomainSsoDetailsRequest(
email = email,
),
)
}

View file

@ -28,6 +28,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
@ -329,6 +330,13 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
email: String,
): OrganizationDomainSsoDetailsResult
/**
* Get the verified organization domain SSO details for the given [email].
*/
suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): VerifiedOrganizationDomainSsoDetailsResult
/**
* Prevalidates the organization identifier used in an SSO request.
*/

View file

@ -68,6 +68,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
@ -1123,11 +1124,27 @@ class AuthRepositoryImpl(
OrganizationDomainSsoDetailsResult.Success(
isSsoAvailable = it.isSsoAvailable,
organizationIdentifier = it.organizationIdentifier,
verifiedDate = it.verifiedDate,
)
},
onFailure = { OrganizationDomainSsoDetailsResult.Failure },
)
override suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): VerifiedOrganizationDomainSsoDetailsResult = organizationService
.getVerifiedOrganizationDomainSsoDetails(
email = email,
)
.fold(
onSuccess = {
VerifiedOrganizationDomainSsoDetailsResult.Success(
verifiedOrganizationDomainSsoDetails = it.verifiedOrganizationDomainSsoDetails,
)
},
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure },
)
override suspend fun prevalidateSso(
organizationIdentifier: String,
): PrevalidateSsoResult = identityService

View file

@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.auth.repository.model
import java.time.ZonedDateTime
/**
* Response types when checking for an email's claimed domain organization.
*/
@ -9,10 +11,12 @@ sealed class OrganizationDomainSsoDetailsResult {
*
* @property isSsoAvailable Indicates if SSO is available for the email address.
* @property organizationIdentifier The claimed organization identifier for the email address.
* @property verifiedDate The date and time when the domain was verified.
*/
data class Success(
val isSsoAvailable: Boolean,
val organizationIdentifier: String,
val verifiedDate: ZonedDateTime?,
) : OrganizationDomainSsoDetailsResult()
/**

View file

@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse.VerifiedOrganizationDomainSsoDetail
/**
* Response types when checking for an email's claimed domain organization.
*/
sealed class VerifiedOrganizationDomainSsoDetailsResult {
/**
* The request was successful.
*
* @property verifiedOrganizationDomainSsoDetails The verified organization domain SSO details.
*/
data class Success(
val verifiedOrganizationDomainSsoDetails: List<VerifiedOrganizationDomainSsoDetail>,
) : VerifiedOrganizationDomainSsoDetailsResult()
/**
* The request failed.
*/
data object Failure : VerifiedOrganizationDomainSsoDetailsResult()
}

View file

@ -32,6 +32,7 @@ sealed class FlagKey<out T : Any> {
OnboardingCarousel,
ImportLoginsFlow,
SshKeyCipherItems,
VerifiedSsoDomainEndpoint,
)
}
}
@ -89,6 +90,14 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the feature flag key for the new verified SSO domain endpoint feature.
*/
data object VerifiedSsoDomainEndpoint : FlagKey<Boolean>() {
override val keyName: String = "pm-12337-refactor-sso-details-endpoint"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the key for a [Boolean] flag to be used in tests.

View file

@ -9,12 +9,15 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SSO_URI
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.util.baseIdentityUrl
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
@ -43,6 +46,7 @@ private const val RANDOM_STRING_LENGTH = 64
class EnterpriseSignOnViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
private val featureFlagManager: FeatureFlagManager,
private val generatorRepository: GeneratorRepository,
private val networkConnectionManager: NetworkConnectionManager,
private val savedStateHandle: SavedStateHandle,
@ -123,6 +127,10 @@ class EnterpriseSignOnViewModel @Inject constructor(
is EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken -> {
handleOnReceiveCaptchaToken(action)
}
is EnterpriseSignOnAction.Internal.OnVerifiedOrganizationDomainSsoDetailsReceive -> {
handleOnVerifiedOrganizationDomainSsoDetailsReceive(action)
}
}
}
@ -209,6 +217,54 @@ class EnterpriseSignOnViewModel @Inject constructor(
}
}
private fun handleOnVerifiedOrganizationDomainSsoDetailsReceive(
action: EnterpriseSignOnAction.Internal.OnVerifiedOrganizationDomainSsoDetailsReceive,
) {
when (val orgDetails = action.verifiedOrgDomainSsoDetailsResult) {
is VerifiedOrganizationDomainSsoDetailsResult.Failure -> {
mutableStateFlow.update {
it.copy(
dialogState = null,
orgIdentifierInput = authRepository.rememberedOrgIdentifier ?: "",
)
}
}
is VerifiedOrganizationDomainSsoDetailsResult.Success -> {
handleOnVerifiedOrganizationDomainSsoDetailsSuccess(orgDetails)
}
}
}
private fun handleOnVerifiedOrganizationDomainSsoDetailsSuccess(
orgDetails: VerifiedOrganizationDomainSsoDetailsResult.Success,
) {
if (orgDetails.verifiedOrganizationDomainSsoDetails.isEmpty()) {
mutableStateFlow.update {
it.copy(
dialogState = null,
orgIdentifierInput = authRepository.rememberedOrgIdentifier ?: "",
)
}
return
}
val organizationIdentifier = orgDetails
.verifiedOrganizationDomainSsoDetails
.first()
.organizationIdentifier
mutableStateFlow.update {
it.copy(
orgIdentifierInput = organizationIdentifier,
)
}
// If the email address is associated with a claimed organization we can proceed to the
// prevalidation step.
prevalidateSso()
}
private fun handleOnOrganizationDomainSsoDetailsReceive(
action: EnterpriseSignOnAction.Internal.OnOrganizationDomainSsoDetailsReceive,
) {
@ -226,7 +282,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
private fun handleOnOrganizationDomainSsoDetailsSuccess(
orgDetails: OrganizationDomainSsoDetailsResult.Success,
) {
if (!orgDetails.isSsoAvailable) {
if (!orgDetails.isSsoAvailable || orgDetails.verifiedDate == null) {
mutableStateFlow.update {
it.copy(
dialogState = null,
@ -375,6 +431,16 @@ class EnterpriseSignOnViewModel @Inject constructor(
)
}
viewModelScope.launch {
if (featureFlagManager.getFeatureFlag(key = FlagKey.VerifiedSsoDomainEndpoint)) {
val result = authRepository.getVerifiedOrganizationDomainSsoDetails(
email = EnterpriseSignOnArgs(savedStateHandle).emailAddress,
)
sendAction(
EnterpriseSignOnAction.Internal.OnVerifiedOrganizationDomainSsoDetailsReceive(
result,
),
)
} else {
val result = authRepository.getOrganizationDomainSsoDetails(
email = EnterpriseSignOnArgs(savedStateHandle).emailAddress,
)
@ -383,6 +449,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
)
}
}
}
private suspend fun prepareAndLaunchCustomTab(
organizationIdentifier: String,
@ -561,6 +628,13 @@ sealed class EnterpriseSignOnAction {
* A captcha callback result has been received
*/
data class OnReceiveCaptchaToken(val tokenResult: CaptchaCallbackTokenResult) : Internal()
/**
* A result was received when requesting an [VerifiedOrganizationDomainSsoDetailsResult].
*/
data class OnVerifiedOrganizationDomainSsoDetailsReceive(
val verifiedOrgDomainSsoDetailsResult: VerifiedOrganizationDomainSsoDetailsResult,
) : Internal()
}
}

View file

@ -28,6 +28,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.OnboardingFlow,
FlagKey.ImportLoginsFlow,
FlagKey.SshKeyCipherItems,
FlagKey.VerifiedSsoDomainEndpoint,
-> BooleanFlagItem(
label = flagKey.getDisplayLabel(),
key = flagKey as FlagKey<Boolean>,
@ -71,4 +72,5 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.OnboardingFlow -> stringResource(R.string.onboarding_flow)
FlagKey.ImportLoginsFlow -> stringResource(R.string.import_logins_flow)
FlagKey.SshKeyCipherItems -> stringResource(R.string.ssh_key_cipher_item_types)
FlagKey.VerifiedSsoDomainEndpoint -> stringResource(R.string.verified_sso_domain_verified)
}

View file

@ -1073,6 +1073,7 @@ Do you want to switch to this account?</string>
<string name="bitwarden_tools">Bitwarden Tools</string>
<string name="got_it">Got it</string>
<string name="no_logins_were_imported">No logins were imported</string>
<string name="verified_sso_domain_verified">Verified SSO Domain Endpoint</string>
<string name="logins_imported">Logins imported</string>
<string name="remember_to_delete_your_imported_password_file_from_your_computer">Remember to delete your imported password file from your computer</string>
<string name="type_ssh_key">SSH key</string>

View file

@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedOrgan
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
import com.x8bit.bitwarden.data.platform.util.asSuccess
import kotlinx.coroutines.test.runTest
@ -13,6 +14,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import retrofit2.create
import java.time.ZonedDateTime
class OrganizationServiceTest : BaseServiceTest() {
private val authenticatedOrganizationApi: AuthenticatedOrganizationApi = retrofit.create()
@ -55,7 +57,9 @@ class OrganizationServiceTest : BaseServiceTest() {
runTest {
val email = "test@gmail.com"
server.enqueue(
MockResponse().setResponseCode(200).setBody(ORGANIZATION_DOMAIN_SSO_DETAILS_JSON),
MockResponse()
.setResponseCode(200)
.setBody(ORGANIZATION_DOMAIN_SSO_DETAILS_JSON),
)
val result = organizationService.getOrganizationDomainSsoDetails(email)
assertEquals(ORGANIZATION_DOMAIN_SSO_BODY.asSuccess(), result)
@ -74,7 +78,9 @@ class OrganizationServiceTest : BaseServiceTest() {
fun `getOrganizationAutoEnrollStatus when response is success should return valid response`() =
runTest {
server.enqueue(
MockResponse().setResponseCode(200).setBody(ORGANIZATION_AUTO_ENROLL_STATUS_JSON),
MockResponse()
.setResponseCode(200)
.setBody(ORGANIZATION_AUTO_ENROLL_STATUS_JSON),
)
val result = organizationService.getOrganizationAutoEnrollStatus("orgId")
assertEquals(ORGANIZATION_AUTO_ENROLL_STATUS_RESPONSE.asSuccess(), result)
@ -91,7 +97,9 @@ class OrganizationServiceTest : BaseServiceTest() {
@Test
fun `getOrganizationKeys when response is success should return valid response`() = runTest {
server.enqueue(
MockResponse().setResponseCode(200).setBody(ORGANIZATION_KEYS_JSON),
MockResponse()
.setResponseCode(200)
.setBody(ORGANIZATION_KEYS_JSON),
)
val result = organizationService.getOrganizationKeys("orgId")
assertEquals(ORGANIZATION_KEYS_RESPONSE.asSuccess(), result)
@ -103,6 +111,30 @@ class OrganizationServiceTest : BaseServiceTest() {
val result = organizationService.getOrganizationKeys("orgId")
assertTrue(result.isFailure)
}
@Suppress("MaxLineLength")
@Test
fun `getVerifiedOrganizationDomainSsoDetails when response is success should return valid response`() =
runTest {
server.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(ORGANIZATION_VERIFIED_DOMAIN_SSO_DETAILS_JSON),
)
val result =
organizationService.getVerifiedOrganizationDomainSsoDetails("example@bitwarden.com")
assertEquals(ORGANIZATION_VERIFIED_DOMAIN_SSO_DETAILS_RESPONSE.asSuccess(), result)
}
@Suppress("MaxLineLength")
@Test
fun `getVerifiedOrganizationDomainSsoDetails when response is an error should return an error`() =
runTest {
server.enqueue(MockResponse().setResponseCode(400))
val result =
organizationService.getVerifiedOrganizationDomainSsoDetails("example@bitwarden.com")
assertTrue(result.isFailure)
}
}
private const val ORGANIZATION_AUTO_ENROLL_STATUS_JSON = """
@ -130,6 +162,7 @@ private const val ORGANIZATION_DOMAIN_SSO_DETAILS_JSON = """
private val ORGANIZATION_DOMAIN_SSO_BODY = OrganizationDomainSsoDetailsResponseJson(
isSsoAvailable = true,
organizationIdentifier = "Test Org",
verifiedDate = ZonedDateTime.parse("2024-09-13T00:00:00.000Z"),
)
private const val ORGANIZATION_KEYS_JSON = """
@ -143,3 +176,26 @@ private val ORGANIZATION_KEYS_RESPONSE = OrganizationKeysResponseJson(
privateKey = "privateKey",
publicKey = "publicKey",
)
private const val ORGANIZATION_VERIFIED_DOMAIN_SSO_DETAILS_JSON = """
{
"data": [
{
"organizationIdentifier": "Test Identifier",
"organizationName": "Bitwarden",
"domainName": "bitwarden.com"
}
]
}
"""
private val ORGANIZATION_VERIFIED_DOMAIN_SSO_DETAILS_RESPONSE =
VerifiedOrganizationDomainSsoDetailsResponse(
verifiedOrganizationDomainSsoDetails = listOf(
VerifiedOrganizationDomainSsoDetailsResponse.VerifiedOrganizationDomainSsoDetail(
organizationIdentifier = "Test Identifier",
organizationName = "Bitwarden",
domainName = "bitwarden.com",
),
),
)

View file

@ -43,6 +43,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserD
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
@ -85,6 +86,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
@ -149,6 +151,7 @@ import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
@Suppress("LargeClass")
class AuthRepositoryTest {
@ -5217,6 +5220,7 @@ class AuthRepositoryTest {
} returns OrganizationDomainSsoDetailsResponseJson(
isSsoAvailable = true,
organizationIdentifier = "Test Org",
verifiedDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
)
.asSuccess()
val result = repository.getOrganizationDomainSsoDetails(email)
@ -5224,11 +5228,53 @@ class AuthRepositoryTest {
OrganizationDomainSsoDetailsResult.Success(
isSsoAvailable = true,
organizationIdentifier = "Test Org",
verifiedDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
),
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `getVerifiedOrganizationDomainSsoDetails Success should return Success`() = runTest {
val email = "test@gmail.com"
coEvery {
organizationService.getVerifiedOrganizationDomainSsoDetails(email)
} returns VerifiedOrganizationDomainSsoDetailsResponse(
verifiedOrganizationDomainSsoDetails = listOf(
VerifiedOrganizationDomainSsoDetailsResponse.VerifiedOrganizationDomainSsoDetail(
organizationIdentifier = "Test Identifier",
organizationName = "Bitwarden",
domainName = "bitwarden.com",
),
),
).asSuccess()
val result = repository.getVerifiedOrganizationDomainSsoDetails(email)
assertEquals(
VerifiedOrganizationDomainSsoDetailsResult.Success(
verifiedOrganizationDomainSsoDetails = listOf(
VerifiedOrganizationDomainSsoDetailsResponse.VerifiedOrganizationDomainSsoDetail(
organizationIdentifier = "Test Identifier",
organizationName = "Bitwarden",
domainName = "bitwarden.com",
),
),
),
result,
)
}
@Test
fun `getVerifiedOrganizationDomainSsoDetails Failure should return Failure `() = runTest {
val email = "test@gmail.com"
val throwable = Throwable()
coEvery {
organizationService.getVerifiedOrganizationDomainSsoDetails(email)
} returns throwable.asFailure()
val result = repository.getVerifiedOrganizationDomainSsoDetails(email)
assertEquals(VerifiedOrganizationDomainSsoDetailsResult.Failure, result)
}
@Test
fun `prevalidateSso Failure should return Failure `() = runTest {
val organizationId = "organizationid"

View file

@ -4,14 +4,18 @@ import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.util.FakeNetworkConnectionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
@ -34,6 +38,7 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
@Suppress("LargeClass")
class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
@ -54,6 +59,12 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
private val generatorRepository: GeneratorRepository = FakeGeneratorRepository()
private val featureFlagManager = mockk<FeatureFlagManager>() {
every {
getFeatureFlag(FlagKey.VerifiedSsoDomainEndpoint)
} returns false
}
@BeforeEach
fun setUp() {
mockkStatic(::generateUriForSso)
@ -694,6 +705,37 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
isSsoAvailable = false,
organizationIdentifier = "Bitwarden without SSO",
verifiedDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
)
coEvery {
authRepository.getOrganizationDomainSsoDetails(any())
} returns orgDetails
coEvery {
authRepository.rememberedOrgIdentifier
} returns "Bitwarden"
val viewModel = createViewModel(dismissInitialDialog = false)
assertEquals(
DEFAULT_STATE.copy(orgIdentifierInput = "Bitwarden"),
viewModel.stateFlow.value,
)
coVerify(exactly = 1) {
authRepository.getOrganizationDomainSsoDetails(DEFAULT_EMAIL)
authRepository.rememberedOrgIdentifier
}
}
@Suppress("MaxLineLength")
@Test
fun `OrganizationDomainSsoDetails success with no verified date available should make a request, hide the dialog, and update the org input based on the remembered org`() =
runTest {
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
isSsoAvailable = true,
organizationIdentifier = "Bitwarden without SSO",
verifiedDate = null,
)
coEvery {
@ -723,6 +765,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
isSsoAvailable = true,
organizationIdentifier = "",
verifiedDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
)
coEvery {
@ -757,6 +800,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
isSsoAvailable = true,
organizationIdentifier = "Bitwarden with SSO",
verifiedDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
)
coEvery {
@ -784,6 +828,109 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `VerifiedOrganizationDomainSsoDetails success with valid organization should make a request then attempt to login`() =
runTest {
val orgDetails = VerifiedOrganizationDomainSsoDetailsResult.Success(
verifiedOrganizationDomainSsoDetails = listOf(
VerifiedOrganizationDomainSsoDetailsResponse.VerifiedOrganizationDomainSsoDetail(
organizationIdentifier = "Bitwarden with SSO",
organizationName = "Bitwarden",
domainName = "bitwarden.com",
),
),
)
coEvery {
authRepository.getVerifiedOrganizationDomainSsoDetails(any())
} returns orgDetails
coEvery {
featureFlagManager.getFeatureFlag(FlagKey.VerifiedSsoDomainEndpoint)
} returns true
// Just hang on this request; login is tested elsewhere
coEvery {
authRepository.prevalidateSso(any())
} just awaits
val viewModel = createViewModel(dismissInitialDialog = false)
assertEquals(
DEFAULT_STATE.copy(
orgIdentifierInput = "Bitwarden with SSO",
dialogState = EnterpriseSignOnState.DialogState.Loading(
message = R.string.logging_in.asText(),
),
),
viewModel.stateFlow.value,
)
coVerify(exactly = 1) {
authRepository.getVerifiedOrganizationDomainSsoDetails(DEFAULT_EMAIL)
}
}
@Suppress("MaxLineLength")
@Test
fun `VerifiedOrganizationDomainSsoDetails success with no verified domains should make a request, hide the dialog, and update the org input based on the remembered org`() =
runTest {
val orgDetails = VerifiedOrganizationDomainSsoDetailsResult.Success(
verifiedOrganizationDomainSsoDetails = emptyList(),
)
coEvery {
authRepository.getVerifiedOrganizationDomainSsoDetails(any())
} returns orgDetails
coEvery {
featureFlagManager.getFeatureFlag(FlagKey.VerifiedSsoDomainEndpoint)
} returns true
val viewModel = createViewModel(dismissInitialDialog = false)
assertEquals(
DEFAULT_STATE.copy(
orgIdentifierInput = "",
dialogState = null,
),
viewModel.stateFlow.value,
)
coVerify(exactly = 1) {
authRepository.getVerifiedOrganizationDomainSsoDetails(DEFAULT_EMAIL)
}
}
@Suppress("MaxLineLength")
@Test
fun `VerifiedOrganizationDomainSsoDetails failure should make a request, hide dialog, load from remembered org identifier`() =
runTest {
coEvery {
authRepository.getVerifiedOrganizationDomainSsoDetails(any())
} returns VerifiedOrganizationDomainSsoDetailsResult.Failure
coEvery {
featureFlagManager.getFeatureFlag(FlagKey.VerifiedSsoDomainEndpoint)
} returns true
coEvery {
authRepository.rememberedOrgIdentifier
} returns "Bitwarden"
val viewModel = createViewModel(dismissInitialDialog = false)
assertEquals(
DEFAULT_STATE.copy(
orgIdentifierInput = "Bitwarden",
dialogState = null,
),
viewModel.stateFlow.value,
)
coVerify(exactly = 1) {
authRepository.getVerifiedOrganizationDomainSsoDetails(DEFAULT_EMAIL)
}
}
@Suppress("LongParameterList")
private fun createViewModel(
initialState: EnterpriseSignOnState? = null,
@ -802,6 +949,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
): EnterpriseSignOnViewModel = EnterpriseSignOnViewModel(
authRepository = authRepository,
environmentRepository = environmentRepository,
featureFlagManager = featureFlagManager,
generatorRepository = generatorRepository,
networkConnectionManager = FakeNetworkConnectionManager(isNetworkConnected),
savedStateHandle = savedStateHandle,

View file

@ -113,6 +113,7 @@ private val DEFAULT_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
FlagKey.OnboardingFlow to true,
FlagKey.ImportLoginsFlow to true,
FlagKey.SshKeyCipherItems to true,
FlagKey.VerifiedSsoDomainEndpoint to true,
)
private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
@ -122,6 +123,7 @@ private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
FlagKey.OnboardingFlow to false,
FlagKey.ImportLoginsFlow to false,
FlagKey.SshKeyCipherItems to false,
FlagKey.VerifiedSsoDomainEndpoint to false,
)
private val DEFAULT_STATE = DebugMenuState(