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