bitwarden_vault/cipher/cipher_client/admin/
create.rs1use bitwarden_api_api::models::{CipherCreateRequestModel, CipherRequestModel};
2use bitwarden_core::{
3 ApiError, MissingFieldError, NotAuthenticatedError, UserId, key_management::KeySlotIds,
4};
5use bitwarden_crypto::{CryptoError, IdentifyKey, KeyStore};
6use bitwarden_error::bitwarden_error;
7use thiserror::Error;
8#[cfg(feature = "wasm")]
9use wasm_bindgen::prelude::*;
10
11use crate::{
12 Cipher, CipherView, VaultParseError,
13 cipher::{
14 cipher::{EncryptMode, PartialCipher, StrictDecrypt},
15 cipher_client::create::convert_request_to_cipher_view,
16 },
17 cipher_client::{
18 admin::CipherAdminClient, create::CipherCreateRequest, should_use_blob_encryption,
19 },
20};
21
22#[allow(missing_docs)]
23#[bitwarden_error(flat)]
24#[derive(Debug, Error)]
25pub enum CreateCipherAdminError {
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}
37
38impl<T> From<bitwarden_api_api::apis::Error<T>> for CreateCipherAdminError {
39 fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
40 Self::Api(val.into())
41 }
42}
43
44async fn create_cipher(
46 view: CipherView,
47 encrypted_for: UserId,
48 api_client: &bitwarden_api_api::apis::ApiClient,
49 key_store: &KeyStore<KeySlotIds>,
50 use_strict_decryption: bool,
51 use_blob: bool,
52) -> Result<CipherView, CreateCipherAdminError> {
53 let collection_ids = view.collection_ids.clone();
54 let folder_id = view.folder_id;
57 let favorite = view.favorite;
58
59 let mode = if use_blob {
65 EncryptMode::Blob(view)
66 } else {
67 EncryptMode::Legacy(view)
68 };
69 let cipher: Cipher = key_store.encrypt(mode)?;
70 let mut cipher_request: CipherRequestModel = cipher.try_into()?;
71 cipher_request.encrypted_for = Some(encrypted_for.into());
72
73 let mut cipher: Cipher = api_client
74 .ciphers_api()
75 .post_admin(Some(CipherCreateRequestModel {
76 collection_ids: Some(collection_ids.iter().cloned().map(Into::into).collect()),
77 cipher: Box::new(cipher_request),
78 }))
79 .await?
80 .merge_with_cipher(None)?;
81
82 cipher.collection_ids = collection_ids;
83 cipher.folder_id = folder_id;
84 cipher.favorite = favorite;
85 cipher.edit = true;
86 cipher.view_password = true;
87
88 Ok(if use_strict_decryption {
89 key_store.decrypt(&StrictDecrypt(cipher))?
90 } else {
91 key_store.decrypt(&cipher)?
92 })
93}
94
95#[allow(deprecated)]
96#[cfg_attr(feature = "wasm", wasm_bindgen)]
97impl CipherAdminClient {
98 pub async fn create(
101 &self,
102 request: CipherCreateRequest,
103 ) -> Result<CipherView, CreateCipherAdminError> {
104 let key_store = self.client.internal.get_key_store();
105 let config = self.client.internal.get_api_configurations();
106
107 let user_id = self
108 .client
109 .internal
110 .get_user_id()
111 .ok_or(NotAuthenticatedError)?;
112
113 let mut view: CipherView = convert_request_to_cipher_view(request);
114
115 if self.client.flags().get().await.enable_cipher_key_encryption {
118 let key = view.key_identifier();
119 view.generate_cipher_key(&mut key_store.context(), key)?;
120 }
121
122 let use_blob = should_use_blob_encryption(&self.client, view.organization_id);
123
124 create_cipher(
125 view,
126 user_id,
127 &config.api_client,
128 key_store,
129 self.is_strict_decrypt().await,
130 use_blob,
131 )
132 .await
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use bitwarden_api_api::models::CipherMiniResponseModel;
139 use bitwarden_core::{OrganizationId, key_management::SymmetricKeySlotId};
140 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
141 use chrono::Utc;
142
143 use super::*;
144 use crate::{CipherRepromptType, CipherViewType, LoginView};
145
146 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
147 const TEST_COLLECTION_ID: &str = "73546b86-8802-4449-ad2a-69ea981b4ffd";
148 const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
149 const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
150
151 #[tokio::test]
152 async fn test_create_org_cipher() {
153 let api_client = bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
154 mock.ciphers_api
155 .expect_post_admin()
156 .returning(move |request| {
157 let request = request.unwrap();
158
159 Ok(CipherMiniResponseModel {
160 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
161 organization_id: request
162 .cipher
163 .organization_id
164 .and_then(|id| id.parse().ok()),
165 name: Some(request.cipher.name.clone()),
166 r#type: request.cipher.r#type,
167 creation_date: Some(
168 Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
169 ),
170 revision_date: Some(
171 Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
172 ),
173 ..Default::default()
174 })
175 });
176 });
177
178 let store: KeyStore<KeySlotIds> = KeyStore::default();
179 #[allow(deprecated)]
180 let _ = store.context_mut().set_symmetric_key(
181 SymmetricKeySlotId::User,
182 SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
183 );
184 #[allow(deprecated)]
185 let _ = store.context_mut().set_symmetric_key(
186 SymmetricKeySlotId::Organization(TEST_ORG_ID.parse::<OrganizationId>().unwrap()),
187 SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
188 );
189
190 let test_folder_id: crate::FolderId =
191 "a4e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
192 let test_collection_id: bitwarden_collections::collection::CollectionId =
193 TEST_COLLECTION_ID.parse().unwrap();
194
195 let view: CipherView = convert_request_to_cipher_view(CipherCreateRequest {
196 organization_id: Some(TEST_ORG_ID.parse().unwrap()),
197 collection_ids: vec![test_collection_id],
198 folder_id: Some(test_folder_id),
199 name: "Test Cipher".into(),
200 notes: None,
201 favorite: true,
202 reprompt: CipherRepromptType::None,
203 r#type: CipherViewType::Login(LoginView {
204 username: None,
205 password: None,
206 password_revision_date: None,
207 uris: None,
208 totp: None,
209 autofill_on_page_load: None,
210 fido2_credentials: None,
211 }),
212 fields: vec![],
213 archived_date: None,
214 });
215
216 let response = create_cipher(
217 view.clone(),
218 TEST_USER_ID.parse().unwrap(),
219 &api_client,
220 &store,
221 false,
222 false,
223 )
224 .await
225 .unwrap();
226
227 assert_eq!(response.id, Some(TEST_CIPHER_ID.parse().unwrap()));
228 assert_eq!(response.organization_id, view.organization_id);
229 assert_eq!(response.collection_ids, view.collection_ids);
231 assert_eq!(response.folder_id, view.folder_id);
232 assert_eq!(response.favorite, view.favorite);
233 assert!(response.edit, "edit should be true after admin create");
234 assert!(
235 response.view_password,
236 "view_password should be true after admin create"
237 );
238 }
239}