bitwarden_vault/cipher/cipher_client/admin/
get.rs1use bitwarden_api_api::models::CipherMiniDetailsResponseModelListResponseModel;
2use bitwarden_core::{ApiError, OrganizationId, key_management::KeySlotIds};
3use bitwarden_crypto::KeyStore;
4use bitwarden_error::bitwarden_error;
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::wasm_bindgen;
8
9use crate::{
10 Cipher, VaultParseError,
11 cipher::cipher::{ListOrganizationCiphersResult, PartialCipher, StrictDecrypt},
12 cipher_client::admin::CipherAdminClient,
13};
14
15#[allow(missing_docs)]
16#[bitwarden_error(flat)]
17#[derive(Debug, Error)]
18pub enum GetAssignedOrgCiphersAdminError {
19 #[error(transparent)]
20 Api(#[from] ApiError),
21 #[error(transparent)]
22 VaultParse(#[from] VaultParseError),
23}
24
25impl<T> From<bitwarden_api_api::apis::Error<T>> for GetAssignedOrgCiphersAdminError {
26 fn from(value: bitwarden_api_api::apis::Error<T>) -> Self {
27 Self::Api(value.into())
28 }
29}
30
31#[allow(missing_docs)]
32#[bitwarden_error(flat)]
33#[derive(Debug, Error)]
34pub enum GetOrganizationCiphersAdminError {
35 #[error(transparent)]
36 VaultParse(#[from] VaultParseError),
37 #[error(transparent)]
38 Api(#[from] ApiError),
39}
40
41pub async fn list_org_ciphers(
43 org_id: OrganizationId,
44 include_member_items: bool,
45 api_client: &bitwarden_api_api::apis::ApiClient,
46 key_store: &KeyStore<KeySlotIds>,
47 use_strict_decryption: bool,
48) -> Result<ListOrganizationCiphersResult, GetOrganizationCiphersAdminError> {
49 let api = api_client.ciphers_api();
50 let response: CipherMiniDetailsResponseModelListResponseModel = api
51 .get_organization_ciphers(Some(org_id.into()), Some(include_member_items))
52 .await
53 .map_err(ApiError::from)?;
54 let ciphers = response
55 .data
56 .into_iter()
57 .flatten()
58 .map(|model| model.merge_with_cipher(None))
59 .collect::<Result<Vec<_>, _>>()?;
60
61 let list_views = if use_strict_decryption {
62 let wrapped: Vec<StrictDecrypt<Cipher>> =
63 ciphers.iter().cloned().map(StrictDecrypt).collect();
64 let (list_views, _failures) = key_store.decrypt_list_with_failures(&wrapped);
65 list_views
66 } else {
67 let (list_views, _failures) = key_store.decrypt_list_with_failures(&ciphers);
68 list_views
69 };
70 Ok(ListOrganizationCiphersResult {
71 ciphers,
72 list_views,
73 })
74}
75
76#[cfg_attr(feature = "wasm", wasm_bindgen)]
77impl CipherAdminClient {
78 pub async fn list_assigned_org_ciphers(
80 &self,
81 org_id: OrganizationId,
82 ) -> Result<ListOrganizationCiphersResult, GetAssignedOrgCiphersAdminError> {
83 use bitwarden_api_api::models::CipherDetailsResponseModelListResponseModel;
84
85 let response: CipherDetailsResponseModelListResponseModel = self
86 .api_configurations
87 .api_client
88 .ciphers_api()
89 .get_assigned_organization_ciphers(Some(org_id.into()))
90 .await?;
91
92 let ciphers = response
93 .data
94 .into_iter()
95 .flatten()
96 .map(|model| model.merge_with_cipher(None))
97 .collect::<Result<Vec<_>, _>>()?;
98
99 let list_views = if self.is_strict_decrypt().await {
100 let wrapped: Vec<StrictDecrypt<Cipher>> =
101 ciphers.iter().cloned().map(StrictDecrypt).collect();
102 let (list_views, _failures) = self.key_store.decrypt_list_with_failures(&wrapped);
103 list_views
104 } else {
105 let (list_views, _failures) = self.key_store.decrypt_list_with_failures(&ciphers);
106 list_views
107 };
108 Ok(ListOrganizationCiphersResult {
109 ciphers,
110 list_views,
111 })
112 }
113
114 pub async fn list_org_ciphers(
116 &self,
117 org_id: OrganizationId,
118 include_member_items: bool,
119 ) -> Result<ListOrganizationCiphersResult, GetOrganizationCiphersAdminError> {
120 list_org_ciphers(
121 org_id,
122 include_member_items,
123 &self.api_configurations.api_client,
124 &self.key_store,
125 self.is_strict_decrypt().await,
126 )
127 .await
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use std::sync::Arc;
134
135 use bitwarden_api_api::{
136 apis::ApiClient,
137 models::{
138 CipherDetailsResponseModel, CipherDetailsResponseModelListResponseModel,
139 CipherMiniDetailsResponseModel, CipherMiniDetailsResponseModelListResponseModel,
140 },
141 };
142 use bitwarden_core::{
143 client::ApiConfigurations, key_management::create_test_crypto_with_user_key,
144 };
145 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
146 use chrono::Utc;
147
148 use super::*;
149 use crate::{Cipher, CipherType, Login};
150
151 const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
152 const TEST_CIPHER_ID_1: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
153 const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
154
155 fn create_test_client(api_client: ApiClient) -> CipherAdminClient {
156 #[allow(deprecated)]
157 CipherAdminClient {
158 key_store: create_test_crypto_with_user_key(SymmetricCryptoKey::make(
159 SymmetricKeyAlgorithm::Aes256CbcHmac,
160 )),
161 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
162 client: bitwarden_core::Client::new_test(None),
163 }
164 }
165
166 fn mock_mini_cipher(cipher_id: &str) -> CipherMiniDetailsResponseModel {
167 let cipher = generate_test_cipher();
168 CipherMiniDetailsResponseModel {
169 id: cipher_id.parse().ok(),
170 name: cipher.name.as_ref().map(ToString::to_string),
171 r#type: Some(cipher.r#type.into()),
172 login: cipher.login.clone().map(|l| Box::new(l.into())),
173 creation_date: Some(Utc::now().to_rfc3339()),
174 revision_date: Some(Utc::now().to_rfc3339()),
175 ..Default::default()
176 }
177 }
178
179 fn mock_details_cipher(cipher_id: &str) -> CipherDetailsResponseModel {
180 CipherDetailsResponseModel {
181 id: Some(cipher_id.parse().unwrap()),
182 name: Some("2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".to_string()),
183 r#type: Some(bitwarden_api_api::models::CipherType::Login),
184 login: Some(Box::new(bitwarden_api_api::models::CipherLoginModel::default())),
185 creation_date: Some(Utc::now().to_rfc3339()),
186 revision_date: Some(Utc::now().to_rfc3339()),
187 ..Default::default()
188 }
189 }
190
191 fn generate_test_cipher() -> Cipher {
192 Cipher {
193 id: TEST_CIPHER_ID_1.parse().ok(),
194 name: Some("2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap()),
195 r#type: CipherType::Login,
196 notes: Default::default(),
197 organization_id: Default::default(),
198 folder_id: Default::default(),
199 favorite: Default::default(),
200 reprompt: Default::default(),
201 fields: Default::default(),
202 collection_ids: Default::default(),
203 key: Default::default(),
204 login: Some(Login {
205 username: None,
206 password: None,
207 password_revision_date: None,
208 uris: None,
209 totp: None,
210 autofill_on_page_load: None,
211 fido2_credentials: None,
212 }),
213 identity: Default::default(),
214 card: Default::default(),
215 secure_note: Default::default(),
216 ssh_key: Default::default(),
217 bank_account: Default::default(),
218 drivers_license: Default::default(),
219 passport: Default::default(),
220 organization_use_totp: Default::default(),
221 edit: Default::default(),
222 permissions: Default::default(),
223 view_password: Default::default(),
224 local_data: Default::default(),
225 attachments: Default::default(),
226 password_history: Default::default(),
227 creation_date: Default::default(),
228 deleted_date: Default::default(),
229 revision_date: Default::default(),
230 archived_date: Default::default(),
231 data: Default::default(),
232 }
233 }
234
235 #[tokio::test]
236 async fn test_list_org_ciphers_all_success() {
237 let api_client = ApiClient::new_mocked(move |mock| {
238 mock.ciphers_api
239 .expect_get_organization_ciphers()
240 .returning(move |_org_id, _include_member_items| {
241 Ok(CipherMiniDetailsResponseModelListResponseModel {
242 object: None,
243 data: Some(vec![
244 mock_mini_cipher(TEST_CIPHER_ID_1),
245 mock_mini_cipher(TEST_CIPHER_ID_2),
246 ]),
247 continuation_token: None,
248 })
249 });
250 });
251
252 let client = create_test_client(api_client);
253 let result = client
254 .list_org_ciphers(TEST_ORG_ID.parse().unwrap(), true)
255 .await
256 .unwrap();
257
258 assert_eq!(result.ciphers.len(), 2);
259 assert_eq!(result.list_views.len(), 2);
260 assert_eq!(result.ciphers[0].id, TEST_CIPHER_ID_1.parse().ok());
261 assert_eq!(result.ciphers[1].id, TEST_CIPHER_ID_2.parse().ok());
262 }
263
264 #[tokio::test]
265 async fn test_list_org_ciphers_with_failures() {
266 let api_client = ApiClient::new_mocked(move |mock| {
267 mock.ciphers_api
268 .expect_get_organization_ciphers()
269 .returning(move |_org_id, _include_member_items| {
270 let mut bad = mock_mini_cipher(TEST_CIPHER_ID_2);
271 bad.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".to_string());
272 Ok(CipherMiniDetailsResponseModelListResponseModel {
273 object: None,
274 data: Some(vec![mock_mini_cipher(TEST_CIPHER_ID_1), bad]),
275 continuation_token: None,
276 })
277 });
278 });
279
280 let client = create_test_client(api_client);
281 let result = client
282 .list_org_ciphers(TEST_ORG_ID.parse().unwrap(), true)
283 .await
284 .unwrap();
285
286 assert_eq!(result.ciphers.len(), 2);
287 assert_eq!(result.list_views.len(), 1);
288 }
289
290 #[tokio::test]
291 async fn test_list_org_ciphers_empty() {
292 let api_client = ApiClient::new_mocked(move |mock| {
293 mock.ciphers_api
294 .expect_get_organization_ciphers()
295 .returning(move |_org_id, _include_member_items| {
296 Ok(CipherMiniDetailsResponseModelListResponseModel {
297 object: None,
298 data: Some(vec![]),
299 continuation_token: None,
300 })
301 });
302 });
303
304 let client = create_test_client(api_client);
305 let result = client
306 .list_org_ciphers(TEST_ORG_ID.parse().unwrap(), false)
307 .await
308 .unwrap();
309
310 assert!(result.ciphers.is_empty());
311 assert!(result.list_views.is_empty());
312 }
313
314 #[tokio::test]
315 async fn test_list_assigned_org_ciphers_success() {
316 let api_client = ApiClient::new_mocked(|mock| {
317 mock.ciphers_api
318 .expect_get_assigned_organization_ciphers()
319 .returning(|_| {
320 Ok(CipherDetailsResponseModelListResponseModel {
321 object: None,
322 data: Some(vec![
323 mock_details_cipher(TEST_CIPHER_ID_1),
324 mock_details_cipher(TEST_CIPHER_ID_2),
325 ]),
326 continuation_token: None,
327 })
328 });
329 });
330
331 let client = create_test_client(api_client);
332 let result = client
333 .list_assigned_org_ciphers(TEST_ORG_ID.parse().unwrap())
334 .await
335 .unwrap();
336
337 assert_eq!(result.ciphers.len(), 2);
338 assert_eq!(result.list_views.len(), 2);
339 }
340
341 #[tokio::test]
342 async fn test_list_assigned_org_ciphers_with_failures() {
343 let api_client = ApiClient::new_mocked(|mock| {
344 mock.ciphers_api
345 .expect_get_assigned_organization_ciphers()
346 .returning(|_| {
347 let mut bad = mock_details_cipher(TEST_CIPHER_ID_2);
348 bad.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".to_string());
349 Ok(CipherDetailsResponseModelListResponseModel {
350 object: None,
351 data: Some(vec![mock_details_cipher(TEST_CIPHER_ID_1), bad]),
352 continuation_token: None,
353 })
354 });
355 });
356
357 let client = create_test_client(api_client);
358 let result = client
359 .list_assigned_org_ciphers(TEST_ORG_ID.parse().unwrap())
360 .await
361 .unwrap();
362
363 assert_eq!(result.ciphers.len(), 2);
364 assert_eq!(result.list_views.len(), 1);
365 assert_eq!(result.list_views[0].id, TEST_CIPHER_ID_1.parse().ok());
366 }
367
368 #[tokio::test]
369 async fn test_list_assigned_org_ciphers_empty() {
370 let api_client = ApiClient::new_mocked(|mock| {
371 mock.ciphers_api
372 .expect_get_assigned_organization_ciphers()
373 .returning(|_| {
374 Ok(CipherDetailsResponseModelListResponseModel {
375 object: None,
376 data: Some(vec![]),
377 continuation_token: None,
378 })
379 });
380 });
381
382 let client = create_test_client(api_client);
383 let result = client
384 .list_assigned_org_ciphers(TEST_ORG_ID.parse().unwrap())
385 .await
386 .unwrap();
387
388 assert!(result.ciphers.is_empty());
389 assert!(result.list_views.is_empty());
390 }
391}