1use super::{Error, ErrorKind, Result};
3use serde::Serialize;
4use serde_json::Value;
5use std::{fmt, time::Duration};
6
7const WITHHELD: &str = "[withheld]";
8const SAFE_TEXT: &[&str] = &[
9 "Bad Request",
10 "Unauthorized",
11 "Forbidden",
12 "Not Found",
13 "Method Not Allowed",
14 "Conflict",
15 "Unsupported Media Type",
16 "Unprocessable Entity",
17 "Too Many Requests",
18 "Internal Server Error",
19 "Bad Gateway",
20 "Service Unavailable",
21 "Gateway Timeout",
22 "Validation failed",
23 "Invalid request payload",
24 "Access denied",
25 "must not be blank",
26 "must not be null",
27 "must not be empty",
28 "is required",
29 "Required request part 'json' is not present",
30 "Required request part 'file' is not present",
31 "Content-Type 'application/octet-stream' is not supported",
32 "Content-Type 'text/plain' is not supported",
33];
34const SAFE_PROPERTIES: &[&str] = &[
35 "name",
36 "category",
37 "description",
38 "type",
39 "classification",
40 "tags",
41 "file",
42 "json",
43 "originalFileName",
44 "original_file_name",
45 "regionName",
46 "region_name",
47 "isCaseSensitive",
48 "is_case_sensitive",
49 "profileType",
50 "profile_type",
51 "detectionRules",
52 "detection_rules",
53 "dataPatterns",
54 "data_patterns",
55 "supportedConfidenceLevels",
56 "supported_confidence_levels",
57];
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
61#[serde(rename_all = "lowercase")]
62#[non_exhaustive]
63pub enum FailureKind {
64 Http,
65 Network,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum ErrorType {
72 ServerSideError,
73 ClientSideError,
74 UserRequestPayloadError,
75 MissingVariable,
76 SdkError,
77 OAuthError,
78 ResponseValidation,
79}
80impl ErrorType {
81 pub const fn as_str(self) -> &'static str {
82 match self {
83 Self::ServerSideError => "AISEC_SERVER_SIDE_ERROR",
84 Self::ClientSideError => "AISEC_CLIENT_SIDE_ERROR",
85 Self::UserRequestPayloadError => "AISEC_USER_REQUEST_PAYLOAD_ERROR",
86 Self::MissingVariable => "AISEC_MISSING_VARIABLE",
87 Self::SdkError => "AISEC_SDK_ERROR",
88 Self::OAuthError => "AISEC_OAUTH_ERROR",
89 Self::ResponseValidation => "AISEC_RESPONSE_VALIDATION",
90 }
91 }
92}
93
94#[derive(Clone, PartialEq, Eq, Serialize)]
95pub struct ProblemFieldError {
96 property: &'static str,
97 reason: &'static str,
98}
99impl ProblemFieldError {
100 pub fn property(&self) -> &'static str {
101 self.property
102 }
103 pub fn reason(&self) -> &'static str {
104 self.reason
105 }
106}
107impl fmt::Debug for ProblemFieldError {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 f.write_str("ProblemFieldError([REDACTED])")
110 }
111}
112
113#[derive(Clone, PartialEq, Eq, Serialize)]
115pub struct ProblemDetails {
116 #[serde(skip_serializing_if = "Option::is_none")]
117 title: Option<&'static str>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 detail: Option<&'static str>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 errors: Option<Vec<ProblemFieldError>>,
122 redacted: bool,
123}
124impl ProblemDetails {
125 pub fn from_value(value: &Value) -> Option<Self> {
127 let record = value.as_object()?;
128 let title_or_detail = record.contains_key("title") || record.contains_key("detail");
129 if !title_or_detail
130 && (!record.contains_key("errors")
131 || ["message", "error_message", "msg", "data", "error"]
132 .iter()
133 .any(|key| record.contains_key(*key)))
134 {
135 return None;
136 }
137 let mut result = Self {
138 title: None,
139 detail: None,
140 errors: None,
141 redacted: false,
142 };
143 if let Some(value) = record.get("title").filter(|value| !value.is_null()) {
144 result.title = Some(sanitize(value, SAFE_TEXT, &mut result.redacted));
145 }
146 if let Some(value) = record.get("detail").filter(|value| !value.is_null()) {
147 result.detail = Some(sanitize(value, SAFE_TEXT, &mut result.redacted));
148 }
149 if let Some(errors) = record.get("errors").and_then(Value::as_array) {
150 result.errors = Some(
151 errors
152 .iter()
153 .take(20)
154 .map(|entry| ProblemFieldError {
155 property: sanitize(
156 entry.get("property").unwrap_or(&Value::Null),
157 SAFE_PROPERTIES,
158 &mut result.redacted,
159 ),
160 reason: sanitize(
161 entry.get("reason").unwrap_or(&Value::Null),
162 SAFE_TEXT,
163 &mut result.redacted,
164 ),
165 })
166 .collect(),
167 );
168 result.redacted |= errors.len() > 20;
169 } else if record.get("errors").is_some_and(|value| !value.is_null()) {
170 result.redacted = true;
171 }
172 Some(result)
173 }
174 pub fn parse(bytes: &[u8]) -> Result<Option<Self>> {
176 if bytes.len() > 1024 * 1024 {
177 return Err(Error::new(ErrorKind::Limit));
178 }
179 Ok(crate::json_decode::from_slice(bytes)
180 .ok()
181 .as_ref()
182 .and_then(Self::from_value))
183 }
184 pub fn title(&self) -> Option<&'static str> {
185 self.title
186 }
187 pub fn detail(&self) -> Option<&'static str> {
188 self.detail
189 }
190 pub fn errors(&self) -> Option<&[ProblemFieldError]> {
191 self.errors.as_deref()
192 }
193 pub fn redacted(&self) -> bool {
194 self.redacted
195 }
196}
197impl fmt::Debug for ProblemDetails {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 f.debug_struct("ProblemDetails")
200 .field("redacted", &self.redacted)
201 .field("field_count", &self.errors.as_ref().map_or(0, Vec::len))
202 .finish_non_exhaustive()
203 }
204}
205fn sanitize(value: &Value, allowed: &'static [&'static str], redacted: &mut bool) -> &'static str {
206 if let Some(value) = value.as_str()
207 && let Some(known) = allowed.iter().copied().find(|known| *known == value)
208 {
209 return known;
210 }
211 *redacted = true;
212 WITHHELD
213}
214
215#[derive(Clone, Default)]
217pub struct ErrorMetadata {
218 pub(super) problem: Option<ProblemDetails>,
219 pub(super) failure_kind: Option<FailureKind>,
220 pub(super) status: Option<u16>,
221 pub(super) retry_after: Option<Duration>,
222}
223impl ErrorMetadata {
224 pub fn new() -> Self {
225 Self::default()
226 }
227 pub fn with_problem(mut self, value: ProblemDetails) -> Self {
228 self.problem = Some(value);
229 self
230 }
231 pub fn with_failure_kind(mut self, value: FailureKind) -> Self {
232 self.failure_kind = Some(value);
233 self
234 }
235 pub fn with_status(mut self, value: u16) -> Result<Self> {
236 if !(100..=999).contains(&value) {
237 return Err(Error::new(ErrorKind::Validation));
238 }
239 self.status = Some(value);
240 Ok(self)
241 }
242 pub fn with_retry_after(mut self, value: Duration) -> Self {
243 self.retry_after = Some(value);
244 self
245 }
246 pub fn problem(&self) -> Option<&ProblemDetails> {
247 self.problem.as_ref()
248 }
249 pub fn failure_kind(&self) -> Option<FailureKind> {
250 self.failure_kind
251 }
252 pub fn status(&self) -> Option<u16> {
253 self.status
254 }
255 pub fn retry_after(&self) -> Option<Duration> {
256 self.retry_after
257 }
258}
259impl fmt::Debug for ErrorMetadata {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 f.debug_struct("ErrorMetadata")
262 .field("status", &self.status)
263 .field("failure_kind", &self.failure_kind)
264 .field("retry_after", &self.retry_after)
265 .field("has_problem", &self.problem.is_some())
266 .finish()
267 }
268}
269
270pub(crate) fn body_retry_after(bytes: &[u8]) -> Option<Duration> {
272 let body: Value = crate::json_decode::from_slice(bytes).ok()?;
273 let retry = body.get("retry_after")?.as_object()?;
274 let interval = retry.get("interval")?.as_f64()?;
275 if !interval.is_finite() || interval < 0.0 {
276 return None;
277 }
278 let unit = retry.get("unit")?.as_str()?;
279 if unit.len() > 16 {
280 return None;
281 }
282 let seconds = match unit.to_ascii_lowercase().as_str() {
283 "ms" | "msec" | "msecs" | "millisecond" | "milliseconds" => interval / 1000.0,
284 "s" | "sec" | "secs" | "second" | "seconds" => interval,
285 "m" | "min" | "mins" | "minute" | "minutes" => interval * 60.0,
286 _ => return None,
287 };
288 Duration::try_from_secs_f64(seconds).ok()
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use serde_json::json;
295 #[test]
296 fn allowlists_are_exact_and_unknown_text_never_survives() {
297 for text in SAFE_TEXT {
298 let problem = ProblemDetails::from_value(&json!({"title":text})).unwrap();
299 assert_eq!(problem.title(), Some(*text));
300 assert!(!problem.redacted());
301 }
302 let problem = ProblemDetails::from_value(&json!({"title":"echoed secret", "detail":17,
303 "errors":[{"property":"signed-secret-url","reason":"prompt"}], "extension":"secret"}))
304 .unwrap();
305 assert_eq!(problem.title(), Some(WITHHELD));
306 assert_eq!(problem.detail(), Some(WITHHELD));
307 assert!(problem.redacted());
308 assert_eq!(problem.errors().unwrap()[0].property(), WITHHELD);
309 let encoded = serde_json::to_string(&problem).unwrap();
310 assert!(!encoded.contains("secret"));
311 assert!(!format!("{problem:?}").contains(WITHHELD));
312 for property in SAFE_PROPERTIES {
313 let problem = ProblemDetails::from_value(
314 &json!({"errors":[{"property":property,"reason":"is required"}]}),
315 )
316 .unwrap();
317 assert_eq!(problem.errors().unwrap()[0].property(), *property);
318 assert!(!problem.redacted());
319 }
320 }
321 #[test]
322 fn unrelated_errors_null_and_malformed_entries_match_source() {
323 for value in [
324 json!(null),
325 json!([]),
326 json!({}),
327 json!({"message":"legacy","errors":[]}),
328 ] {
329 assert!(ProblemDetails::from_value(&value).is_none());
330 }
331 let value = json!({"title":null,"detail":null,"errors":null});
332 let result = ProblemDetails::from_value(&value).unwrap();
333 assert_eq!(
334 serde_json::to_value(&result).unwrap(),
335 json!({"redacted":false})
336 );
337 let problem = ProblemDetails::from_value(&json!({"errors":[null, {}, 1]})).unwrap();
338 assert!(problem.redacted());
339 assert_eq!(problem.errors().unwrap().len(), 3);
340 let problem = ProblemDetails::from_value(
341 &json!({"errors":vec![json!({"property":"name","reason":"is required"}); 21]}),
342 )
343 .unwrap();
344 assert_eq!(problem.errors().unwrap().len(), 20);
345 assert!(problem.redacted());
346 assert!(ProblemDetails::parse(&vec![b' '; 1024 * 1024 + 1]).is_err());
347 }
348 #[test]
349 fn body_retry_metadata_is_bounded_and_does_not_authorize_replay() {
350 for (unit, expected) in [("MS", 0.002), ("seconds", 2.0), ("MIN", 120.0)] {
351 let bytes =
352 serde_json::to_vec(&json!({"retry_after":{"interval":2,"unit":unit}})).unwrap();
353 assert_eq!(
354 body_retry_after(&bytes),
355 Some(Duration::from_secs_f64(expected))
356 );
357 }
358 for value in [
359 json!({"interval":-1,"unit":"s"}),
360 json!({"interval":"2","unit":"s"}),
361 json!({"interval":1,"unit":"days"}),
362 ] {
363 assert!(
364 body_retry_after(&serde_json::to_vec(&json!({"retry_after":value})).unwrap())
365 .is_none()
366 );
367 }
368 }
369}