PM-39863: feat: Remove the Accepted Policies feature flag (#7416)

This commit is contained in:
David Perez authored and GitHub committed 2026-09-22 17:01:30 +00:00
1 parent a608ec6cd3
commit bb93209ef9
6 files changed
+72 -285

No files matched your search

@@ -1,8 +1,6 @@
package com.x8bit.bitwarden.data.platform.manager
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
@@ -86,14 +84,7 @@ class PolicyManagerImpl(
?.toSdkPolicyViews(),
organizations = authDiskSource
.getOrganizations(userId = userId)
?.map {
OrganizationPolicyData(
organizationUserPolicyContext = it.toSdkOrganizationPolicyContext(),
organizationShouldUsePolicies = it.permissions.shouldManagePolicies,
)
},
isPoliciesInAcceptedStateEnabled = featureFlagManager
.getFeatureFlag(key = FlagKey.PoliciesInAcceptedState),
?.map { it.toSdkOrganizationPolicyContext() },
)
.orEmpty()
@@ -113,21 +104,12 @@ class PolicyManagerImpl(
.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 ->
.map { organizations -> organizations?.map { it.toSdkOrganizationPolicyContext() } },
) { policies, organizations ->
filterPolicies(
type = type,
policies = policies,
organizations = organizations,
isPoliciesInAcceptedStateEnabled = isEnabled,
)
}
// We do not have any policies yet if it is null, so do not emit at all.
@@ -136,66 +118,20 @@ class PolicyManagerImpl(
private fun filterPolicies(
type: PolicyType,
policies: List<PolicyView>?,
organizations: List<OrganizationPolicyData>?,
isPoliciesInAcceptedStateEnabled: Boolean,
organizations: List<OrganizationUserPolicyContext>?,
): List<PolicyView>? =
when {
policies == null -> null
policies.isEmpty() -> emptyList()
isPoliciesInAcceptedStateEnabled -> {
else -> {
authSdkSource
.filterPolicies(
policies = policies,
policyType = type,
organizations = organizations
?.map { it.organizationUserPolicyContext }
.orEmpty(),
organizations = organizations.orEmpty(),
)
.getOrElse { emptyList() }
}
else -> {
// Legacy flow
val organizationIdsWithActivePolicies = organizations
?.filter {
@Suppress("MaxLineLength")
it.organizationUserPolicyContext.usePolicies &&
(it.organizationUserPolicyContext.status == OrganizationUserStatusType.ACCEPTED ||
it.organizationUserPolicyContext.status == OrganizationUserStatusType.CONFIRMED) &&
!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 OrganizationPolicyData.isOrganizationExemptFromPolicies(
policyType: PolicyType,
): Boolean =
when (policyType) {
PolicyType.MAXIMUM_VAULT_TIMEOUT -> {
this.organizationUserPolicyContext.role == OrganizationUserType.OWNER
}
PolicyType.MASTER_PASSWORD,
PolicyType.PASSWORD_GENERATOR,
PolicyType.REMOVE_UNLOCK_WITH_PIN,
PolicyType.RESTRICTED_ITEM_TYPES,
-> false
else -> {
this.organizationUserPolicyContext.role == OrganizationUserType.OWNER ||
this.organizationUserPolicyContext.role == OrganizationUserType.ADMIN ||
this.organizationShouldUsePolicies
}
}
/**
@@ -254,8 +190,3 @@ class PolicyManagerImpl(
)
}
}
private data class OrganizationPolicyData(
val organizationUserPolicyContext: OrganizationUserPolicyContext,
val organizationShouldUsePolicies: Boolean,
)
@@ -3,7 +3,6 @@ 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.SendAccessTypeJson
@@ -39,15 +38,8 @@ class PolicyManagerTest {
every { getOrganizationsFlow(USER_ID) } returns mutableOrganizationsFlow
}
private val authSdkSource: AuthSdkSource = mockk()
private val mutablePoliciesInAcceptedStateFlagFlow = MutableStateFlow(true)
private val mutableSendControlsFlagFlow = MutableStateFlow(false)
private val featureFlagManager: FeatureFlagManager = mockk {
every {
getFeatureFlagFlow(key = FlagKey.PoliciesInAcceptedState)
} returns mutablePoliciesInAcceptedStateFlagFlow
every { getFeatureFlag(key = FlagKey.PoliciesInAcceptedState) } answers {
mutablePoliciesInAcceptedStateFlagFlow.value
}
every {
getFeatureFlagFlow(key = FlagKey.SendControls)
} returns mutableSendControlsFlagFlow
@@ -123,124 +115,6 @@ 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
@@ -995,76 +869,75 @@ class PolicyManagerTest {
)
}
@Suppress("MaxLineLength")
@Test
fun `getEffectiveSendPolicyFlow should re-emit when the underlying policies change`() = runTest {
val userStateJson = mockk<UserStateJson> {
every { activeUserId } returns USER_ID
}
val organizations = createMockOrganizationNetwork(
number = 1,
isEnabled = true,
shouldUsePolicies = true,
)
every {
authSdkSource.filterPolicies(
policies = any(),
organizations = any(),
policyType = PolicyType.SEND_CONTROLS,
)
} returns emptyList<PolicyView>().asSuccess()
every {
authSdkSource.filterPolicies(
policies = any(),
organizations = any(),
policyType = PolicyType.SEND_OPTIONS,
)
} returns emptyList<PolicyView>().asSuccess()
every {
authSdkSource.filterPolicies(
policies = any(),
organizations = any(),
policyType = PolicyType.DISABLE_SEND,
)
} returnsMany listOf(
emptyList<PolicyView>().asSuccess(),
listOf(createMockPolicyView(organizationId = organizations.id, enabled = true))
.asSuccess(),
)
mutableUserStateFlow.value = userStateJson
mutableOrganizationsFlow.value = listOf(organizations)
mutablePolicyFlow.value = listOf(
createMockPolicy(organizationId = organizations.id, isEnabled = true),
)
val expectedPolicy = EffectiveSendPolicy(
allowedDomains = null,
allowedSendTypes = null,
deletionHours = null,
disableHideEmail = false,
disableSend = false,
whoCanAccess = null,
)
policyManager
.getEffectiveSendPolicyFlow()
.test {
assertEquals(expectedPolicy, awaitItem())
mutablePolicyFlow.value = listOf(
createMockPolicy(
number = 2,
organizationId = organizations.id,
isEnabled = true,
),
)
assertEquals(expectedPolicy.copy(disableSend = true), awaitItem())
fun `getEffectiveSendPolicyFlow should re-emit when the underlying policies change`() =
runTest {
val userStateJson = mockk<UserStateJson> {
every { activeUserId } returns USER_ID
}
}
val organizations = createMockOrganizationNetwork(
number = 1,
isEnabled = true,
shouldUsePolicies = true,
)
every {
authSdkSource.filterPolicies(
policies = any(),
organizations = any(),
policyType = PolicyType.SEND_CONTROLS,
)
} returns emptyList<PolicyView>().asSuccess()
every {
authSdkSource.filterPolicies(
policies = any(),
organizations = any(),
policyType = PolicyType.SEND_OPTIONS,
)
} returns emptyList<PolicyView>().asSuccess()
every {
authSdkSource.filterPolicies(
policies = any(),
organizations = any(),
policyType = PolicyType.DISABLE_SEND,
)
} returnsMany listOf(
emptyList<PolicyView>().asSuccess(),
listOf(createMockPolicyView(organizationId = organizations.id, enabled = true))
.asSuccess(),
)
mutableUserStateFlow.value = userStateJson
mutableOrganizationsFlow.value = listOf(organizations)
mutablePolicyFlow.value = listOf(
createMockPolicy(organizationId = organizations.id, isEnabled = true),
)
val expectedPolicy = EffectiveSendPolicy(
allowedDomains = null,
allowedSendTypes = null,
deletionHours = null,
disableHideEmail = false,
disableSend = false,
whoCanAccess = null,
)
policyManager
.getEffectiveSendPolicyFlow()
.test {
assertEquals(expectedPolicy, awaitItem())
mutablePolicyFlow.value = listOf(
createMockPolicy(
number = 2,
organizationId = organizations.id,
isEnabled = true,
),
)
assertEquals(expectedPolicy.copy(disableSend = true), awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `getEffectiveSendPolicyFlow should re-emit when the SendControls feature flag changes`() =
runTest {
@@ -44,7 +44,6 @@ sealed class FlagKey<out T : Any> {
NewItemTypes,
DebugDisableSelfHostPremiumCheck,
FillAssistTargetingRules,
PoliciesInAcceptedState,
FedRamp,
Vfo1Foundation,
SendControls,
@@ -169,14 +168,6 @@ 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
}
/**
* Data object holding the feature flag key for the FedRAMP feature.
*/
@@ -56,10 +56,6 @@ class FlagKeyTest {
FlagKey.ManageDevices.keyName,
"pm-4516-devices-add-last-activity-date",
)
assertEquals(
FlagKey.PoliciesInAcceptedState.keyName,
"pm-34145-policies-in-accepted-state",
)
assertEquals(
FlagKey.FedRamp.keyName,
"fedramp-gov-region",
@@ -106,7 +102,6 @@ class FlagKeyTest {
FlagKey.NewItemTypes,
FlagKey.FillAssistTargetingRules,
FlagKey.ManageDevices,
FlagKey.PoliciesInAcceptedState,
FlagKey.FedRamp,
FlagKey.Vfo1Foundation,
FlagKey.SendControls,
@@ -38,7 +38,6 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.NewItemTypes,
FlagKey.FillAssistTargetingRules,
FlagKey.DebugDisableSelfHostPremiumCheck,
FlagKey.PoliciesInAcceptedState,
FlagKey.FedRamp,
FlagKey.Vfo1Foundation,
FlagKey.SendControls,
@@ -104,7 +103,6 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.V2EncryptionTde -> stringResource(BitwardenString.v2_encryption_tde)
FlagKey.NewItemTypes -> stringResource(BitwardenString.new_item_types)
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)
}
@@ -58,7 +58,6 @@
<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>
<string name="fed_ramp">FedRAMP</string>
<string name="vfo1_foundation">VFO-1 foundation</string>
<string name="send_controls">Send Controls</string>