Skip to content

[PM-44810] fix: Ignore "Vault Items" send type parsing [bug-factory poc] - #7488

Open
andrebispo5 wants to merge 2 commits into
mainfrom
PM-44810-send-controls-vault-items
Open

andrebispo5 wants to merge 2 commits into
mainfrom
PM-44810-send-controls-vault-items

Conversation

@andrebispo5

@andrebispo5 andrebispo5 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

branch: PM-44810-send-controls-vault-items
base: main

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-44810

📔 Objective

Automated fix produced by the bug factory (AI-49 proof of concept). Needs review from the owning team before merge.

verified: genuine, live, unduplicated. SendTypeJson has no value for 2 and no serializer default (SendTypeJson.kt:11-30). A SendControls policy listing "Vault Items" therefore fails to decode as a whole and is silently dropped (JsonExtensions.kt:37-46). The same code ships in v2026.9.1-bwpm.

Why SafeListSerializer instead of adding the Item type

The fix drops send types this client does not know when decoding allowedSendTypes, instead of adding the Item type (2) to SendTypeJson. Adding it now is not safe:

  • Today a synced Send of type 2 fails to decode and is silently removed by the SafeListSerializer that SyncResponseJson.sends already uses. Adding ITEM would let those Sends reach the SDK and the UI.
  • The UI still has TODO("[PM-41095] Support Item SendType") branches for the Item type (for example the Send list in SendDataExtensions.kt), which throw at runtime. A user with a single Item Send would crash the Send tab.
  • For a policy that only allows Vault Items, SendViewModel maps the single allowed type to a SendItemType when it is created, which has no Item value.

Adding SendTypeJson.ITEM and handling it properly belongs to PM-41095 (Support Item SendType).

📸 Screenshots

None: this fix is proven by a JVM unit test, and the factory drove no device (code-only target).

🧪 Proof

./gradlew :app:testStandardDebugUnitTest --tests "com.x8bit.bitwarden.data.auth.repository.util.SyncResponseJsonExtensionsTest" on the base code (must fail) exited 1; with the fix (must pass) exited 0.

Base run
nitTest NO-SOURCE
> Task :app:transformStandardDebugUnitTestClassesWithAsm FROM-CACHE

> Task :app:testStandardDebugUnitTest FAILED

SyncResponseJsonExtensionsTest > policyInformation drops unknown allowedSendTypes and keeps the rest of the SendControls policy() FAILED
    org.opentest4j.AssertionFailedError at SyncResponseJsonExtensionsTest.kt:215

[Incubating] Problems report is available at: file://<repo>/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.8.0/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
205 actionable tasks: 3 executed, 8 from cache, 194 up-to-date

The google-services Gradle plugin needs to be applied on a project with com.android.application or com.android.dynamic-feature.

14 tests completed, 1 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:testStandardDebugUnitTest'.
> There were failing tests. See the report at: file://<repo>/app/build/reports/tests/testStandardDebugUnitTest/index.html

* Try:
> Run with --scan to get full insights from a Build Scan (powered by Develocity).

BUILD FAILED in 16s

Fix run
TO-DATE
> Task :app:processStandardDebugUnitTestManifest UP-TO-DATE
> Task :app:generateStandardDebugUnitTestConfig UP-TO-DATE
> Task :app:koverFindJar UP-TO-DATE
> Task :app:processStandardDebugJavaRes UP-TO-DATE
> Task :app:processStandardDebugUnitTestJavaRes UP-TO-DATE
> Task :app:hiltCollectClassesStandardDebugUnitTest UP-TO-DATE
> Task :app:bundleStandardDebugClassesToRuntimeJar
> Task :app:hiltAggregateDepsStandardDebugUnitTest UP-TO-DATE
> Task :app:hiltJavaCompileStandardDebugUnitTest NO-SOURCE
> Task :app:transformStandardDebugUnitTestClassesWithAsm FROM-CACHE
> Task :app:testStandardDebugUnitTest

[Incubating] Problems report is available at: file://<repo>/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.8.0/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 16s
205 actionable tasks: 3 executed, 8 from cache, 194 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.0/userguide/configuration_cache_enabling.html

The google-services Gradle plugin needs to be applied on a project with com.android.application or com.android.dynamic-feature.

🤖 Automated review

  • Round 1: 0 blocking finding(s)

⏰ Reminders before merge

  • An engineer from the owning team has reviewed this (AI-49 AC i18n #4)
  • Feedback captured as findings

…"Vault Items,"… [bug-factory poc]

Automated fix from the AI-49 bug factory. Proof: ./gradlew :app:testStandardDebugUnitTest --tests "com.x8bit.bitwarden.data.auth.repository.util.SyncResponseJsonExtensionsTest" must fail on base and pass with the fix.
@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context t:bug Change Type - Bug labels Oct 14, 2026
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.80%. Comparing base (43d7b8b) to head (dad597c).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7488   +/-   ##
=======================================
  Coverage   86.80%   86.80%           
=======================================
  Files         895      895           
  Lines       65676    65676           
  Branches     9384     9384           
=======================================
  Hits        57007    57007           
  Misses       5189     5189           
  Partials     3480     3480           
Flag Coverage Δ
app-data 18.68% <ø> (+0.02%) ⬆️
app-ui-auth-tools 18.68% <ø> (ø)
app-ui-platform 16.49% <ø> (ø)
app-ui-vault 26.88% <ø> (ø)
authenticator 6.03% <ø> (ø)
lib-core-network-bridge 4.11% <ø> (ø)
lib-data-ui 1.25% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andrebispo5
andrebispo5 marked this pull request as ready for review October 15, 2026 09:32
@andrebispo5
andrebispo5 requested review from a team and david-livefront as code owners October 15, 2026 09:32
Copilot AI balanced review requested due to automatic review settings October 15, 2026 09:32
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

This PR applies SafeListSerializer to PolicyInformation.SendControls.allowedSendTypes. Send types this client does not know, such as Vault Items (2), are now dropped instead of failing the decode of the whole SendControls policy. The serializer change itself is correct, and the two new tests cover it. The issue is downstream: a policy that allows only Vault Items now decodes to an empty list, and the create flow treats an empty list as unrestricted while the view flow treats it as nothing allowed.

Code Review Details
  • ⚠️ : An empty allowedSendTypes (a policy that allows only Vault Items) still lets the user create Text and File Sends through the type picker, while ViewSendViewModel marks every existing Send as not allowed
    • app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/PolicyInformation.kt:152

@SerialName("deletionHours")
val deletionHours: Int?,

@Serializable(with = SafeListSerializer::class)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: A policy that allows only Vault Items now decodes to allowedSendTypes = emptyList(), and the create flow treats that list as "no restriction".

Details and fix

This PR makes [2] decode to emptyList(), and the new test asserts exactly that. The consumers then disagree about what an empty list means:

  • SendViewModel.singleAllowedSendType uses allowedSendTypes?.singleOrNull(), which gives null for an empty list. So handleAddSendClick shows the full type picker and the user can start a Text or File Send, both of which the policy forbids.
  • ViewSendViewModel.isSendTypeAllowed uses allowedSendTypes?.any { ... } ?: true, which gives false for an empty list. So every existing Send is shown as TypeNotAllowed.

The result: the list screen says existing Text and File Sends are not allowed, while the add button still lets the user create new ones. This is no worse than the base branch, where the whole policy was dropped. But the empty-list state is new, and nothing handles it.

Suggested fix: handle the empty list explicitly. One option is to resolve it in PolicyManagerImpl.resolveEffectiveSendPolicy (for example, treat an empty allowedSendTypes as disableSend = true until PM-41095 adds Item support). Another is to have SendViewModel block creation when the list is non-null and empty. Add a test that covers whichever you choose.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude This will be handled by another ticket, we just need to make sure that when a restriction is applied it is not being ignored.

@claude claude Bot Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @andrebispo5's task in 1m 28s —— View job


Confirmed — restrictions are enforced, and this PR is what stops them being ignored

  • Read the PR diff
  • Trace allowedSendTypes from decode → PolicyManagerImpl → SendViewModel / AddEditSendViewModel / ViewSendViewModel
  • Build the enforcement matrix for every policy shape
  • Confirm the residual case is PM-41095 only

@andrebispo5 Agreed, and the code backs you up — the "restriction being ignored" case is exactly what this PR fixes, and it is wider than just the type list.

Why the ignore is wider than allowedSendTypes

Policy.policyInformation decodes the whole blob with decodeFromStringOrNull (SyncResponseJsonExtensions.kt:157), so one unknown enum value returns null for the entire SendControls object. PolicyManagerImpl.resolveEffectiveSendPolicy then drops it in mapNotNull (PolicyManagerImpl.kt:166-169), and that org never enters organizationIdsWithSendControls (line 171). Every field of that policy is lost — not just the type list, but disableSend, disableHideEmail, deletionHours, whoCanAccess and allowedDomains too.

So on main, an admin who ticks "Vault Items" alongside Text gets a policy where even disableSend is silently ignored on Android. That is the real fail-open, and this PR closes it.

Enforcement matrix

allowedSendTypes from server main With this PR
absent / null policy enforced, no type restriction unchanged
[0], [1], [0,1] policy enforced unchanged
[0,2] (Text + Vault Items) ⚠️ whole policy dropped — disableSend, deletionHours, whoCanAccess, disableHideEmail, allowedDomains and the type restriction all ignored ✅ decodes to [TEXT]; singleAllowedSendType resolves to Text, so the picker is skipped and only a Text Send can be created (SendViewModel.kt:335-343, 940-943), types filter hidden (line 535), and every other field is enforced
[2] only ⚠️ whole policy dropped ✅ all non-type fields enforced; type dimension decodes to emptyList() and stays permissive → PM-41095

The [0,2] row is the one an admin actually produces when they allow Text plus Vault Items, and it goes from "nothing enforced" to "fully enforced, Text only". No restriction that this client can express is ignored after the change.

The one residual case, and why it is correctly deferred

[2]-only decodes to emptyList(), and singleOrNull() on an empty list is null, so the type dimension reads as unrestricted. Two things make that the right call for this PR:

  1. Even in that case the PR is strictly stronger than main — disableSend, deletionHours, whoCanAccess, disableHideEmail and allowedDomains from that same policy now apply, where before they were thrown away with it.
  2. There is no correct client behaviour available until SendTypeJson.ITEM exists. The Item-type TODO branches would throw, and SendItemType has no Item value — which is PM-41095, as you say.

Worth confirming server-side (not verifiable from this repo): that the policy editor enforces at least one type selected, so a [2]-only policy is not something a client can receive in the first place. If that holds, the residual row is unreachable today.

The view side is unaffected either way: policyRestriction is gated on isSendDisabled, which comes from sendView.disabled set by the server (ViewSendViewModel.kt:272, 331-340), not derived from the allowlist.

No blocking findings. No code changes made.

Note: I could not run Gradle in this job (the test command was not in the allowed tool list), so the verification above is from reading the code plus the test run already recorded in the PR description.
· Branch PM-44810-send-controls-vault-items

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The creation flow still offers disallowed types when filtering leaves an empty allowlist.

1 open finding
What changed in this PR

Preserves Send Controls policies containing unsupported Send types without enabling Item Send support.

Changes:

  • Filters unknown allowedSendTypes values during decoding.
  • Tests mixed supported/unsupported lists and unknown-only lists.
File Description
app/​src/​test/​kotlin/​com/​x8bit/​bitwarden/​data/​auth/​repository/​util/​SyncResponseJsonExtensionsTest.kt Adds regression tests for unknown Send types.
app/​src/​main/​kotlin/​com/​x8bit/​bitwarden/​data/​auth/​repository/​model/​PolicyInformation.kt Applies and documents tolerant allowlist decoding.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +152 to 154
@Serializable(with = SafeListSerializer::class)
@SerialName("allowedSendTypes")
val allowedSendTypes: List<SendTypeJson>?,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This is not a real case but one created but this new type not being handled at the moment. This will be addressed in PM-41095 ticket. The Send Policy needs to have at least one type enabled otherwise it is disabled and no policy is applied.

@andrebispo5 andrebispo5 changed the title [PM-44810] fix: [Defect] Android - When Send Control Policy includes "Vault Items,"… [bug-factory poc] [PM-44810] fix: Ignore "Vault Items" send type parsing[bug-factory poc] Oct 15, 2026
@andrebispo5 andrebispo5 changed the title [PM-44810] fix: Ignore "Vault Items" send type parsing[bug-factory poc] [PM-44810] fix: Ignore "Vault Items" send type parsing [bug-factory poc] Oct 15, 2026
@bitwarden bitwarden deleted a comment from claude Bot Oct 15, 2026
@SerialName("deletionHours")
val deletionHours: Int?,

@Serializable(with = SafeListSerializer::class)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — withdrawing my earlier finding

@andrebispo5 I re-read both consumers and the behaviour matches what you describe. No code changes made.

Create side — SendViewModel.kt:940-943: allowedSendTypes?.singleOrNull()?.toSendItemType() returns null for an empty list, and isAddSendTypePickerVisible (SendViewModel.kt:535) is singleAllowedSendType == null. An empty allowlist falls through to the unrestricted path — exactly the "as if it is not in enforcement" semantics you describe, and the same user-visible outcome as the base branch where the whole policy was dropped. The PR additionally makes the rest of the policy (disableHideEmail, deletionHours, allowedDomains) survive instead of being lost with it, which is a strict improvement.

View side — my review claimed every existing Send would be marked TypeNotAllowed. That was wrong about the gating. policyRestriction returns null unless isSendDisabled is true (ViewSendViewModel.kt:334), and isSendDisabled is sendView.disabled (line 272) — set by the server, not derived from the allowlist. So the branch is only reachable for Sends the server has already marked non-compliant, and for a Vault-Items-only policy TypeNotAllowed is the accurate label for a Text Send there. Nothing regresses for compliant Sends, so the create/view inconsistency I described does not occur.

That leaves no behavioural gap in this PR. The rest is genuinely PM-41095 — once SendTypeJson.ITEM exists and the Item-type TODO branches are filled in, the allowlist decodes non-empty and enforcement follows automatically.

One thing worth confirming on your side, since it is server-side knowledge I cannot verify from this repo: that "at least one type enabled" is enforced at policy-save time, so a client can never receive a policy whose entire allowlist consists of types it does not know. If that invariant ever loosens, the empty-list-to-unrestricted path becomes a real fail-open. Not a blocker here either way.

No blocking findings from me on this change.

Note: my original comment could not be edited — the comment-update tool returned 404 Not Found on every retry after the first write, so this is posted as a new comment on the same line. View job run

@SerialName("deletionHours")
val deletionHours: Int?,

@Serializable(with = SafeListSerializer::class)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this can just be @Contextual

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me try, but I believe it will discard the other policy options.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup tested and it does not work it will make it all null.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:password-manager Bitwarden Password Manager app context t:bug Change Type - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants