bitwarden_organization_invite_link/
organization_invite_link.rs1use 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#[derive(Debug, Clone)]
21pub(crate) struct OrganizationInviteLink {
22 pub(crate) id: Uuid,
24 pub(crate) code: Uuid,
26 pub(crate) organization_id: OrganizationId,
28 pub(crate) allowed_domains: Vec<String>,
30 pub(crate) invite: Invite,
32 pub(crate) supports_confirmation: bool,
34 pub(crate) creation_date: DateTime<Utc>,
36}
37
38impl TryFrom<OrganizationInviteLinkResponseModel> for OrganizationInviteLink {
39 type Error = InviteLinkError;
40
41 fn try_from(response: OrganizationInviteLinkResponseModel) -> Result<Self, Self::Error> {
42 Ok(Self {
43 id: require!(response.id),
44 code: require!(response.code),
45 organization_id: OrganizationId::new(require!(response.organization_id)),
46 allowed_domains: response.allowed_domains.unwrap_or_default(),
47 invite: require!(response.invite).parse()?,
48 supports_confirmation: response.supports_confirmation.unwrap_or(false),
49 creation_date: require!(response.creation_date)
50 .parse()
51 .map_err(|_| InviteLinkError::ParseFailure("creation_date"))?,
52 })
53 }
54}
55
56impl OrganizationInviteLink {
57 pub(crate) fn into_view(
59 self,
60 ctx: &mut KeyStoreContext<KeySlotIds>,
61 ) -> Result<OrganizationInviteLinkView, InviteLinkError> {
62 let org_id = self.organization_id;
63
64 let org_key = SymmetricKeySlotId::Organization(org_id);
66 let invite_key = self
67 .invite
68 .unseal_invite_key_with_organization_key(org_key, ctx)?;
69 let invite_secret = self.invite.get_invite_secret(invite_key, ctx)?;
70
71 let invite_secret_str: String = (&invite_secret).into();
72 let code = self.code;
73 let url_fragment = format!("#/join/{org_id}/{code}?key={invite_secret_str}");
74
75 Ok(OrganizationInviteLinkView {
76 id: self.id,
77 organization_id: self.organization_id,
78 allowed_domains: self.allowed_domains,
79 supports_confirmation: self.supports_confirmation,
80 creation_date: self.creation_date,
81 url_fragment,
82 })
83 }
84}
85
86#[derive(Serialize, Deserialize, Clone)]
88#[bitwarden_ffi::wasm_record]
89#[serde(rename_all = "camelCase")]
90pub struct OrganizationInviteLinkView {
91 pub id: Uuid,
93 pub organization_id: OrganizationId,
95 pub allowed_domains: Vec<String>,
97 pub supports_confirmation: bool,
99 pub creation_date: DateTime<Utc>,
101 pub url_fragment: String,
104}
105
106impl std::fmt::Debug for OrganizationInviteLinkView {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.debug_struct("OrganizationInviteLinkView")
110 .field("id", &self.id)
111 .field("organization_id", &self.organization_id)
112 .field("allowed_domains", &self.allowed_domains)
113 .field("supports_confirmation", &self.supports_confirmation)
114 .field("creation_date", &self.creation_date)
115 .field("url_fragment", &"[REDACTED]")
116 .finish()
117 }
118}
119
120#[derive(Serialize, Deserialize, Debug, Clone)]
123#[bitwarden_ffi::wasm_record]
124#[serde(rename_all = "camelCase")]
125pub struct OrganizationInviteLinkStatusView {
126 pub organization_name: String,
128 pub links_enabled: bool,
130 pub seats_available: bool,
132 pub supports_confirmation: bool,
134 pub sso: Option<OrganizationInviteLinkSsoView>,
136}
137
138#[derive(Serialize, Deserialize, Debug, Clone)]
140#[bitwarden_ffi::wasm_record]
141#[serde(rename_all = "camelCase")]
142pub struct OrganizationInviteLinkSsoView {
143 pub org_sso_id: Option<String>,
145 pub required: bool,
147}
148
149impl From<OrganizationInviteLinkSsoResponseModel> for OrganizationInviteLinkSsoView {
150 fn from(response: OrganizationInviteLinkSsoResponseModel) -> Self {
151 Self {
152 org_sso_id: response.org_sso_id,
153 required: response.required.unwrap_or(false),
154 }
155 }
156}
157
158impl TryFrom<OrganizationInviteLinkStatusResponseModel> for OrganizationInviteLinkStatusView {
159 type Error = InviteLinkError;
160
161 fn try_from(response: OrganizationInviteLinkStatusResponseModel) -> Result<Self, Self::Error> {
162 Ok(Self {
163 organization_name: require!(response.organization_name),
164 links_enabled: response.links_enabled.unwrap_or(false),
165 seats_available: response.seats_available.unwrap_or(false),
166 supports_confirmation: response.supports_confirmation.unwrap_or(false),
167 sso: response.sso.map(|sso| (*sso).into()),
168 })
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn view_debug_redacts_url_fragment() {
178 let view = OrganizationInviteLinkView {
179 id: Uuid::new_v4(),
180 organization_id: OrganizationId::new_v4(),
181 allowed_domains: vec!["example.com".to_string()],
182 supports_confirmation: true,
183 creation_date: Utc::now(),
184 url_fragment: "#/join/org/code?key=super-secret".to_string(),
185 };
186
187 let debug = format!("{view:?}");
188
189 assert!(!debug.contains("super-secret"), "secret leaked: {debug}");
190 assert!(debug.contains("[REDACTED]"));
191 assert!(debug.contains("example.com"));
192 }
193}