mirror of
https://github.com/bitwarden/android.git
synced 2026-06-10 16:46:10 -05:00
PM-38140 Feat: SDK policy filters (#6979)
This commit is contained in:
@@ -11,6 +11,9 @@ import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.policies.OrganizationUserPolicyContext
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.bitwarden.policies.PolicyView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
|
||||
/**
|
||||
@@ -134,4 +137,13 @@ interface AuthSdkSource {
|
||||
passwordStrength: PasswordStrength,
|
||||
policy: MasterPasswordPolicyOptions,
|
||||
): Result<Boolean>
|
||||
|
||||
/**
|
||||
* Applies the appropriate filters for determining what policies apply to the user.
|
||||
*/
|
||||
fun filterPolicies(
|
||||
policies: List<PolicyView>,
|
||||
organizations: List<OrganizationUserPolicyContext>,
|
||||
policyType: PolicyType,
|
||||
): Result<List<PolicyView>>
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.policies.OrganizationUserPolicyContext
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.bitwarden.policies.PolicyView
|
||||
import com.bitwarden.sdk.AuthClient
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull
|
||||
@@ -221,4 +224,16 @@ class AuthSdkSourceImpl(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun filterPolicies(
|
||||
policies: List<PolicyView>,
|
||||
organizations: List<OrganizationUserPolicyContext>,
|
||||
policyType: PolicyType,
|
||||
): Result<List<PolicyView>> = runCatchingWithLogs {
|
||||
globalClient.policies().filterByType(
|
||||
policies = policies,
|
||||
organizationUserPolicyContexts = organizations,
|
||||
policyType = policyType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import timber.log.Timber
|
||||
abstract class BaseSdkSource(
|
||||
protected val sdkClientManager: SdkClientManager,
|
||||
) {
|
||||
/**
|
||||
* Helper function to retrieve the global [Client] synchronously.
|
||||
*/
|
||||
protected val globalClient get() = sdkClientManager.globalClient
|
||||
|
||||
/**
|
||||
* Helper function to retrieve the [Client] associated with the given [userId].
|
||||
*/
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.bitwarden.network.model.OrganizationStatusType
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.organizations.OrganizationUserStatusType
|
||||
import com.bitwarden.organizations.OrganizationUserType
|
||||
import com.bitwarden.policies.OrganizationUserPolicyContext
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.bitwarden.policies.PolicyView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toSdkOrganizationPolicyContext
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toSdkPolicyViews
|
||||
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
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
/**
|
||||
* The default [PolicyManager] implementation. This class is responsible for
|
||||
@@ -22,6 +25,8 @@ import kotlinx.coroutines.flow.mapNotNull
|
||||
*/
|
||||
class PolicyManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : PolicyManager {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getActivePoliciesFlow(type: PolicyType): Flow<List<PolicyView>> =
|
||||
@@ -29,18 +34,7 @@ class PolicyManagerImpl(
|
||||
.activeUserIdChangesFlow
|
||||
.flatMapLatest { activeUserId ->
|
||||
activeUserId
|
||||
?.let { userId ->
|
||||
authDiskSource
|
||||
.getPoliciesFlow(userId = userId)
|
||||
.map { it?.toSdkPolicyViews() }
|
||||
.mapNotNull {
|
||||
filterPolicies(
|
||||
userId = userId,
|
||||
type = type,
|
||||
policies = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
?.let { userId -> getAppliedPolicyViewsFlow(userId = userId, type = type) }
|
||||
?: emptyFlow()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
@@ -49,13 +43,7 @@ class PolicyManagerImpl(
|
||||
authDiskSource
|
||||
.userState
|
||||
?.activeUserId
|
||||
?.let { userId ->
|
||||
filterPolicies(
|
||||
userId = userId,
|
||||
type = type,
|
||||
policies = getPolicyViews(userId = userId),
|
||||
)
|
||||
}
|
||||
?.let { userId -> getUserPolicies(userId = userId, type = type) }
|
||||
.orEmpty()
|
||||
|
||||
override fun getUserPolicies(
|
||||
@@ -64,9 +52,20 @@ class PolicyManagerImpl(
|
||||
): List<PolicyView> =
|
||||
this
|
||||
.filterPolicies(
|
||||
userId = userId,
|
||||
type = type,
|
||||
policies = getPolicyViews(userId = userId),
|
||||
policies = authDiskSource
|
||||
.getPolicies(userId = userId)
|
||||
?.toSdkPolicyViews(),
|
||||
organizations = authDiskSource
|
||||
.getOrganizations(userId = userId)
|
||||
?.map {
|
||||
OrganizationPolicyData(
|
||||
organizationUserPolicyContext = it.toSdkOrganizationPolicyContext(),
|
||||
organizationShouldUsePolicies = it.permissions.shouldManagePolicies,
|
||||
)
|
||||
},
|
||||
isPoliciesInAcceptedStateEnabled = featureFlagManager
|
||||
.getFeatureFlag(key = FlagKey.PoliciesInAcceptedState),
|
||||
)
|
||||
.orEmpty()
|
||||
|
||||
@@ -77,66 +76,100 @@ class PolicyManagerImpl(
|
||||
.firstOrNull()
|
||||
?.organizationId
|
||||
|
||||
/**
|
||||
* A helper method to filter policies.
|
||||
*/
|
||||
private fun filterPolicies(
|
||||
private fun getAppliedPolicyViewsFlow(
|
||||
userId: String,
|
||||
type: PolicyType,
|
||||
policies: List<PolicyView>?,
|
||||
): List<PolicyView>? {
|
||||
policies ?: return null
|
||||
if (policies.isEmpty()) return emptyList()
|
||||
|
||||
// Get a list of the user's organizations that enforce policies.
|
||||
val organizationIdsWithActivePolicies = authDiskSource
|
||||
.getOrganizations(userId)
|
||||
?.filter {
|
||||
it.shouldUsePolicies &&
|
||||
it.status >= OrganizationStatusType.ACCEPTED &&
|
||||
!isOrganizationExemptFromPolicies(organization = it, policyType = type)
|
||||
}
|
||||
?.map { it.id }
|
||||
): Flow<List<PolicyView>> = combine(
|
||||
authDiskSource
|
||||
.getPoliciesFlow(userId = userId)
|
||||
.map { it?.toSdkPolicyViews() },
|
||||
authDiskSource
|
||||
.getOrganizationsFlow(userId = userId)
|
||||
.map { organizations ->
|
||||
organizations?.map {
|
||||
OrganizationPolicyData(
|
||||
organizationUserPolicyContext = it.toSdkOrganizationPolicyContext(),
|
||||
organizationShouldUsePolicies = it.permissions.shouldManagePolicies,
|
||||
)
|
||||
}
|
||||
},
|
||||
featureFlagManager.getFeatureFlagFlow(key = FlagKey.PoliciesInAcceptedState),
|
||||
) { policies, organizations, isEnabled ->
|
||||
this
|
||||
.filterPolicies(
|
||||
type = type,
|
||||
policies = policies,
|
||||
organizations = organizations,
|
||||
isPoliciesInAcceptedStateEnabled = isEnabled,
|
||||
)
|
||||
.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.enabled &&
|
||||
organizationIdsWithActivePolicies.contains(it.organizationId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterPolicies(
|
||||
type: PolicyType,
|
||||
policies: List<PolicyView>?,
|
||||
organizations: List<OrganizationPolicyData>?,
|
||||
isPoliciesInAcceptedStateEnabled: Boolean,
|
||||
): List<PolicyView>? =
|
||||
when {
|
||||
policies == null -> null
|
||||
policies.isEmpty() -> emptyList()
|
||||
isPoliciesInAcceptedStateEnabled -> {
|
||||
authSdkSource
|
||||
.filterPolicies(
|
||||
policies = policies,
|
||||
policyType = type,
|
||||
organizations = organizations
|
||||
?.map { it.organizationUserPolicyContext }
|
||||
.orEmpty(),
|
||||
)
|
||||
.getOrElse { emptyList() }
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Legacy flow
|
||||
val organizationIdsWithActivePolicies = organizations
|
||||
?.filter {
|
||||
@Suppress("MaxLineLength")
|
||||
it.organizationUserPolicyContext.usePolicies &&
|
||||
it.organizationUserPolicyContext.status >= OrganizationUserStatusType.ACCEPTED &&
|
||||
!it.isOrganizationExemptFromPolicies(policyType = type)
|
||||
}
|
||||
?.map { it.organizationUserPolicyContext.id }
|
||||
.orEmpty()
|
||||
return policies.filter {
|
||||
it.type == type &&
|
||||
it.enabled &&
|
||||
organizationIdsWithActivePolicies.contains(it.organizationId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to determine if the organization is exempt from policies.
|
||||
*/
|
||||
private fun isOrganizationExemptFromPolicies(
|
||||
organization: SyncResponseJson.Profile.Organization,
|
||||
private fun OrganizationPolicyData.isOrganizationExemptFromPolicies(
|
||||
policyType: PolicyType,
|
||||
): Boolean =
|
||||
when (policyType) {
|
||||
PolicyType.MAXIMUM_VAULT_TIMEOUT -> {
|
||||
organization.type == OrganizationType.OWNER
|
||||
this.organizationUserPolicyContext.role == OrganizationUserType.OWNER
|
||||
}
|
||||
|
||||
PolicyType.PASSWORD_GENERATOR,
|
||||
PolicyType.REMOVE_UNLOCK_WITH_PIN,
|
||||
PolicyType.RESTRICTED_ITEM_TYPES,
|
||||
-> {
|
||||
false
|
||||
}
|
||||
-> false
|
||||
|
||||
else -> {
|
||||
(organization.type == OrganizationType.OWNER ||
|
||||
organization.type == OrganizationType.ADMIN) ||
|
||||
organization.permissions.shouldManagePolicies
|
||||
this.organizationUserPolicyContext.role == OrganizationUserType.OWNER ||
|
||||
this.organizationUserPolicyContext.role == OrganizationUserType.ADMIN ||
|
||||
this.organizationShouldUsePolicies
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPolicyViews(
|
||||
userId: String,
|
||||
): List<PolicyView>? = authDiskSource
|
||||
.getPolicies(userId = userId)
|
||||
?.toSdkPolicyViews()
|
||||
}
|
||||
|
||||
private data class OrganizationPolicyData(
|
||||
val organizationUserPolicyContext: OrganizationUserPolicyContext,
|
||||
val organizationShouldUsePolicies: Boolean,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,13 @@ import com.bitwarden.sdk.Client
|
||||
*/
|
||||
interface SdkClientManager {
|
||||
|
||||
/**
|
||||
* Synchronously returns a [Client] that is unassociated with any user. It cannot be used for
|
||||
* anything that performs a network requests. If the client is not yet ready, this will block
|
||||
* until it is ready.
|
||||
*/
|
||||
val globalClient: Client
|
||||
|
||||
/**
|
||||
* Returns the cached [Client] instance for the given [userId], otherwise creates and caches
|
||||
* a new one and returns it.
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.os.Build
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.util.concurrentMapOf
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.data.manager.NativeLibraryManager
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkPlatformApiFactory
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Primary implementation of [SdkClientManager].
|
||||
*/
|
||||
class SdkClientManagerImpl(
|
||||
nativeLibraryManager: NativeLibraryManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
sdkRepoFactory: SdkRepositoryFactory,
|
||||
sdkPlatformApiFactory: SdkPlatformApiFactory,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
@@ -38,7 +45,9 @@ class SdkClientManagerImpl(
|
||||
}
|
||||
},
|
||||
) : SdkClientManager {
|
||||
private val userIdToClientMap = mutableMapOf<String, Client>()
|
||||
private val userIdToClientMap = concurrentMapOf<String, Client>()
|
||||
private val ioScope = CoroutineScope(context = dispatcherManager.io)
|
||||
private val globalClientDeferred: Deferred<Client>
|
||||
|
||||
init {
|
||||
// The SDK requires access to Android APIs that were not made public until API 31. In order
|
||||
@@ -47,8 +56,13 @@ class SdkClientManagerImpl(
|
||||
if (!isBuildVersionAtLeast(Build.VERSION_CODES.S)) {
|
||||
nativeLibraryManager.loadLibrary("bitwarden_uniffi")
|
||||
}
|
||||
// Initialize this now, so that we can access it synchronously later on.
|
||||
globalClientDeferred = ioScope.async { clientProvider(null, null) }
|
||||
}
|
||||
|
||||
override val globalClient: Client
|
||||
get() = runBlocking { globalClientDeferred.await() }
|
||||
|
||||
override suspend fun getOrCreateClient(
|
||||
userId: String,
|
||||
): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider(userId, null) }
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.bitwarden.network.model.BitwardenServiceClientConfig
|
||||
import com.bitwarden.network.service.EventService
|
||||
import com.bitwarden.network.service.PushService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
@@ -220,11 +221,13 @@ object PlatformManagerModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkClientManager(
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
nativeLibraryManager: NativeLibraryManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
sdkPlatformApiFactory: SdkPlatformApiFactory,
|
||||
): SdkClientManager = SdkClientManagerImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
nativeLibraryManager = nativeLibraryManager,
|
||||
sdkRepoFactory = sdkRepositoryFactory,
|
||||
@@ -262,8 +265,12 @@ object PlatformManagerModule {
|
||||
@Singleton
|
||||
fun providePolicyManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
authSdkSource: AuthSdkSource,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): PolicyManager = PolicyManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
authSdkSource = authSdkSource,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -21,6 +21,7 @@ class ScopedVaultSdkSourceImpl(
|
||||
sdkPlatformApiFactory: SdkPlatformApiFactory,
|
||||
vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
|
||||
sdkClientManager = SdkClientManagerImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
// We do not want to have the real NativeLibraryManager used here to avoid
|
||||
// initializing the library twice.
|
||||
nativeLibraryManager = object : NativeLibraryManager {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.util
|
||||
|
||||
import com.bitwarden.network.model.OrganizationStatusType
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.organizations.OrganizationUserStatusType
|
||||
import com.bitwarden.organizations.OrganizationUserType
|
||||
import com.bitwarden.policies.OrganizationUserPolicyContext
|
||||
|
||||
/**
|
||||
* Converts a list of network [SyncResponseJson.Profile.Organization] models to a list of SDK
|
||||
* [OrganizationUserPolicyContext].
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
fun List<SyncResponseJson.Profile.Organization>.toSdkOrganizationPolicyContexts(): List<OrganizationUserPolicyContext> =
|
||||
this.map { it.toSdkOrganizationPolicyContext() }
|
||||
|
||||
/**
|
||||
* Converts a network [SyncResponseJson.Profile.Organization] model to an SDK
|
||||
* [OrganizationUserPolicyContext].
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
fun SyncResponseJson.Profile.Organization.toSdkOrganizationPolicyContext(): OrganizationUserPolicyContext =
|
||||
OrganizationUserPolicyContext(
|
||||
id = this.id,
|
||||
status = this.status.toSdkOrganizationUserStatusType,
|
||||
role = this.type.toSdkOrganizationUserType,
|
||||
enabled = this.isEnabled,
|
||||
usePolicies = this.shouldUsePolicies,
|
||||
isProviderUser = this.isProviderUser,
|
||||
)
|
||||
|
||||
private val OrganizationStatusType.toSdkOrganizationUserStatusType: OrganizationUserStatusType
|
||||
get() = when (this) {
|
||||
OrganizationStatusType.REVOKED -> OrganizationUserStatusType.REVOKED
|
||||
OrganizationStatusType.INVITED -> OrganizationUserStatusType.INVITED
|
||||
OrganizationStatusType.ACCEPTED -> OrganizationUserStatusType.ACCEPTED
|
||||
OrganizationStatusType.CONFIRMED -> OrganizationUserStatusType.CONFIRMED
|
||||
}
|
||||
|
||||
private val OrganizationType.toSdkOrganizationUserType: OrganizationUserType
|
||||
get() = when (this) {
|
||||
OrganizationType.OWNER -> OrganizationUserType.OWNER
|
||||
OrganizationType.ADMIN -> OrganizationUserType.ADMIN
|
||||
OrganizationType.USER -> OrganizationUserType.USER
|
||||
OrganizationType.MANAGER -> OrganizationUserType.ADMIN
|
||||
OrganizationType.CUSTOM -> OrganizationUserType.CUSTOM
|
||||
}
|
||||
@@ -16,9 +16,13 @@ import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.policies.OrganizationUserPolicyContext
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.bitwarden.policies.PolicyView
|
||||
import com.bitwarden.sdk.AuthClient
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.bitwarden.sdk.PlatformClient
|
||||
import com.bitwarden.sdk.PoliciesClient
|
||||
import com.bitwarden.sdk.RegistrationClient
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
@@ -41,11 +45,14 @@ class AuthSdkSourceTest {
|
||||
private val clientPlatform = mockk<PlatformClient> {
|
||||
coEvery { loadFlags(any()) } just runs
|
||||
}
|
||||
private val clientPolicies = mockk<PoliciesClient>()
|
||||
private val client = mockk<Client> {
|
||||
every { auth() } returns clientAuth
|
||||
every { platform() } returns clientPlatform
|
||||
every { policies() } returns clientPolicies
|
||||
}
|
||||
private val sdkClientManager = mockk<SdkClientManager> {
|
||||
every { globalClient } returns client
|
||||
coEvery { getOrCreateClient(userId = any()) } returns client
|
||||
}
|
||||
|
||||
@@ -520,4 +527,35 @@ class AuthSdkSourceTest {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterPolicies should call SDK and return a Result with the correct data`() =
|
||||
runBlocking {
|
||||
val policies = listOf(mockk<PolicyView>())
|
||||
val organizations = listOf(mockk<OrganizationUserPolicyContext>())
|
||||
val policyType = mockk<PolicyType>()
|
||||
val expectedResult = listOf(mockk<PolicyView>())
|
||||
coEvery {
|
||||
clientPolicies.filterByType(
|
||||
policies = policies,
|
||||
organizationUserPolicyContexts = organizations,
|
||||
policyType = policyType,
|
||||
)
|
||||
} returns expectedResult
|
||||
|
||||
val result = authSkdSource.filterPolicies(
|
||||
policies = policies,
|
||||
organizations = organizations,
|
||||
policyType = policyType,
|
||||
)
|
||||
|
||||
assertEquals(expectedResult.asSuccess(), result)
|
||||
coVerify(exactly = 1) {
|
||||
clientPolicies.filterByType(
|
||||
policies = policies,
|
||||
organizationUserPolicyContexts = organizations,
|
||||
policyType = policyType,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.network.model.OrganizationStatusType
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.PolicyTypeJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.createMockOrganizationNetwork
|
||||
import com.bitwarden.network.model.createMockPolicy
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.bitwarden.policies.PolicyView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPolicyView
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
@@ -17,72 +22,85 @@ import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
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.Instant
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class PolicyManagerTest {
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserStateJson?>(null)
|
||||
private val mutablePolicyFlow = MutableStateFlow<List<SyncResponseJson.Policy>?>(null)
|
||||
private val mutableOrganizationsFlow =
|
||||
MutableStateFlow<List<SyncResponseJson.Profile.Organization>?>(null)
|
||||
private val authDiskSource: AuthDiskSource = mockk {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { getPoliciesFlow(USER_ID) } returns mutablePolicyFlow
|
||||
every { getOrganizationsFlow(USER_ID) } returns mutableOrganizationsFlow
|
||||
}
|
||||
private val authSdkSource: AuthSdkSource = mockk()
|
||||
private val mutablePoliciesInAcceptedStateFlagFlow = MutableStateFlow(true)
|
||||
private val featureFlagManager: FeatureFlagManager = mockk {
|
||||
every {
|
||||
getFeatureFlagFlow(key = FlagKey.PoliciesInAcceptedState)
|
||||
} returns mutablePoliciesInAcceptedStateFlagFlow
|
||||
every { getFeatureFlag(key = FlagKey.PoliciesInAcceptedState) } answers {
|
||||
mutablePoliciesInAcceptedStateFlagFlow.value
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var policyManager: PolicyManager
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
policyManager = PolicyManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
)
|
||||
}
|
||||
private val policyManager: PolicyManager = PolicyManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
authSdkSource = authSdkSource,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `currentUserPoliciesListFlow should emit changes to current user's policy data`() =
|
||||
fun `getActivePoliciesFlow should emit changes to current user's policy data`() =
|
||||
runTest {
|
||||
val userStateJson = mockk<UserStateJson> {
|
||||
every { activeUserId } returns USER_ID
|
||||
}
|
||||
val organizationsOne = createMockOrganizationNetwork(
|
||||
val organizations = createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
isEnabled = true,
|
||||
shouldUsePolicies = true,
|
||||
)
|
||||
val organizationsTwo = createMockOrganizationNetwork(
|
||||
number = 2,
|
||||
isEnabled = true,
|
||||
shouldUsePolicies = true,
|
||||
)
|
||||
val storedPolicyOne = createMockPolicy(
|
||||
isEnabled = true,
|
||||
number = 1,
|
||||
organizationId = organizationsOne.id,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
val storedPolicyTwo = createMockPolicy(
|
||||
isEnabled = true,
|
||||
number = 2,
|
||||
organizationId = organizationsTwo.id,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
val expectedPolicyOne = createMockPolicyView(
|
||||
enabled = true,
|
||||
number = 1,
|
||||
organizationId = organizationsOne.id,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyType.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
val expectedPolicyTwo = createMockPolicyView(
|
||||
enabled = true,
|
||||
number = 2,
|
||||
organizationId = organizationsTwo.id,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyType.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
every {
|
||||
authDiskSource.getOrganizations(USER_ID)
|
||||
} returns listOf(organizationsOne) andThen listOf(organizationsTwo)
|
||||
authSdkSource.filterPolicies(
|
||||
policies = any(),
|
||||
organizations = any(),
|
||||
policyType = any(),
|
||||
)
|
||||
} returnsMany listOf(
|
||||
listOf(expectedPolicyOne).asSuccess(),
|
||||
listOf(expectedPolicyTwo).asSuccess(),
|
||||
)
|
||||
|
||||
mutableUserStateFlow.value = userStateJson
|
||||
mutableOrganizationsFlow.value = listOf(organizations)
|
||||
mutablePolicyFlow.value = listOf(storedPolicyOne)
|
||||
|
||||
policyManager
|
||||
@@ -96,11 +114,127 @@ class PolicyManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getActivePoliciesFlow when feature flag is false should emit policies using legacy filtering`() =
|
||||
runTest {
|
||||
mutablePoliciesInAcceptedStateFlagFlow.value = false
|
||||
|
||||
val userStateJson = mockk<UserStateJson> {
|
||||
every { activeUserId } returns USER_ID
|
||||
}
|
||||
val organizations = createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
isEnabled = true,
|
||||
shouldUsePolicies = true,
|
||||
)
|
||||
val storedPolicyOne = createMockPolicy(
|
||||
isEnabled = true,
|
||||
number = 1,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
val expectedPolicyOne = createMockPolicyView(
|
||||
enabled = true,
|
||||
number = 1,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyType.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
val storedPolicyTwo = createMockPolicy(
|
||||
isEnabled = true,
|
||||
number = 2,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
val expectedPolicyTwo = createMockPolicyView(
|
||||
enabled = true,
|
||||
number = 2,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyType.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
mutableUserStateFlow.value = userStateJson
|
||||
mutableOrganizationsFlow.value = listOf(organizations)
|
||||
mutablePolicyFlow.value = listOf(storedPolicyOne)
|
||||
|
||||
policyManager
|
||||
.getActivePoliciesFlow(type = PolicyType.MAXIMUM_VAULT_TIMEOUT)
|
||||
.test {
|
||||
assertEquals(listOf(expectedPolicyOne), awaitItem())
|
||||
|
||||
mutablePolicyFlow.value = listOf(storedPolicyOne, storedPolicyTwo)
|
||||
assertEquals(listOf(expectedPolicyOne, expectedPolicyTwo), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getActivePoliciesFlow when feature flag is false should emit empty list when organization status is below ACCEPTED`() =
|
||||
runTest {
|
||||
mutablePoliciesInAcceptedStateFlagFlow.value = false
|
||||
|
||||
val userStateJson = mockk<UserStateJson> {
|
||||
every { activeUserId } returns USER_ID
|
||||
}
|
||||
val organizations = createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
isEnabled = true,
|
||||
shouldUsePolicies = true,
|
||||
status = OrganizationStatusType.INVITED,
|
||||
)
|
||||
val storedPolicy = createMockPolicy(
|
||||
isEnabled = true,
|
||||
number = 1,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
mutableUserStateFlow.value = userStateJson
|
||||
mutableOrganizationsFlow.value = listOf(organizations)
|
||||
mutablePolicyFlow.value = listOf(storedPolicy)
|
||||
|
||||
policyManager
|
||||
.getActivePoliciesFlow(type = PolicyType.MAXIMUM_VAULT_TIMEOUT)
|
||||
.test {
|
||||
assertEquals(emptyList<PolicyView>(), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getActivePoliciesFlow when feature flag is false should emit empty list when organization does not use policies`() =
|
||||
runTest {
|
||||
mutablePoliciesInAcceptedStateFlagFlow.value = false
|
||||
|
||||
val userStateJson = mockk<UserStateJson> {
|
||||
every { activeUserId } returns USER_ID
|
||||
}
|
||||
val organizations = createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
isEnabled = true,
|
||||
shouldUsePolicies = false,
|
||||
)
|
||||
val storedPolicy = createMockPolicy(
|
||||
isEnabled = true,
|
||||
number = 1,
|
||||
organizationId = organizations.id,
|
||||
type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
mutableUserStateFlow.value = userStateJson
|
||||
mutableOrganizationsFlow.value = listOf(organizations)
|
||||
mutablePolicyFlow.value = listOf(storedPolicy)
|
||||
|
||||
policyManager
|
||||
.getActivePoliciesFlow(type = PolicyType.MAXIMUM_VAULT_TIMEOUT)
|
||||
.test {
|
||||
assertEquals(emptyList<PolicyView>(), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getActivePolicies returns empty list if user id is null`() {
|
||||
every {
|
||||
authDiskSource.userState
|
||||
} returns null
|
||||
every { authDiskSource.userState } returns null
|
||||
|
||||
assertTrue(policyManager.getActivePolicies(type = PolicyType.MASTER_PASSWORD).isEmpty())
|
||||
}
|
||||
@@ -127,6 +261,13 @@ class PolicyManagerTest {
|
||||
isEnabled = true,
|
||||
),
|
||||
)
|
||||
every {
|
||||
authSdkSource.filterPolicies(
|
||||
policies = any(),
|
||||
organizations = any(),
|
||||
policyType = any(),
|
||||
)
|
||||
} returns emptyList<PolicyView>().asSuccess()
|
||||
|
||||
assertTrue(policyManager.getActivePolicies(type = PolicyType.MASTER_PASSWORD).isEmpty())
|
||||
}
|
||||
@@ -153,6 +294,13 @@ class PolicyManagerTest {
|
||||
isEnabled = true,
|
||||
),
|
||||
)
|
||||
every {
|
||||
authSdkSource.filterPolicies(
|
||||
policies = any(),
|
||||
organizations = any(),
|
||||
policyType = any(),
|
||||
)
|
||||
} returns emptyList<PolicyView>().asSuccess()
|
||||
|
||||
assertTrue(policyManager.getActivePolicies(type = PolicyType.MASTER_PASSWORD).isEmpty())
|
||||
}
|
||||
@@ -176,6 +324,9 @@ class PolicyManagerTest {
|
||||
),
|
||||
)
|
||||
every { authDiskSource.getPolicies(USER_ID) } returns listOf(storedPolicy)
|
||||
every {
|
||||
authSdkSource.filterPolicies(any(), any(), any())
|
||||
} returns listOf(expectedPolicy).asSuccess()
|
||||
|
||||
assertEquals(
|
||||
listOf(expectedPolicy),
|
||||
@@ -207,6 +358,16 @@ class PolicyManagerTest {
|
||||
type = PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
),
|
||||
)
|
||||
every {
|
||||
authSdkSource.filterPolicies(any(), any(), any())
|
||||
} returns listOf(
|
||||
createMockPolicyView(
|
||||
organizationId = "mockId-3",
|
||||
enabled = true,
|
||||
type = PolicyType.PASSWORD_GENERATOR,
|
||||
),
|
||||
)
|
||||
.asSuccess()
|
||||
|
||||
assertTrue(policyManager.getActivePolicies(type = PolicyType.PASSWORD_GENERATOR).any())
|
||||
}
|
||||
@@ -235,6 +396,16 @@ class PolicyManagerTest {
|
||||
type = PolicyTypeJson.RESTRICT_ITEM_TYPES,
|
||||
),
|
||||
)
|
||||
every {
|
||||
authSdkSource.filterPolicies(any(), any(), any())
|
||||
} returns listOf(
|
||||
createMockPolicyView(
|
||||
organizationId = "mockId-3",
|
||||
enabled = true,
|
||||
type = PolicyType.RESTRICTED_ITEM_TYPES,
|
||||
),
|
||||
)
|
||||
.asSuccess()
|
||||
|
||||
assertTrue(
|
||||
policyManager.getActivePolicies(type = PolicyType.RESTRICTED_ITEM_TYPES).any(),
|
||||
@@ -243,13 +414,9 @@ class PolicyManagerTest {
|
||||
|
||||
@Test
|
||||
fun `getUserPolicies returns empty list if policies is null`() {
|
||||
every {
|
||||
authDiskSource.userState
|
||||
} returns null
|
||||
|
||||
every {
|
||||
authDiskSource.getPolicies(USER_ID)
|
||||
} returns null
|
||||
every { authDiskSource.userState } returns null
|
||||
every { authDiskSource.getPolicies(USER_ID) } returns null
|
||||
every { authDiskSource.getOrganizations(USER_ID) } returns null
|
||||
|
||||
assertEquals(
|
||||
emptyList<SyncResponseJson.Policy>(),
|
||||
@@ -294,6 +461,9 @@ class PolicyManagerTest {
|
||||
every {
|
||||
authDiskSource.getPolicies(USER_ID)
|
||||
} returns storedListOfPolicies
|
||||
every {
|
||||
authSdkSource.filterPolicies(any(), any(), any())
|
||||
} returns expectedListOfPolicies.asSuccess()
|
||||
|
||||
assertEquals(
|
||||
expectedListOfPolicies,
|
||||
@@ -348,6 +518,13 @@ class PolicyManagerTest {
|
||||
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
|
||||
),
|
||||
)
|
||||
every {
|
||||
authSdkSource.filterPolicies(
|
||||
policies = any(),
|
||||
organizations = any(),
|
||||
policyType = any(),
|
||||
)
|
||||
} returns emptyList<PolicyView>().asSuccess()
|
||||
|
||||
assertNull(policyManager.getPersonalOwnershipPolicyOrganizationId())
|
||||
}
|
||||
@@ -378,6 +555,15 @@ class PolicyManagerTest {
|
||||
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
|
||||
),
|
||||
)
|
||||
every {
|
||||
authSdkSource.filterPolicies(any(), any(), any())
|
||||
} returns listOf(
|
||||
createMockPolicyView(
|
||||
organizationId = expectedOrganizationId,
|
||||
enabled = true,
|
||||
),
|
||||
)
|
||||
.asSuccess()
|
||||
|
||||
assertEquals(
|
||||
expectedOrganizationId,
|
||||
@@ -445,6 +631,29 @@ class PolicyManagerTest {
|
||||
revisionDate = middleRevisionDate,
|
||||
),
|
||||
)
|
||||
every {
|
||||
authSdkSource.filterPolicies(any(), any(), any())
|
||||
} returns listOf(
|
||||
createMockPolicyView(
|
||||
number = 3,
|
||||
organizationId = "mockId-3",
|
||||
enabled = true,
|
||||
revisionDate = latestRevisionDate,
|
||||
),
|
||||
createMockPolicyView(
|
||||
number = 1,
|
||||
organizationId = expectedOrganizationId,
|
||||
enabled = true,
|
||||
revisionDate = earliestRevisionDate,
|
||||
),
|
||||
createMockPolicyView(
|
||||
number = 2,
|
||||
organizationId = "mockId-2",
|
||||
enabled = true,
|
||||
revisionDate = middleRevisionDate,
|
||||
),
|
||||
)
|
||||
.asSuccess()
|
||||
|
||||
assertEquals(
|
||||
expectedOrganizationId,
|
||||
@@ -497,6 +706,17 @@ class PolicyManagerTest {
|
||||
revisionDate = laterRevisionDate,
|
||||
),
|
||||
)
|
||||
every {
|
||||
authSdkSource.filterPolicies(any(), any(), any())
|
||||
} returns listOf(
|
||||
createMockPolicyView(
|
||||
number = 2,
|
||||
organizationId = expectedOrganizationId,
|
||||
enabled = true,
|
||||
revisionDate = laterRevisionDate,
|
||||
),
|
||||
)
|
||||
.asSuccess()
|
||||
|
||||
// Should return mockId-2 because mockId-1's organization doesn't enforce policies
|
||||
assertEquals(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.data.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkPlatformApiFactory
|
||||
@@ -99,6 +100,7 @@ class SdkClientManagerTest {
|
||||
}
|
||||
|
||||
private fun createSdkClientManager(): SdkClientManagerImpl = SdkClientManagerImpl(
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
clientProvider = { _, _ -> mockk(relaxed = true) },
|
||||
nativeLibraryManager = mockNativeLibraryManager,
|
||||
featureFlagManager = mockk(),
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||
|
||||
import com.bitwarden.organizations.OrganizationUserStatusType
|
||||
import com.bitwarden.organizations.OrganizationUserType
|
||||
import com.bitwarden.policies.OrganizationUserPolicyContext
|
||||
|
||||
/**
|
||||
* Create a mock [OrganizationUserPolicyContext] with a given [number].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun createMockOrganizationUserPolicyContext(
|
||||
number: Int = 1,
|
||||
id: String = "mockId-$number",
|
||||
status: OrganizationUserStatusType = OrganizationUserStatusType.ACCEPTED,
|
||||
role: OrganizationUserType = OrganizationUserType.ADMIN,
|
||||
enabled: Boolean = false,
|
||||
usePolicies: Boolean = false,
|
||||
isProviderUser: Boolean = false,
|
||||
): OrganizationUserPolicyContext =
|
||||
OrganizationUserPolicyContext(
|
||||
id = id,
|
||||
status = status,
|
||||
role = role,
|
||||
enabled = enabled,
|
||||
usePolicies = usePolicies,
|
||||
isProviderUser = isProviderUser,
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.util
|
||||
|
||||
import com.bitwarden.network.model.OrganizationStatusType
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.createMockOrganizationNetwork
|
||||
import com.bitwarden.organizations.OrganizationUserStatusType
|
||||
import com.bitwarden.organizations.OrganizationUserType
|
||||
import com.bitwarden.policies.OrganizationUserPolicyContext
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockOrganizationUserPolicyContext
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
||||
class VaultSdkOrganizationExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `toSdkOrganizationPolicyContexts should return empty list when given empty list`() {
|
||||
assertEquals(
|
||||
emptyList<OrganizationUserPolicyContext>(),
|
||||
emptyList<SyncResponseJson.Profile.Organization>().toSdkOrganizationPolicyContexts(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSdkOrganizationPolicyContexts should convert all organizations in a list`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
createMockOrganizationUserPolicyContext(number = 1),
|
||||
createMockOrganizationUserPolicyContext(number = 2),
|
||||
),
|
||||
listOf(
|
||||
createMockOrganizationNetwork(number = 1),
|
||||
createMockOrganizationNetwork(number = 2),
|
||||
)
|
||||
.toSdkOrganizationPolicyContexts(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSdkOrganizationPolicyContexts should map all OrganizationStatusType values`() {
|
||||
STATUS_TYPE_MAP.forEach { (inputStatus, expectedStatus) ->
|
||||
assertEquals(
|
||||
listOf(createMockOrganizationUserPolicyContext(status = expectedStatus)),
|
||||
listOf(createMockOrganizationNetwork(number = 1, status = inputStatus))
|
||||
.toSdkOrganizationPolicyContexts(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSdkOrganizationPolicyContexts should map all OrganizationType values`() {
|
||||
ORGANIZATION_TYPE_MAP.forEach { (inputType, expectedRole) ->
|
||||
assertEquals(
|
||||
listOf(createMockOrganizationUserPolicyContext(role = expectedRole)),
|
||||
listOf(createMockOrganizationNetwork(number = 1, type = inputType))
|
||||
.toSdkOrganizationPolicyContexts(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val STATUS_TYPE_MAP: Map<OrganizationStatusType, OrganizationUserStatusType> = mapOf(
|
||||
OrganizationStatusType.REVOKED to OrganizationUserStatusType.REVOKED,
|
||||
OrganizationStatusType.INVITED to OrganizationUserStatusType.INVITED,
|
||||
OrganizationStatusType.ACCEPTED to OrganizationUserStatusType.ACCEPTED,
|
||||
OrganizationStatusType.CONFIRMED to OrganizationUserStatusType.CONFIRMED,
|
||||
)
|
||||
|
||||
private val ORGANIZATION_TYPE_MAP: Map<OrganizationType, OrganizationUserType> = mapOf(
|
||||
OrganizationType.OWNER to OrganizationUserType.OWNER,
|
||||
OrganizationType.ADMIN to OrganizationUserType.ADMIN,
|
||||
OrganizationType.USER to OrganizationUserType.USER,
|
||||
OrganizationType.MANAGER to OrganizationUserType.ADMIN,
|
||||
OrganizationType.CUSTOM to OrganizationUserType.CUSTOM,
|
||||
)
|
||||
@@ -44,6 +44,7 @@ sealed class FlagKey<out T : Any> {
|
||||
NewItemTypes,
|
||||
DebugDisableSelfHostPremiumCheck,
|
||||
FillAssistTargetingRules,
|
||||
PoliciesInAcceptedState,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -161,6 +162,14 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Policies In Accepted State feature.
|
||||
*/
|
||||
data object PoliciesInAcceptedState : FlagKey<Boolean>() {
|
||||
override val keyName: String get() = "pm-34145-policies-in-accepted-state"
|
||||
override val defaultValue: Boolean get() = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug-only flag that, when enabled, makes self-hosted environments behave as cloud
|
||||
* environments for premium-upgrade gating. Used by QA to test the premium upgrade flow
|
||||
|
||||
@@ -56,6 +56,10 @@ class FlagKeyTest {
|
||||
FlagKey.ManageDevices.keyName,
|
||||
"pm-4516-devices-add-last-activity-date",
|
||||
)
|
||||
assertEquals(
|
||||
FlagKey.PoliciesInAcceptedState.keyName,
|
||||
"pm-34145-policies-in-accepted-state",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,6 +78,7 @@ class FlagKeyTest {
|
||||
FlagKey.NewItemTypes,
|
||||
FlagKey.FillAssistTargetingRules,
|
||||
FlagKey.ManageDevices,
|
||||
FlagKey.PoliciesInAcceptedState,
|
||||
).all {
|
||||
!it.defaultValue
|
||||
},
|
||||
|
||||
@@ -10,6 +10,12 @@ import kotlinx.serialization.Serializable
|
||||
*/
|
||||
@Serializable(OrganizationStatusTypeSerializer::class)
|
||||
enum class OrganizationStatusType {
|
||||
/**
|
||||
* The user has been revoked from the organization.
|
||||
*/
|
||||
@SerialName("-1")
|
||||
REVOKED,
|
||||
|
||||
/**
|
||||
* The user has been invited to the organization.
|
||||
*/
|
||||
|
||||
@@ -311,6 +311,9 @@ data class SyncResponseJson(
|
||||
@SerialName("providerType")
|
||||
val providerType: Int?,
|
||||
|
||||
@SerialName("isProviderUser")
|
||||
val isProviderUser: Boolean = false,
|
||||
|
||||
@SerialName("maxCollections")
|
||||
val maxCollections: Int?,
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
|
||||
FlagKey.NewItemTypes,
|
||||
FlagKey.FillAssistTargetingRules,
|
||||
FlagKey.DebugDisableSelfHostPremiumCheck,
|
||||
FlagKey.PoliciesInAcceptedState,
|
||||
-> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
BooleanFlagItem(
|
||||
@@ -95,10 +96,8 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||
FlagKey.V2EncryptionPassword -> stringResource(BitwardenString.v2_encryption_password)
|
||||
FlagKey.V2EncryptionTde -> stringResource(BitwardenString.v2_encryption_tde)
|
||||
FlagKey.NewItemTypes -> stringResource(BitwardenString.new_item_types)
|
||||
FlagKey.FillAssistTargetingRules -> {
|
||||
stringResource(BitwardenString.fill_assist_targeting_rules)
|
||||
}
|
||||
|
||||
FlagKey.FillAssistTargetingRules -> stringResource(BitwardenString.fill_assist_targeting_rules)
|
||||
FlagKey.PoliciesInAcceptedState -> stringResource(BitwardenString.policies_in_accepted_state)
|
||||
FlagKey.DebugDisableSelfHostPremiumCheck -> {
|
||||
stringResource(BitwardenString.debug_disable_self_host_premium_check)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<string name="new_item_types">New Item Types</string>
|
||||
<string name="fill_assist_targeting_rules">Fill Assist Targeting Rules</string>
|
||||
<string name="debug_disable_self_host_premium_check">Debug: Disable self-host premium check</string>
|
||||
<string name="policies_in_accepted_state">Policies in accepted state</string>
|
||||
|
||||
<!-- endregion Debug Menu -->
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user