bitwarden_access_connector/
error.rs1use thiserror::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub(crate) enum Retry {
8 Transient,
10 Permanent,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
17pub(crate) struct SafeDetail(String);
18
19impl SafeDetail {
20 pub(crate) const MAX_LEN: usize = 500;
22
23 fn truncate(s: String) -> String {
25 if s.len() <= Self::MAX_LEN {
26 s
27 } else {
28 let mut end = Self::MAX_LEN;
29 while !s.is_char_boundary(end) {
30 end -= 1;
31 }
32 s.get(..end).unwrap_or_default().to_owned()
34 }
35 }
36
37 #[cfg(test)]
38 pub(crate) fn from_status(status: u16) -> Self {
39 Self(Self::truncate(format!("HTTP {status}")))
40 }
41
42 pub(crate) fn from_exit_code(code: i32) -> Self {
43 Self(Self::truncate(format!("exit code {code}")))
44 }
45
46 pub(crate) fn from_signal(signal: i32) -> Self {
48 Self(Self::truncate(format!("signal {signal}")))
49 }
50
51 pub(crate) fn from_missing_vars(names: &[String]) -> Self {
53 let joined = names.join(", ");
54 Self(Self::truncate(format!("missing vars: {joined}")))
55 }
56
57 pub(crate) fn from_kind(kind: &'static str) -> Self {
59 Self(Self::truncate(format!("error kind: {kind}")))
60 }
61
62 pub(crate) fn from_ldap_result_code(rc: u32) -> Self {
64 Self(Self::truncate(format!("LDAP result code {rc}")))
65 }
66
67 pub(crate) fn timed_out(secs: u64) -> Self {
68 Self(Self::truncate(format!("timed out after {secs}s")))
69 }
70
71 pub(crate) fn from_seal_rejection(reason: &'static str) -> Self {
74 Self(Self::truncate(format!("sealed data rejected: {reason}")))
75 }
76
77 pub(crate) fn from_http_status_and_graph_code(status: u16, graph_code: Option<&str>) -> Self {
79 let s = match graph_code {
80 Some(code) => format!("HTTP {status} ({code})"),
81 None => format!("HTTP {status}"),
82 };
83 Self(Self::truncate(s))
84 }
85
86 pub(crate) fn as_str(&self) -> &str {
87 &self.0
88 }
89}
90
91impl std::fmt::Display for SafeDetail {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 f.write_str(&self.0)
95 }
96}
97
98#[derive(Debug, Error)]
101pub enum AccessConnectorError {
102 #[error("invalid configuration: {0}")]
104 InvalidConfig(String),
105
106 #[error("invalid access connector token: {0}")]
108 InvalidToken(String),
109
110 #[error("identity server unreachable: {0}")]
112 IdentityUnreachable(String),
113
114 #[error("credential refused by identity server: {0}")]
116 CredentialRefused(String),
117
118 #[error("I/O error: {0}")]
120 Io(#[from] std::io::Error),
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn safe_detail_from_status() {
129 let d = SafeDetail::from_status(429);
130 assert_eq!(d.as_str(), "HTTP 429");
131 }
132
133 #[test]
134 fn safe_detail_from_exit_code() {
135 let d = SafeDetail::from_exit_code(2);
136 assert_eq!(d.as_str(), "exit code 2");
137 }
138
139 #[test]
140 fn safe_detail_from_signal() {
141 let d = SafeDetail::from_signal(9);
142 assert_eq!(d.as_str(), "signal 9");
143 }
144
145 #[test]
146 fn safe_detail_from_missing_vars() {
147 let vars = vec!["FOO_CLIENT_ID".to_owned(), "FOO_CLIENT_SECRET".to_owned()];
148 let d = SafeDetail::from_missing_vars(&vars);
149 assert_eq!(d.as_str(), "missing vars: FOO_CLIENT_ID, FOO_CLIENT_SECRET");
150 }
151
152 #[test]
153 fn safe_detail_from_kind() {
154 let d = SafeDetail::from_kind("GraphRequest");
155 assert_eq!(d.as_str(), "error kind: GraphRequest");
156 }
157
158 #[test]
159 fn safe_detail_from_seal_rejection() {
160 let d = SafeDetail::from_seal_rejection("target_method_not_automatic");
161 assert_eq!(
162 d.as_str(),
163 "sealed data rejected: target_method_not_automatic"
164 );
165 }
166
167 #[test]
168 fn safe_detail_from_ldap_result_code() {
169 let d = SafeDetail::from_ldap_result_code(49);
170 assert_eq!(d.as_str(), "LDAP result code 49");
171 }
172
173 #[test]
174 fn safe_detail_timed_out() {
175 let d = SafeDetail::timed_out(60);
176 assert_eq!(d.as_str(), "timed out after 60s");
177 }
178
179 #[test]
180 fn safe_detail_truncated_at_500_chars() {
181 let long = "x".repeat(600);
183 let d = SafeDetail::from_kind(Box::leak(long.into_boxed_str()));
184 assert_eq!(
185 d.as_str().len(),
186 SafeDetail::MAX_LEN,
187 "detail must be capped at MAX_LEN"
188 );
189 }
190
191 #[test]
192 fn safe_detail_exactly_500_chars_not_truncated() {
193 let exactly = "a".repeat(500);
194 let d = SafeDetail(SafeDetail::truncate(exactly.clone()));
196 assert_eq!(d.as_str().len(), 500);
197 }
198
199 #[test]
200 fn safe_detail_truncation_respects_char_boundary() {
201 let base = "a".repeat(499);
203 let long = base + &"é".repeat(10);
204 let d = SafeDetail(SafeDetail::truncate(long));
205 assert!(
206 d.as_str().len() <= SafeDetail::MAX_LEN,
207 "truncated string must not exceed MAX_LEN bytes"
208 );
209 assert!(std::str::from_utf8(d.as_str().as_bytes()).is_ok());
210 }
211}