1use std::sync::Arc;
2
3use bitwarden_api_api::models::{
4 AcceptOrganizationInviteLinkRequestModel, ConfirmOrganizationInviteLinkRequestModel,
5 CreateOrganizationInviteLinkRequestModel, GetOrganizationInviteRequestModel,
6 RefreshOrganizationInviteLinkRequestModel,
7};
8use bitwarden_core::{
9 ApiError, Client, FromClient, MissingFieldError, OrganizationId,
10 client::ApiConfigurations,
11 key_management::{KeySlotIds, PrivateKeySlotId, SymmetricKeySlotId},
12 require,
13};
14use bitwarden_crypto::{
15 CoseKeyThumbprintExt, CryptoError, EncString, KeyStore, PrimitiveEncryptable, PublicKey,
16 SpkiPublicKeyBytes, UnsignedSharedKey,
17};
18use bitwarden_encoding::B64;
19use bitwarden_error::bitwarden_error;
20use bitwarden_organization_crypto::invite::{Invite, InviteKeyBundleError, InviteSecret};
21use thiserror::Error;
22#[cfg(feature = "wasm")]
23use wasm_bindgen::prelude::wasm_bindgen;
24
25use crate::OrganizationInviteLink;
26
27#[bitwarden_error(flat)]
29#[derive(Debug, Error)]
30pub enum InviteLinkError {
31 #[error(transparent)]
33 Invite(#[from] InviteKeyBundleError),
34 #[error(transparent)]
36 Api(#[from] ApiError),
37 #[error(transparent)]
40 Crypto(#[from] CryptoError),
41 #[error(transparent)]
43 MissingField(#[from] MissingFieldError),
44 #[error("Account recovery public key does not match the invite's bound organization key")]
47 RecoveryKeyMismatch,
48}
49
50#[cfg_attr(feature = "wasm", wasm_bindgen)]
52#[derive(FromClient)]
53pub struct InviteLinkClient {
54 pub(crate) key_store: KeyStore<KeySlotIds>,
55 pub(crate) api_configurations: Arc<ApiConfigurations>,
56}
57
58#[cfg_attr(feature = "wasm", wasm_bindgen)]
59impl InviteLinkClient {
60 pub async fn create_invite_link(
68 &self,
69 organization_id: OrganizationId,
70 allowed_domains: Vec<String>,
71 supports_confirmation: bool,
72 ) -> Result<OrganizationInviteLink, InviteLinkError> {
73 let invite = self
74 .make_invite(organization_id, supports_confirmation)
75 .await?;
76
77 let response = self
78 .api_configurations
79 .api_client
80 .organization_invite_links_api()
81 .create(
82 organization_id.into(),
83 Some(CreateOrganizationInviteLinkRequestModel {
84 allowed_domains,
85 invite: String::from(&invite),
86 supports_confirmation: invite.supports_confirmation(),
87 }),
88 )
89 .await
90 .map_err(ApiError::from)?;
91
92 OrganizationInviteLink::try_from(response)
93 }
94
95 pub async fn refresh_invite_link(
98 &self,
99 organization_id: OrganizationId,
100 supports_confirmation: bool,
101 ) -> Result<OrganizationInviteLink, InviteLinkError> {
102 let invite = self
103 .make_invite(organization_id, supports_confirmation)
104 .await?;
105
106 let response = self
107 .api_configurations
108 .api_client
109 .organization_invite_links_api()
110 .refresh(
111 organization_id.into(),
112 Some(RefreshOrganizationInviteLinkRequestModel {
113 invite: String::from(&invite),
114 supports_confirmation: invite.supports_confirmation(),
115 }),
116 )
117 .await
118 .map_err(ApiError::from)?;
119
120 OrganizationInviteLink::try_from(response)
121 }
122
123 #[cfg_attr(feature = "wasm", wasm_bindgen(unchecked_return_type = "InviteSecret"))]
126 pub fn get_invite_secret(
127 &self,
128 organization_id: OrganizationId,
129 invite: Invite,
130 ) -> Result<InviteSecret, InviteLinkError> {
131 let mut ctx = self.key_store.context();
132 let org_key = SymmetricKeySlotId::Organization(organization_id);
133 let invite_key = invite.unseal_invite_key_with_organization_key(org_key, &mut ctx)?;
134 let invite_secret = invite.get_invite_secret(invite_key, &mut ctx)?;
135 Ok(invite_secret)
136 }
137
138 pub async fn accept_and_optionally_confirm(
142 &self,
143 organization_id: OrganizationId,
144 code: String,
145 invite_secret: InviteSecret,
146 default_collection_name: String,
147 enroll_into_account_recovery: bool,
148 ) -> Result<(), InviteLinkError> {
149 let code = uuid::Uuid::parse_str(&code).map_err(|_| MissingFieldError("code"))?;
150
151 let recovery_public_key = if enroll_into_account_recovery {
154 let response = self
155 .api_configurations
156 .api_client
157 .organizations_api()
158 .get_public_key(&organization_id.to_string())
159 .await
160 .map_err(ApiError::from)?;
161 Some(
162 require!(response.public_key)
163 .parse::<B64>()
164 .map_err(|_| MissingFieldError("public_key"))?,
165 )
166 } else {
167 None
168 };
169
170 let invite_response = self
171 .api_configurations
172 .api_client
173 .organization_users_api()
174 .get_invite(Some(GetOrganizationInviteRequestModel {
175 organization_id: organization_id.into(),
176 code,
177 }))
178 .await
179 .map_err(ApiError::from)?;
180
181 let invite: Invite = require!(invite_response.invite).parse()?;
182
183 let request = {
186 let mut ctx = self.key_store.context();
187
188 let invite_key =
190 invite.unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)?;
191
192 let reset_password_key = match &recovery_public_key {
197 Some(recovery_public_key) => {
198 let recovery_public_key =
199 PublicKey::from_der(&SpkiPublicKeyBytes::from(recovery_public_key))?;
200 let bound_thumbprint =
201 invite.get_public_key_thumbprint(invite_key, &mut ctx)?;
202 if bound_thumbprint != recovery_public_key.thumbprint()? {
203 return Err(InviteLinkError::RecoveryKeyMismatch);
204 }
205 Some(
206 UnsignedSharedKey::encapsulate(
207 SymmetricKeySlotId::User,
208 &recovery_public_key,
209 &ctx,
210 )?
211 .to_string(),
212 )
213 }
214 None => None,
215 };
216
217 if invite.supports_confirmation() {
218 let org_key = invite.unseal_organization_key(invite_key, &mut ctx)?;
220 let user_public_key = ctx.get_public_key(PrivateKeySlotId::UserPrivateKey)?;
221 let org_user_key =
222 UnsignedSharedKey::encapsulate(org_key, &user_public_key, &ctx)?.to_string();
223 let default_user_collection_name = default_collection_name
224 .encrypt(&mut ctx, org_key)?
225 .to_string();
226 PendingPost::Confirm(ConfirmOrganizationInviteLinkRequestModel {
227 organization_id: organization_id.into(),
228 code,
229 org_user_key,
230 reset_password_key,
231 default_user_collection_name,
232 })
233 } else {
234 PendingPost::Accept(AcceptOrganizationInviteLinkRequestModel {
235 organization_id: organization_id.into(),
236 code,
237 reset_password_key,
238 })
239 }
240 };
241
242 let organization_users_api = self.api_configurations.api_client.organization_users_api();
243 match request {
244 PendingPost::Confirm(model) => organization_users_api
245 .confirm_invite_link(Some(model))
246 .await
247 .map_err(ApiError::from)?,
248 PendingPost::Accept(model) => organization_users_api
249 .accept_invite_link(Some(model))
250 .await
251 .map_err(ApiError::from)?,
252 }
253
254 Ok(())
255 }
256
257 async fn make_invite(
259 &self,
260 organization_id: OrganizationId,
261 supports_confirmation: bool,
262 ) -> Result<Invite, InviteLinkError> {
263 let wrapped_private_key_response = self
264 .api_configurations
265 .api_client
266 .organizations_api()
267 .get_private_key(organization_id.into())
268 .await
269 .map_err(ApiError::from)?;
270
271 let wrapped_private_key: EncString =
272 require!(wrapped_private_key_response.private_key).parse()?;
273
274 let mut ctx = self.key_store.context();
275 let org_key = SymmetricKeySlotId::Organization(organization_id);
276 let (_, mut invite) =
277 Invite::make_for_private_key(org_key, &wrapped_private_key, &mut ctx)?;
278
279 if !supports_confirmation {
281 invite.disable_confirmation();
282 }
283
284 Ok(invite)
285 }
286}
287
288enum PendingPost {
291 Confirm(ConfirmOrganizationInviteLinkRequestModel),
292 Accept(AcceptOrganizationInviteLinkRequestModel),
293}
294
295pub trait InviteLinkClientExt {
297 fn invite_link(&self) -> InviteLinkClient;
299}
300
301impl InviteLinkClientExt for Client {
302 fn invite_link(&self) -> InviteLinkClient {
303 InviteLinkClient::from_client(self)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use bitwarden_api_api::{
310 apis::ApiClient,
311 models::{
312 OrganizationInviteLinkResponseModel, OrganizationInviteResponseModel,
313 OrganizationPrivateKeyResponseModel, OrganizationPublicKeyResponseModel,
314 },
315 };
316 use bitwarden_core::{
317 client::ApiConfigurations, key_management::create_test_crypto_with_user_and_org_key,
318 };
319 use bitwarden_crypto::{
320 PublicKeyEncryptionAlgorithm, SymmetricCryptoKey, SymmetricKeyAlgorithm,
321 };
322
323 use super::*;
324
325 fn make_client(org_id: OrganizationId, api_client: ApiClient) -> InviteLinkClient {
326 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
327 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
328 let key_store = create_test_crypto_with_user_and_org_key(user_key, org_id, org_key);
329 {
332 let mut ctx = key_store.context_mut();
333 let local = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
334 ctx.persist_private_key(local, PrivateKeySlotId::UserPrivateKey)
335 .expect("persisting the user private key should work");
336 }
337 InviteLinkClient {
338 key_store,
339 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
340 }
341 }
342
343 fn wrapped_org_private_key(client: &InviteLinkClient, org_id: OrganizationId) -> String {
346 let mut ctx = client.key_store.context();
347 let org_key = SymmetricKeySlotId::Organization(org_id);
348 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
349 ctx.wrap_private_key(org_key, private_key)
350 .unwrap()
351 .to_string()
352 }
353
354 fn echo_link_response(
357 org_id: uuid::Uuid,
358 allowed_domains: Vec<String>,
359 invite: String,
360 supports_confirmation: bool,
361 ) -> OrganizationInviteLinkResponseModel {
362 OrganizationInviteLinkResponseModel {
363 object: None,
364 id: Some(uuid::Uuid::new_v4()),
365 code: Some(uuid::Uuid::new_v4()),
366 organization_id: Some(org_id),
367 allowed_domains: Some(allowed_domains),
368 invite: Some(invite),
369 supports_confirmation: Some(supports_confirmation),
370 creation_date: Some("2024-01-01T00:00:00Z".to_string()),
371 }
372 }
373
374 fn build_invite(
377 client: &InviteLinkClient,
378 org_id: OrganizationId,
379 ) -> (InviteSecret, Invite, B64) {
380 let mut ctx = client.key_store.context();
381 let org_key = SymmetricKeySlotId::Organization(org_id);
382 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
383 let org_public_key = B64::from(
384 ctx.get_public_key(private_key)
385 .unwrap()
386 .to_der()
387 .unwrap()
388 .as_ref(),
389 );
390 let wrapped = ctx.wrap_private_key(org_key, private_key).unwrap();
391 let (secret, invite) = Invite::make_for_private_key(org_key, &wrapped, &mut ctx).unwrap();
392 (secret, invite, org_public_key)
393 }
394
395 #[tokio::test]
400 #[ignore = "Manual test to generate integration-test fixtures"]
401 async fn generate_integration_test_fixtures() {
402 let org_id: OrganizationId = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap();
403 let core = Client::init_test_account(
404 bitwarden_core::client::test_accounts::test_bitwarden_com_account(),
405 )
406 .await;
407 let client = core.invite_link();
408
409 let mut ctx = client.key_store.context();
410 let org_key = SymmetricKeySlotId::Organization(org_id);
411 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
412 let public_key = B64::from(
413 ctx.get_public_key(private_key)
414 .unwrap()
415 .to_der()
416 .unwrap()
417 .as_ref(),
418 );
419 let wrapped = ctx.wrap_private_key(org_key, private_key).unwrap();
420 let (secret, invite) = Invite::make_for_private_key(org_key, &wrapped, &mut ctx).unwrap();
421
422 let mut no_confirmation = invite.clone();
426 no_confirmation.disable_confirmation();
427 assert!(invite.supports_confirmation() && !no_confirmation.supports_confirmation());
428
429 println!("TEST_ORG_WRAPPED_PRIVATE_KEY = {}", wrapped.to_string());
430 println!("TEST_ORG_PUBLIC_KEY = {public_key}");
431 println!("TEST_INVITE = {}", String::from(&invite));
432 println!(
433 "TEST_INVITE_NO_CONFIRMATION = {}",
434 String::from(&no_confirmation)
435 );
436 println!("TEST_INVITE_SECRET = {}", String::from(&secret));
437 }
438
439 #[tokio::test]
440 async fn create_invite_link_posts_and_returns_link_without_confirmation() {
441 let org_id = OrganizationId::new_v4();
442 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
443 let for_mock = wrapped.clone();
444 let client = make_client(
445 org_id,
446 ApiClient::new_mocked(move |mock| {
447 mock.organizations_api
448 .expect_get_private_key()
449 .returning(move |_org| {
450 Ok(OrganizationPrivateKeyResponseModel {
451 object: None,
452 private_key: for_mock.lock().unwrap().clone(),
453 })
454 })
455 .once();
456 mock.organization_invite_links_api
457 .expect_create()
458 .returning(|org, model| {
459 let model = model.unwrap();
460 Ok(echo_link_response(
461 org,
462 model.allowed_domains,
463 model.invite,
464 model.supports_confirmation,
465 ))
466 })
467 .once();
468 }),
469 );
470 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
471
472 let link = client
473 .create_invite_link(org_id, vec!["example.com".to_string()], false)
474 .await
475 .unwrap();
476
477 assert_eq!(link.allowed_domains, vec!["example.com".to_string()]);
478 assert!(!String::from(&link.invite).is_empty());
479 assert!(!link.invite.supports_confirmation());
480 }
481
482 #[tokio::test]
483 async fn create_invite_link_posts_and_returns_link_with_confirmation() {
484 let org_id = OrganizationId::new_v4();
485 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
486 let for_mock = wrapped.clone();
487 let client = make_client(
488 org_id,
489 ApiClient::new_mocked(move |mock| {
490 mock.organizations_api
491 .expect_get_private_key()
492 .returning(move |_org| {
493 Ok(OrganizationPrivateKeyResponseModel {
494 object: None,
495 private_key: for_mock.lock().unwrap().clone(),
496 })
497 })
498 .once();
499 mock.organization_invite_links_api
500 .expect_create()
501 .returning(|org, model| {
502 let model = model.unwrap();
503 Ok(echo_link_response(
504 org,
505 model.allowed_domains,
506 model.invite,
507 model.supports_confirmation,
508 ))
509 })
510 .once();
511 }),
512 );
513 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
514
515 let link = client
516 .create_invite_link(org_id, vec!["example.com".to_string()], true)
517 .await
518 .unwrap();
519
520 assert_eq!(link.allowed_domains, vec!["example.com".to_string()]);
521 assert!(!String::from(&link.invite).is_empty());
522 assert!(link.invite.supports_confirmation());
523 }
524
525 #[tokio::test]
526 async fn create_invite_link_two_calls_produce_different_invites() {
527 let org_id = OrganizationId::new_v4();
528 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
529 let for_mock = wrapped.clone();
530 let client = make_client(
531 org_id,
532 ApiClient::new_mocked(move |mock| {
533 mock.organizations_api
534 .expect_get_private_key()
535 .returning(move |_org| {
536 Ok(OrganizationPrivateKeyResponseModel {
537 object: None,
538 private_key: for_mock.lock().unwrap().clone(),
539 })
540 })
541 .times(2);
542 mock.organization_invite_links_api
543 .expect_create()
544 .returning(|org, model| {
545 let model = model.unwrap();
546 Ok(echo_link_response(
547 org,
548 model.allowed_domains,
549 model.invite,
550 model.supports_confirmation,
551 ))
552 })
553 .times(2);
554 }),
555 );
556 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
557
558 let link1 = client
559 .create_invite_link(org_id, vec![], false)
560 .await
561 .unwrap();
562 let link2 = client
563 .create_invite_link(org_id, vec![], false)
564 .await
565 .unwrap();
566
567 assert_ne!(String::from(&link1.invite), String::from(&link2.invite));
568 }
569
570 #[tokio::test]
571 async fn create_invite_link_with_unknown_organization_id_fails() {
572 let org_id = OrganizationId::new_v4();
573 let other_org_id = OrganizationId::new_v4();
574 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
575 let for_mock = wrapped.clone();
576 let client = make_client(
577 org_id,
578 ApiClient::new_mocked(move |mock| {
579 mock.organizations_api
580 .expect_get_private_key()
581 .returning(move |_org| {
582 Ok(OrganizationPrivateKeyResponseModel {
583 object: None,
584 private_key: for_mock.lock().unwrap().clone(),
585 })
586 })
587 .once();
588 }),
589 );
590 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
593
594 let result = client.create_invite_link(other_org_id, vec![], false).await;
595
596 assert!(matches!(result, Err(InviteLinkError::Invite(_))));
597 }
598
599 #[tokio::test]
600 async fn create_invite_link_surfaces_api_errors() {
601 let org_id = OrganizationId::new_v4();
602 let client = make_client(
603 org_id,
604 ApiClient::new_mocked(|mock| {
605 mock.organizations_api
606 .expect_get_private_key()
607 .returning(|_org| Err(std::io::Error::other("boom").into()));
608 }),
609 );
610
611 let result = client.create_invite_link(org_id, vec![], false).await;
612
613 assert!(matches!(result, Err(InviteLinkError::Api(_))));
614 }
615
616 #[tokio::test]
617 async fn get_invite_secret_round_trips_to_the_invite_secret() {
618 let org_id = OrganizationId::new_v4();
619 let client = make_client(org_id, ApiClient::new_mocked(|_| {}));
620
621 let (_secret, invite, _org_public_key) = build_invite(&client, org_id);
623 let secret = client.get_invite_secret(org_id, invite).unwrap();
624 assert!(!String::from(&secret).is_empty());
625 }
626
627 #[tokio::test]
628 async fn accept_and_confirm_succeeds_for_confirmable_invite() {
629 let org_id = OrganizationId::new_v4();
630 let recovery = Arc::new(std::sync::Mutex::new(None::<String>));
633 let invite_cell = Arc::new(std::sync::Mutex::new(None::<String>));
634 let recovery_mock = recovery.clone();
635 let invite_mock = invite_cell.clone();
636 let client = make_client(
637 org_id,
638 ApiClient::new_mocked(move |mock| {
639 mock.organizations_api
640 .expect_get_public_key()
641 .returning(move |_id| {
642 Ok(OrganizationPublicKeyResponseModel {
643 object: None,
644 public_key: recovery_mock.lock().unwrap().clone(),
645 })
646 })
647 .once();
648 mock.organization_users_api
649 .expect_get_invite()
650 .returning(move |_model| {
651 Ok(OrganizationInviteResponseModel {
652 invite: invite_mock.lock().unwrap().clone(),
653 })
654 })
655 .once();
656 mock.organization_users_api
657 .expect_confirm_invite_link()
658 .returning(|_model| Ok(()))
659 .once();
660 }),
661 );
662
663 let (secret, invite, org_public_key) = build_invite(&client, org_id);
664 assert!(invite.supports_confirmation());
665 *recovery.lock().unwrap() = Some(String::from(&org_public_key));
667 *invite_cell.lock().unwrap() = Some(String::from(&invite));
668
669 client
670 .accept_and_optionally_confirm(
671 org_id,
672 uuid::Uuid::new_v4().to_string(),
673 secret,
674 "Default".to_string(),
675 true,
676 )
677 .await
678 .unwrap();
679 }
680
681 #[tokio::test]
682 async fn accept_without_enrollment_confirms_without_recovery_key() {
683 let org_id = OrganizationId::new_v4();
684 let invite_cell = Arc::new(std::sync::Mutex::new(None::<String>));
687 let invite_mock = invite_cell.clone();
688 let client = make_client(
689 org_id,
690 ApiClient::new_mocked(move |mock| {
691 mock.organization_users_api
692 .expect_get_invite()
693 .returning(move |_model| {
694 Ok(OrganizationInviteResponseModel {
695 invite: invite_mock.lock().unwrap().clone(),
696 })
697 })
698 .once();
699 mock.organization_users_api
700 .expect_confirm_invite_link()
701 .returning(|_model| Ok(()))
702 .once();
703 }),
704 );
705
706 let (secret, invite, _org_public_key) = build_invite(&client, org_id);
707 *invite_cell.lock().unwrap() = Some(String::from(&invite));
708 client
709 .accept_and_optionally_confirm(
710 org_id,
711 uuid::Uuid::new_v4().to_string(),
712 secret,
713 "Default".to_string(),
714 false,
715 )
716 .await
717 .unwrap();
718 }
719
720 #[tokio::test]
721 async fn accept_without_confirmation_posts_acceptance() {
722 let org_id = OrganizationId::new_v4();
723 let invite_cell = Arc::new(std::sync::Mutex::new(None::<String>));
724 let invite_mock = invite_cell.clone();
725 let client = make_client(
726 org_id,
727 ApiClient::new_mocked(move |mock| {
728 mock.organization_users_api
729 .expect_get_invite()
730 .returning(move |_model| {
731 Ok(OrganizationInviteResponseModel {
732 invite: invite_mock.lock().unwrap().clone(),
733 })
734 })
735 .once();
736 mock.organization_users_api
737 .expect_accept_invite_link()
738 .returning(|_model| Ok(()))
739 .once();
740 }),
741 );
742
743 let (secret, mut invite, _org_public_key) = build_invite(&client, org_id);
745 invite.disable_confirmation();
746 assert!(!invite.supports_confirmation());
747 *invite_cell.lock().unwrap() = Some(String::from(&invite));
748
749 client
750 .accept_and_optionally_confirm(
751 org_id,
752 uuid::Uuid::new_v4().to_string(),
753 secret,
754 "Default".to_string(),
755 false,
756 )
757 .await
758 .unwrap();
759 }
760
761 #[tokio::test]
762 async fn accept_with_mismatched_recovery_key_fails() {
763 let org_id = OrganizationId::new_v4();
764 let recovery = Arc::new(std::sync::Mutex::new(None::<String>));
765 let invite_cell = Arc::new(std::sync::Mutex::new(None::<String>));
766 let recovery_mock = recovery.clone();
767 let invite_mock = invite_cell.clone();
768 let client = make_client(
769 org_id,
770 ApiClient::new_mocked(move |mock| {
771 mock.organizations_api
772 .expect_get_public_key()
773 .returning(move |_id| {
774 Ok(OrganizationPublicKeyResponseModel {
775 object: None,
776 public_key: recovery_mock.lock().unwrap().clone(),
777 })
778 })
779 .once();
780 mock.organization_users_api
781 .expect_get_invite()
782 .returning(move |_model| {
783 Ok(OrganizationInviteResponseModel {
784 invite: invite_mock.lock().unwrap().clone(),
785 })
786 })
787 .once();
788 }),
789 );
790
791 let (secret, invite, _org_public_key) = build_invite(&client, org_id);
792 *invite_cell.lock().unwrap() = Some(String::from(&invite));
793 let unrelated_public_key = {
796 let mut ctx = client.key_store.context();
797 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
798 B64::from(
799 ctx.get_public_key(private_key)
800 .unwrap()
801 .to_der()
802 .unwrap()
803 .as_ref(),
804 )
805 };
806 *recovery.lock().unwrap() = Some(String::from(&unrelated_public_key));
807
808 let result = client
809 .accept_and_optionally_confirm(
810 org_id,
811 uuid::Uuid::new_v4().to_string(),
812 secret,
813 "Default".to_string(),
814 true,
815 )
816 .await;
817
818 assert!(matches!(result, Err(InviteLinkError::RecoveryKeyMismatch)));
819 }
820}