bitwarden_vault/cipher/cipher_client/admin/
create.rs

1use bitwarden_api_api::models::CipherCreateRequestModel;
2use bitwarden_core::{
3    ApiError, MissingFieldError, NotAuthenticatedError, UserId, key_management::KeyIds,
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::cipher::PartialCipher,
14    cipher_client::{
15        admin::CipherAdminClient,
16        create::{CipherCreateRequest, CipherCreateRequestInternal},
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
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    request: CipherCreateRequestInternal,
45    encrypted_for: UserId,
46    api_client: &bitwarden_api_api::apis::ApiClient,
47    key_store: &KeyStore<KeyIds>,
48) -> Result<CipherView, CreateCipherAdminError> {
49    let collection_ids = request.create_request.collection_ids.clone();
50    let mut cipher_request = key_store.encrypt(request)?;
51    cipher_request.encrypted_for = Some(encrypted_for.into());
52
53    let cipher: Cipher = api_client
54        .ciphers_api()
55        .post_admin(Some(CipherCreateRequestModel {
56            collection_ids: Some(collection_ids.into_iter().map(Into::into).collect()),
57            cipher: Box::new(cipher_request),
58        }))
59        .await?
60        .merge_with_cipher(None)?;
61
62    Ok(key_store.decrypt(&cipher)?)
63}
64
65#[cfg_attr(feature = "wasm", wasm_bindgen)]
66impl CipherAdminClient {
67    /// Creates a new [Cipher] for an organization, using the admin server endpoints.
68    /// Creates the Cipher on the server only, does not store it to local state.
69    pub async fn create(
70        &self,
71        request: CipherCreateRequest,
72    ) -> Result<CipherView, CreateCipherAdminError> {
73        let key_store = self.client.internal.get_key_store();
74        let config = self.client.internal.get_api_configurations().await;
75        let mut internal_request: CipherCreateRequestInternal = request.into();
76
77        let user_id = self
78            .client
79            .internal
80            .get_user_id()
81            .ok_or(NotAuthenticatedError)?;
82
83        // TODO: Once this flag is removed, the key generation logic should
84        // be moved closer to the actual encryption logic.
85        if self
86            .client
87            .internal
88            .get_flags()
89            .enable_cipher_key_encryption
90        {
91            let key = internal_request.key_identifier();
92            internal_request.generate_cipher_key(&mut key_store.context(), key)?;
93        }
94
95        create_cipher(internal_request, user_id, &config.api_client, key_store).await
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use bitwarden_api_api::models::CipherMiniResponseModel;
102    use bitwarden_core::{OrganizationId, key_management::SymmetricKeyId};
103    use bitwarden_crypto::SymmetricCryptoKey;
104    use chrono::Utc;
105
106    use super::*;
107    use crate::{CipherRepromptType, CipherViewType, LoginView};
108
109    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
110    const TEST_COLLECTION_ID: &str = "73546b86-8802-4449-ad2a-69ea981b4ffd";
111    const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
112    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
113
114    #[tokio::test]
115    async fn test_create_org_cipher() {
116        let api_client = bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
117            mock.ciphers_api
118                .expect_post_admin()
119                .returning(move |request| {
120                    let request = request.unwrap();
121
122                    Ok(CipherMiniResponseModel {
123                        id: Some(TEST_CIPHER_ID.try_into().unwrap()),
124                        organization_id: request
125                            .cipher
126                            .organization_id
127                            .and_then(|id| id.parse().ok()),
128                        name: Some(request.cipher.name.clone()),
129                        r#type: request.cipher.r#type,
130                        creation_date: Some(
131                            Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
132                        ),
133                        revision_date: Some(
134                            Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
135                        ),
136                        ..Default::default()
137                    })
138                });
139        });
140
141        let store: KeyStore<KeyIds> = KeyStore::default();
142        #[allow(deprecated)]
143        let _ = store.context_mut().set_symmetric_key(
144            SymmetricKeyId::User,
145            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
146        );
147        #[allow(deprecated)]
148        let _ = store.context_mut().set_symmetric_key(
149            SymmetricKeyId::Organization(TEST_ORG_ID.parse::<OrganizationId>().unwrap()),
150            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
151        );
152
153        let cipher_request: CipherCreateRequestInternal = CipherCreateRequest {
154            organization_id: Some(TEST_ORG_ID.parse().unwrap()),
155            collection_ids: vec![TEST_COLLECTION_ID.parse().unwrap()],
156            folder_id: None,
157            name: "Test Cipher".into(),
158            notes: None,
159            favorite: false,
160            reprompt: CipherRepromptType::None,
161            r#type: CipherViewType::Login(LoginView {
162                username: None,
163                password: None,
164                password_revision_date: None,
165                uris: None,
166                totp: None,
167                autofill_on_page_load: None,
168                fido2_credentials: None,
169            }),
170            fields: vec![],
171        }
172        .into();
173
174        let response = create_cipher(
175            cipher_request.clone(),
176            TEST_USER_ID.parse().unwrap(),
177            &api_client,
178            &store,
179        )
180        .await
181        .unwrap();
182
183        assert_eq!(response.id, Some(TEST_CIPHER_ID.parse().unwrap()));
184        assert_eq!(
185            response.organization_id,
186            cipher_request.create_request.organization_id
187        );
188    }
189}