blueprint_auth/
validation.rs1use std::collections::{BTreeMap, HashSet};
2
3const MAX_HEADERS: usize = 8;
5
6const MAX_HEADER_NAME_LEN: usize = 256;
8const MAX_HEADER_VALUE_LEN: usize = 512;
9
10const FORBIDDEN_HEADERS: &[&str] = &[
12 "connection",
13 "keep-alive",
14 "proxy-authenticate",
15 "proxy-authorization",
16 "te",
17 "trailer",
18 "transfer-encoding",
19 "upgrade",
20 "host",
21 "content-length",
22];
23
24pub fn validate_headers(
27 headers: &BTreeMap<String, String>,
28) -> Result<BTreeMap<String, String>, ValidationError> {
29 let forbidden_set: HashSet<String> =
30 FORBIDDEN_HEADERS.iter().map(|h| h.to_lowercase()).collect();
31
32 let mut validated = BTreeMap::new();
33
34 for (name, value) in headers {
35 let name_lower = name.to_lowercase();
37
38 if forbidden_set.contains(&name_lower) {
39 return Err(ValidationError::ForbiddenHeader {
40 header: name.clone(),
41 });
42 }
43
44 if name.len() > MAX_HEADER_NAME_LEN {
45 return Err(ValidationError::HeaderNameTooLong {
46 header: name.clone(),
47 max: MAX_HEADER_NAME_LEN,
48 });
49 }
50
51 if value.len() > MAX_HEADER_VALUE_LEN {
52 return Err(ValidationError::HeaderValueTooLong {
53 header: name.clone(),
54 max: MAX_HEADER_VALUE_LEN,
55 });
56 }
57
58 if !is_valid_header_name(name) {
60 return Err(ValidationError::InvalidHeaderName {
61 header: name.clone(),
62 });
63 }
64
65 if !is_valid_header_value(value) {
67 return Err(ValidationError::InvalidHeaderValue {
68 header: name.clone(),
69 value: value.clone(),
70 });
71 }
72
73 validated.insert(name_lower, value.clone());
76 }
77
78 if validated.len() > MAX_HEADERS {
80 return Err(ValidationError::TooManyHeaders {
81 max: MAX_HEADERS,
82 provided: validated.len(),
83 });
84 }
85
86 Ok(validated)
87}
88
89fn is_valid_header_name(name: &str) -> bool {
91 !name.is_empty()
92 && name
93 .chars()
94 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
95}
96
97fn is_valid_header_value(value: &str) -> bool {
99 value.chars().all(|c| {
100 (c.is_ascii() && !c.is_control()) || c == '\t'
102 })
103}
104
105pub fn hash_user_id(user_id: &str) -> String {
107 use tiny_keccak::{Hasher, Keccak};
108 let mut hasher = Keccak::v256();
109 hasher.update(user_id.as_bytes());
110 let mut output = [0u8; 32];
111 hasher.finalize(&mut output);
112 hex::encode(&output[..16])
114}
115
116pub fn process_headers_with_pii_protection(
120 headers: &BTreeMap<String, String>,
121) -> BTreeMap<String, String> {
122 let mut processed = BTreeMap::new();
123
124 for (name, value) in headers {
125 let processed_value = match name.as_str() {
127 "x-user-id" | "x-user-email" | "x-customer-email" => hash_user_id(value),
129 "x-tenant-id" => {
131 if value.contains('@') {
132 hash_user_id(value)
134 } else if value.len() == 32 && value.chars().all(|c| c.is_ascii_hexdigit()) {
135 value.clone()
137 } else {
138 hash_user_id(value)
140 }
141 }
142 _ => value.clone(),
144 };
145 processed.insert(name.clone(), processed_value);
146 }
147
148 processed
149}
150
151#[derive(Debug, thiserror::Error)]
152pub enum ValidationError {
153 #[error("Too many headers provided: {provided} (max: {max})")]
154 TooManyHeaders { max: usize, provided: usize },
155
156 #[error("Forbidden header: {header}")]
157 ForbiddenHeader { header: String },
158
159 #[error("Header name too long: {header} (max: {max} bytes)")]
160 HeaderNameTooLong { header: String, max: usize },
161
162 #[error("Header value too long for {header} (max: {max} bytes)")]
163 HeaderValueTooLong { header: String, max: usize },
164
165 #[error("Invalid header name: {header}")]
166 InvalidHeaderName { header: String },
167
168 #[error("Invalid header value for {header}: {value}")]
169 InvalidHeaderValue { header: String, value: String },
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_validate_headers_valid() {
178 let mut headers = BTreeMap::new();
179 headers.insert("X-Tenant-Id".to_string(), "abc123".to_string());
180 headers.insert("X-User-Type".to_string(), "premium".to_string());
181
182 let result = validate_headers(&headers);
183 assert!(result.is_ok());
184 let validated = result.unwrap();
185 assert_eq!(validated.len(), 2);
186 }
187
188 #[test]
189 fn test_validate_headers_too_many() {
190 let mut headers = BTreeMap::new();
191 for i in 0..10 {
192 headers.insert(format!("X-Header-{i}"), "value".to_string());
193 }
194
195 let result = validate_headers(&headers);
196 assert!(matches!(
197 result,
198 Err(ValidationError::TooManyHeaders { .. })
199 ));
200 }
201
202 #[test]
203 fn test_validate_headers_forbidden() {
204 let mut headers = BTreeMap::new();
205 headers.insert("Connection".to_string(), "close".to_string());
206
207 let result = validate_headers(&headers);
208 assert!(matches!(
209 result,
210 Err(ValidationError::ForbiddenHeader { .. })
211 ));
212 }
213
214 #[test]
215 fn test_validate_headers_invalid_name() {
216 let mut headers = BTreeMap::new();
217 headers.insert("X-Invalid Header".to_string(), "value".to_string());
218
219 let result = validate_headers(&headers);
220 assert!(matches!(
221 result,
222 Err(ValidationError::InvalidHeaderName { .. })
223 ));
224 }
225
226 #[test]
227 fn test_validate_headers_name_too_long() {
228 let mut headers = BTreeMap::new();
229 let long_name = "X-".to_string() + &"a".repeat(300);
230 headers.insert(long_name, "value".to_string());
231
232 let result = validate_headers(&headers);
233 assert!(matches!(
234 result,
235 Err(ValidationError::HeaderNameTooLong { .. })
236 ));
237 }
238
239 #[test]
240 fn test_validate_headers_value_too_long() {
241 let mut headers = BTreeMap::new();
242 let long_value = "a".repeat(600);
243 headers.insert("X-Test".to_string(), long_value);
244
245 let result = validate_headers(&headers);
246 assert!(matches!(
247 result,
248 Err(ValidationError::HeaderValueTooLong { .. })
249 ));
250 }
251
252 #[test]
253 fn test_hash_user_id() {
254 let user_id = "user123@example.com";
255 let hash1 = hash_user_id(user_id);
256 let hash2 = hash_user_id(user_id);
257
258 assert_eq!(hash1, hash2);
260
261 assert_eq!(hash1.len(), 32);
263
264 let hash3 = hash_user_id("different@example.com");
266 assert_ne!(hash1, hash3);
267 }
268
269 #[test]
270 fn test_valid_header_name() {
271 assert!(is_valid_header_name("X-Tenant-Id"));
272 assert!(is_valid_header_name("X_User_Type"));
273 assert!(is_valid_header_name("Authorization"));
274
275 assert!(!is_valid_header_name(""));
276 assert!(!is_valid_header_name("X Tenant Id"));
277 assert!(!is_valid_header_name("X-Tenant:Id"));
278 }
279
280 #[test]
281 fn test_valid_header_value() {
282 assert!(is_valid_header_value("abc123"));
283 assert!(is_valid_header_value("Bearer token123"));
284 assert!(is_valid_header_value("value with spaces"));
285
286 assert!(!is_valid_header_value("value\nwith\nnewlines"));
287 assert!(!is_valid_header_value("value\0with\0nulls"));
288 }
289
290 #[test]
291 fn test_header_case_insensitive_override() {
292 let mut headers = BTreeMap::new();
293 headers.insert("X-Tenant-Id".to_string(), "first_value".to_string());
295 headers.insert("x-tenant-id".to_string(), "second_value".to_string());
297 headers.insert("X-TENANT-ID".to_string(), "third_value".to_string());
299
300 let result = validate_headers(&headers).unwrap();
301
302 assert_eq!(result.len(), 1);
304 assert!(result.contains_key("x-tenant-id"));
305
306 let value = result.get("x-tenant-id").unwrap();
310 assert!(
311 value == "first_value" || value == "second_value" || value == "third_value",
312 "Value should be one of the provided values, got: {value}"
313 );
314 }
315
316 #[test]
317 fn test_multiple_headers_case_insensitive() {
318 let mut headers = BTreeMap::new();
319 headers.insert("X-User-Id".to_string(), "user123".to_string());
320 headers.insert("x-tenant-id".to_string(), "tenant456".to_string());
321 headers.insert("X-TENANT-ID".to_string(), "tenant789".to_string()); headers.insert("Content-Type".to_string(), "application/json".to_string());
323
324 let result = validate_headers(&headers).unwrap();
325
326 assert_eq!(result.len(), 3);
328 assert!(result.contains_key("x-user-id"));
329 assert!(result.contains_key("x-tenant-id"));
330 assert!(result.contains_key("content-type"));
331
332 let tenant_value = result.get("x-tenant-id").unwrap();
334 assert!(
335 tenant_value == "tenant456" || tenant_value == "tenant789",
336 "Tenant ID should be one of the override values"
337 );
338 }
339}