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 mut view: CipherView = convert_request_to_cipher_view(request);
113
114        // TODO: Once this flag is removed, the key generation logic should
115        // be moved directly into the CompositeEncryptable implementation.
116        if self.client.flags().get().await.enable_cipher_key_encryption {
117            let key = view.key_identifier();
118            view.generate_cipher_key(&mut key_store.context(), key)?;
119        }
120
121        let use_blob = should_use_blob_encryption(&key_store.context(), view.organization_id);
122
123        create_cipher(
124            view,
125            user_id,
126            &config.api_client,
127            key_store,
128            self.is_strict_decrypt().await,
129            use_blob,
130        )
131        .await
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use bitwarden_api_api::models::CipherMiniResponseModel;
138    use bitwarden_core::{OrganizationId, key_management::SymmetricKeySlotId};
139    use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
140    use chrono::Utc;
141
142    use super::*;
143    use crate::{CipherRepromptType, CipherViewType, LoginView};
144
145    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
146    const TEST_COLLECTION_ID: &str = "73546b86-8802-4449-ad2a-69ea981b4ffd";
147    const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
148    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
149
150    #[tokio::test]
151    async fn test_create_org_cipher() {
152        let api_client = bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
153            mock.ciphers_api
154                .expect_post_admin()
155                .returning(move |request| {
156                    let request = request.unwrap();
157
158                    Ok(CipherMiniResponseModel {
159                        id: Some(TEST_CIPHER_ID.try_into().unwrap()),
160                        organization_id: request
161                            .cipher
162                            .organization_id
163                            .and_then(|id| id.parse().ok()),
164                        name: Some(request.cipher.name.clone()),
165                        r#type: request.cipher.r#type,
166                        creation_date: Some(
167                            Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
168                        ),
169                        revision_date: Some(
170                            Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
171                        ),
172                        ..Default::default()
173                    })
174                });
175        });
176
177        let store: KeyStore<KeySlotIds> = KeyStore::default();
178        #[allow(deprecated)]
179        let _ = store.context_mut().set_symmetric_key(
180            SymmetricKeySlotId::User,
181            SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
182        );
183        #[allow(deprecated)]
184        let _ = store.context_mut().set_symmetric_key(
185            SymmetricKeySlotId::Organization(TEST_ORG_ID.parse::<OrganizationId>().unwrap()),
186            SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
187        );
188
189        let test_folder_id: crate::FolderId =
190            "a4e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
191        let test_collection_id: bitwarden_collections::collection::CollectionId =
192            TEST_COLLECTION_ID.parse().unwrap();
193
194        let view: CipherView = convert_request_to_cipher_view(CipherCreateRequest {
195            organization_id: Some(TEST_ORG_ID.parse().unwrap()),
196            collection_ids: vec![test_collection_id],
197            folder_id: Some(test_folder_id),
198            name: "Test Cipher".into(),
199            notes: None,
200            favorite: true,
201            reprompt: CipherRepromptType::None,
202            r#type: CipherViewType::Login(LoginView {
203                username: None,
204                password: None,
205                password_revision_date: None,
206                uris: None,
207                totp: None,
208                autofill_on_page_load: None,
209                fido2_credentials: None,
210            }),
211            fields: vec![],
212            archived_date: None,
213        });
214
215        let response = create_cipher(
216            view.clone(),
217            TEST_USER_ID.parse().unwrap(),
218            &api_client,
219            &store,
220            false,
221            false,
222        )
223        .await
224        .unwrap();
225
226        assert_eq!(response.id, Some(TEST_CIPHER_ID.parse().unwrap()));
227        assert_eq!(response.organization_id, view.organization_id);
228        // Fields omitted from CipherMiniResponseModel must be preserved from the request.
229        assert_eq!(response.collection_ids, view.collection_ids);
230        assert_eq!(response.folder_id, view.folder_id);
231        assert_eq!(response.favorite, view.favorite);
232        assert!(response.edit, "edit should be true after admin create");
233        assert!(
234            response.view_password,
235            "view_password should be true after admin create"
236        );
237    }
238}