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