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