[PM-40527] feat: Add SendControls policy data model and feature flags (#7179)

This commit is contained in:
André Bispo
2026-07-27 09:59:09 +00:00
committed by GitHub
parent dd343e0952
commit 845e20b027
10 changed files with 148 additions and 0 deletions
@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.bitwarden.network.model.SendTypeJson
import com.bitwarden.network.model.SendAccessTypeJson
import com.bitwarden.network.model.SyncResponseJson
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -113,6 +115,42 @@ sealed class PolicyInformation {
val shouldDisableHideEmail: Boolean?,
) : PolicyInformation()
/**
* Represents a policy enforcing rules on the creation and sharing of Sends. Supersedes the
* disable-send policy and [SendOptions] when the `pm-31885-send-controls` feature flag is
* active.
*
* @property disableSend Whether the ability to create and edit Sends is disabled.
* @property disableHideEmail Whether the user should have the ability to hide their email
* address from Send recipients.
* @property whoCanAccess The access type Sends are restricted to, if any.
* @property allowedDomains A comma-separated list of email domains recipients must belong to
* when [whoCanAccess] is [SendAccessTypeJson.SPECIFIC_PEOPLE].
* @property deletionHours The number of hours until a Send is deleted, if enforced.
* @property allowedSendTypes The types of Sends that are allowed to be created, if
* restricted.
*/
@Serializable
data class SendControls(
@SerialName("disableSend")
val disableSend: Boolean?,
@SerialName("disableHideEmail")
val disableHideEmail: Boolean?,
@SerialName("whoCanAccess")
val whoCanAccess: SendAccessTypeJson?,
@SerialName("allowedDomains")
val allowedDomains: String?,
@SerialName("deletionHours")
val deletionHours: Int?,
@SerialName("allowedSendTypes")
val allowedSendTypes: List<SendTypeJson>?,
) : PolicyInformation()
/**
* Represents a policy enforcing rules on the user's vault timeout settings.
*/
@@ -153,6 +153,10 @@ val PolicyView.policyInformation: PolicyInformation?
JSON.decodeFromStringOrNull<PolicyInformation.SendOptions>(it)
}
PolicyType.SEND_CONTROLS -> {
JSON.decodeFromStringOrNull<PolicyInformation.SendControls>(it)
}
else -> null
}
}
@@ -34,6 +34,7 @@ inline fun <reified T : PolicyInformation> getPolicyType(): PolicyType =
PolicyInformation.MasterPassword::class.java -> PolicyType.MASTER_PASSWORD
PolicyInformation.PasswordGenerator::class.java -> PolicyType.PASSWORD_GENERATOR
PolicyInformation.SendOptions::class.java -> PolicyType.SEND_OPTIONS
PolicyInformation.SendControls::class.java -> PolicyType.SEND_CONTROLS
PolicyInformation.VaultTimeout::class.java -> PolicyType.MAXIMUM_VAULT_TIMEOUT
else -> {
@@ -1,6 +1,8 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.bitwarden.network.model.OrganizationType
import com.bitwarden.network.model.SendTypeJson
import com.bitwarden.network.model.SendAccessTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.createMockOrganizationNetwork
import com.bitwarden.network.model.createMockPermissions
@@ -172,6 +174,27 @@ class SyncResponseJsonExtensionsTest {
)
}
@Test
fun `policyInformation converts the SendControls Json data to policy information`() {
val policyInformation = PolicyInformation.SendControls(
disableSend = false,
disableHideEmail = true,
whoCanAccess = SendAccessTypeJson.SPECIFIC_PEOPLE,
allowedDomains = "bitwarden.com",
deletionHours = 168,
allowedSendTypes = listOf(SendTypeJson.TEXT),
)
val policy = createMockPolicyView(
type = PolicyType.SEND_CONTROLS,
data = Json.encodeToString(policyInformation),
)
assertEquals(
policyInformation,
policy.policyInformation,
)
}
@Test
fun `policyInformation returns null policy information for null data`() {
val masterPasswordPolicy = createMockPolicyView(
@@ -70,6 +70,14 @@ class PolicyManagerExtensionsTest {
)
}
@Test
fun `getPolicyType with SendControls should map to appropriate PolicyTypeJson`() {
assertEquals(
PolicyType.SEND_CONTROLS,
getPolicyType<PolicyInformation.SendControls>(),
)
}
@Test
fun `getPolicyType with VaultTimeout should map to appropriate PolicyTypeJson`() {
assertEquals(
@@ -46,6 +46,8 @@ sealed class FlagKey<out T : Any> {
FillAssistTargetingRules,
PoliciesInAcceptedState,
FedRamp,
SendControls,
SendControlsExistingSends,
)
}
}
@@ -189,6 +191,23 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key for the consolidated Send Controls policy.
*/
data object SendControls : FlagKey<Boolean>() {
override val keyName: String = "pm-31885-send-controls"
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key for enforcing the Send Controls policy against
* Sends created before the policy existed.
*/
data object SendControlsExistingSends : FlagKey<Boolean>() {
override val keyName: String = "pm-31885-send-controls-existing-sends"
override val defaultValue: Boolean = false
}
//region Dummy keys for testing
/**
* Data object holding the key for a [Boolean] flag to be used in tests.
@@ -64,6 +64,14 @@ class FlagKeyTest {
FlagKey.FedRamp.keyName,
"fedramp-gov-region",
)
assertEquals(
FlagKey.SendControls.keyName,
"pm-31885-send-controls",
)
assertEquals(
FlagKey.SendControlsExistingSends.keyName,
"pm-31885-send-controls-existing-sends",
)
}
@Test
@@ -84,6 +92,8 @@ class FlagKeyTest {
FlagKey.ManageDevices,
FlagKey.PoliciesInAcceptedState,
FlagKey.FedRamp,
FlagKey.SendControls,
FlagKey.SendControlsExistingSends,
).all {
!it.defaultValue
},
@@ -0,0 +1,37 @@
package com.bitwarden.network.model
import androidx.annotation.Keep
import com.bitwarden.core.data.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents who is allowed to view a Send under the `SendControls` policy.
*/
@Serializable(SendAccessTypeSerializer::class)
enum class SendAccessTypeJson {
/**
* Anyone with the link can view the Send.
*/
@SerialName("0")
ANY,
/**
* Only individuals with the password set on the Send can view it.
*/
@SerialName("1")
PASSWORD_PROTECTED,
/**
* Only specific people, identified by email, can view the Send.
*/
@SerialName("2")
SPECIFIC_PEOPLE,
}
@Keep
private class SendAccessTypeSerializer :
BaseEnumeratedIntSerializer<SendAccessTypeJson>(
className = "SendAccessTypeJson",
values = SendAccessTypeJson.entries.toTypedArray(),
)
@@ -40,6 +40,8 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.DebugDisableSelfHostPremiumCheck,
FlagKey.PoliciesInAcceptedState,
FlagKey.FedRamp,
FlagKey.SendControls,
FlagKey.SendControlsExistingSends,
-> {
@Suppress("UNCHECKED_CAST")
BooleanFlagItem(
@@ -104,4 +106,8 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
}
FlagKey.FedRamp -> stringResource(BitwardenString.fed_ramp)
FlagKey.SendControls -> stringResource(BitwardenString.send_controls)
FlagKey.SendControlsExistingSends -> {
stringResource(BitwardenString.send_controls_existing_sends)
}
}
@@ -57,6 +57,8 @@
<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="send_controls">Send Controls</string>
<string name="send_controls_existing_sends">Send Controls - Existing Sends</string>
<!-- endregion Debug Menu -->
</resources>