Skip to main content

bitwarden_vault/cipher/cipher_client/admin/
create.rs

1use 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
42/// Wraps the API call to create a cipher using the admin endpoint, for easier testing.
43async 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    // CipherMiniResponseModel does not include folder_id, favorite, or edit — save them from
52    // the view before it is consumed so they can be applied to the merged result.
53    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    /// Creates a new [Cipher] for an organization, using the admin server endpoints.
86    /// Creates the Cipher on the server only, does not store it to local state.
87    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        // TODO: Once this flag is removed, the key generation logic should
103        // be moved directly into the CompositeEncryptable implementation.
104        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        // Fields omitted from CipherMiniResponseModel must be preserved from the request.
213        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}