Skip to main content

bitwarden_vault/cipher/cipher_client/admin/
create.rs

1use bitwarden_api_api::models::CipherCreateRequestModel;
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::cipher::{PartialCipher, StrictDecrypt},
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
42/// Wraps the API call to create a cipher using the admin endpoint, for easier testing.
43async fn create_cipher(
44    request: CipherCreateRequestInternal,
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 = request.create_request.collection_ids.clone();
51    // CipherMiniResponseModel does not include folder_id, favorite, or edit — save them from
52    // the request before it is consumed so they can be applied to the merged result.
53    let folder_id = request.create_request.folder_id;
54    let favorite = request.create_request.favorite;
55    let mut cipher_request = key_store.encrypt(request)?;
56    cipher_request.encrypted_for = Some(encrypted_for.into());
57
58    let mut cipher: Cipher = api_client
59        .ciphers_api()
60        .post_admin(Some(CipherCreateRequestModel {
61            collection_ids: Some(collection_ids.iter().cloned().map(Into::into).collect()),
62            cipher: Box::new(cipher_request),
63        }))
64        .await?
65        .merge_with_cipher(None)?;
66
67    cipher.collection_ids = collection_ids;
68    cipher.folder_id = folder_id;
69    cipher.favorite = favorite;
70    cipher.edit = true;
71    cipher.view_password = true;
72
73    if use_strict_decryption {
74        Ok(key_store.decrypt(&StrictDecrypt(cipher))?)
75    } else {
76        Ok(key_store.decrypt(&cipher)?)
77    }
78}
79
80#[cfg_attr(feature = "wasm", wasm_bindgen)]
81impl CipherAdminClient {
82    /// Creates a new [Cipher] for an organization, using the admin server endpoints.
83    /// Creates the Cipher on the server only, does not store it to local state.
84    pub async fn create(
85        &self,
86        request: CipherCreateRequest,
87    ) -> Result<CipherView, CreateCipherAdminError> {
88        let key_store = self.client.internal.get_key_store();
89        let config = self.client.internal.get_api_configurations();
90        let mut internal_request: CipherCreateRequestInternal = request.into();
91
92        let user_id = self
93            .client
94            .internal
95            .get_user_id()
96            .ok_or(NotAuthenticatedError)?;
97
98        // TODO: Once this flag is removed, the key generation logic should
99        // be moved closer to the actual encryption logic.
100        if self
101            .client
102            .internal
103            .get_flags()
104            .await
105            .enable_cipher_key_encryption
106        {
107            let key = internal_request.key_identifier();
108            internal_request.generate_cipher_key(&mut key_store.context(), key)?;
109        }
110
111        create_cipher(
112            internal_request,
113            user_id,
114            &config.api_client,
115            key_store,
116            self.is_strict_decrypt().await,
117        )
118        .await
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use bitwarden_api_api::models::CipherMiniResponseModel;
125    use bitwarden_core::{OrganizationId, key_management::SymmetricKeySlotId};
126    use bitwarden_crypto::SymmetricCryptoKey;
127    use chrono::Utc;
128
129    use super::*;
130    use crate::{CipherRepromptType, CipherViewType, LoginView};
131
132    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
133    const TEST_COLLECTION_ID: &str = "73546b86-8802-4449-ad2a-69ea981b4ffd";
134    const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
135    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
136
137    #[tokio::test]
138    async fn test_create_org_cipher() {
139        let api_client = bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
140            mock.ciphers_api
141                .expect_post_admin()
142                .returning(move |request| {
143                    let request = request.unwrap();
144
145                    Ok(CipherMiniResponseModel {
146                        id: Some(TEST_CIPHER_ID.try_into().unwrap()),
147                        organization_id: request
148                            .cipher
149                            .organization_id
150                            .and_then(|id| id.parse().ok()),
151                        name: Some(request.cipher.name.clone()),
152                        r#type: request.cipher.r#type,
153                        creation_date: Some(
154                            Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
155                        ),
156                        revision_date: Some(
157                            Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
158                        ),
159                        ..Default::default()
160                    })
161                });
162        });
163
164        let store: KeyStore<KeySlotIds> = KeyStore::default();
165        #[allow(deprecated)]
166        let _ = store.context_mut().set_symmetric_key(
167            SymmetricKeySlotId::User,
168            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
169        );
170        #[allow(deprecated)]
171        let _ = store.context_mut().set_symmetric_key(
172            SymmetricKeySlotId::Organization(TEST_ORG_ID.parse::<OrganizationId>().unwrap()),
173            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
174        );
175
176        let test_folder_id: crate::FolderId =
177            "a4e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
178        let test_collection_id: bitwarden_collections::collection::CollectionId =
179            TEST_COLLECTION_ID.parse().unwrap();
180
181        let cipher_request: CipherCreateRequestInternal = CipherCreateRequest {
182            organization_id: Some(TEST_ORG_ID.parse().unwrap()),
183            collection_ids: vec![test_collection_id],
184            folder_id: Some(test_folder_id),
185            name: "Test Cipher".into(),
186            notes: None,
187            favorite: true,
188            reprompt: CipherRepromptType::None,
189            r#type: CipherViewType::Login(LoginView {
190                username: None,
191                password: None,
192                password_revision_date: None,
193                uris: None,
194                totp: None,
195                autofill_on_page_load: None,
196                fido2_credentials: None,
197            }),
198            fields: vec![],
199        }
200        .into();
201
202        let response = create_cipher(
203            cipher_request.clone(),
204            TEST_USER_ID.parse().unwrap(),
205            &api_client,
206            &store,
207            false,
208        )
209        .await
210        .unwrap();
211
212        assert_eq!(response.id, Some(TEST_CIPHER_ID.parse().unwrap()));
213        assert_eq!(
214            response.organization_id,
215            cipher_request.create_request.organization_id
216        );
217        // Fields omitted from CipherMiniResponseModel must be preserved from the request.
218        assert_eq!(
219            response.collection_ids,
220            cipher_request.create_request.collection_ids
221        );
222        assert_eq!(response.folder_id, cipher_request.create_request.folder_id);
223        assert_eq!(response.favorite, cipher_request.create_request.favorite);
224        assert!(response.edit, "edit should be true after admin create");
225        assert!(
226            response.view_password,
227            "view_password should be true after admin create"
228        );
229    }
230}