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().await;
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
148 .client
149 .internal
150 .get_api_configurations()
151 .await
152 .api_client,
153 self.client.internal.get_key_store(),
154 )
155 .await
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use bitwarden_api_api::{apis::ApiClient, models::CipherMiniResponseModel};
162 use bitwarden_core::key_management::SymmetricKeyId;
163 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
164
165 use super::*;
166 use crate::{CipherId, CipherRepromptType, CipherType, LoginView};
167
168 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
169 const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
170
171 fn generate_test_cipher() -> CipherView {
172 CipherView {
173 id: Some(TEST_CIPHER_ID.parse().unwrap()),
174 organization_id: None,
175 folder_id: None,
176 collection_ids: vec![],
177 key: None,
178 name: "Test Login".to_string(),
179 notes: None,
180 r#type: CipherType::Login,
181 login: Some(LoginView {
182 username: Some("[email protected]".to_string()),
183 password: Some("password123".to_string()),
184 password_revision_date: None,
185 uris: None,
186 totp: None,
187 autofill_on_page_load: None,
188 fido2_credentials: None,
189 }),
190 identity: None,
191 card: None,
192 secure_note: None,
193 ssh_key: None,
194 favorite: false,
195 reprompt: CipherRepromptType::None,
196 organization_use_totp: true,
197 edit: true,
198 permissions: None,
199 view_password: true,
200 local_data: None,
201 attachments: None,
202 fields: None,
203 password_history: None,
204 creation_date: "2025-01-01T00:00:00Z".parse().unwrap(),
205 deleted_date: None,
206 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
207 archived_date: None,
208 }
209 }
210
211 #[tokio::test]
212 async fn test_edit_cipher() {
213 let store: KeyStore<KeyIds> = KeyStore::default();
214 #[allow(deprecated)]
215 let _ = store.context_mut().set_symmetric_key(
216 SymmetricKeyId::User,
217 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
218 );
219
220 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
221
222 let api_client = ApiClient::new_mocked(move |mock| {
223 mock.ciphers_api
224 .expect_put_admin()
225 .returning(move |_id, body| {
226 let body = body.unwrap();
227 Ok(CipherMiniResponseModel {
228 object: Some("cipher".to_string()),
229 id: Some(cipher_id.into()),
230 name: Some(body.name),
231 r#type: body.r#type,
232 organization_id: body
233 .organization_id
234 .as_ref()
235 .and_then(|id| uuid::Uuid::parse_str(id).ok()),
236 reprompt: body.reprompt,
237 key: body.key,
238 notes: body.notes,
239 organization_use_totp: Some(true),
240 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
241 creation_date: Some("2025-01-01T00:00:00Z".to_string()),
242 deleted_date: None,
243 login: body.login,
244 card: body.card,
245 identity: body.identity,
246 secure_note: body.secure_note,
247 ssh_key: body.ssh_key,
248 fields: body.fields,
249 password_history: body.password_history,
250 attachments: None,
251 data: None,
252 archived_date: None,
253 })
254 })
255 .once();
256 });
257
258 let original_cipher_view = generate_test_cipher();
259 let mut cipher_view = original_cipher_view.clone();
260 cipher_view.name = "New Cipher Name".to_string();
261
262 let request: CipherEditRequest = cipher_view.try_into().unwrap();
263
264 let result = edit_cipher(
265 &store,
266 &api_client,
267 TEST_USER_ID.parse().unwrap(),
268 original_cipher_view,
269 request,
270 )
271 .await
272 .unwrap();
273
274 assert_eq!(result.id, Some(cipher_id));
275 assert_eq!(result.name, "New Cipher Name");
276 }
277
278 #[tokio::test]
279 async fn test_edit_cipher_http_error() {
280 let store: KeyStore<KeyIds> = KeyStore::default();
281 #[allow(deprecated)]
282 let _ = store.context_mut().set_symmetric_key(
283 SymmetricKeyId::User,
284 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
285 );
286
287 let api_client = ApiClient::new_mocked(move |mock| {
288 mock.ciphers_api
289 .expect_put_admin()
290 .returning(move |_id, _body| {
291 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
292 "Simulated error",
293 )))
294 });
295 });
296 let orig_cipher_view = generate_test_cipher();
297 let cipher_view = orig_cipher_view.clone();
298 let request: CipherEditRequest = cipher_view.try_into().unwrap();
299 let result = edit_cipher(
300 &store,
301 &api_client,
302 TEST_USER_ID.parse().unwrap(),
303 orig_cipher_view,
304 request,
305 )
306 .await;
307
308 assert!(result.is_err());
309 assert!(matches!(result.unwrap_err(), EditCipherAdminError::Api(_)));
310 }
311}