bitwarden_vault/cipher/cipher_client/admin/
create.rs1use bitwarden_api_api::models::CipherCreateRequestModel;
2use bitwarden_core::{
3 ApiError, MissingFieldError, NotAuthenticatedError, UserId, key_management::KeyIds,
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::cipher::PartialCipher,
14 cipher_client::{
15 admin::CipherAdminClient,
16 create::{CipherCreateRequest, CipherCreateRequestInternal},
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
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 request: CipherCreateRequestInternal,
45 encrypted_for: UserId,
46 api_client: &bitwarden_api_api::apis::ApiClient,
47 key_store: &KeyStore<KeyIds>,
48) -> Result<CipherView, CreateCipherAdminError> {
49 let collection_ids = request.create_request.collection_ids.clone();
50 let mut cipher_request = key_store.encrypt(request)?;
51 cipher_request.encrypted_for = Some(encrypted_for.into());
52
53 let cipher: Cipher = api_client
54 .ciphers_api()
55 .post_admin(Some(CipherCreateRequestModel {
56 collection_ids: Some(collection_ids.into_iter().map(Into::into).collect()),
57 cipher: Box::new(cipher_request),
58 }))
59 .await?
60 .merge_with_cipher(None)?;
61
62 Ok(key_store.decrypt(&cipher)?)
63}
64
65#[cfg_attr(feature = "wasm", wasm_bindgen)]
66impl CipherAdminClient {
67 pub async fn create(
70 &self,
71 request: CipherCreateRequest,
72 ) -> Result<CipherView, CreateCipherAdminError> {
73 let key_store = self.client.internal.get_key_store();
74 let config = self.client.internal.get_api_configurations().await;
75 let mut internal_request: CipherCreateRequestInternal = request.into();
76
77 let user_id = self
78 .client
79 .internal
80 .get_user_id()
81 .ok_or(NotAuthenticatedError)?;
82
83 if self
86 .client
87 .internal
88 .get_flags()
89 .enable_cipher_key_encryption
90 {
91 let key = internal_request.key_identifier();
92 internal_request.generate_cipher_key(&mut key_store.context(), key)?;
93 }
94
95 create_cipher(internal_request, user_id, &config.api_client, key_store).await
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use bitwarden_api_api::models::CipherMiniResponseModel;
102 use bitwarden_core::{OrganizationId, key_management::SymmetricKeyId};
103 use bitwarden_crypto::SymmetricCryptoKey;
104 use chrono::Utc;
105
106 use super::*;
107 use crate::{CipherRepromptType, CipherViewType, LoginView};
108
109 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
110 const TEST_COLLECTION_ID: &str = "73546b86-8802-4449-ad2a-69ea981b4ffd";
111 const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
112 const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
113
114 #[tokio::test]
115 async fn test_create_org_cipher() {
116 let api_client = bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
117 mock.ciphers_api
118 .expect_post_admin()
119 .returning(move |request| {
120 let request = request.unwrap();
121
122 Ok(CipherMiniResponseModel {
123 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
124 organization_id: request
125 .cipher
126 .organization_id
127 .and_then(|id| id.parse().ok()),
128 name: Some(request.cipher.name.clone()),
129 r#type: request.cipher.r#type,
130 creation_date: Some(
131 Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
132 ),
133 revision_date: Some(
134 Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
135 ),
136 ..Default::default()
137 })
138 });
139 });
140
141 let store: KeyStore<KeyIds> = KeyStore::default();
142 #[allow(deprecated)]
143 let _ = store.context_mut().set_symmetric_key(
144 SymmetricKeyId::User,
145 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
146 );
147 #[allow(deprecated)]
148 let _ = store.context_mut().set_symmetric_key(
149 SymmetricKeyId::Organization(TEST_ORG_ID.parse::<OrganizationId>().unwrap()),
150 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
151 );
152
153 let cipher_request: CipherCreateRequestInternal = CipherCreateRequest {
154 organization_id: Some(TEST_ORG_ID.parse().unwrap()),
155 collection_ids: vec![TEST_COLLECTION_ID.parse().unwrap()],
156 folder_id: None,
157 name: "Test Cipher".into(),
158 notes: None,
159 favorite: false,
160 reprompt: CipherRepromptType::None,
161 r#type: CipherViewType::Login(LoginView {
162 username: None,
163 password: None,
164 password_revision_date: None,
165 uris: None,
166 totp: None,
167 autofill_on_page_load: None,
168 fido2_credentials: None,
169 }),
170 fields: vec![],
171 }
172 .into();
173
174 let response = create_cipher(
175 cipher_request.clone(),
176 TEST_USER_ID.parse().unwrap(),
177 &api_client,
178 &store,
179 )
180 .await
181 .unwrap();
182
183 assert_eq!(response.id, Some(TEST_CIPHER_ID.parse().unwrap()));
184 assert_eq!(
185 response.organization_id,
186 cipher_request.create_request.organization_id
187 );
188 }
189}