-
Notifications
You must be signed in to change notification settings - Fork 24
[PM-25821] Migrate Cipher Admin operation API calls to SDK #560
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Great job! No new security vulnerabilities introduced in this pull request |
🔍 SDK Breaking Change Detection ResultsSDK Version:
Breaking change detection completed. View SDK workflow |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #560 +/- ##
==========================================
+ Coverage 78.59% 78.64% +0.05%
==========================================
Files 281 288 +7
Lines 29131 30635 +1504
==========================================
+ Hits 22895 24093 +1198
- Misses 6236 6542 +306 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
aa8a96d to
7dc66a5
Compare
… vault/pm-25821/cipher-admin-ops
|
Claude finished @nikwithak's task —— View job Code Review CompleteTodo List
Overall Assessment: REQUEST CHANGESI've completed a comprehensive code review of PR #560. The code follows good architectural patterns and has strong test coverage, but there are 2 critical issues that must be fixed before merge. Critical Issues Found❌ Finding 1: Dead Code - Unused KeyStore instantiation in restore_manyLocation: The let store: KeyStore<KeyIds> = KeyStore::default();
#[allow(deprecated)]
let _ = store.context_mut().set_symmetric_key(
SymmetricKeyId::User,
SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
);Fix: Remove lines 60-65 entirely. The function correctly uses the
|
|
Be mindful that the autogenerated API bindings do not have any graceful handling of malformed data. I.e. an enum outside the valid range errors out. https://bitwarden.atlassian.net/browse/PM-6169 tracks improvements to this, and if this improvement is required reach out to platform to ensure it's prioritized. CC @trmartin4 |
Hinton
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would consider moving all the admin commands into a separate client. While they share some common logic I think they fundamentally behave differently and it might be confusing for consumers to call an admin method and not have the state react?
CiphersClient & CiphersAdminClient
| /// Creates a new [Cipher] and saves it to the server. | ||
| pub async fn create( | ||
| &self, | ||
| request: CipherCreateRequest, | ||
| ) -> Result<CipherView, CreateCipherError> { | ||
| self.create_cipher(request, vec![], false).await | ||
| } | ||
|
|
||
| /// Creates a new [Cipher] for an organization, and saves it to the server. | ||
| pub async fn create_org_cipher( | ||
| &self, | ||
| request: CipherCreateRequest, | ||
| collection_ids: Vec<CollectionId>, | ||
| ) -> Result<CipherView, CreateCipherError> { | ||
| self.create_cipher(request, collection_ids, false).await | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contract here is weird. CipherCreteRequest allows you to specify an organization id. That implies you can make orgnaizational items.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree here - the CipherCreateRequest was built to match the valid fields on the API request model, but when we create an org cipher, the client calls the separate (POST /ciphers/create) endpoint, which takes the collection_Ids. The standard POST /ciphers endpoint does not have collection_ids in the response model.
I'm thinking of modifying the CipherCreateRequest to have both org_id and collection_ids, and writing the logic to call the POST /ciphers/create endpoint iff they are both present on the request; LMK your thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea that seems saner imho. Ideally we can combine the server endpoints at some point?
| #[error(transparent)] | ||
| Decrypt(#[from] DecryptError), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: What operation requires decrypt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the function input for these requests is plaintext, and the SDK handles encryption before sending to the server, we return the updated object decrypted as well. example
We return a CryptoError in other instances (e.g. the edit_cipher implementation here), but this call returns a DecryptError. I will look into casting it approriately to use the same error.
Tangentially: Do you think it makes sense to use this approach - returning the decrypted CipherView object to the consumer, rather than the encrypted Cipher (all of it locally only)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the long term I would love to have Cipher be an internal concern of the SDK and we only expose decrypted views. Now in the meanwhile we have to make tradeoffs to move us forward and I don't think it's an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upcasting it to a CryptoError is probably better. I think that should be as simple as doing CryptoError::Decrypt(err)
| #[error(transparent)] | ||
| Api(#[from] ApiError), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The methods putting Api errors in CipherError should really be updated to not use cipher error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only one still using CipherError is the share operations, which calls existing functions that currently returns CipherError already (e.g. https://github.com/bitwarden/sdk-internal/blob/vault/pm-25821/cipher-admin-ops/crates/bitwarden-vault/src/cipher/cipher_client/share_cipher.rs#L180-L184) - I think we can migrate this one to its own error type in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea sounds good.
| edit: Default::default(), | ||
| favorite: Default::default(), | ||
| folder_id: Default::default(), | ||
| permissions: Default::default(), | ||
| view_password: Default::default(), | ||
| local_data: Default::default(), | ||
| collection_ids: Default::default(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reflection: Putting default values here can be dangerous, because there is nothing to indicate the Cipher model is incomplete.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately several API operations don't return the full cipher, only the CipherMiniResponseModel - for the Admin operations, since we don't update the repository, the intent is to still return a consistent type the user can handle, if needed - the concern is valid though. I think we can:
- Merge it with the known existing Cipher data (if it's available),
- Continue with this approach, knowing it's primarily for the admin operations which don't update local state - if this route, I can move the logic to be private to the admin operations, rather than a blanket
Fromimplementation, - Return the
CipherMiniResponseModelas-is, or create aMiniCipherViewtype for mapping / returning,which doesn't expose these fields at all.
Do you have a preference? Feel free to ping me on Slack when you're free for a deeper discussion - there are a handful of API operations that currently only return a subset of data like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we just need to be careful. In some ways It would be nice to make an alias to indicate "hey you can't trust this object", or define a new struct with fields optional.
This is not a blocking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a new PartialCipher trait that can merge with an Option<Cipher>, replacing the TryFrom implementaiton: 58e01df
Hinton
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of my issues are resolved and I think this looks generally good. There are couple of smaller things and improvements to be made but once resolved feel free to not request another review from me.
| mod get; | ||
| mod restore; | ||
|
|
||
| #[allow(missing_docs)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Document the purpose of this client and compare it against the regular CipherClient?
| edit: Default::default(), | ||
| favorite: Default::default(), | ||
| folder_id: Default::default(), | ||
| permissions: Default::default(), | ||
| view_password: Default::default(), | ||
| local_data: Default::default(), | ||
| collection_ids: Default::default(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we just need to be careful. In some ways It would be nice to make an alias to indicate "hey you can't trust this object", or define a new struct with fields optional.
This is not a blocking.
| #[error(transparent)] | ||
| Api(#[from] ApiError), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea sounds good.
| #[error(transparent)] | ||
| Decrypt(#[from] DecryptError), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the long term I would love to have Cipher be an internal concern of the SDK and we only expose decrypted views. Now in the meanwhile we have to make tradeoffs to move us forward and I don't think it's an issue.
| pub async fn delete_many( | ||
| &self, | ||
| cipher_ids: Vec<CipherId>, | ||
| organization_id: Option<OrganizationId>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: should organization_id be required? Looking at the server code it is.
| async fn delete_cipher( | ||
| cipher_id: CipherId, | ||
| api_client: &bitwarden_api_api::apis::ApiClient, | ||
| repository: &(impl Repository<Cipher> + ?Sized), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For folders we use generics instead of anonymous type parameters:
pub(super) async fn create_folder<R: Repository<Folder> + ?Sized>(
key_store: &KeyStore<KeyIds>,
api_client: &bitwarden_api_api::apis::ApiClient,
repository: &R,
request: FolderAddEditRequest,
) -> Result<FolderView, CreateFolderError> {Do we want to be consistent? I don't believe there is a performance hit for either pattern though.
| #[error(transparent)] | ||
| Decrypt(#[from] DecryptError), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upcasting it to a CryptoError is probably better. I think that should be as simple as doing CryptoError::Decrypt(err)
| VaultParse(#[from] VaultParseError), | ||
| #[error(transparent)] | ||
| RepositoryError(#[from] RepositoryError), | ||
| #[error(transparent)] | ||
| Api(#[from] ApiError), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue: Remove? Now that you have the admin error these can be removed right?
| Ok(()) | ||
| } | ||
|
|
||
| pub(crate) fn soft_delete(&mut self) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Document?
| pub(crate) fn soft_delete(&mut self) { | |
| /// Marks the cipher as soft deleted by setting `deletion_date` to now. | |
| pub(crate) fn soft_delete(&mut self) { |
| /// Used to convert from API response models to full Cipher structs, | ||
| /// without losing local data that may not be present in the API response. | ||
| pub(crate) trait PartialCipher { | ||
| fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Why accept an optional cipher? The function might be nicer if cipher is required and you just do and_then on callers.

🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-25821
📔 Objective
Migrates the logic for orchestrating API calls for several operations related to the Admin API functionality. Adds the following operations to
CiphersClient, which call the appropriate API endpoints:PR Notes: Sorry for the heft - The line count is high on this PR, but a majority of it is unit tests. This should have been split into multiple tickets, in hindsight.
Note also that this hasn't been tested directly with the client yet, and so additional changes may be needed if bugs are discovered when integrating into the clients (future tickets) - these operations are not currently used anywhere.
⏰ Reminders before review
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 confirmedissue 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