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