Skip to main content

bitwarden_organization_invite_link/
invite_link_client.rs

1use std::sync::Arc;
2
3use bitwarden_core::{
4    Client, FromClient, OrganizationId, client::ApiConfigurations, key_management::KeySlotIds,
5};
6use bitwarden_crypto::KeyStore;
7use bitwarden_organization_crypto::invite::{Invite, InviteSecret};
8
9use crate::{
10    AcceptInviteLinkError, InviteLinkAdminClient, InviteLinkError, InviteLinkUserClient,
11    OrganizationInviteLink,
12};
13
14/// Client for organization invite link operations.
15///
16/// This is a thin entry point that exposes two focused sub-clients: [`InviteLinkAdminClient`] (via
17/// [`admin`](Self::admin)) for administrator CRUD operations, and [`InviteLinkUserClient`] (via
18/// [`user`](Self::user)) for invitee flows. The other methods on this type are deprecated redirects
19/// kept for backwards compatibility.
20#[bitwarden_ffi::wasm_object]
21#[derive(FromClient)]
22pub struct InviteLinkClient {
23    pub(crate) key_store: KeyStore<KeySlotIds>,
24    pub(crate) api_configurations: Arc<ApiConfigurations>,
25}
26
27// The deprecated redirects below call methods on the sub-clients (some of which are themselves
28// deprecated), and the `wasm_bindgen`-generated shims call the redirects; both would otherwise emit
29// deprecation warnings from generated code we cannot annotate individually.
30#[allow(deprecated)]
31#[bitwarden_ffi::wasm_export]
32impl InviteLinkClient {
33    /// Administrative (organization-key) invite link operations.
34    pub fn admin(&self) -> InviteLinkAdminClient {
35        InviteLinkAdminClient {
36            key_store: self.key_store.clone(),
37            api_configurations: self.api_configurations.clone(),
38        }
39    }
40
41    /// Invitee (user) invite link operations.
42    pub fn user(&self) -> InviteLinkUserClient {
43        InviteLinkUserClient {
44            key_store: self.key_store.clone(),
45            api_configurations: self.api_configurations.clone(),
46        }
47    }
48
49    /// Creates a new organization invite and posts it to the server.
50    #[deprecated(note = "Use `invite_link().admin().create(...)` instead")]
51    pub async fn create_invite_link(
52        &self,
53        organization_id: OrganizationId,
54        allowed_domains: Vec<String>,
55        supports_confirmation: bool,
56    ) -> Result<OrganizationInviteLink, InviteLinkError> {
57        self.admin()
58            .create_invite_link(organization_id, allowed_domains, supports_confirmation)
59            .await
60    }
61
62    /// Refresh an existing invite link.
63    /// This generates a new code and secret.
64    #[deprecated(note = "Use `invite_link().admin().refresh(...)` instead")]
65    pub async fn refresh_invite_link(
66        &self,
67        organization_id: OrganizationId,
68        supports_confirmation: bool,
69    ) -> Result<OrganizationInviteLink, InviteLinkError> {
70        self.admin()
71            .refresh_invite_link(organization_id, supports_confirmation)
72            .await
73    }
74
75    /// Updates whether an existing invite link supports confirmation, re-sealing the given invite
76    /// accordingly and persisting it to the server.
77    #[deprecated(note = "Use `invite_link().admin().update_confirmation(...)` instead")]
78    pub async fn set_invite_confirmation(
79        &self,
80        organization_id: OrganizationId,
81        invite: Invite,
82        supports_confirmation: bool,
83    ) -> Result<OrganizationInviteLink, InviteLinkError> {
84        self.admin()
85            .set_invite_confirmation(organization_id, invite, supports_confirmation)
86            .await
87    }
88
89    /// Using the organization key, recovers the [`InviteSecret`] from the invite carried in the
90    /// given [`OrganizationInviteLink`] so an admin can reconstruct the invite link.
91    #[deprecated(
92        note = "Use `invite_link().admin().create(...)` or `invite_link().admin().refresh(...)`, which return an `OrganizationInviteLinkView`, instead"
93    )]
94    #[cfg_attr(feature = "wasm", wasm_bindgen(unchecked_return_type = "InviteSecret"))]
95    pub fn get_invite_secret(
96        &self,
97        organization_id: OrganizationId,
98        invite: Invite,
99    ) -> Result<InviteSecret, InviteLinkError> {
100        self.admin().get_invite_secret(organization_id, invite)
101    }
102
103    /// Accepts an organization invite for the current user, optionally enrolling into account
104    /// recovery (when `enroll_into_account_recovery` is set) and — when the invite supports
105    /// confirmation — self-confirming.
106    #[deprecated(note = "Use `invite_link().user().accept_and_optionally_confirm(...)` instead")]
107    pub async fn accept_and_optionally_confirm(
108        &self,
109        organization_id: OrganizationId,
110        code: String,
111        invite_secret: InviteSecret,
112        default_collection_name: String,
113        enroll_into_account_recovery: bool,
114    ) -> Result<(), AcceptInviteLinkError> {
115        self.user()
116            .accept_and_optionally_confirm(
117                organization_id,
118                code,
119                invite_secret,
120                default_collection_name,
121                enroll_into_account_recovery,
122            )
123            .await
124    }
125}
126
127/// Extension trait that exposes [`InviteLinkClient`] on [`Client`].
128pub trait InviteLinkClientExt {
129    /// Returns an [`InviteLinkClient`]
130    fn invite_link(&self) -> InviteLinkClient;
131}
132
133impl InviteLinkClientExt for Client {
134    fn invite_link(&self) -> InviteLinkClient {
135        InviteLinkClient::from_client(self)
136    }
137}
138
139#[cfg(test)]
140#[allow(deprecated)]
141mod tests {
142    use std::sync::Arc;
143
144    use bitwarden_api_api::{
145        apis::ApiClient,
146        models::{OrganizationInviteLinkResponseModel, OrganizationPrivateKeyResponseModel},
147    };
148    use bitwarden_core::{
149        client::ApiConfigurations, key_management::create_test_crypto_with_user_and_org_key,
150    };
151    use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
152
153    use super::*;
154
155    fn make_client(org_id: OrganizationId, api_client: ApiClient) -> InviteLinkClient {
156        let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
157        let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
158        let key_store = create_test_crypto_with_user_and_org_key(user_key, org_id, org_key);
159        InviteLinkClient {
160            key_store,
161            api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
162        }
163    }
164
165    /// The deprecated `create_invite_link` redirect on the parent client must still route through
166    /// to the admin sub-client and post a link, preserving backwards compatibility.
167    #[tokio::test]
168    async fn create_invite_link_redirects_to_admin_client() {
169        use bitwarden_core::key_management::SymmetricKeySlotId;
170        use bitwarden_crypto::PublicKeyEncryptionAlgorithm;
171
172        let org_id = OrganizationId::new_v4();
173        let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
174        let for_mock = wrapped.clone();
175        let client = make_client(
176            org_id,
177            ApiClient::new_mocked(move |mock| {
178                mock.organizations_api
179                    .expect_get_private_key()
180                    .returning(move |_org| {
181                        Ok(OrganizationPrivateKeyResponseModel {
182                            object: None,
183                            private_key: for_mock.lock().unwrap().clone(),
184                        })
185                    })
186                    .once();
187                mock.organization_invite_links_api
188                    .expect_create()
189                    .returning(|org, model| {
190                        let model = model.unwrap();
191                        Ok(OrganizationInviteLinkResponseModel {
192                            object: None,
193                            id: Some(uuid::Uuid::new_v4()),
194                            code: Some(uuid::Uuid::new_v4()),
195                            organization_id: Some(org),
196                            allowed_domains: Some(model.allowed_domains),
197                            invite: Some(model.invite),
198                            supports_confirmation: Some(model.supports_confirmation),
199                            creation_date: Some("2024-01-01T00:00:00Z".to_string()),
200                        })
201                    })
202                    .once();
203            }),
204        );
205
206        // Wrap a private key under the client's org key, mirroring the server's `get_private_key`.
207        let wrapped_key = {
208            let mut ctx = client.key_store.context();
209            let org_key = SymmetricKeySlotId::Organization(org_id);
210            let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
211            ctx.wrap_private_key(org_key, private_key)
212                .unwrap()
213                .to_string()
214        };
215        *wrapped.lock().unwrap() = Some(wrapped_key);
216
217        let link = client
218            .create_invite_link(org_id, vec!["example.com".to_string()], false)
219            .await
220            .unwrap();
221
222        assert_eq!(link.allowed_domains, vec!["example.com".to_string()]);
223        assert!(!link.supports_confirmation);
224    }
225}