Skip to content

Conversation

@theMickster
Copy link
Contributor

🎟️ Tracking

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

📔 Objective

Analyzed the functionality found in both the client and server code regarding deleting of attachments and chose to implement as simply as possible. I found these three steps: removal of the file from storage, removal from the collection of attachments, and an update of the RevisionDate { see Vault/Services/Implementations/CipherService.cs for details}.

Unless I am missing something & we are in some unforeseen data state with a Chiper, I do not think we need to concern ourselves with the rest of the payload returned and going through the re-mapping of data that should not have changed. Also, given that the payload from the delete attachments API endpoints is an entity instead of a response model, I feel we only seek to complicate things further by over engineering.

More than welcome to other perspectives.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation
    team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed
    issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@claude
Copy link

claude bot commented Nov 13, 2025

Claude finished @theMickster's task —— View job


Code Review Summary

I've reviewed the latest commits implementing attachment deletion functionality. The code follows good patterns with comprehensive test coverage, but there are critical issues that must be addressed before merging.


Critical Issues

Finding 1: ❌ Race condition causing potential data loss remains unaddressed

nikwithak's concern about the race condition in delete_attachment.rs:35-41 is valid and has not been resolved. The current implementation creates a scenario where concurrent updates can be lost:

Race Condition Example
Time 0: Cipher has password="old", attachments=[A, B], revision_date=T0
Time 1: Client A fetches cipher
Time 2: Client B fetches cipher  
Time 3: Client A updates password to "new" → Server updates revision_date=T3
Time 4: Client B deletes attachment A → Gets cipher with password="new", revision_date=T4
Time 5: Client B's local state still has password="old"
Time 6: Client B saves cipher with password="old", revision_date=T4
Result: Client A's password update is lost

The API returns a full models::Cipher entity (crates/bitwarden-api-api/src/models/cipher.rs:16-45), but the implementation at delete_attachment.rs:32-41 only uses revision_date and ignores all other fields. While the PR description argues this is simpler, the tradeoff is data loss vulnerability in concurrent usage scenarios.

Resolution Required: Either:

  1. Implement impl TryFrom<models::Cipher> for Cipher and replace the local cipher entirely (as dani-garcia and nikwithak suggested), OR
  2. Document why this race condition is acceptable with clear rationale in code comments

Finding 2: ❌ Feature-specific errors polluting general CipherError enum

Hinton correctly identified that adding DeleteAttachment and DeleteAttachmentAdmin to CipherError violates separation of concerns. The latest commit (038a9ef) correctly addressed this by:

  • Creating a dedicated CipherDeleteAttachmentError enum (delete_attachment.rs:89-101)
  • Removing the feature-specific variants from CipherError

Status: ✅ Resolved in commit 038a9ef


Issues Requiring Attention

Finding 3: ⚠️ Unused redundant double dereference removed

dani-garcia's suggestion about removing the manual deref was implemented in commit 5c89411:

// Before: let cipher_response = *boxed_cipher;
// After: let cipher_response: Box<models::Cipher> = require!(response.cipher);

Status: ✅ Resolved in commit 5c89411


Finding 4: ⚠️ String parameter changed to &str

dani-garcia's suggestion to change attachment_id: String to attachment_id: &str was implemented in commit 5c89411, improving the API by avoiding unnecessary string consumption.

Status: ✅ Resolved in commit 5c89411


Code Quality Observations

Finding 5: 💭 Test coverage at 92.85% with 28 uncovered lines

Codecov reports 92.85% patch coverage with 28 lines missing coverage in delete_attachment.rs. The test suite is comprehensive with tests for:

  • ✅ Success cases (both admin and non-admin)
  • ✅ Error cases (API errors, missing cipher)
  • ✅ Edge cases (cipher with no attachments)

The uncovered lines are likely error path branches. While 92.85% is good, consider adding tests for:

  • Invalid revision_date format from API
  • Repository save failures

PR Metadata Review

Finding 6: 💭 PR title and description could be more specific

The PR title "PM-25820 - Implement the ability to delete an attachment" is adequate but could be more specific about the scope:

  • Current: Describes what was added
  • Better: "Add delete_attachment and delete_attachment_as_admin methods to CiphersClient"

The Objective section provides good context about the implementation approach, though the justification for ignoring the full API response needs reconsideration given the race condition concern.


Recommendations

Must address before merge:

  1. Resolve Finding 1: Implement full cipher replacement from API response OR document acceptance of race condition with clear justification

Should consider:
2. 💭 Add tests for remaining error paths to achieve higher coverage
3. 💭 Consider adding inline documentation about why only revision_date is used (if race condition is deemed acceptable)


Good Practices Observed

  • Comprehensive test coverage with realistic fixtures
  • Proper separation of concerns with standalone testable function
  • Both admin and non-admin API variants supported
  • Proper error handling with dedicated error type

Commits Reviewed: 82dbf96 (initial), 5c89411 (suggestions), 038a9ef (error handling)

Copilot finished reviewing on behalf of theMickster November 13, 2025 12:21
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements the ability to delete attachments from ciphers by adding two new public methods to CiphersClient: delete_attachment for regular users and delete_attachment_as_admin for administrators. The implementation follows a minimal approach, only updating the cipher's revision date and removing the deleted attachment from the attachments collection, rather than re-syncing all cipher data from the server response.

Key Changes

  • Added standalone delete_attachment function that handles API calls, updates the cipher's revision date from the server response, removes the deleted attachment from the local cipher, and persists the updated cipher to the repository
  • Added two new error variants to CipherError for handling delete attachment API errors
  • Comprehensive test coverage including success cases, error cases, and edge cases like missing ciphers and ciphers with no attachments

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
crates/bitwarden-vault/src/cipher/cipher_client/delete_attachment.rs New module containing the delete attachment implementation and comprehensive unit tests covering various scenarios
crates/bitwarden-vault/src/cipher/cipher.rs Added two new error variants (DeleteAttachment and DeleteAttachmentAdmin) to CipherError enum for API error handling
crates/bitwarden-vault/src/cipher/cipher_client/mod.rs Added module declaration for the new delete_attachment module

@github-actions
Copy link
Contributor

github-actions bot commented Nov 13, 2025

Logo
Checkmarx One – Scan Summary & Details051f8402-3d28-4c02-823c-4aab3f90adc4

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Nov 13, 2025

Codecov Report

❌ Patch coverage is 92.71357% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.86%. Comparing base (c60a5d7) to head (038a9ef).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
...ault/src/cipher/cipher_client/delete_attachment.rs 92.71% 29 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #557      +/-   ##
==========================================
- Coverage   79.94%   79.86%   -0.08%     
==========================================
  Files         298      301       +3     
  Lines       32129    32636     +507     
==========================================
+ Hits        25686    26066     +380     
- Misses       6443     6570     +127     

☔ View full report in Codecov by Sentry.
📢 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.

@theMickster theMickster changed the title Implement the ability to delete an attachment [PM-25820] - Implement the ability to delete an attachment Nov 13, 2025
Copy link
Member

@dani-garcia dani-garcia left a comment

Choose a reason for hiding this comment

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

Looks pretty good to me, just some small suggestions and a possible future improvement.

}

repository
.set(cipher_id.to_string(), cipher.clone())
Copy link
Member

Choose a reason for hiding this comment

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

Non-blocking improvement: The API seems to return the full cipher, so rather than manually modify the cipher we have locally, it might be easier and less error prone to just take the response cipher and stick it into the repository. This might need some conversion function like impl TryFrom<models::Cipher> for Cipher to be implemented though, so we can leave that for later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So it appears to bring back the full Cipher, but it does not It's just the Cipher entity which lacks a few properties.

Comment on lines +31 to +37
cipher.revision_date = require!(cipher_response.revision_date)
.parse()
.map_err(Into::<VaultParseError>::into)?;

if let Some(ref mut attachments) = cipher.attachments {
attachments.retain(|a| a.id.as_deref() != Some(attachment_id));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ There may be an edge case here - you're updating the revision date, but without syncing other fields from the response . That could cause an issue if another client has updated the cipher since the last sync, which would enable overwriting the changes from that update.

e.g.
Client A -> Changes password on Cipher.
Client B -> Deletes an Attachment
Client B -> Changes name of Cipher
Client A -- Original password change is overwritten (Client B didn't sync the latest password, but did have the correct revision_date)

In line with @dani-garcia's comment below, I think it's best to store the cipher in the response directly, which will also save you these steps of manually updating the cipher.

Copy link
Contributor Author

@theMickster theMickster Nov 14, 2025

Choose a reason for hiding this comment

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

Ok; I have added a note in Slack for us to chat @nikwithak, @gbubemismith & @shane-melton.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 17, 2025

🔍 SDK Breaking Change Detection Results

SDK Version: vault/pm-25820/cipher-delete-attachment-02 (038a9ef)
Completed: 2025-11-17 14:56:50 UTC
Total Time: 215s

Client Status Details
typescript ✅ No breaking changes detected TypeScript compilation passed with new SDK version - View Details

Breaking change detection completed. View SDK workflow

@theMickster theMickster added help wanted Extra attention is needed question Further information is requested and removed help wanted Extra attention is needed labels Nov 17, 2025
@theMickster theMickster marked this pull request as draft November 17, 2025 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

question Further information is requested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants