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::{EncryptMode, PartialCipher, StrictDecrypt},
15        cipher_client::create::convert_request_to_cipher_view,
16    },
17    cipher_client::{
18        admin::CipherAdminClient, create::CipherCreateRequest, should_use_blob_encryption,
19    },
20};
21
22#[allow(missing_docs)]
23#[bitwarden_error(flat)]
24#[derive(Debug, Error)]
25pub enum CreateCipherAdminError {
26    #[error(transparent)]
27    Crypto(#[from] CryptoError),
28    #[error(transparent)]
29    Api(#[from] ApiError),
30    #[error(transparent)]
31    VaultParse(#[from] VaultParseError),
32    #[error(transparent)]
33    MissingField(#[from] MissingFieldError),
34    #[error(transparent)]
35    NotAuthenticated(#[from] NotAuthenticatedError),
36}
37
38impl<T> From<bitwarden_api_api::apis::Error<T>> for CreateCipherAdminError {
39    fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
40        Self::Api(val.into())
41    }
42}
43
44/// Wraps the API call to create a cipher using the admin endpoint, for easier testing.
45async fn create_cipher(
46    view: CipherView,
47    encrypted_for: UserId,
48    api_client: &bitwarden_api_api::apis::ApiClient,
49    key_store: &KeyStore<KeySlotIds>,
50    use_strict_decryption: bool,
51    use_blob: bool,
52) -> Result<CipherView, CreateCipherAdminError> {
53    let collection_ids = view.collection_ids.clone();
54    // CipherMiniResponseModel does not include folder_id, favorite, or edit — save them from
55    // the view before it is consumed so they can be applied to the merged result.
56    let folder_id = view.folder_id;
57    let favorite = view.favorite;
58
59    // Admin endpoints operate on organization-owned ciphers, which aren't
60    // expected to use blob encryption yet — `should_use_blob_encryption`
61    // returns `false` for any `Some(org)` today. Routing through the same
62    // dispatcher means org blob support (PM-32430) flips on automatically
63    // here when the helper learns to return `true` for orgs.
64    let mode = if use_blob {
65        EncryptMode::Blob(view)
66    } else {
67        EncryptMode::Legacy(view)
68    };
69    let cipher: Cipher = key_store.encrypt(mode)?;
70    let mut cipher_request: CipherRequestModel = cipher.try_into()?;
71    cipher_request.encrypted_for = Some(encrypted_for.into());
72
73    let mut cipher: Cipher = api_client
74        .ciphers_api()
75        .post_admin(Some(CipherCreateRequestModel {
76            collection_ids: Some(collection_ids.iter().cloned().map(Into::into).collect()),
77            cipher: Box::new(cipher_request),
78        }))
79        .await?
80        .merge_with_cipher(None)?;
81
82    cipher.collection_ids = collection_ids;
83    cipher.folder_id = folder_id;
84    cipher.favorite = favorite;
85    cipher.edit = true;
86    cipher.view_password = true;
87
88    Ok(if use_strict_decryption {
89        key_store.decrypt(&StrictDecrypt(cipher))?
90    } else {
91        key_store.decrypt(&cipher)?
92    })
93}
94
95#[allow(deprecated)]
96#[cfg_attr(feature = "wasm", wasm_bindgen)]
97impl CipherAdminClient {
98    /// Creates a new [Cipher] for an organization, using the admin server endpoints.
99    /// Creates the Cipher on the server only, does not store it to local state.
100    pub async fn create(
101        &self,
102        request: CipherCreateRequest,
103    ) -> Result<CipherView, CreateCipherAdminError> {
104        let key_store = self.client.internal.get_key_store();
105        let config = self.client.internal.get_api_configurations();
106
107        let user_id = self
108            .client
109            .internal
110            .get_user_id()
111            .ok_or(NotAuthenticatedError)?;
112
113        let mut view: CipherView = convert_request_to_cipher_view(request);
114
115        // TODO: Once this flag is removed, the key generation logic should
116        // be moved directly into the CompositeEncryptable implementation.
117        if self.client.flags().get().await.enable_cipher_key_encryption {
118            let key = view.key_identifier();
119            view.generate_cipher_key(&mut key_store.context(), key)?;
120        }
121
122        let use_blob = should_use_blob_encryption(&self.client, view.organization_id);
123
124        create_cipher(
125            view,
126            user_id,
127            &config.api_client,
128            key_store,
129            self.is_strict_decrypt().await,
130            use_blob,
131        )
132        .await
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use bitwarden_api_api::models::CipherMiniResponseModel;
139    use bitwarden_core::{OrganizationId, key_management::SymmetricKeySlotId};
140    use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
141    use chrono::Utc;
142
143    use super::*;
144    use crate::{CipherRepromptType, CipherViewType, LoginView};
145
146    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
147    const TEST_COLLECTION_ID: &str = "73546b86-8802-4449-ad2a-69ea981b4ffd";
148    const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
149    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
150
151    #[tokio::test]
152    async fn test_create_org_cipher() {
153        let api_client = bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
154            mock.ciphers_api
155                .expect_post_admin()
156                .returning(move |request| {
157                    let request = request.unwrap();
158
159                    Ok(CipherMiniResponseModel {
160                        id: Some(TEST_CIPHER_ID.try_into().unwrap()),
161                        organization_id: request
162                            .cipher
163                            .organization_id
164                            .and_then(|id| id.parse().ok()),
165                        name: Some(request.cipher.name.clone()),
166                        r#type: request.cipher.r#type,
167                        creation_date: Some(
168                            Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
169                        ),
170                        revision_date: Some(
171                            Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
172                        ),
173                        ..Default::default()
174                    })
175                });
176        });
177
178        let store: KeyStore<KeySlotIds> = KeyStore::default();
179        #[allow(deprecated)]
180        let _ = store.context_mut().set_symmetric_key(
181            SymmetricKeySlotId::User,
182            SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
183        );
184        #[allow(deprecated)]
185        let _ = store.context_mut().set_symmetric_key(
186            SymmetricKeySlotId::Organization(TEST_ORG_ID.parse::<OrganizationId>().unwrap()),
187            SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
188        );
189
190        let test_folder_id: crate::FolderId =
191            "a4e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
192        let test_collection_id: bitwarden_collections::collection::CollectionId =
193            TEST_COLLECTION_ID.parse().unwrap();
194
195        let view: CipherView = convert_request_to_cipher_view(CipherCreateRequest {
196            organization_id: Some(TEST_ORG_ID.parse().unwrap()),
197            collection_ids: vec![test_collection_id],
198            folder_id: Some(test_folder_id),
199            name: "Test Cipher".into(),
200            notes: None,
201            favorite: true,
202            reprompt: CipherRepromptType::None,
203            r#type: CipherViewType::Login(LoginView {
204                username: None,
205                password: None,
206                password_revision_date: None,
207                uris: None,
208                totp: None,
209                autofill_on_page_load: None,
210                fido2_credentials: None,
211            }),
212            fields: vec![],
213            archived_date: None,
214        });
215
216        let response = create_cipher(
217            view.clone(),
218            TEST_USER_ID.parse().unwrap(),
219            &api_client,
220            &store,
221            false,
222            false,
223        )
224        .await
225        .unwrap();
226
227        assert_eq!(response.id, Some(TEST_CIPHER_ID.parse().unwrap()));
228        assert_eq!(response.organization_id, view.organization_id);
229        // Fields omitted from CipherMiniResponseModel must be preserved from the request.
230        assert_eq!(response.collection_ids, view.collection_ids);
231        assert_eq!(response.folder_id, view.folder_id);
232        assert_eq!(response.favorite, view.favorite);
233        assert!(response.edit, "edit should be true after admin create");
234        assert!(
235            response.view_password,
236            "view_password should be true after admin create"
237        );
238    }
239}