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