Skip to main content

bitwarden_organization_invite_link/
organization_invite_link.rs

1use bitwarden_api_api::models::{
2    OrganizationInviteLinkResponseModel, OrganizationInviteLinkSsoResponseModel,
3    OrganizationInviteLinkStatusResponseModel,
4};
5use bitwarden_core::{
6    OrganizationId,
7    key_management::{KeySlotIds, SymmetricKeySlotId},
8    require,
9};
10use bitwarden_crypto::KeyStoreContext;
11use bitwarden_organization_crypto::invite::Invite;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16use crate::InviteLinkError;
17
18/// An organization invite link as persisted by the server.
19#[derive(Serialize, Deserialize, Debug, Clone)]
20#[bitwarden_ffi::wasm_record]
21#[serde(rename_all = "camelCase")]
22pub struct OrganizationInviteLink {
23    /// Unique identifier of the invite link.
24    pub id: Uuid,
25    /// The invite code invitees present when accepting or confirming the invite.
26    pub code: Uuid,
27    /// The organization this invite link belongs to.
28    pub organization_id: OrganizationId,
29    /// Email domains permitted to redeem this invite link.
30    pub allowed_domains: Vec<String>,
31    /// The sealed cryptographic invite carried in the invite link.
32    pub invite: Invite,
33    /// Whether invitees can self-confirm using this invite link.
34    pub supports_confirmation: bool,
35    /// When the invite link was created.
36    pub creation_date: DateTime<Utc>,
37}
38
39impl TryFrom<OrganizationInviteLinkResponseModel> for OrganizationInviteLink {
40    type Error = InviteLinkError;
41
42    fn try_from(response: OrganizationInviteLinkResponseModel) -> Result<Self, Self::Error> {
43        Ok(Self {
44            id: require!(response.id),
45            code: require!(response.code),
46            organization_id: OrganizationId::new(require!(response.organization_id)),
47            allowed_domains: response.allowed_domains.unwrap_or_default(),
48            invite: require!(response.invite).parse()?,
49            supports_confirmation: response.supports_confirmation.unwrap_or(false),
50            creation_date: require!(response.creation_date)
51                .parse()
52                .map_err(|_| InviteLinkError::ParseFailure("creation_date"))?,
53        })
54    }
55}
56
57impl OrganizationInviteLink {
58    /// Converts the invite link to a view model with URL for display.
59    pub fn to_view(
60        self,
61        ctx: &mut KeyStoreContext<KeySlotIds>,
62    ) -> Result<OrganizationInviteLinkView, InviteLinkError> {
63        let org_id = self.organization_id;
64
65        // Unwrap the invite secret
66        let org_key = SymmetricKeySlotId::Organization(org_id);
67        let invite_key = self
68            .invite
69            .unseal_invite_key_with_organization_key(org_key, ctx)?;
70        let invite_secret = self.invite.get_invite_secret(invite_key, ctx)?;
71
72        let invite_secret_str: String = (&invite_secret).into();
73        let code = self.code;
74        let url_fragment = format!("#/join/{org_id}/{code}?key={invite_secret_str}");
75
76        Ok(OrganizationInviteLinkView {
77            id: self.id,
78            organization_id: self.organization_id,
79            allowed_domains: self.allowed_domains,
80            supports_confirmation: self.supports_confirmation,
81            creation_date: self.creation_date,
82            url_fragment,
83        })
84    }
85}
86
87/// An organization invite link with reconstructed URL for display by the client.
88#[derive(Serialize, Deserialize, Debug, Clone)]
89#[bitwarden_ffi::wasm_record]
90#[serde(rename_all = "camelCase")]
91pub struct OrganizationInviteLinkView {
92    /// Unique identifier of the invite link.
93    pub id: Uuid,
94    /// The organization this invite link belongs to.
95    pub organization_id: OrganizationId,
96    /// Email domains permitted to redeem this invite link.
97    pub allowed_domains: Vec<String>,
98    /// Whether invitees can self-confirm using this invite link.
99    pub supports_confirmation: bool,
100    /// When the invite link was created.
101    pub creation_date: DateTime<Utc>,
102    /// The invite link URL fragment (to be appended on the web vault URL).
103    /// Example format: #/join/{org_id}/{code}?key={key}
104    pub url_fragment: String,
105}
106
107/// The status of an organization invite link, used to verify basic availability before an invitee
108/// attempts to accept.
109#[derive(Serialize, Deserialize, Debug, Clone)]
110#[bitwarden_ffi::wasm_record]
111#[serde(rename_all = "camelCase")]
112pub struct OrganizationInviteLinkStatusView {
113    /// The name of the organization the invite link belongs to.
114    pub organization_name: String,
115    /// Whether invite links are currently enabled for the organization.
116    pub links_enabled: bool,
117    /// Whether the organization has seats available for new members.
118    pub seats_available: bool,
119    /// Whether invitees can self-confirm using this invite link.
120    pub supports_confirmation: bool,
121    /// SSO details for the organization, when SSO is configured.
122    pub sso: Option<OrganizationInviteLinkSsoView>,
123}
124
125/// SSO details for an organization referenced by an invite link status.
126#[derive(Serialize, Deserialize, Debug, Clone)]
127#[bitwarden_ffi::wasm_record]
128#[serde(rename_all = "camelCase")]
129pub struct OrganizationInviteLinkSsoView {
130    /// The organization's SSO identifier, when configured.
131    pub org_sso_id: Option<String>,
132    /// Whether SSO is required to redeem this invite link.
133    pub required: bool,
134}
135
136impl From<OrganizationInviteLinkSsoResponseModel> for OrganizationInviteLinkSsoView {
137    fn from(response: OrganizationInviteLinkSsoResponseModel) -> Self {
138        Self {
139            org_sso_id: response.org_sso_id,
140            required: response.required.unwrap_or(false),
141        }
142    }
143}
144
145impl TryFrom<OrganizationInviteLinkStatusResponseModel> for OrganizationInviteLinkStatusView {
146    type Error = InviteLinkError;
147
148    fn try_from(response: OrganizationInviteLinkStatusResponseModel) -> Result<Self, Self::Error> {
149        Ok(Self {
150            organization_name: require!(response.organization_name),
151            links_enabled: response.links_enabled.unwrap_or(false),
152            seats_available: response.seats_available.unwrap_or(false),
153            supports_confirmation: response.supports_confirmation.unwrap_or(false),
154            sso: response.sso.map(|sso| (*sso).into()),
155        })
156    }
157}