bitwarden_importers/importers/onepassword/access/
sign_in.rs1use std::fmt;
4
5use super::error::OnePasswordError;
6
7const MAX_SUBDOMAIN_LENGTH: usize = 63;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
18#[cfg_attr(
19 feature = "wasm",
20 derive(serde::Serialize, serde::Deserialize, tsify::Tsify)
21)]
22pub enum SignInDomain {
23 Global,
25 Europe,
27 Canada,
29 Enterprise,
31}
32
33impl SignInDomain {
34 pub fn as_str(&self) -> &'static str {
36 match self {
37 SignInDomain::Global => "1password.com",
38 SignInDomain::Europe => "1password.eu",
39 SignInDomain::Canada => "1password.ca",
40 SignInDomain::Enterprise => "ent.1password.com",
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
51#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
52#[cfg_attr(
53 feature = "wasm",
54 derive(serde::Serialize, serde::Deserialize, tsify::Tsify),
55 tsify(from_wasm_abi)
56)]
57pub struct SignInAddress {
58 pub subdomain: String,
60 pub domain: SignInDomain,
62}
63
64impl SignInAddress {
65 pub(crate) fn normalize(&mut self) -> Result<(), OnePasswordError> {
66 self.subdomain = self.subdomain.trim().to_lowercase();
67 self.validate()
68 }
69
70 fn validate(&self) -> Result<(), OnePasswordError> {
71 let invalid = |reason: &str| {
72 Err(OnePasswordError::InvalidSignInAddress(format!(
73 "subdomain '{}': {reason}",
74 self.subdomain
75 )))
76 };
77
78 if self.subdomain.is_empty() {
79 return invalid("it is empty");
80 }
81 if self.subdomain.len() > MAX_SUBDOMAIN_LENGTH {
82 return invalid(&format!(
83 "it is longer than {MAX_SUBDOMAIN_LENGTH} characters"
84 ));
85 }
86 if !self
87 .subdomain
88 .bytes()
89 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
90 {
91 return invalid("only letters, digits and dashes are allowed");
92 }
93 if self.subdomain.starts_with('-') || self.subdomain.ends_with('-') {
94 return invalid("it starts or ends with a dash");
95 }
96
97 Ok(())
98 }
99}
100
101impl fmt::Display for SignInAddress {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(f, "{}.{}", self.subdomain, self.domain.as_str())
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn address(subdomain: &str) -> Result<SignInAddress, OnePasswordError> {
112 let mut address = SignInAddress {
113 subdomain: subdomain.into(),
114 domain: SignInDomain::Global,
115 };
116 address.normalize()?;
117 Ok(address)
118 }
119
120 #[test]
121 fn address_joins_the_subdomain_and_the_domain() {
122 for (domain, expected) in [
123 (SignInDomain::Global, "my.1password.com"),
124 (SignInDomain::Europe, "my.1password.eu"),
125 (SignInDomain::Canada, "my.1password.ca"),
126 (SignInDomain::Enterprise, "my.ent.1password.com"),
127 ] {
128 let address = SignInAddress {
129 subdomain: "my".into(),
130 domain,
131 };
132 assert_eq!(address.to_string(), expected);
133 assert_eq!(address.domain, domain);
134 }
135 }
136
137 #[test]
138 fn address_cleans_up_the_subdomain() {
139 assert_eq!(
140 address(" ACME-Team \n")
141 .expect("a valid subdomain")
142 .to_string(),
143 "acme-team.1password.com"
144 );
145 }
146
147 #[test]
149 fn address_rejects_a_subdomain_that_is_not_a_label() {
150 for subdomain in [
151 "",
152 " ",
153 "my.1password.com",
154 "evil.com/x",
155 "[email protected]",
156 "my?x=",
157 "my#x",
158 "my team",
159 "-my",
160 "my-",
161 &"m".repeat(MAX_SUBDOMAIN_LENGTH + 1),
162 ] {
163 let error = address(subdomain).expect_err("not a DNS label");
164 assert!(
165 matches!(&error, OnePasswordError::InvalidSignInAddress(_)),
166 "unexpected error for '{subdomain}': {error}"
167 );
168 }
169 }
170
171 #[test]
172 fn address_accepts_the_longest_label() {
173 address(&"m".repeat(MAX_SUBDOMAIN_LENGTH)).expect("63 characters is a valid label");
174 }
175
176 #[test]
177 fn address_constructed_as_a_record_is_still_validated() {
178 let mut address = SignInAddress {
179 subdomain: "evil.com/x".into(),
180 domain: SignInDomain::Global,
181 };
182
183 assert!(matches!(
184 address.normalize(),
185 Err(OnePasswordError::InvalidSignInAddress(_))
186 ));
187 }
188}