bitwarden_vault/cipher/cipher_client/admin/
edit.rs1use bitwarden_api_api::{apis::ApiClient, models::CipherCollectionsRequestModel};
2use bitwarden_collections::collection::CollectionId;
3use bitwarden_core::{
4 ApiError, MissingFieldError, NotAuthenticatedError, UserId, key_management::KeyIds,
5};
6use bitwarden_crypto::{CryptoError, IdentifyKey, KeyStore};
7use bitwarden_error::bitwarden_error;
8use bitwarden_state::repository::RepositoryError;
9use thiserror::Error;
10#[cfg(feature = "wasm")]
11use wasm_bindgen::prelude::*;
12
13use super::CipherAdminClient;
14use crate::{
15 Cipher, CipherId, CipherView, DecryptError, ItemNotFoundError, VaultParseError,
16 cipher::cipher::PartialCipher,
17 cipher_client::edit::{CipherEditRequest, CipherEditRequestInternal},
18};
19
20#[allow(missing_docs)]
21#[bitwarden_error(flat)]
22#[derive(Debug, Error)]
23pub enum EditCipherAdminError {
24 #[error(transparent)]
25 ItemNotFound(#[from] ItemNotFoundError),
26 #[error(transparent)]
27 Crypto(#[from] CryptoError),
28 #[error(transparent)]
29 Api(#[from] ApiError),
30 #[error(transparent)]
31 VaultParse(#[from] VaultParseError),
32 #[error(transparent)]
33 MissingField(#[from] MissingFieldError),
34 #[error(transparent)]
35 NotAuthenticated(#[from] NotAuthenticatedError),
36 #[error(transparent)]
37 Repository(#[from] RepositoryError),
38 #[error(transparent)]
39 Uuid(#[from] uuid::Error),
40 #[error(transparent)]
41 Decrypt(#[from] DecryptError),
42}
43
44impl<T> From<bitwarden_api_api::apis::Error<T>> for EditCipherAdminError {
45 fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
46 Self::Api(val.into())
47 }
48}
49
50async fn edit_cipher(
51 key_store: &KeyStore<KeyIds>,
52 api_client: &bitwarden_api_api::apis::ApiClient,
53 encrypted_for: UserId,
54 original_cipher_view: CipherView,
55 request: CipherEditRequest,
56) -> Result<CipherView, EditCipherAdminError> {
57 let cipher_id = request.id;
58 let request = CipherEditRequestInternal::new(request, &original_cipher_view);
59
60 let mut cipher_request = key_store.encrypt(request)?;
61 cipher_request.encrypted_for = Some(encrypted_for.into());
62
63 let orig_cipher = key_store.encrypt(original_cipher_view)?;
64
65 let cipher: Cipher = api_client
66 .ciphers_api()
67 .put_admin(cipher_id.into(), Some(cipher_request))
68 .await
69 .map_err(ApiError::from)?
70 .merge_with_cipher(Some(orig_cipher))?;
71
72 Ok(key_store.decrypt(&cipher)?)
73}
74
75pub async fn add_to_collections(
77 cipher_id: CipherId,
78 collection_ids: Vec<CollectionId>,
79 api_client: &ApiClient,
80 key_store: &KeyStore<KeyIds>,
81) -> Result<CipherView, EditCipherAdminError> {
82 let req = CipherCollectionsRequestModel {
83 collection_ids: collection_ids
84 .into_iter()
85 .map(|id| id.to_string())
86 .collect(),
87 };
88
89 let api = api_client.ciphers_api();
90 let cipher: Cipher = api
91 .put_collections_admin(&cipher_id.to_string(), Some(req))
92 .await?
93 .merge_with_cipher(None)?;
94
95 Ok(key_store.decrypt(&cipher)?)
96}
97
98#[cfg_attr(feature = "wasm", wasm_bindgen)]
99impl CipherAdminClient {
100 pub async fn edit(
102 &self,
103 mut request: CipherEditRequest,
104 original_cipher_view: CipherView,
105 ) -> Result<CipherView, EditCipherAdminError> {
106 let key_store = self.client.internal.get_key_store();
107 let config = self.client.internal.get_api_configurations();
108
109 let user_id = self
110 .client
111 .internal
112 .get_user_id()
113 .ok_or(NotAuthenticatedError)?;
114
115 if request.key.is_none()
118 && self
119 .client
120 .internal
121 .get_flags()
122 .enable_cipher_key_encryption
123 {
124 let key = request.key_identifier();
125 request.generate_cipher_key(&mut key_store.context(), key)?;
126 }
127
128 edit_cipher(
129 key_store,
130 &config.api_client,
131 user_id,
132 original_cipher_view,
133 request,
134 )
135 .await
136 }
137
138 pub async fn update_collection(
140 &self,
141 cipher_id: CipherId,
142 collection_ids: Vec<CollectionId>,
143 ) -> Result<CipherView, EditCipherAdminError> {
144 add_to_collections(
145 cipher_id,
146 collection_ids,
147 &self.client.internal.get_api_configurations().api_client,
148 self.client.internal.get_key_store(),
149 )
150 .await
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use bitwarden_api_api::{apis::ApiClient, models::CipherMiniResponseModel};
157 use bitwarden_core::key_management::SymmetricKeyId;
158 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
159
160 use super::*;
161 use crate::{CipherId, CipherRepromptType, CipherType, LoginView};
162
163 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
164 const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
165
166 fn generate_test_cipher() -> CipherView {
167 CipherView {
168 id: Some(TEST_CIPHER_ID.parse().unwrap()),
169 organization_id: None,
170 folder_id: None,
171 collection_ids: vec![],
172 key: None,
173 name: "Test Login".to_string(),
174 notes: None,
175 r#type: CipherType::Login,
176 login: Some(LoginView {
177 username: Some("[email protected]".to_string()),
178 password: Some("password123".to_string()),
179 password_revision_date: None,
180 uris: None,
181 totp: None,
182 autofill_on_page_load: None,
183 fido2_credentials: None,
184 }),
185 identity: None,
186 card: None,
187 secure_note: None,
188 ssh_key: None,
189 favorite: false,
190 reprompt: CipherRepromptType::None,
191 organization_use_totp: true,
192 edit: true,
193 permissions: None,
194 view_password: true,
195 local_data: None,
196 attachments: None,
197 attachment_decryption_failures: None,
198 fields: None,
199 password_history: None,
200 creation_date: "2025-01-01T00:00:00Z".parse().unwrap(),
201 deleted_date: None,
202 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
203 archived_date: None,
204 }
205 }
206
207 #[tokio::test]
208 async fn test_edit_cipher() {
209 let store: KeyStore<KeyIds> = KeyStore::default();
210 #[allow(deprecated)]
211 let _ = store.context_mut().set_symmetric_key(
212 SymmetricKeyId::User,
213 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
214 );
215
216 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
217
218 let api_client = ApiClient::new_mocked(move |mock| {
219 mock.ciphers_api
220 .expect_put_admin()
221 .returning(move |_id, body| {
222 let body = body.unwrap();
223 Ok(CipherMiniResponseModel {
224 object: Some("cipher".to_string()),
225 id: Some(cipher_id.into()),
226 name: Some(body.name),
227 r#type: body.r#type,
228 organization_id: body
229 .organization_id
230 .as_ref()
231 .and_then(|id| uuid::Uuid::parse_str(id).ok()),
232 reprompt: body.reprompt,
233 key: body.key,
234 notes: body.notes,
235 organization_use_totp: Some(true),
236 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
237 creation_date: Some("2025-01-01T00:00:00Z".to_string()),
238 deleted_date: None,
239 login: body.login,
240 card: body.card,
241 identity: body.identity,
242 secure_note: body.secure_note,
243 ssh_key: body.ssh_key,
244 fields: body.fields,
245 password_history: body.password_history,
246 attachments: None,
247 data: None,
248 })
249 })
250 .once();
251 });
252
253 let original_cipher_view = generate_test_cipher();
254 let mut cipher_view = original_cipher_view.clone();
255 cipher_view.name = "New Cipher Name".to_string();
256
257 let request: CipherEditRequest = cipher_view.try_into().unwrap();
258
259 let result = edit_cipher(
260 &store,
261 &api_client,
262 TEST_USER_ID.parse().unwrap(),
263 original_cipher_view,
264 request,
265 )
266 .await
267 .unwrap();
268
269 assert_eq!(result.id, Some(cipher_id));
270 assert_eq!(result.name, "New Cipher Name");
271 }
272
273 #[tokio::test]
274 async fn test_edit_cipher_http_error() {
275 let store: KeyStore<KeyIds> = KeyStore::default();
276 #[allow(deprecated)]
277 let _ = store.context_mut().set_symmetric_key(
278 SymmetricKeyId::User,
279 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
280 );
281
282 let api_client = ApiClient::new_mocked(move |mock| {
283 mock.ciphers_api
284 .expect_put_admin()
285 .returning(move |_id, _body| {
286 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
287 "Simulated error",
288 )))
289 });
290 });
291 let orig_cipher_view = generate_test_cipher();
292 let cipher_view = orig_cipher_view.clone();
293 let request: CipherEditRequest = cipher_view.try_into().unwrap();
294 let result = edit_cipher(
295 &store,
296 &api_client,
297 TEST_USER_ID.parse().unwrap(),
298 orig_cipher_view,
299 request,
300 )
301 .await;
302
303 assert!(result.is_err());
304 assert!(matches!(result.unwrap_err(), EditCipherAdminError::Api(_)));
305 }
306}