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