Skip to content

feat(vault): plumb the PAM partialData gating marker through the cipher model - #22169

Closed
Hinton wants to merge 1 commit into
pam/sdk-record-should-includefrom
pam/cipher-partial-data
Closed

feat(vault): plumb the PAM partialData gating marker through the cipher model#22169
Hinton wants to merge 1 commit into
pam/sdk-record-should-includefrom
pam/cipher-partial-data

Conversation

@Hinton

@Hinton Hinton commented Jul 31, 2026

Copy link
Copy Markdown
Member

🎟️ Tracking

PAM cipher gating, 2/4. Stacked on #22168 (needs its shouldInclude hook).
Server side: bitwarden/server#8114 (field) and #8115 (behavior).

📔 Objective

The server withholds a PAM-gated cipher's sensitive fields and returns a reduced blob
instead — the encrypted name plus, for logins, the encrypted URIs. This carries that blob
from the wire down to CipherView, where UI surfaces can use its presence as the "this row
is gated" signal.

  • CipherResponse reads PartialData (accepting a string or an already-parsed object)
    and lifts the encrypted Name onto name so the normal decrypt path handles it. For
    logins it also lifts Uris, building the login from Uris alone — a deliberate
    allowlist, so an over-sharing blob can never leak a password or TOTP onto a gated view.
    A malformed blob keeps the marker and drops the name: failing to parse must not un-gate
    the row.
  • The marker is carried through CipherDataCipherCipherView.
  • CipherRecordMapper.shouldInclude hides gated rows from the SDK (no partial-data
    decrypt path; it rejects sparse ciphers). They stay in client state.
  • DefaultCipherEncryptionService re-attaches the marker after the SDK round trip
    drops it, on both the single and bulk decrypt paths.

Note for reviewers

This is additive to the existing data field, which carries the full blob-encrypted
payload (PM-32696). The two are unrelated and both are kept.

Also adds leaseGated, the transient companion marker for a cipher served under active
access. It is deliberately never persisted to CipherData nor serialized in fromJSON,
and has no producer yet — declared here because the cipher-view banner seam (#22171)
forwards it.

Verification

npm test -- libs/common/src/vault libs/common/src/platform/services/sdk — 784 passed
across 47 suites. npm run test:types clean.

New cipher.response.spec.ts covers the name/URI lift, that no secret login field is ever
lifted, the malformed-blob and already-parsed-object paths, and that a real top-level Name
wins over the blob's.

🚨 Breaking Changes

None — purely additive. With no server sending PartialData, partialData is always
absent and every path behaves exactly as before.

…er model

The server withholds a PAM-gated cipher's sensitive fields and returns a
reduced blob instead - the encrypted name plus, for logins, the encrypted
URIs. This carries that blob from the wire down to CipherView, where UI
surfaces can use its presence as the "this row is gated" signal.

- CipherResponse reads PartialData (accepting a string or an already-parsed
  object) and lifts the encrypted Name onto `name` so the normal decrypt
  path handles it. For logins it also lifts Uris, building the login from
  Uris alone so an over-sharing blob can never leak a password or TOTP onto
  a gated view. A malformed blob keeps the marker and drops the name -
  failing to parse must not un-gate the row.
- The marker is persisted through CipherData -> Cipher -> CipherView.
- CipherRecordMapper.shouldInclude hides gated rows from the SDK, which has
  no partial-data decrypt path and rejects sparse ciphers. They stay in
  client state.
- DefaultCipherEncryptionService re-attaches the marker after the SDK round
  trip drops it, on both the single and bulk decrypt paths.

Note this is additive to the existing `data` field, which carries the full
blob-encrypted payload; the two are unrelated.

Also adds `leaseGated`, the transient companion marker for a full cipher
served under an active lease. It is deliberately never persisted or
serialized, and has no producer until the PAM leased-cipher fetcher lands -
declared here because the cipher-view banner seam forwards it.
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 8 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (pam/sdk-record-should-include@03754f7). Learn more about missing BASE report.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
libs/common/src/vault/models/domain/cipher.ts 33.33% 0 Missing and 2 partials ⚠️
...ommon/src/vault/models/response/cipher.response.ts 81.81% 0 Missing and 2 partials ⚠️
libs/common/src/vault/models/view/cipher.view.ts 50.00% 1 Missing and 1 partial ⚠️
libs/common/src/vault/models/data/cipher.data.ts 0.00% 0 Missing and 1 partial ⚠️
...ommon/src/vault/models/domain/cipher-sdk-mapper.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                        @@
##             pam/sdk-record-should-include   #22169   +/-   ##
================================================================
  Coverage                                 ?   52.73%           
================================================================
  Files                                    ?     4203           
  Lines                                    ?   133184           
  Branches                                 ?    20966           
================================================================
  Hits                                     ?    70230           
  Misses                                   ?    57816           
  Partials                                 ?     5138           

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

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Hinton added a commit that referenced this pull request Aug 6, 2026
…pivot)

Pivots the PAM cipher-gating client work: instead of hiding gated rows
from the SDK and hand-decrypting them, gated rows now flow through the
SDK, which decrypts the reduced envelope into a `partial` view
(bitwarden/sdk-internal#1359).

- Revert the SdkRecordMapper `shouldInclude` filter and the
  `decryptPartialCiphers` path (the last caller of the deprecated
  `Cipher.decrypt`). Gated rows go through `decryptMany*` like any cipher.
- CipherResponse passes `partialData` through verbatim (no client lift);
  `Cipher.toSdkCipher`/`fromSdkCipher` map it both ways so it round-trips
  losslessly (re-attach hack removed). The decrypted view's gating marker
  is the SDK's `partial` boolean.
- Vault seams read `view.partial`; `CipherOpenVerdict` gains a `handled`
  case so the open gate can block the open and surface the "Privileged
  Controls license required" dialog (unlicensed-user design).
- Move the "Privileged" badge into a dedicated "Controlled access" column,
  shown only when a PAM-enabled org (`Organization.usePam`) is in view and
  the badge seam is provided.

Depends on sdk-internal#1359 (adds Cipher.partial_data + the `partial`
view flag + the restricted decrypt path); the `@bitwarden/sdk-internal`
bump lands once that publishes. Supersedes #22168, #22169, #22170.
@Hinton

Hinton commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Superseded by #22171 — the PAM cipher-gating client work pivoted to route gated ciphers through the SDK (sdk-internal#1359) instead of filtering them out. See #22171.

@Hinton Hinton closed this Aug 6, 2026
Hinton added a commit that referenced this pull request Aug 6, 2026
…pivot)

Pivots the PAM cipher-gating client work: instead of hiding gated rows
from the SDK and hand-decrypting them, gated rows now flow through the
SDK, which decrypts the reduced envelope into a `partial` view
(bitwarden/sdk-internal#1359).

- Revert the SdkRecordMapper `shouldInclude` filter and the
  `decryptPartialCiphers` path (the last caller of the deprecated
  `Cipher.decrypt`). Gated rows go through `decryptMany*` like any cipher.
- CipherResponse passes `partialData` through verbatim (no client lift);
  `Cipher.toSdkCipher`/`fromSdkCipher` map it both ways so it round-trips
  losslessly (re-attach hack removed). The decrypted view's gating marker
  is the SDK's `partial` boolean.
- Vault seams read `view.partial`; `CipherOpenVerdict` gains a `handled`
  case so the open gate can block the open and surface the "Privileged
  Controls license required" dialog (unlicensed-user design).
- Move the "Privileged" badge into a dedicated "Controlled access" column,
  shown only when a PAM-enabled org (`Organization.usePam`) is in view and
  the badge seam is provided.

Depends on sdk-internal#1359 (adds Cipher.partial_data + the `partial`
view flag + the restricted decrypt path); the `@bitwarden/sdk-internal`
bump lands once that publishes. Supersedes #22168, #22169, #22170.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant