Create policy manager (#899)

This commit is contained in:
Shannon Draeker 2024-01-31 19:14:27 -07:00 committed by Álison Fernandes
parent 794b68d364
commit d538e37606
21 changed files with 488 additions and 173 deletions

View file

@ -86,11 +86,6 @@ interface AuthRepository : AuthenticatorProvider {
*/
val passwordPolicies: List<PolicyInformation.MasterPassword>
/**
* Return whether there are any export vault policies enabled for the current user.
*/
val hasExportVaultPoliciesEnabled: Boolean
/**
* The reason for resetting the password.
*/

View file

@ -56,7 +56,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
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.currentUserPoliciesListFlow
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
@ -65,8 +64,10 @@ import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
@ -123,6 +124,7 @@ class AuthRepositoryImpl(
private val vaultRepository: VaultRepository,
private val userLogoutManager: UserLogoutManager,
private val pushManager: PushManager,
private val policyManager: PolicyManager,
dispatcherManager: DispatcherManager,
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
) : AuthRepository {
@ -235,21 +237,7 @@ class AuthRepositoryImpl(
by mutableHasPendingAccountAdditionStateFlow::value
override val passwordPolicies: List<PolicyInformation.MasterPassword>
get() = activeUserId?.let { userId ->
authDiskSource
.getPolicies(userId)
?.filter { it.type == PolicyTypeJson.MASTER_PASSWORD && it.isEnabled }
?.mapNotNull { it.policyInformation as? PolicyInformation.MasterPassword }
.orEmpty()
} ?: emptyList()
override val hasExportVaultPoliciesEnabled: Boolean
get() = activeUserId?.let { userId ->
authDiskSource
.getPolicies(userId)
?.any { it.type == PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT && it.isEnabled }
?: false
} ?: false
get() = policyManager.getActivePolicies()
override val passwordResetReason: ForcePasswordResetReason?
get() = authDiskSource
@ -274,7 +262,8 @@ class AuthRepositoryImpl(
.launchIn(unconfinedScope)
// When the policies for the user have been set, complete the login process.
authDiskSource.currentUserPoliciesListFlow
policyManager
.getActivePoliciesFlow(type = PolicyTypeJson.MASTER_PASSWORD)
.onEach { policies ->
val userId = activeUserId ?: return@onEach
@ -1148,7 +1137,6 @@ class AuthRepositoryImpl(
// If there are no master password policies that are enabled and should be
// enforced on login, the check should complete.
val passwordPolicies = policyList
.filter { it.type == PolicyTypeJson.MASTER_PASSWORD && it.isEnabled }
.mapNotNull { it.policyInformation as? PolicyInformation.MasterPassword }
.filter { it.enforceOnLogin == true }

View file

@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@ -53,6 +54,7 @@ object AuthRepositoryModule {
vaultRepository: VaultRepository,
userLogoutManager: UserLogoutManager,
pushManager: PushManager,
policyManager: PolicyManager,
): AuthRepository = AuthRepositoryImpl(
clock = clock,
accountsService = accountsService,
@ -71,5 +73,6 @@ object AuthRepositoryModule {
vaultRepository = vaultRepository,
userLogoutManager = userLogoutManager,
pushManager = pushManager,
policyManager = policyManager,
)
}

View file

@ -2,12 +2,10 @@ package com.x8bit.bitwarden.data.auth.repository.util
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
@ -55,22 +53,3 @@ val AuthDiskSource.userOrganizationsListFlow: Flow<List<UserOrganizations>>
) { values -> values.toList() }
}
.distinctUntilChanged()
/**
* Returns a [Flow] that emits distinct updates to the
* current user's [SyncResponseJson.Policy] list.
*/
@OptIn(ExperimentalCoroutinesApi::class)
val AuthDiskSource.currentUserPoliciesListFlow: Flow<List<SyncResponseJson.Policy>?>
get() =
this
.userStateFlow
.flatMapLatest { userStateJson ->
userStateJson
?.activeUserId
?.let { activeUserId ->
this.getPoliciesFlow(activeUserId)
}
?: emptyFlow()
}
.distinctUntilChanged()

View file

@ -0,0 +1,20 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
/**
* A manager for pulling policies from the local data store and filtering them as needed.
*/
interface PolicyManager {
/**
* Returns a flow of all the active policies of the given type.
*/
fun getActivePoliciesFlow(type: PolicyTypeJson): Flow<List<SyncResponseJson.Policy>>
/**
* Get all the policies of the given [type] that are enabled and applicable to the user.
*/
fun getActivePolicies(type: PolicyTypeJson): List<SyncResponseJson.Policy>
}

View file

@ -0,0 +1,101 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationStatusType
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
/**
* The default [PolicyManager] implementation. This class is responsible for
* loading policies for the current user and filtering them as needed.
*/
class PolicyManagerImpl(
private val authDiskSource: AuthDiskSource,
) : PolicyManager {
@OptIn(ExperimentalCoroutinesApi::class)
override fun getActivePoliciesFlow(type: PolicyTypeJson): Flow<List<SyncResponseJson.Policy>> =
authDiskSource
.userStateFlow
.flatMapLatest { userStateJson ->
userStateJson
?.activeUserId
?.let { activeUserId ->
authDiskSource.getPoliciesFlow(activeUserId)
.map {
filterPolicies(
userId = activeUserId,
type = type,
policies = it,
)
}
}
?: emptyFlow()
}
.distinctUntilChanged()
override fun getActivePolicies(type: PolicyTypeJson): List<SyncResponseJson.Policy> =
authDiskSource
.userState
?.activeUserId
?.let { userId ->
filterPolicies(
userId = userId,
type = type,
policies = authDiskSource.getPolicies(userId = userId),
)
}
?: emptyList()
/**
* A helper method to filter policies.
*/
private fun filterPolicies(
userId: String,
type: PolicyTypeJson,
policies: List<SyncResponseJson.Policy>?,
): List<SyncResponseJson.Policy> {
if (policies.isNullOrEmpty()) return emptyList()
// Get a list of the user's organizations that enforce policies.
val organizationIdsWithActivePolicies = authDiskSource
.getOrganizations(userId)
?.filter {
it.shouldUsePolicies &&
it.isEnabled &&
it.status >= OrganizationStatusType.ACCEPTED &&
!isOrganizationExemptFromPolicies(it, type)
}
?.map { it.id }
.orEmpty()
// Filter the policies based on the type, whether the policy is active,
// and whether the organization rules except the user from the policy.
return policies.filter {
it.type == type &&
it.isEnabled &&
organizationIdsWithActivePolicies.contains(it.organizationId)
}
}
/**
* A helper method to determine if the organization is exempt from policies.
*/
private fun isOrganizationExemptFromPolicies(
organization: SyncResponseJson.Profile.Organization,
policyType: PolicyTypeJson,
): Boolean =
if (policyType == PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT) {
organization.type == OrganizationType.OWNER
} else {
(organization.type == OrganizationType.OWNER ||
organization.type == OrganizationType.ADMIN) ||
organization.permissions.shouldManagePolicies
}
}

View file

@ -20,6 +20,8 @@ import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
@ -124,6 +126,14 @@ object PlatformManagerModule {
context = application.applicationContext,
)
@Provides
@Singleton
fun providePolicyManager(
authDiskSource: AuthDiskSource,
): PolicyManager = PolicyManagerImpl(
authDiskSource = authDiskSource,
)
@Provides
@Singleton
fun providePushManager(

View file

@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.platform.manager.util
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
/**
* Get a list of active policies with the data decoded to the specified type.
*/
inline fun <reified T : PolicyInformation> PolicyManager.getActivePolicies(): List<T> {
val type = when (T::class.java) {
PolicyInformation.MasterPassword::class.java -> PolicyTypeJson.MASTER_PASSWORD
PolicyInformation.PasswordGenerator::class.java -> PolicyTypeJson.PASSWORD_GENERATOR
else -> {
throw IllegalStateException(
"Looks like you are missing a branch in your when statement. Update " +
"getActivePolicies() to handle all PolicyInformation implementations.",
)
}
}
return this
.getActivePolicies(type = type)
.mapNotNull { it.policyInformation as? T }
}

View file

@ -8,8 +8,9 @@ import com.bitwarden.generators.PasswordGeneratorRequest
import com.bitwarden.generators.UsernameGeneratorRequest
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
@ -26,7 +27,6 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandom
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
@ -48,12 +48,14 @@ import kotlin.math.max
* Default implementation of [GeneratorRepository].
*/
@Singleton
@Suppress("LongParameterList")
class GeneratorRepositoryImpl(
private val generatorSdkSource: GeneratorSdkSource,
private val generatorDiskSource: GeneratorDiskSource,
private val authDiskSource: AuthDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
private val policyManager: PolicyManager,
dispatcherManager: DispatcherManager,
) : GeneratorRepository {
@ -201,10 +203,9 @@ class GeneratorRepositoryImpl(
},
)
@Suppress("LongMethod", "ReturnCount", "CyclomaticComplexMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun getPasswordGeneratorPolicy(): PolicyInformation.PasswordGenerator? {
val userId = authDiskSource.userState?.activeUserId ?: return null
val policies = authDiskSource.getPolicies(userId) ?: return null
val policies: List<PolicyInformation.PasswordGenerator> = policyManager.getActivePolicies()
var minLength: Int? = null
var useUpper = false
@ -218,8 +219,7 @@ class GeneratorRepositoryImpl(
var includeNumber = false
var isPassphrasePresent = false
policies.filter { it.type == PolicyTypeJson.PASSWORD_GENERATOR && it.isEnabled }
.mapNotNull { it.policyInformation as? PolicyInformation.PasswordGenerator }
policies
.forEach { policy ->
if (policy.defaultType == PolicyInformation.PasswordGenerator.TYPE_PASSPHRASE) {
isPassphrasePresent = true

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.tools.generator.repository.di
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
@ -30,6 +31,7 @@ object GeneratorRepositoryModule {
vaultSdkSource: VaultSdkSource,
passwordHistoryDiskSource: PasswordHistoryDiskSource,
dispatcherManager: DispatcherManager,
policyManager: PolicyManager,
): GeneratorRepository = GeneratorRepositoryImpl(
generatorSdkSource = generatorSdkSource,
generatorDiskSource = generatorDiskSource,
@ -37,5 +39,6 @@ object GeneratorRepositoryModule {
vaultSdkSource = vaultSdkSource,
passwordHistoryDiskSource = passwordHistoryDiskSource,
dispatcherManager = dispatcherManager,
policyManager = policyManager,
)
}

View file

@ -0,0 +1,36 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents a user's status in an organization.
*/
@Serializable(OrganizationStatusTypeSerializer::class)
enum class OrganizationStatusType {
/**
* The user has been invited to the organization.
*/
@SerialName("0")
INVITED,
/**
* The user has accepted the invite to the organization.
*/
@SerialName("1")
ACCEPTED,
/**
* The user has been confirmed in the organization.
*/
@SerialName("2")
CONFIRMED,
}
@Keep
private class OrganizationStatusTypeSerializer :
BaseEnumeratedIntSerializer<OrganizationStatusType>(
OrganizationStatusType.entries.toTypedArray(),
)

View file

@ -0,0 +1,47 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents a user's role in an organization.
*/
@Serializable(OrganizationTypeSerializer::class)
enum class OrganizationType {
/**
* The user is an owner of the organization.
*/
@SerialName("0")
OWNER,
/**
* The user is an admin in the organization.
*/
@SerialName("1")
ADMIN,
/**
* The user is an ordinary user in the organization.
*/
@SerialName("2")
USER,
/**
* The user is a manager in the organization.
*/
@SerialName("3")
MANAGER,
/**
* The user has a custom role in the organization.
*/
@SerialName("4")
CUSTOM,
}
@Keep
private class OrganizationTypeSerializer : BaseEnumeratedIntSerializer<OrganizationType>(
OrganizationType.entries.toTypedArray(),
)

View file

@ -246,7 +246,7 @@ data class SyncResponseJson(
val keyConnectorUrl: String?,
@SerialName("type")
val type: Int,
val type: OrganizationType,
@SerialName("seats")
val seats: Int?,
@ -326,7 +326,7 @@ data class SyncResponseJson(
val familySponsorshipValidUntil: ZonedDateTime?,
@SerialName("status")
val status: Int,
val status: OrganizationStatusType,
)
/**

View file

@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
@ -26,6 +28,7 @@ private const val KEY_STATE = "state"
@HiltViewModel
class ExportVaultViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val policyManager: PolicyManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<ExportVaultState, ExportVaultEvent, ExportVaultAction>(
initialState = savedStateHandle[KEY_STATE]
@ -33,7 +36,9 @@ class ExportVaultViewModel @Inject constructor(
dialogState = null,
exportFormat = ExportVaultFormat.JSON,
passwordInput = "",
policyPreventsExport = authRepository.hasExportVaultPoliciesEnabled,
policyPreventsExport = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT)
.any(),
),
) {
init {

View file

@ -70,6 +70,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJson
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -79,6 +80,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
@ -194,10 +196,16 @@ class AuthRepositoryTest {
private val mutableLogoutFlow = bufferedMutableSharedFlow<Unit>()
private val mutableSyncOrgKeysFlow = bufferedMutableSharedFlow<Unit>()
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
private val pushManager: PushManager = mockk {
every { logoutFlow } returns mutableLogoutFlow
every { syncOrgKeysFlow } returns mutableSyncOrgKeysFlow
}
private val policyManager: PolicyManager = mockk {
every {
getActivePoliciesFlow(type = PolicyTypeJson.MASTER_PASSWORD)
} returns mutableActivePolicyFlow
}
private var elapsedRealtimeMillis = 123456789L
@ -219,6 +227,7 @@ class AuthRepositoryTest {
userLogoutManager = userLogoutManager,
dispatcherManager = dispatcherManager,
pushManager = pushManager,
policyManager = policyManager,
elapsedRealtimeMillisProvider = { elapsedRealtimeMillis },
)
@ -406,9 +415,8 @@ class AuthRepositoryTest {
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
// Set policies that will fail the password.
fakeAuthDiskSource.storePolicies(
userId = USER_ID_1,
policies = listOf(
mutableActivePolicyFlow.emit(
listOf(
createMockPolicy(
type = PolicyTypeJson.MASTER_PASSWORD,
isEnabled = true,
@ -509,38 +517,6 @@ class AuthRepositoryTest {
assertNull(repository.rememberedOrgIdentifier)
}
@Test
fun `hasExportVaultPoliciesEnabled checks if any export vault policies are enabled`() {
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
// No stored policies returns false.
assertFalse(repository.hasExportVaultPoliciesEnabled)
// Stored but disabled policies returns false.
fakeAuthDiskSource.storePolicies(
userId = USER_ID_1,
policies = listOf(
createMockPolicy(
type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
isEnabled = false,
),
),
)
assertFalse(repository.hasExportVaultPoliciesEnabled)
// Stored enabled policies returns true.
fakeAuthDiskSource.storePolicies(
userId = USER_ID_1,
policies = listOf(
createMockPolicy(
type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
isEnabled = true,
),
),
)
assertTrue(repository.hasExportVaultPoliciesEnabled)
}
@Test
fun `passwordResetReason should pull from the user's profile in AuthDiskSource`() = runTest {
val updatedProfile = ACCOUNT_1.profile.copy(
@ -3702,7 +3678,7 @@ class AuthRepositoryTest {
fun `validatePasswordAgainstPolicy validates password against policy requirements`() = runTest {
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
// A helper method to set a policy in the store with the given parameters.
// A helper method to set a policy with the given parameters.
fun setPolicy(
minLength: Int = 0,
minComplexity: Int? = null,
@ -3711,22 +3687,21 @@ class AuthRepositoryTest {
requireNumbers: Boolean = false,
requireSpecial: Boolean = false,
) {
fakeAuthDiskSource.storePolicies(
userId = USER_ID_1,
policies = listOf(
createMockPolicy(
type = PolicyTypeJson.MASTER_PASSWORD,
isEnabled = true,
data = buildJsonObject {
put(key = "minLength", value = minLength)
put(key = "minComplexity", value = minComplexity)
put(key = "requireUpper", value = requireUpper)
put(key = "requireLower", value = requireLower)
put(key = "requireNumbers", value = requireNumbers)
put(key = "requireSpecial", value = requireSpecial)
put(key = "enforceOnLogin", value = true)
},
),
every {
policyManager.getActivePolicies(type = PolicyTypeJson.MASTER_PASSWORD)
} returns listOf(
createMockPolicy(
type = PolicyTypeJson.MASTER_PASSWORD,
isEnabled = true,
data = buildJsonObject {
put(key = "minLength", value = minLength)
put(key = "minComplexity", value = minComplexity)
put(key = "requireUpper", value = requireUpper)
put(key = "requireLower", value = requireLower)
put(key = "requireNumbers", value = requireNumbers)
put(key = "requireSpecial", value = requireSpecial)
put(key = "enforceOnLogin", value = true)
},
),
)
}

View file

@ -8,7 +8,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
@ -155,37 +154,4 @@ class AuthDiskSourceExtensionsTest {
)
}
}
@Test
fun `currentUserPoliciesListFlow should emit changes to current user's policy data`() =
runTest {
val userId = "userId1"
val userStateJson = mockk<UserStateJson>() {
every { activeUserId } returns userId
}
authDiskSource.apply {
userState = userStateJson
storePolicies(
userId = userId,
policies = listOf(createMockPolicy()),
)
}
authDiskSource.currentUserPoliciesListFlow.test {
assertEquals(
listOf(createMockPolicy()),
awaitItem(),
)
authDiskSource.storePolicies(
userId = userId,
policies = listOf(createMockPolicy(number = 3)),
)
assertEquals(
listOf(createMockPolicy(number = 3)),
awaitItem(),
)
}
}
}

View file

@ -0,0 +1,144 @@
package com.x8bit.bitwarden.data.platform.manager
import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class PolicyManagerTest {
private val mutableUserStateFlow = MutableStateFlow<UserStateJson?>(null)
private val mutablePolicyFlow = MutableStateFlow<List<SyncResponseJson.Policy>?>(null)
private val authDiskSource: AuthDiskSource = mockk {
every { userStateFlow } returns mutableUserStateFlow
every { getPoliciesFlow(USER_ID) } returns mutablePolicyFlow
}
private lateinit var policyManager: PolicyManager
@BeforeEach
fun setUp() {
policyManager = PolicyManagerImpl(
authDiskSource = authDiskSource,
)
}
@Test
fun `currentUserPoliciesListFlow should emit changes to current user's policy data`() =
runTest {
val userStateJson = mockk<UserStateJson> {
every { activeUserId } returns USER_ID
}
val organizationsOne = createMockOrganization(
number = 1,
isEnabled = true,
shouldUsePolicies = true,
)
val organizationsTwo = createMockOrganization(
number = 2,
isEnabled = true,
shouldUsePolicies = true,
)
val expectedPolicyOne = createMockPolicy(
isEnabled = true,
number = 1,
organizationId = organizationsOne.id,
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
)
val expectedPolicyTwo = createMockPolicy(
isEnabled = true,
number = 2,
organizationId = organizationsTwo.id,
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
)
every {
authDiskSource.getOrganizations(USER_ID)
} returns listOf(organizationsOne) andThen listOf(organizationsTwo)
mutableUserStateFlow.value = userStateJson
mutablePolicyFlow.value = listOf(expectedPolicyOne)
policyManager
.getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
.test {
assertEquals(listOf(expectedPolicyOne), awaitItem())
mutablePolicyFlow.value = listOf(expectedPolicyTwo)
assertEquals(listOf(expectedPolicyTwo), awaitItem())
}
}
@Test
fun `getActivePolicies returns empty list if user id is null`() {
every {
authDiskSource.userState
} returns null
assertTrue(policyManager.getActivePolicies(type = PolicyTypeJson.MASTER_PASSWORD).isEmpty())
}
@Test
fun `getActivePolicies returns empty list if the policies are not active`() {
val userState: UserStateJson = mockk {
every { activeUserId } returns USER_ID
}
every { authDiskSource.userState } returns userState
every {
authDiskSource.getOrganizations(USER_ID)
} returns listOf(
createMockOrganization(
number = 3,
isEnabled = true,
),
)
every {
authDiskSource.getPolicies(USER_ID)
} returns listOf(
createMockPolicy(
organizationId = "mockId-3",
isEnabled = true,
),
)
assertTrue(policyManager.getActivePolicies(type = PolicyTypeJson.MASTER_PASSWORD).isEmpty())
}
@Test
fun `getActivePolicies returns active and applied policies`() {
val userState: UserStateJson = mockk {
every { activeUserId } returns USER_ID
}
every { authDiskSource.userState } returns userState
every {
authDiskSource.getOrganizations(USER_ID)
} returns listOf(
createMockOrganization(
number = 3,
isEnabled = false,
),
)
every {
authDiskSource.getPolicies(USER_ID)
} returns listOf(
createMockPolicy(
organizationId = "mockId-3",
isEnabled = true,
),
)
assertTrue(policyManager.getActivePolicies(type = PolicyTypeJson.MASTER_PASSWORD).isEmpty())
}
}
private const val USER_ID = "userId"

View file

@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserD
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
@ -72,6 +73,7 @@ class GeneratorRepositoryTest {
private val passwordHistoryDiskSource: PasswordHistoryDiskSource = mockk()
private val vaultSdkSource: VaultSdkSource = mockk()
private val dispatcherManager = FakeDispatcherManager()
private val policyManager: PolicyManager = mockk()
private val repository = GeneratorRepositoryImpl(
generatorSdkSource = generatorSdkSource,
@ -80,6 +82,7 @@ class GeneratorRepositoryTest {
passwordHistoryDiskSource = passwordHistoryDiskSource,
vaultSdkSource = vaultSdkSource,
dispatcherManager = dispatcherManager,
policyManager = policyManager,
)
@AfterEach
@ -769,35 +772,35 @@ class GeneratorRepositoryTest {
@Suppress("MaxLineLength")
@Test
fun `getPasswordGeneratorPolicy returns default settings when no policies are present`() = runTest {
val userId = "testUserId"
coEvery { authDiskSource.userState?.activeUserId } returns userId
coEvery { authDiskSource.getPolicies(userId) } returns emptyList()
fun `getPasswordGeneratorPolicy returns default settings when no policies are present`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.PASSWORD_GENERATOR)
} returns emptyList()
val policy = repository.getPasswordGeneratorPolicy()
val policy = repository.getPasswordGeneratorPolicy()
val expectedPolicy = PolicyInformation.PasswordGenerator(
defaultType = "password",
minLength = null,
useUpper = false,
useLower = false,
useNumbers = false,
useSpecial = false,
minNumbers = null,
minSpecial = null,
minNumberWords = null,
capitalize = false,
includeNumber = false,
)
val expectedPolicy = PolicyInformation.PasswordGenerator(
defaultType = "password",
minLength = null,
useUpper = false,
useLower = false,
useNumbers = false,
useSpecial = false,
minNumbers = null,
minSpecial = null,
minNumberWords = null,
capitalize = false,
includeNumber = false,
)
assertNotNull(policy)
assertEquals(expectedPolicy, policy)
}
assertNotNull(policy)
assertEquals(expectedPolicy, policy)
}
@Suppress("MaxLineLength")
@Test
fun `getPasswordGeneratorPolicy applies strictest settings from multiple policies`() = runTest {
val userId = "testUserId"
val policy1 = PolicyInformation.PasswordGenerator(
defaultType = "password",
minLength = 8,
@ -840,8 +843,7 @@ class GeneratorRepositoryTest {
organizationId = "id2",
),
)
coEvery { authDiskSource.userState?.activeUserId } returns userId
coEvery { authDiskSource.getPolicies(userId) } returns policies
every { policyManager.getActivePolicies(type = PolicyTypeJson.PASSWORD_GENERATOR) } returns policies
val resultPolicy = repository.getPasswordGeneratorPolicy()

View file

@ -7,12 +7,13 @@ import kotlinx.serialization.json.JsonObject
*/
fun createMockPolicy(
number: Int = 1,
organizationId: String = "mockOrganizationId-$number",
type: PolicyTypeJson = PolicyTypeJson.MASTER_PASSWORD,
isEnabled: Boolean = false,
data: JsonObject? = null,
): SyncResponseJson.Policy =
SyncResponseJson.Policy(
organizationId = "mockOrganizationId-$number",
organizationId = organizationId,
id = "mockId-$number",
type = type,
isEnabled = isEnabled,

View file

@ -30,13 +30,17 @@ fun createMockProfile(number: Int): SyncResponseJson.Profile =
/**
* Create a mock [SyncResponseJson.Profile.Organization] with a given [number].
*/
fun createMockOrganization(number: Int): SyncResponseJson.Profile.Organization =
fun createMockOrganization(
number: Int,
isEnabled: Boolean = false,
shouldUsePolicies: Boolean = false,
): SyncResponseJson.Profile.Organization =
SyncResponseJson.Profile.Organization(
shouldUsePolicies = false,
shouldUsePolicies = shouldUsePolicies,
keyConnectorUrl = "mockKeyConnectorUrl-$number",
type = 1,
type = OrganizationType.ADMIN,
seats = 1,
isEnabled = false,
isEnabled = isEnabled,
providerType = 1,
maxCollections = 1,
isSelfHost = false,
@ -60,7 +64,7 @@ fun createMockOrganization(number: Int): SyncResponseJson.Profile.Organization =
name = "mockName-$number",
shouldUseApi = false,
familySponsorshipValidUntil = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
status = 1,
status = OrganizationStatusType.ACCEPTED,
)
/**

View file

@ -5,6 +5,9 @@ import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.exportvault.model.ExportVaultFormat
@ -16,15 +19,21 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class ExportVaultViewModelTest : BaseViewModelTest() {
private val authRepository: AuthRepository = mockk {
every { hasExportVaultPoliciesEnabled } returns false
private val authRepository: AuthRepository = mockk()
private val policyManager: PolicyManager = mockk {
every {
getActivePolicies(type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT)
} returns emptyList()
}
private val savedStateHandle = SavedStateHandle()
@Test
fun `initial state should be correct`() = runTest {
every { authRepository.hasExportVaultPoliciesEnabled } returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT)
} returns listOf(createMockPolicy())
val viewModel = createViewModel()
viewModel.stateFlow.test {
@ -185,6 +194,7 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
private fun createViewModel(): ExportVaultViewModel =
ExportVaultViewModel(
authRepository = authRepository,
policyManager = policyManager,
savedStateHandle = savedStateHandle,
)
}