diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt index 7fcc3a20e0..0877b05ac3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt @@ -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 + + /** + * Applies the appropriate filters for determining what policies apply to the user. + */ + fun filterPolicies( + policies: List, + organizations: List, + policyType: PolicyType, + ): Result> } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt index 5dbfaf8043..e5ea91f426 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt @@ -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, + organizations: List, + policyType: PolicyType, + ): Result> = runCatchingWithLogs { + globalClient.policies().filterByType( + policies = policies, + organizationUserPolicyContexts = organizations, + policyType = policyType, + ) + } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/sdk/BaseSdkSource.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/sdk/BaseSdkSource.kt index cf8b3064ea..33e7eeb5da 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/sdk/BaseSdkSource.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/sdk/BaseSdkSource.kt @@ -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]. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/PolicyManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/PolicyManagerImpl.kt index 276700c2e0..51febc4595 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/PolicyManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/PolicyManagerImpl.kt @@ -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> = @@ -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 = 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?, - ): List? { - 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> = 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?, + organizations: List?, + isPoliciesInAcceptedStateEnabled: Boolean, + ): List? = + 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? = authDiskSource - .getPolicies(userId = userId) - ?.toSdkPolicyViews() } + +private data class OrganizationPolicyData( + val organizationUserPolicyContext: OrganizationUserPolicyContext, + val organizationShouldUsePolicies: Boolean, +) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManager.kt index e2798cd92d..1ecf13830d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManager.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManager.kt @@ -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. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt index c92829f2c8..5f176f4023 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt @@ -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() + private val userIdToClientMap = concurrentMapOf() + private val ioScope = CoroutineScope(context = dispatcherManager.io) + private val globalClientDeferred: Deferred 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) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index 72bc1dbe8d..d66908241a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -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 diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/ScopedVaultSdkSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/ScopedVaultSdkSourceImpl.kt index 9e29b90606..bd18dec554 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/ScopedVaultSdkSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/ScopedVaultSdkSourceImpl.kt @@ -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 { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkOrganizationExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkOrganizationExtensions.kt new file mode 100644 index 0000000000..4278510395 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkOrganizationExtensions.kt @@ -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.toSdkOrganizationPolicyContexts(): List = + 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 + } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt index 5a7379a5b3..07e976e269 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt @@ -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 { coEvery { loadFlags(any()) } just runs } + private val clientPolicies = mockk() private val client = mockk { every { auth() } returns clientAuth every { platform() } returns clientPlatform + every { policies() } returns clientPolicies } private val sdkClientManager = mockk { + 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()) + val organizations = listOf(mockk()) + val policyType = mockk() + val expectedResult = listOf(mockk()) + 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, + ) + } + } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/PolicyManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/PolicyManagerTest.kt index b984c4ef4c..1cf930caed 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/PolicyManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/PolicyManagerTest.kt @@ -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(null) private val mutablePolicyFlow = MutableStateFlow?>(null) + private val mutableOrganizationsFlow = + MutableStateFlow?>(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 { 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 { + 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 { + 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(), 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 { + 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(), 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().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().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(), @@ -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().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( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt index 031dab400e..b974b355f6 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt @@ -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(), diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/OrganizationUserPolicyContextUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/OrganizationUserPolicyContextUtil.kt new file mode 100644 index 0000000000..f5e771c3b8 --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/OrganizationUserPolicyContextUtil.kt @@ -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, + ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkOrganizationExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkOrganizationExtensionsTest.kt new file mode 100644 index 0000000000..23102adb48 --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkOrganizationExtensionsTest.kt @@ -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(), + emptyList().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 = 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 = 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, +) diff --git a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt index d438b6a96f..cb3d85c614 100644 --- a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt +++ b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt @@ -44,6 +44,7 @@ sealed class FlagKey { NewItemTypes, DebugDisableSelfHostPremiumCheck, FillAssistTargetingRules, + PoliciesInAcceptedState, ) } } @@ -161,6 +162,14 @@ sealed class FlagKey { override val defaultValue: Boolean = false } + /** + * Data object holding the feature flag key for the Policies In Accepted State feature. + */ + data object PoliciesInAcceptedState : FlagKey() { + 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 diff --git a/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt b/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt index 14003043ae..238bd7a56a 100644 --- a/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt +++ b/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt @@ -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 }, diff --git a/network/src/main/kotlin/com/bitwarden/network/model/OrganizationStatusType.kt b/network/src/main/kotlin/com/bitwarden/network/model/OrganizationStatusType.kt index 8e5b991460..e3583e12ad 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/OrganizationStatusType.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/OrganizationStatusType.kt @@ -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. */ diff --git a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt index 81f8297587..949c1afb0a 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt @@ -311,6 +311,9 @@ data class SyncResponseJson( @SerialName("providerType") val providerType: Int?, + @SerialName("isProviderUser") + val isProviderUser: Boolean = false, + @SerialName("maxCollections") val maxCollections: Int?, diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt index 1cc2bb7c8b..354550c2af 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt @@ -38,6 +38,7 @@ fun FlagKey.ListItemContent( FlagKey.NewItemTypes, FlagKey.FillAssistTargetingRules, FlagKey.DebugDisableSelfHostPremiumCheck, + FlagKey.PoliciesInAcceptedState, -> { @Suppress("UNCHECKED_CAST") BooleanFlagItem( @@ -95,10 +96,8 @@ private fun FlagKey.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) } diff --git a/ui/src/main/res/values/strings_non_localized.xml b/ui/src/main/res/values/strings_non_localized.xml index d784af3cd7..e40236a015 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -54,6 +54,7 @@ New Item Types Fill Assist Targeting Rules Debug: Disable self-host premium check + Policies in accepted state