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)]
17pub enum SignInDomain {
18 Global,
20 Europe,
22 Canada,
24 Enterprise,
26}
27
28impl SignInDomain {
29 pub fn as_str(&self) -> &'static str {
31 match self {
32 SignInDomain::Global => "1password.com",
33 SignInDomain::Europe => "1password.eu",
34 SignInDomain::Canada => "1password.ca",
35 SignInDomain::Enterprise => "ent.1password.com",
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct SignInAddress {
47 subdomain: String,
48 domain: SignInDomain,
49}
50
51impl SignInAddress {
52 pub fn new(subdomain: &str, domain: SignInDomain) -> Result<SignInAddress, OnePasswordError> {
57 let subdomain = subdomain.trim().to_lowercase();
58 validate_subdomain(&subdomain)?;
59
60 Ok(SignInAddress { subdomain, domain })
61 }
62
63 pub fn domain(&self) -> SignInDomain {
65 self.domain
66 }
67}
68
69impl fmt::Display for SignInAddress {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "{}.{}", self.subdomain, self.domain.as_str())
72 }
73}
74
75fn validate_subdomain(subdomain: &str) -> Result<(), OnePasswordError> {
78 let invalid = |reason: &str| {
79 Err(OnePasswordError::Internal(format!(
80 "invalid subdomain '{subdomain}': {reason}"
81 )))
82 };
83
84 if subdomain.is_empty() {
85 return invalid("it is empty");
86 }
87 if subdomain.len() > MAX_SUBDOMAIN_LENGTH {
88 return invalid(&format!(
89 "it is longer than {MAX_SUBDOMAIN_LENGTH} characters"
90 ));
91 }
92 if !subdomain
93 .bytes()
94 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
95 {
96 return invalid("only letters, digits and dashes are allowed");
97 }
98 if subdomain.starts_with('-') || subdomain.ends_with('-') {
99 return invalid("it starts or ends with a dash");
100 }
101
102 Ok(())
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn address(subdomain: &str) -> Result<SignInAddress, OnePasswordError> {
110 SignInAddress::new(subdomain, SignInDomain::Global)
111 }
112
113 #[test]
114 fn address_joins_the_subdomain_and_the_domain() {
115 for (domain, expected) in [
116 (SignInDomain::Global, "my.1password.com"),
117 (SignInDomain::Europe, "my.1password.eu"),
118 (SignInDomain::Canada, "my.1password.ca"),
119 (SignInDomain::Enterprise, "my.ent.1password.com"),
120 ] {
121 let address = SignInAddress::new("my", domain).expect("a valid subdomain");
122 assert_eq!(address.to_string(), expected);
123 assert_eq!(address.domain(), domain);
124 }
125 }
126
127 #[test]
128 fn address_cleans_up_the_subdomain() {
129 assert_eq!(
130 address(" ACME-Team \n")
131 .expect("a valid subdomain")
132 .to_string(),
133 "acme-team.1password.com"
134 );
135 }
136
137 #[test]
139 fn address_rejects_a_subdomain_that_is_not_a_label() {
140 for subdomain in [
141 "",
142 " ",
143 "my.1password.com",
144 "evil.com/x",
145 "[email protected]",
146 "my?x=",
147 "my#x",
148 "my team",
149 "-my",
150 "my-",
151 &"m".repeat(MAX_SUBDOMAIN_LENGTH + 1),
152 ] {
153 let error = address(subdomain).expect_err("not a DNS label");
154 assert!(
155 error.to_string().contains("invalid subdomain"),
156 "unexpected error for '{subdomain}': {error}"
157 );
158 }
159 }
160
161 #[test]
162 fn address_accepts_the_longest_label() {
163 address(&"m".repeat(MAX_SUBDOMAIN_LENGTH)).expect("63 characters is a valid label");
164 }
165}