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