mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
[PM-12406] Introduce new endpoint and replace SSO details response flow (#4177)
This commit is contained in:
parent
c2537f329d
commit
e5e0464929
18 changed files with 490 additions and 12 deletions
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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?,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue