1use std::{
4 collections::{HashMap, HashSet},
5 fmt,
6 net::IpAddr,
7 path::{Path, PathBuf},
8 str::FromStr,
9 sync::{
10 Arc, RwLock,
11 atomic::{AtomicU64, Ordering},
12 },
13};
14
15use axum::http::HeaderMap;
16use stateset_embedded::Commerce;
17
18use crate::{
19 error::HttpError,
20 events_replay::{DEFAULT_REPLAY_CAPACITY, EventReplayBuffer, EventReplayRegistry},
21 middleware::X_TENANT_ID,
22};
23
24pub(crate) const DEFAULT_MAX_TENANT_DBS: usize = 256;
26
27#[derive(Debug)]
28struct TenantCacheEntry {
29 commerce: Arc<Commerce>,
30 last_access_tick: u64,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct TenantCacheMetrics {
36 pub enabled: bool,
38 pub max_cached_dbs: usize,
40 pub cached_dbs: usize,
42 pub in_use_cached_dbs: usize,
44 pub hits: u64,
46 pub misses: u64,
48 pub evictions: u64,
50 pub rejections: u64,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub(crate) struct MetricsAccessMetrics {
57 pub(crate) requests_total: u64,
58 pub(crate) allowed_total: u64,
59 pub(crate) allowed_peer_total: u64,
60 pub(crate) allowed_forwarded_trusted_proxy_total: u64,
61 pub(crate) allowed_forwarded_without_peer_total: u64,
62 pub(crate) allowed_unavailable_total: u64,
63 pub(crate) denied_ip_total: u64,
64 pub(crate) denied_ip_not_allowed_total: u64,
65 pub(crate) denied_missing_peer_ip_with_trusted_proxies_total: u64,
66 pub(crate) denied_auth_total: u64,
67 pub(crate) denied_auth_header_missing_total: u64,
68 pub(crate) denied_auth_header_invalid_total: u64,
69 pub(crate) denied_auth_header_invalid_encoding_total: u64,
70 pub(crate) denied_auth_header_invalid_scheme_total: u64,
71 pub(crate) denied_auth_header_malformed_total: u64,
72 pub(crate) denied_auth_header_multiple_total: u64,
73 pub(crate) denied_auth_header_oversized_total: u64,
74 pub(crate) denied_auth_token_mismatch_total: u64,
75 pub(crate) denied_forwarded_missing_total: u64,
76 pub(crate) denied_forwarded_invalid_total: u64,
77 pub(crate) denied_forwarded_oversized_total: u64,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct MetricsHeaderLimits {
83 forwarded_header_value_bytes: usize,
84 x_forwarded_for_header_value_bytes: usize,
85 x_real_ip_header_value_bytes: usize,
86 authorization_header_value_bytes: usize,
87}
88
89impl MetricsHeaderLimits {
90 pub const DEFAULT_FORWARDED_HEADER_VALUE_BYTES: usize = 2048;
92 pub const DEFAULT_X_FORWARDED_FOR_HEADER_VALUE_BYTES: usize = 2048;
94 pub const DEFAULT_X_REAL_IP_HEADER_VALUE_BYTES: usize = 512;
96 pub const DEFAULT_AUTHORIZATION_HEADER_VALUE_BYTES: usize = 2048;
98
99 pub fn new(
101 forwarded_header_value_bytes: usize,
102 x_forwarded_for_header_value_bytes: usize,
103 x_real_ip_header_value_bytes: usize,
104 ) -> Result<Self, HttpError> {
105 Self::new_with_authorization(
106 forwarded_header_value_bytes,
107 x_forwarded_for_header_value_bytes,
108 x_real_ip_header_value_bytes,
109 Self::DEFAULT_AUTHORIZATION_HEADER_VALUE_BYTES,
110 )
111 }
112
113 pub fn new_with_authorization(
117 forwarded_header_value_bytes: usize,
118 x_forwarded_for_header_value_bytes: usize,
119 x_real_ip_header_value_bytes: usize,
120 authorization_header_value_bytes: usize,
121 ) -> Result<Self, HttpError> {
122 if forwarded_header_value_bytes == 0 {
123 return Err(HttpError::BadRequest(
124 "metrics forwarded header limit must be greater than zero".to_string(),
125 ));
126 }
127 if x_forwarded_for_header_value_bytes == 0 {
128 return Err(HttpError::BadRequest(
129 "metrics x-forwarded-for header limit must be greater than zero".to_string(),
130 ));
131 }
132 if x_real_ip_header_value_bytes == 0 {
133 return Err(HttpError::BadRequest(
134 "metrics x-real-ip header limit must be greater than zero".to_string(),
135 ));
136 }
137 if authorization_header_value_bytes == 0 {
138 return Err(HttpError::BadRequest(
139 "metrics authorization header limit must be greater than zero".to_string(),
140 ));
141 }
142
143 Ok(Self {
144 forwarded_header_value_bytes,
145 x_forwarded_for_header_value_bytes,
146 x_real_ip_header_value_bytes,
147 authorization_header_value_bytes,
148 })
149 }
150
151 #[must_use]
153 pub const fn forwarded_header_value_bytes(self) -> usize {
154 self.forwarded_header_value_bytes
155 }
156
157 #[must_use]
159 pub const fn x_forwarded_for_header_value_bytes(self) -> usize {
160 self.x_forwarded_for_header_value_bytes
161 }
162
163 #[must_use]
165 pub const fn x_real_ip_header_value_bytes(self) -> usize {
166 self.x_real_ip_header_value_bytes
167 }
168
169 #[must_use]
171 pub const fn authorization_header_value_bytes(self) -> usize {
172 self.authorization_header_value_bytes
173 }
174}
175
176impl Default for MetricsHeaderLimits {
177 fn default() -> Self {
178 Self {
179 forwarded_header_value_bytes: Self::DEFAULT_FORWARDED_HEADER_VALUE_BYTES,
180 x_forwarded_for_header_value_bytes: Self::DEFAULT_X_FORWARDED_FOR_HEADER_VALUE_BYTES,
181 x_real_ip_header_value_bytes: Self::DEFAULT_X_REAL_IP_HEADER_VALUE_BYTES,
182 authorization_header_value_bytes: Self::DEFAULT_AUTHORIZATION_HEADER_VALUE_BYTES,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
189pub struct IpCidr {
190 network: IpAddr,
191 prefix_len: u8,
192}
193
194impl IpCidr {
195 pub fn new(network: IpAddr, prefix_len: u8) -> Result<Self, HttpError> {
197 let max_prefix = match network {
198 IpAddr::V4(_) => 32,
199 IpAddr::V6(_) => 128,
200 };
201 if prefix_len > max_prefix {
202 return Err(HttpError::BadRequest(format!(
203 "invalid CIDR prefix length {prefix_len} for {network}"
204 )));
205 }
206 Ok(Self { network, prefix_len })
207 }
208
209 #[must_use]
211 pub fn contains(self, ip: IpAddr) -> bool {
212 match (self.network, ip) {
213 (IpAddr::V4(network), IpAddr::V4(ip)) => {
214 let mask = if self.prefix_len == 0 {
215 0
216 } else {
217 u32::MAX << (32 - u32::from(self.prefix_len))
218 };
219 (u32::from(network) & mask) == (u32::from(ip) & mask)
220 }
221 (IpAddr::V6(network), IpAddr::V6(ip)) => {
222 let mask = if self.prefix_len == 0 {
223 0
224 } else {
225 u128::MAX << (128 - u32::from(self.prefix_len))
226 };
227 (u128::from(network) & mask) == (u128::from(ip) & mask)
228 }
229 _ => false,
230 }
231 }
232
233 #[must_use]
235 pub const fn prefix_len(self) -> u8 {
236 self.prefix_len
237 }
238
239 #[must_use]
241 pub const fn network(self) -> IpAddr {
242 self.network
243 }
244}
245
246impl fmt::Display for IpCidr {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 write!(f, "{}/{}", self.network, self.prefix_len)
249 }
250}
251
252impl FromStr for IpCidr {
253 type Err = HttpError;
254
255 fn from_str(value: &str) -> Result<Self, Self::Err> {
256 let value = value.trim();
257 if value.is_empty() {
258 return Err(HttpError::BadRequest("CIDR cannot be empty".to_string()));
259 }
260
261 if let Some((ip, prefix)) = value.split_once('/') {
262 let network = ip.trim().parse::<IpAddr>().map_err(|error| {
263 HttpError::BadRequest(format!("invalid CIDR address '{}': {error}", ip.trim()))
264 })?;
265 let prefix_len = prefix.trim().parse::<u8>().map_err(|error| {
266 HttpError::BadRequest(format!(
267 "invalid CIDR prefix '{}' in '{}': {error}",
268 prefix.trim(),
269 value
270 ))
271 })?;
272 Self::new(network, prefix_len)
273 } else {
274 let ip = value.parse::<IpAddr>().map_err(|error| {
275 HttpError::BadRequest(format!("invalid IP address '{value}': {error}"))
276 })?;
277 let prefix = match ip {
278 IpAddr::V4(_) => 32,
279 IpAddr::V6(_) => 128,
280 };
281 Self::new(ip, prefix)
282 }
283 }
284}
285
286#[derive(Clone)]
289pub struct AppState {
290 commerce: Arc<Commerce>,
291 tenant_db_dir: Option<Arc<PathBuf>>,
292 ignore_tenant_header: bool,
293 tenant_cache: Arc<RwLock<HashMap<String, TenantCacheEntry>>>,
294 tenant_access_clock: Arc<AtomicU64>,
295 tenant_cache_hits: Arc<AtomicU64>,
296 tenant_cache_misses: Arc<AtomicU64>,
297 tenant_cache_evictions: Arc<AtomicU64>,
298 tenant_cache_rejections: Arc<AtomicU64>,
299 metrics_scrape_requests: Arc<AtomicU64>,
300 metrics_scrape_allowed: Arc<AtomicU64>,
301 metrics_scrape_allowed_peer: Arc<AtomicU64>,
302 metrics_scrape_allowed_forwarded_trusted_proxy: Arc<AtomicU64>,
303 metrics_scrape_allowed_forwarded_without_peer: Arc<AtomicU64>,
304 metrics_scrape_allowed_unavailable: Arc<AtomicU64>,
305 metrics_scrape_denied_ip: Arc<AtomicU64>,
306 metrics_scrape_denied_ip_not_allowed: Arc<AtomicU64>,
307 metrics_scrape_denied_missing_peer_ip_with_trusted_proxies: Arc<AtomicU64>,
308 metrics_scrape_denied_auth: Arc<AtomicU64>,
309 metrics_scrape_denied_auth_header_missing: Arc<AtomicU64>,
310 metrics_scrape_denied_auth_header_invalid: Arc<AtomicU64>,
311 metrics_scrape_denied_auth_header_invalid_encoding: Arc<AtomicU64>,
312 metrics_scrape_denied_auth_header_invalid_scheme: Arc<AtomicU64>,
313 metrics_scrape_denied_auth_header_malformed: Arc<AtomicU64>,
314 metrics_scrape_denied_auth_header_multiple: Arc<AtomicU64>,
315 metrics_scrape_denied_auth_header_oversized: Arc<AtomicU64>,
316 metrics_scrape_denied_auth_token_mismatch: Arc<AtomicU64>,
317 metrics_scrape_denied_forwarded_missing: Arc<AtomicU64>,
318 metrics_scrape_denied_forwarded_invalid: Arc<AtomicU64>,
319 metrics_scrape_denied_forwarded_oversized: Arc<AtomicU64>,
320 metrics_bearer_token: Option<Arc<str>>,
321 metrics_ip_allowlist: Option<Arc<HashSet<IpAddr>>>,
322 metrics_ip_cidr_allowlist: Option<Arc<Vec<IpCidr>>>,
323 metrics_trusted_proxies: Option<Arc<Vec<IpCidr>>>,
324 metrics_header_limits: MetricsHeaderLimits,
325 max_tenant_dbs: usize,
326 event_replay: EventReplayRegistry,
327}
328
329impl fmt::Debug for AppState {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 f.debug_struct("AppState")
332 .field("commerce", &"Commerce { .. }")
333 .field(
334 "tenant_db_dir",
335 &self.tenant_db_dir.as_deref().map(|path| path.display().to_string()),
336 )
337 .field("ignore_tenant_header", &self.ignore_tenant_header)
338 .field(
339 "metrics_bearer_token",
340 &self.metrics_bearer_token.as_ref().map(|_| "<redacted>"),
341 )
342 .field(
343 "metrics_ip_allowlist_size",
344 &self.metrics_ip_allowlist.as_ref().map(|allowlist| allowlist.len()),
345 )
346 .field(
347 "metrics_ip_cidr_allowlist_size",
348 &self.metrics_ip_cidr_allowlist.as_ref().map(|allowlist| allowlist.len()),
349 )
350 .field(
351 "metrics_trusted_proxies_size",
352 &self.metrics_trusted_proxies.as_ref().map(|proxies| proxies.len()),
353 )
354 .field("metrics_scrape_requests", &self.metrics_scrape_requests.load(Ordering::Relaxed))
355 .field("metrics_scrape_allowed", &self.metrics_scrape_allowed.load(Ordering::Relaxed))
356 .field(
357 "metrics_scrape_allowed_peer",
358 &self.metrics_scrape_allowed_peer.load(Ordering::Relaxed),
359 )
360 .field(
361 "metrics_scrape_allowed_forwarded_trusted_proxy",
362 &self.metrics_scrape_allowed_forwarded_trusted_proxy.load(Ordering::Relaxed),
363 )
364 .field(
365 "metrics_scrape_allowed_forwarded_without_peer",
366 &self.metrics_scrape_allowed_forwarded_without_peer.load(Ordering::Relaxed),
367 )
368 .field(
369 "metrics_scrape_allowed_unavailable",
370 &self.metrics_scrape_allowed_unavailable.load(Ordering::Relaxed),
371 )
372 .field(
373 "metrics_scrape_denied_ip",
374 &self.metrics_scrape_denied_ip.load(Ordering::Relaxed),
375 )
376 .field(
377 "metrics_scrape_denied_ip_not_allowed",
378 &self.metrics_scrape_denied_ip_not_allowed.load(Ordering::Relaxed),
379 )
380 .field(
381 "metrics_scrape_denied_missing_peer_ip_with_trusted_proxies",
382 &self
383 .metrics_scrape_denied_missing_peer_ip_with_trusted_proxies
384 .load(Ordering::Relaxed),
385 )
386 .field(
387 "metrics_scrape_denied_auth",
388 &self.metrics_scrape_denied_auth.load(Ordering::Relaxed),
389 )
390 .field(
391 "metrics_scrape_denied_auth_header_missing",
392 &self.metrics_scrape_denied_auth_header_missing.load(Ordering::Relaxed),
393 )
394 .field(
395 "metrics_scrape_denied_auth_header_invalid",
396 &self.metrics_scrape_denied_auth_header_invalid.load(Ordering::Relaxed),
397 )
398 .field(
399 "metrics_scrape_denied_auth_header_invalid_encoding",
400 &self.metrics_scrape_denied_auth_header_invalid_encoding.load(Ordering::Relaxed),
401 )
402 .field(
403 "metrics_scrape_denied_auth_header_invalid_scheme",
404 &self.metrics_scrape_denied_auth_header_invalid_scheme.load(Ordering::Relaxed),
405 )
406 .field(
407 "metrics_scrape_denied_auth_header_malformed",
408 &self.metrics_scrape_denied_auth_header_malformed.load(Ordering::Relaxed),
409 )
410 .field(
411 "metrics_scrape_denied_auth_header_multiple",
412 &self.metrics_scrape_denied_auth_header_multiple.load(Ordering::Relaxed),
413 )
414 .field(
415 "metrics_scrape_denied_auth_header_oversized",
416 &self.metrics_scrape_denied_auth_header_oversized.load(Ordering::Relaxed),
417 )
418 .field(
419 "metrics_scrape_denied_auth_token_mismatch",
420 &self.metrics_scrape_denied_auth_token_mismatch.load(Ordering::Relaxed),
421 )
422 .field(
423 "metrics_scrape_denied_forwarded_missing",
424 &self.metrics_scrape_denied_forwarded_missing.load(Ordering::Relaxed),
425 )
426 .field(
427 "metrics_scrape_denied_forwarded_invalid",
428 &self.metrics_scrape_denied_forwarded_invalid.load(Ordering::Relaxed),
429 )
430 .field(
431 "metrics_scrape_denied_forwarded_oversized",
432 &self.metrics_scrape_denied_forwarded_oversized.load(Ordering::Relaxed),
433 )
434 .field("metrics_header_limits", &self.metrics_header_limits)
435 .field("max_tenant_dbs", &self.max_tenant_dbs)
436 .field("event_replay", &self.event_replay)
437 .finish()
438 }
439}
440
441fn is_valid_tenant_id(value: &str) -> bool {
442 let trimmed = value.trim();
443 if trimmed.is_empty() || trimmed.len() > 64 {
444 return false;
445 }
446 trimmed.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
450}
451
452#[must_use]
454pub(crate) fn tenant_id_from_headers(headers: &HeaderMap) -> Option<String> {
455 headers
456 .get(&X_TENANT_ID)
457 .and_then(|value| value.to_str().ok())
458 .map(str::trim)
459 .filter(|value| !value.is_empty())
460 .map(ToOwned::to_owned)
461}
462
463impl AppState {
464 #[must_use]
466 pub fn new(commerce: Commerce) -> Self {
467 Self::new_with_tenant_db_dir(commerce, None)
468 }
469
470 #[must_use]
472 pub fn new_with_tenant_db_dir(commerce: Commerce, tenant_db_dir: Option<PathBuf>) -> Self {
473 Self {
474 commerce: Arc::new(commerce),
475 tenant_db_dir: tenant_db_dir.map(Arc::new),
476 ignore_tenant_header: false,
477 tenant_cache: Arc::new(RwLock::new(HashMap::new())),
478 tenant_access_clock: Arc::new(AtomicU64::new(0)),
479 tenant_cache_hits: Arc::new(AtomicU64::new(0)),
480 tenant_cache_misses: Arc::new(AtomicU64::new(0)),
481 tenant_cache_evictions: Arc::new(AtomicU64::new(0)),
482 tenant_cache_rejections: Arc::new(AtomicU64::new(0)),
483 metrics_scrape_requests: Arc::new(AtomicU64::new(0)),
484 metrics_scrape_allowed: Arc::new(AtomicU64::new(0)),
485 metrics_scrape_allowed_peer: Arc::new(AtomicU64::new(0)),
486 metrics_scrape_allowed_forwarded_trusted_proxy: Arc::new(AtomicU64::new(0)),
487 metrics_scrape_allowed_forwarded_without_peer: Arc::new(AtomicU64::new(0)),
488 metrics_scrape_allowed_unavailable: Arc::new(AtomicU64::new(0)),
489 metrics_scrape_denied_ip: Arc::new(AtomicU64::new(0)),
490 metrics_scrape_denied_ip_not_allowed: Arc::new(AtomicU64::new(0)),
491 metrics_scrape_denied_missing_peer_ip_with_trusted_proxies: Arc::new(AtomicU64::new(0)),
492 metrics_scrape_denied_auth: Arc::new(AtomicU64::new(0)),
493 metrics_scrape_denied_auth_header_missing: Arc::new(AtomicU64::new(0)),
494 metrics_scrape_denied_auth_header_invalid: Arc::new(AtomicU64::new(0)),
495 metrics_scrape_denied_auth_header_invalid_encoding: Arc::new(AtomicU64::new(0)),
496 metrics_scrape_denied_auth_header_invalid_scheme: Arc::new(AtomicU64::new(0)),
497 metrics_scrape_denied_auth_header_malformed: Arc::new(AtomicU64::new(0)),
498 metrics_scrape_denied_auth_header_multiple: Arc::new(AtomicU64::new(0)),
499 metrics_scrape_denied_auth_header_oversized: Arc::new(AtomicU64::new(0)),
500 metrics_scrape_denied_auth_token_mismatch: Arc::new(AtomicU64::new(0)),
501 metrics_scrape_denied_forwarded_missing: Arc::new(AtomicU64::new(0)),
502 metrics_scrape_denied_forwarded_invalid: Arc::new(AtomicU64::new(0)),
503 metrics_scrape_denied_forwarded_oversized: Arc::new(AtomicU64::new(0)),
504 metrics_bearer_token: None,
505 metrics_ip_allowlist: None,
506 metrics_ip_cidr_allowlist: None,
507 metrics_trusted_proxies: None,
508 metrics_header_limits: MetricsHeaderLimits::default(),
509 max_tenant_dbs: DEFAULT_MAX_TENANT_DBS,
510 event_replay: EventReplayRegistry::new(DEFAULT_REPLAY_CAPACITY),
511 }
512 }
513
514 #[must_use]
516 pub fn with_tenant_db_dir(mut self, tenant_db_dir: impl Into<PathBuf>) -> Self {
517 self.tenant_db_dir = Some(Arc::new(tenant_db_dir.into()));
518 self
519 }
520
521 #[must_use]
530 pub const fn with_ignore_tenant_header(mut self) -> Self {
531 self.ignore_tenant_header = true;
532 self
533 }
534
535 #[must_use]
537 pub const fn ignores_tenant_header(&self) -> bool {
538 self.ignore_tenant_header
539 }
540
541 #[must_use]
547 pub fn with_max_tenant_dbs(mut self, max_tenant_dbs: usize) -> Self {
548 self.max_tenant_dbs = max_tenant_dbs.max(1);
549 self
550 }
551
552 #[must_use]
554 pub fn with_metrics_bearer_auth(mut self, token: impl Into<String>) -> Self {
555 self.metrics_bearer_token = Some(Arc::<str>::from(token.into()));
556 self
557 }
558
559 #[must_use]
561 pub fn without_metrics_auth(mut self) -> Self {
562 self.metrics_bearer_token = None;
563 self
564 }
565
566 #[must_use]
570 pub fn with_metrics_ip_allowlist<I>(mut self, ips: I) -> Self
571 where
572 I: IntoIterator<Item = IpAddr>,
573 {
574 let set: HashSet<IpAddr> = ips.into_iter().collect();
575 self.metrics_ip_allowlist = if set.is_empty() { None } else { Some(Arc::new(set)) };
576 self
577 }
578
579 #[must_use]
581 pub fn without_metrics_ip_allowlist(mut self) -> Self {
582 self.metrics_ip_allowlist = None;
583 self
584 }
585
586 #[must_use]
590 pub fn with_metrics_ip_cidr_allowlist<I>(mut self, cidrs: I) -> Self
591 where
592 I: IntoIterator<Item = IpCidr>,
593 {
594 let mut cidrs: Vec<IpCidr> = cidrs.into_iter().collect();
595 cidrs.sort_unstable();
596 cidrs.dedup();
597 self.metrics_ip_cidr_allowlist =
598 if cidrs.is_empty() { None } else { Some(Arc::new(cidrs)) };
599 self
600 }
601
602 #[must_use]
604 pub fn without_metrics_ip_cidr_allowlist(mut self) -> Self {
605 self.metrics_ip_cidr_allowlist = None;
606 self
607 }
608
609 #[must_use]
614 pub fn with_metrics_trusted_proxies<I>(mut self, cidrs: I) -> Self
615 where
616 I: IntoIterator<Item = IpCidr>,
617 {
618 let mut proxies: Vec<IpCidr> = cidrs.into_iter().collect();
619 proxies.sort_unstable();
620 proxies.dedup();
621 self.metrics_trusted_proxies =
622 if proxies.is_empty() { None } else { Some(Arc::new(proxies)) };
623 self
624 }
625
626 #[must_use]
628 pub fn without_metrics_trusted_proxies(mut self) -> Self {
629 self.metrics_trusted_proxies = None;
630 self
631 }
632
633 #[must_use]
635 pub const fn with_metrics_header_limits(mut self, limits: MetricsHeaderLimits) -> Self {
636 self.metrics_header_limits = limits;
637 self
638 }
639
640 #[must_use]
642 pub fn tenant_db_dir(&self) -> Option<&Path> {
643 self.tenant_db_dir.as_deref().map(std::path::PathBuf::as_path)
644 }
645
646 #[must_use]
648 pub fn commerce(&self) -> &Commerce {
649 &self.commerce
650 }
651
652 pub(crate) fn event_replay_buffer(&self, commerce: &Arc<Commerce>) -> Arc<EventReplayBuffer> {
658 self.event_replay.buffer_for(commerce)
659 }
660
661 #[cfg(test)]
663 #[must_use]
664 pub(crate) fn with_event_replay_capacity(mut self, capacity: usize) -> Self {
665 self.event_replay = EventReplayRegistry::new(capacity);
666 self
667 }
668
669 #[must_use]
671 pub fn metrics_bearer_auth_token(&self) -> Option<&str> {
672 self.metrics_bearer_token.as_deref()
673 }
674
675 #[must_use]
677 pub fn metrics_ip_allowlist(&self) -> Option<Vec<IpAddr>> {
678 self.metrics_ip_allowlist.as_ref().map(|allowlist| {
679 let mut ips: Vec<IpAddr> = allowlist.iter().copied().collect();
680 ips.sort_unstable();
681 ips
682 })
683 }
684
685 #[must_use]
687 pub fn metrics_ip_cidr_allowlist(&self) -> Option<Vec<IpCidr>> {
688 self.metrics_ip_cidr_allowlist.as_ref().map(|cidrs| cidrs.as_ref().clone())
689 }
690
691 #[must_use]
693 pub fn metrics_ip_allowlist_len(&self) -> usize {
694 self.metrics_ip_allowlist.as_ref().map_or(0, |allowlist| allowlist.len())
695 }
696
697 #[must_use]
699 pub fn metrics_ip_cidr_allowlist_len(&self) -> usize {
700 self.metrics_ip_cidr_allowlist.as_ref().map_or(0, |cidrs| cidrs.len())
701 }
702
703 #[must_use]
705 pub const fn has_metrics_ip_allowlist(&self) -> bool {
706 self.metrics_ip_allowlist.is_some() || self.metrics_ip_cidr_allowlist.is_some()
707 }
708
709 #[must_use]
711 pub fn metrics_trusted_proxies(&self) -> Option<Vec<IpCidr>> {
712 self.metrics_trusted_proxies.as_ref().map(|proxies| proxies.as_ref().clone())
713 }
714
715 #[must_use]
717 pub fn metrics_trusted_proxies_len(&self) -> usize {
718 self.metrics_trusted_proxies.as_ref().map_or(0, |proxies| proxies.len())
719 }
720
721 pub(crate) fn record_metrics_scrape_attempt(&self) {
723 self.metrics_scrape_requests.fetch_add(1, Ordering::Relaxed);
724 }
725
726 pub(crate) fn record_metrics_scrape_allowed(&self) {
728 self.metrics_scrape_allowed.fetch_add(1, Ordering::Relaxed);
729 }
730
731 pub(crate) fn record_metrics_scrape_allowed_peer(&self) {
733 self.metrics_scrape_allowed_peer.fetch_add(1, Ordering::Relaxed);
734 }
735
736 pub(crate) fn record_metrics_scrape_allowed_forwarded_trusted_proxy(&self) {
738 self.metrics_scrape_allowed_forwarded_trusted_proxy.fetch_add(1, Ordering::Relaxed);
739 }
740
741 pub(crate) fn record_metrics_scrape_allowed_forwarded_without_peer(&self) {
743 self.metrics_scrape_allowed_forwarded_without_peer.fetch_add(1, Ordering::Relaxed);
744 }
745
746 pub(crate) fn record_metrics_scrape_allowed_unavailable(&self) {
748 self.metrics_scrape_allowed_unavailable.fetch_add(1, Ordering::Relaxed);
749 }
750
751 pub(crate) fn record_metrics_scrape_denied_ip(&self) {
753 self.metrics_scrape_denied_ip.fetch_add(1, Ordering::Relaxed);
754 }
755
756 pub(crate) fn record_metrics_scrape_denied_ip_not_allowed(&self) {
758 self.metrics_scrape_denied_ip_not_allowed.fetch_add(1, Ordering::Relaxed);
759 }
760
761 pub(crate) fn record_metrics_scrape_denied_missing_peer_ip_with_trusted_proxies(&self) {
763 self.metrics_scrape_denied_missing_peer_ip_with_trusted_proxies
764 .fetch_add(1, Ordering::Relaxed);
765 }
766
767 pub(crate) fn record_metrics_scrape_denied_auth(&self) {
769 self.metrics_scrape_denied_auth.fetch_add(1, Ordering::Relaxed);
770 }
771
772 pub(crate) fn record_metrics_scrape_denied_auth_header_missing(&self) {
774 self.metrics_scrape_denied_auth_header_missing.fetch_add(1, Ordering::Relaxed);
775 }
776
777 pub(crate) fn record_metrics_scrape_denied_auth_header_invalid(&self) {
779 self.metrics_scrape_denied_auth_header_invalid.fetch_add(1, Ordering::Relaxed);
780 }
781
782 pub(crate) fn record_metrics_scrape_denied_auth_header_invalid_encoding(&self) {
784 self.metrics_scrape_denied_auth_header_invalid_encoding.fetch_add(1, Ordering::Relaxed);
785 }
786
787 pub(crate) fn record_metrics_scrape_denied_auth_header_invalid_scheme(&self) {
789 self.metrics_scrape_denied_auth_header_invalid_scheme.fetch_add(1, Ordering::Relaxed);
790 }
791
792 pub(crate) fn record_metrics_scrape_denied_auth_header_malformed(&self) {
794 self.metrics_scrape_denied_auth_header_malformed.fetch_add(1, Ordering::Relaxed);
795 }
796
797 pub(crate) fn record_metrics_scrape_denied_auth_header_multiple(&self) {
799 self.metrics_scrape_denied_auth_header_multiple.fetch_add(1, Ordering::Relaxed);
800 }
801
802 pub(crate) fn record_metrics_scrape_denied_auth_header_oversized(&self) {
804 self.metrics_scrape_denied_auth_header_oversized.fetch_add(1, Ordering::Relaxed);
805 }
806
807 pub(crate) fn record_metrics_scrape_denied_auth_token_mismatch(&self) {
809 self.metrics_scrape_denied_auth_token_mismatch.fetch_add(1, Ordering::Relaxed);
810 }
811
812 pub(crate) fn record_metrics_scrape_denied_forwarded_missing(&self) {
814 self.metrics_scrape_denied_forwarded_missing.fetch_add(1, Ordering::Relaxed);
815 }
816
817 pub(crate) fn record_metrics_scrape_denied_forwarded_invalid(&self) {
819 self.metrics_scrape_denied_forwarded_invalid.fetch_add(1, Ordering::Relaxed);
820 }
821
822 pub(crate) fn record_metrics_scrape_denied_forwarded_oversized(&self) {
824 self.metrics_scrape_denied_forwarded_oversized.fetch_add(1, Ordering::Relaxed);
825 }
826
827 #[must_use]
829 pub(crate) fn metrics_access_metrics(&self) -> MetricsAccessMetrics {
830 MetricsAccessMetrics {
831 requests_total: self.metrics_scrape_requests.load(Ordering::Relaxed),
832 allowed_total: self.metrics_scrape_allowed.load(Ordering::Relaxed),
833 allowed_peer_total: self.metrics_scrape_allowed_peer.load(Ordering::Relaxed),
834 allowed_forwarded_trusted_proxy_total: self
835 .metrics_scrape_allowed_forwarded_trusted_proxy
836 .load(Ordering::Relaxed),
837 allowed_forwarded_without_peer_total: self
838 .metrics_scrape_allowed_forwarded_without_peer
839 .load(Ordering::Relaxed),
840 allowed_unavailable_total: self
841 .metrics_scrape_allowed_unavailable
842 .load(Ordering::Relaxed),
843 denied_ip_total: self.metrics_scrape_denied_ip.load(Ordering::Relaxed),
844 denied_ip_not_allowed_total: self
845 .metrics_scrape_denied_ip_not_allowed
846 .load(Ordering::Relaxed),
847 denied_missing_peer_ip_with_trusted_proxies_total: self
848 .metrics_scrape_denied_missing_peer_ip_with_trusted_proxies
849 .load(Ordering::Relaxed),
850 denied_auth_total: self.metrics_scrape_denied_auth.load(Ordering::Relaxed),
851 denied_auth_header_missing_total: self
852 .metrics_scrape_denied_auth_header_missing
853 .load(Ordering::Relaxed),
854 denied_auth_header_invalid_total: self
855 .metrics_scrape_denied_auth_header_invalid
856 .load(Ordering::Relaxed),
857 denied_auth_header_invalid_encoding_total: self
858 .metrics_scrape_denied_auth_header_invalid_encoding
859 .load(Ordering::Relaxed),
860 denied_auth_header_invalid_scheme_total: self
861 .metrics_scrape_denied_auth_header_invalid_scheme
862 .load(Ordering::Relaxed),
863 denied_auth_header_malformed_total: self
864 .metrics_scrape_denied_auth_header_malformed
865 .load(Ordering::Relaxed),
866 denied_auth_header_multiple_total: self
867 .metrics_scrape_denied_auth_header_multiple
868 .load(Ordering::Relaxed),
869 denied_auth_header_oversized_total: self
870 .metrics_scrape_denied_auth_header_oversized
871 .load(Ordering::Relaxed),
872 denied_auth_token_mismatch_total: self
873 .metrics_scrape_denied_auth_token_mismatch
874 .load(Ordering::Relaxed),
875 denied_forwarded_missing_total: self
876 .metrics_scrape_denied_forwarded_missing
877 .load(Ordering::Relaxed),
878 denied_forwarded_invalid_total: self
879 .metrics_scrape_denied_forwarded_invalid
880 .load(Ordering::Relaxed),
881 denied_forwarded_oversized_total: self
882 .metrics_scrape_denied_forwarded_oversized
883 .load(Ordering::Relaxed),
884 }
885 }
886
887 #[must_use]
889 pub const fn metrics_header_limits(&self) -> MetricsHeaderLimits {
890 self.metrics_header_limits
891 }
892
893 #[must_use]
895 pub const fn has_metrics_trusted_proxies(&self) -> bool {
896 self.metrics_trusted_proxies.is_some()
897 }
898
899 #[must_use]
901 pub fn is_metrics_trusted_proxy(&self, ip: IpAddr) -> bool {
902 self.metrics_trusted_proxies
903 .as_ref()
904 .is_some_and(|proxies| proxies.iter().any(|proxy| proxy.contains(ip)))
905 }
906
907 #[must_use]
909 pub fn is_metrics_ip_allowed(&self, ip: IpAddr) -> bool {
910 if !self.has_metrics_ip_allowlist() {
911 return true;
912 }
913
914 self.metrics_ip_allowlist.as_ref().is_some_and(|allowlist| allowlist.contains(&ip))
915 || self
916 .metrics_ip_cidr_allowlist
917 .as_ref()
918 .is_some_and(|cidrs| cidrs.iter().any(|cidr| cidr.contains(ip)))
919 }
920
921 #[must_use]
923 pub fn tenant_cache_metrics(&self) -> TenantCacheMetrics {
924 let (cached_dbs, in_use_cached_dbs) = self
925 .tenant_cache
926 .read()
927 .map(|cache| {
928 let cached = cache.len();
929 let in_use =
930 cache.values().filter(|entry| Arc::strong_count(&entry.commerce) > 1).count();
931 (cached, in_use)
932 })
933 .unwrap_or((0, 0));
934
935 TenantCacheMetrics {
936 enabled: self.tenant_db_dir.is_some(),
937 max_cached_dbs: self.max_tenant_dbs,
938 cached_dbs,
939 in_use_cached_dbs,
940 hits: self.tenant_cache_hits.load(Ordering::Relaxed),
941 misses: self.tenant_cache_misses.load(Ordering::Relaxed),
942 evictions: self.tenant_cache_evictions.load(Ordering::Relaxed),
943 rejections: self.tenant_cache_rejections.load(Ordering::Relaxed),
944 }
945 }
946
947 pub fn commerce_for_tenant(&self, tenant_id: Option<&str>) -> Result<Arc<Commerce>, HttpError> {
954 let Some(base_dir) = self.tenant_db_dir.as_deref() else {
955 let carries_tenant = tenant_id.map(str::trim).is_some_and(|value| !value.is_empty());
956 if carries_tenant && !self.ignore_tenant_header {
957 return Err(HttpError::BadRequest(
958 "tenant routing is not enabled on this deployment; remove the x-tenant-id \
959 header or configure a tenant database directory"
960 .to_string(),
961 ));
962 }
963 return Ok(Arc::clone(&self.commerce));
964 };
965
966 let tenant_id = tenant_id
967 .map(str::trim)
968 .filter(|value| !value.is_empty())
969 .ok_or_else(|| HttpError::BadRequest("missing x-tenant-id header".to_string()))?;
970
971 if !is_valid_tenant_id(tenant_id) {
972 return Err(HttpError::BadRequest("invalid x-tenant-id header".to_string()));
973 }
974
975 let access_tick = self.next_tenant_access_tick();
976 let mut cache = self.tenant_cache.write().map_err(|_| {
977 HttpError::InternalError("tenant cache lock poisoned while writing".to_string())
978 })?;
979 if let Some(existing) = cache.get_mut(tenant_id) {
980 existing.last_access_tick = access_tick;
981 self.tenant_cache_hits.fetch_add(1, Ordering::Relaxed);
982 return Ok(Arc::clone(&existing.commerce));
983 }
984 self.tenant_cache_misses.fetch_add(1, Ordering::Relaxed);
985 drop(cache);
986
987 std::fs::create_dir_all(base_dir).map_err(|error| {
988 HttpError::InternalError(format!(
989 "failed to create tenant database directory {}: {error}",
990 base_dir.display()
991 ))
992 })?;
993
994 let canonical_base = base_dir.canonicalize().map_err(|error| {
995 HttpError::InternalError(format!(
996 "failed to canonicalize tenant database directory {}: {error}",
997 base_dir.display()
998 ))
999 })?;
1000 let db_path = canonical_base.join(format!("{tenant_id}.db"));
1001 if db_path.parent() != Some(canonical_base.as_path())
1005 || !db_path.starts_with(&canonical_base)
1006 {
1007 return Err(HttpError::BadRequest("invalid x-tenant-id header".to_string()));
1008 }
1009 let db_path_str = db_path.to_string_lossy().into_owned();
1010 let created = Arc::new(Commerce::new(&db_path_str).map_err(|error| {
1011 HttpError::InternalError(format!(
1012 "failed to initialize tenant database for '{tenant_id}': {error}"
1013 ))
1014 })?);
1015
1016 let mut cache = self.tenant_cache.write().map_err(|_| {
1017 HttpError::InternalError("tenant cache lock poisoned while writing".to_string())
1018 })?;
1019 let access_tick = self.next_tenant_access_tick();
1020 if let Some(existing) = cache.get_mut(tenant_id) {
1021 existing.last_access_tick = access_tick;
1022 self.tenant_cache_hits.fetch_add(1, Ordering::Relaxed);
1023 return Ok(Arc::clone(&existing.commerce));
1024 }
1025 if cache.len() >= self.max_tenant_dbs {
1026 self.evict_lru_idle_tenant(&mut cache)?;
1027 }
1028 let entry = cache.entry(tenant_id.to_string()).or_insert_with(|| TenantCacheEntry {
1029 commerce: Arc::clone(&created),
1030 last_access_tick: access_tick,
1031 });
1032 Ok(Arc::clone(&entry.commerce))
1033 }
1034
1035 pub(crate) async fn run_blocking<F, T>(
1049 &self,
1050 tenant_id: Option<&str>,
1051 f: F,
1052 ) -> Result<T, HttpError>
1053 where
1054 F: FnOnce(&Commerce) -> Result<T, HttpError> + Send + 'static,
1055 T: Send + 'static,
1056 {
1057 let commerce = self.commerce_for_tenant(tenant_id)?;
1058 tokio::task::spawn_blocking(move || f(&commerce)).await.map_err(|join_error| {
1059 HttpError::InternalError(format!("blocking commerce task failed: {join_error}"))
1060 })?
1061 }
1062
1063 fn next_tenant_access_tick(&self) -> u64 {
1064 self.tenant_access_clock.fetch_add(1, Ordering::Relaxed)
1065 }
1066
1067 fn evict_lru_idle_tenant(
1068 &self,
1069 cache: &mut HashMap<String, TenantCacheEntry>,
1070 ) -> Result<(), HttpError> {
1071 let Some((tenant_id, _)) = cache
1072 .iter()
1073 .filter(|(_, entry)| Arc::strong_count(&entry.commerce) == 1)
1074 .min_by_key(|(_, entry)| entry.last_access_tick)
1075 .map(|(tenant_id, entry)| (tenant_id.clone(), entry.last_access_tick))
1076 else {
1077 self.tenant_cache_rejections.fetch_add(1, Ordering::Relaxed);
1078 return Err(HttpError::TooManyRequests(format!(
1079 "tenant database limit reached (max: {}); all cached tenant engines are currently in use",
1080 self.max_tenant_dbs
1081 )));
1082 };
1083
1084 cache.remove(&tenant_id);
1085 self.tenant_cache_evictions.fetch_add(1, Ordering::Relaxed);
1086 Ok(())
1087 }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::*;
1093 use stateset_core::CreateCustomer;
1094 use uuid::Uuid;
1095
1096 fn test_commerce() -> Commerce {
1097 Commerce::new(":memory:").expect("in-memory Commerce")
1098 }
1099
1100 #[test]
1101 fn tenant_id_validation_rejects_path_like_values() {
1102 assert!(is_valid_tenant_id("tenant-1"));
1103 assert!(is_valid_tenant_id("Tenant_2"));
1104 assert!(is_valid_tenant_id(&"a".repeat(64)));
1105
1106 assert!(!is_valid_tenant_id(""));
1107 assert!(!is_valid_tenant_id(" "));
1108 assert!(!is_valid_tenant_id(&"a".repeat(65)));
1109 assert!(!is_valid_tenant_id("."));
1110 assert!(!is_valid_tenant_id(".."));
1111 assert!(!is_valid_tenant_id("tenant.1"));
1112 assert!(!is_valid_tenant_id("../etc/passwd"));
1113 assert!(!is_valid_tenant_id("./tenant"));
1114 assert!(!is_valid_tenant_id("a/b"));
1115 assert!(!is_valid_tenant_id("a\\b"));
1116 assert!(!is_valid_tenant_id("/absolute"));
1117 }
1118
1119 #[test]
1120 fn tenant_header_without_tenant_routing_is_rejected() {
1121 let state = AppState::new(test_commerce());
1122 let err = state.commerce_for_tenant(Some("tenant-1")).unwrap_err();
1123 assert!(matches!(err, HttpError::BadRequest(_)));
1124 assert!(err.to_string().contains("tenant routing is not enabled"));
1125 }
1126
1127 #[test]
1128 fn missing_tenant_header_without_tenant_routing_uses_shared_engine() {
1129 let state = AppState::new(test_commerce());
1130 let shared = state.commerce_for_tenant(None).expect("shared engine");
1131 assert!(Arc::ptr_eq(&shared, &state.commerce));
1132 let blank = state.commerce_for_tenant(Some(" ")).expect("shared engine");
1134 assert!(Arc::ptr_eq(&blank, &state.commerce));
1135 }
1136
1137 #[test]
1138 fn ignore_tenant_header_escape_hatch_restores_fallthrough() {
1139 let state = AppState::new(test_commerce()).with_ignore_tenant_header();
1140 assert!(state.ignores_tenant_header());
1141 let shared = state.commerce_for_tenant(Some("tenant-1")).expect("shared engine");
1142 assert!(Arc::ptr_eq(&shared, &state.commerce));
1143 }
1144
1145 #[test]
1146 fn tenant_routing_rejects_traversal_attempts() {
1147 let base = std::env::temp_dir().join(format!("stateset-state-tenant-{}", Uuid::new_v4()));
1148 let state = AppState::new(test_commerce()).with_tenant_db_dir(&base);
1149 for candidate in ["..", ".", "../other", "./x", "/etc/passwd", "a/b", "a\\b", "a.b"] {
1150 let err = state.commerce_for_tenant(Some(candidate)).unwrap_err();
1151 assert!(matches!(err, HttpError::BadRequest(_)), "{candidate} should be rejected");
1152 }
1153 let engine = state.commerce_for_tenant(Some("tenant-ok")).expect("tenant engine");
1155 assert!(!Arc::ptr_eq(&engine, &state.commerce));
1156 assert!(base.join("tenant-ok.db").exists());
1157 let _ = std::fs::remove_dir_all(&base);
1158 }
1159
1160 #[test]
1161 fn state_debug_impl() {
1162 let state = AppState::new(test_commerce());
1163 let dbg = format!("{state:?}");
1164 assert!(dbg.contains("AppState"));
1165 }
1166
1167 #[test]
1168 fn state_clone() {
1169 let state = AppState::new(test_commerce());
1170 let cloned = state.clone();
1171 assert!(Arc::ptr_eq(&state.commerce, &cloned.commerce));
1173 }
1174
1175 #[test]
1176 fn state_accessor() {
1177 let state = AppState::new(test_commerce());
1178 let _commerce = state.commerce();
1180 }
1181
1182 #[test]
1183 fn metrics_auth_configuration_round_trip() {
1184 let state = AppState::new(test_commerce()).with_metrics_bearer_auth("metrics-token");
1185 assert_eq!(state.metrics_bearer_auth_token(), Some("metrics-token"));
1186
1187 let disabled = state.without_metrics_auth();
1188 assert_eq!(disabled.metrics_bearer_auth_token(), None);
1189 }
1190
1191 #[test]
1192 fn metrics_ip_allowlist_configuration_round_trip() {
1193 let state = AppState::new(test_commerce())
1194 .with_metrics_ip_allowlist(["127.0.0.1".parse().unwrap(), "10.0.0.1".parse().unwrap()]);
1195 assert_eq!(
1196 state.metrics_ip_allowlist().unwrap(),
1197 vec!["10.0.0.1".parse::<IpAddr>().unwrap(), "127.0.0.1".parse::<IpAddr>().unwrap()]
1198 );
1199 assert_eq!(state.metrics_ip_allowlist_len(), 2);
1200 assert_eq!(state.metrics_ip_cidr_allowlist_len(), 0);
1201 assert!(state.metrics_ip_cidr_allowlist().is_none());
1202 assert!(state.has_metrics_ip_allowlist());
1203 assert!(state.is_metrics_ip_allowed("127.0.0.1".parse().unwrap()));
1204 assert!(!state.is_metrics_ip_allowed("192.168.1.10".parse().unwrap()));
1205
1206 let disabled = state.without_metrics_ip_allowlist();
1207 assert!(disabled.metrics_ip_allowlist().is_none());
1208 assert_eq!(disabled.metrics_ip_allowlist_len(), 0);
1209 assert!(!disabled.has_metrics_ip_allowlist());
1210 assert!(disabled.is_metrics_ip_allowed("192.168.1.10".parse().unwrap()));
1211 }
1212
1213 #[test]
1214 fn metrics_ip_cidr_allowlist_configuration_round_trip() {
1215 let state = AppState::new(test_commerce()).with_metrics_ip_cidr_allowlist([
1216 "10.0.0.0/8".parse().unwrap(),
1217 "127.0.0.1".parse().unwrap(),
1218 ]);
1219 assert_eq!(
1220 state.metrics_ip_cidr_allowlist().unwrap(),
1221 vec!["10.0.0.0/8".parse().unwrap(), "127.0.0.1/32".parse().unwrap()]
1222 );
1223 assert_eq!(state.metrics_ip_allowlist_len(), 0);
1224 assert_eq!(state.metrics_ip_cidr_allowlist_len(), 2);
1225 assert!(state.metrics_ip_allowlist().is_none());
1226 assert!(state.has_metrics_ip_allowlist());
1227 assert!(state.is_metrics_ip_allowed("10.5.6.7".parse().unwrap()));
1228 assert!(state.is_metrics_ip_allowed("127.0.0.1".parse().unwrap()));
1229 assert!(!state.is_metrics_ip_allowed("203.0.113.10".parse().unwrap()));
1230
1231 let disabled = state.without_metrics_ip_cidr_allowlist();
1232 assert!(disabled.metrics_ip_cidr_allowlist().is_none());
1233 assert_eq!(disabled.metrics_ip_cidr_allowlist_len(), 0);
1234 assert!(!disabled.has_metrics_ip_allowlist());
1235 assert!(disabled.is_metrics_ip_allowed("203.0.113.10".parse().unwrap()));
1236 }
1237
1238 #[test]
1239 fn metrics_ip_allowlists_are_union_when_both_configured() {
1240 let state = AppState::new(test_commerce())
1241 .with_metrics_ip_allowlist(["203.0.113.10".parse().unwrap()])
1242 .with_metrics_ip_cidr_allowlist(["10.0.0.0/8".parse().unwrap()]);
1243
1244 assert!(state.is_metrics_ip_allowed("203.0.113.10".parse().unwrap()));
1245 assert!(state.is_metrics_ip_allowed("10.1.2.3".parse().unwrap()));
1246 assert!(!state.is_metrics_ip_allowed("192.168.1.1".parse().unwrap()));
1247 }
1248
1249 #[test]
1250 fn ip_cidr_parsing_and_contains() {
1251 let cidr = "10.0.0.0/8".parse::<IpCidr>().unwrap();
1252 assert!(cidr.contains("10.1.2.3".parse().unwrap()));
1253 assert!(!cidr.contains("11.1.2.3".parse().unwrap()));
1254
1255 let host = "127.0.0.1".parse::<IpCidr>().unwrap();
1256 assert_eq!(host.to_string(), "127.0.0.1/32");
1257 assert!(host.contains("127.0.0.1".parse().unwrap()));
1258 assert!(!host.contains("127.0.0.2".parse().unwrap()));
1259 }
1260
1261 #[test]
1262 fn metrics_trusted_proxies_configuration_round_trip() {
1263 let state = AppState::new(test_commerce()).with_metrics_trusted_proxies([
1264 "10.0.0.0/8".parse().unwrap(),
1265 "127.0.0.1".parse().unwrap(),
1266 ]);
1267
1268 let proxies = state.metrics_trusted_proxies().unwrap();
1269 assert_eq!(proxies.len(), 2);
1270 assert_eq!(state.metrics_trusted_proxies_len(), 2);
1271 assert!(state.has_metrics_trusted_proxies());
1272 assert!(state.is_metrics_trusted_proxy("10.9.8.7".parse().unwrap()));
1273 assert!(state.is_metrics_trusted_proxy("127.0.0.1".parse().unwrap()));
1274 assert!(!state.is_metrics_trusted_proxy("192.168.1.10".parse().unwrap()));
1275
1276 let disabled = state.without_metrics_trusted_proxies();
1277 assert!(disabled.metrics_trusted_proxies().is_none());
1278 assert_eq!(disabled.metrics_trusted_proxies_len(), 0);
1279 assert!(!disabled.has_metrics_trusted_proxies());
1280 assert!(!disabled.is_metrics_trusted_proxy("127.0.0.1".parse().unwrap()));
1281 }
1282
1283 #[test]
1284 fn metrics_header_limits_configuration_round_trip() {
1285 let limits = MetricsHeaderLimits::new_with_authorization(1024, 1536, 256, 768).unwrap();
1286 let state = AppState::new(test_commerce()).with_metrics_header_limits(limits);
1287 assert_eq!(state.metrics_header_limits(), limits);
1288 assert_eq!(state.metrics_header_limits().authorization_header_value_bytes(), 768);
1289 }
1290
1291 #[test]
1292 fn metrics_header_limits_reject_zero_values() {
1293 assert!(MetricsHeaderLimits::new(0, 1024, 256).is_err());
1294 assert!(MetricsHeaderLimits::new(1024, 0, 256).is_err());
1295 assert!(MetricsHeaderLimits::new(1024, 1024, 0).is_err());
1296 assert!(MetricsHeaderLimits::new_with_authorization(1024, 1024, 256, 0).is_err());
1297 }
1298
1299 #[test]
1300 fn metrics_access_metrics_track_counters() {
1301 let state = AppState::new(test_commerce());
1302 let initial = state.metrics_access_metrics();
1303 assert_eq!(initial.requests_total, 0);
1304 assert_eq!(initial.allowed_total, 0);
1305 assert_eq!(initial.allowed_peer_total, 0);
1306 assert_eq!(initial.allowed_forwarded_trusted_proxy_total, 0);
1307 assert_eq!(initial.allowed_forwarded_without_peer_total, 0);
1308 assert_eq!(initial.allowed_unavailable_total, 0);
1309 assert_eq!(initial.denied_ip_total, 0);
1310 assert_eq!(initial.denied_ip_not_allowed_total, 0);
1311 assert_eq!(initial.denied_missing_peer_ip_with_trusted_proxies_total, 0);
1312 assert_eq!(initial.denied_auth_total, 0);
1313 assert_eq!(initial.denied_auth_header_missing_total, 0);
1314 assert_eq!(initial.denied_auth_header_invalid_total, 0);
1315 assert_eq!(initial.denied_auth_header_invalid_encoding_total, 0);
1316 assert_eq!(initial.denied_auth_header_invalid_scheme_total, 0);
1317 assert_eq!(initial.denied_auth_header_malformed_total, 0);
1318 assert_eq!(initial.denied_auth_header_multiple_total, 0);
1319 assert_eq!(initial.denied_auth_header_oversized_total, 0);
1320 assert_eq!(initial.denied_auth_token_mismatch_total, 0);
1321 assert_eq!(initial.denied_forwarded_missing_total, 0);
1322 assert_eq!(initial.denied_forwarded_invalid_total, 0);
1323 assert_eq!(initial.denied_forwarded_oversized_total, 0);
1324
1325 state.record_metrics_scrape_attempt();
1326 state.record_metrics_scrape_denied_auth();
1327 state.record_metrics_scrape_denied_auth_header_missing();
1328 state.record_metrics_scrape_attempt();
1329 state.record_metrics_scrape_denied_ip();
1330 state.record_metrics_scrape_denied_ip_not_allowed();
1331 state.record_metrics_scrape_denied_forwarded_missing();
1332 state.record_metrics_scrape_attempt();
1333 state.record_metrics_scrape_denied_auth();
1334 state.record_metrics_scrape_denied_auth_header_invalid();
1335 state.record_metrics_scrape_denied_auth_header_invalid_encoding();
1336 state.record_metrics_scrape_denied_auth_header_invalid();
1337 state.record_metrics_scrape_denied_auth_header_invalid_scheme();
1338 state.record_metrics_scrape_denied_auth_header_invalid();
1339 state.record_metrics_scrape_denied_auth_header_malformed();
1340 state.record_metrics_scrape_attempt();
1341 state.record_metrics_scrape_denied_auth();
1342 state.record_metrics_scrape_denied_auth_header_multiple();
1343 state.record_metrics_scrape_attempt();
1344 state.record_metrics_scrape_denied_auth();
1345 state.record_metrics_scrape_denied_auth_header_oversized();
1346 state.record_metrics_scrape_attempt();
1347 state.record_metrics_scrape_denied_auth();
1348 state.record_metrics_scrape_denied_auth_token_mismatch();
1349 state.record_metrics_scrape_attempt();
1350 state.record_metrics_scrape_denied_ip();
1351 state.record_metrics_scrape_denied_forwarded_invalid();
1352 state.record_metrics_scrape_attempt();
1353 state.record_metrics_scrape_denied_ip();
1354 state.record_metrics_scrape_denied_missing_peer_ip_with_trusted_proxies();
1355 state.record_metrics_scrape_denied_forwarded_oversized();
1356 state.record_metrics_scrape_attempt();
1357 state.record_metrics_scrape_allowed();
1358 state.record_metrics_scrape_allowed_peer();
1359 state.record_metrics_scrape_allowed_forwarded_trusted_proxy();
1360 state.record_metrics_scrape_allowed_forwarded_without_peer();
1361 state.record_metrics_scrape_allowed_unavailable();
1362
1363 let snapshot = state.metrics_access_metrics();
1364 assert_eq!(snapshot.requests_total, 9);
1365 assert_eq!(snapshot.allowed_total, 1);
1366 assert_eq!(snapshot.allowed_peer_total, 1);
1367 assert_eq!(snapshot.allowed_forwarded_trusted_proxy_total, 1);
1368 assert_eq!(snapshot.allowed_forwarded_without_peer_total, 1);
1369 assert_eq!(snapshot.allowed_unavailable_total, 1);
1370 assert_eq!(snapshot.denied_ip_total, 3);
1371 assert_eq!(snapshot.denied_ip_not_allowed_total, 1);
1372 assert_eq!(snapshot.denied_missing_peer_ip_with_trusted_proxies_total, 1);
1373 assert_eq!(snapshot.denied_auth_total, 5);
1374 assert_eq!(snapshot.denied_auth_header_missing_total, 1);
1375 assert_eq!(snapshot.denied_auth_header_invalid_total, 3);
1376 assert_eq!(snapshot.denied_auth_header_invalid_encoding_total, 1);
1377 assert_eq!(snapshot.denied_auth_header_invalid_scheme_total, 1);
1378 assert_eq!(snapshot.denied_auth_header_malformed_total, 1);
1379 assert_eq!(snapshot.denied_auth_header_multiple_total, 1);
1380 assert_eq!(snapshot.denied_auth_header_oversized_total, 1);
1381 assert_eq!(snapshot.denied_auth_token_mismatch_total, 1);
1382 assert_eq!(snapshot.denied_forwarded_missing_total, 1);
1383 assert_eq!(snapshot.denied_forwarded_invalid_total, 1);
1384 assert_eq!(snapshot.denied_forwarded_oversized_total, 1);
1385 }
1386
1387 #[test]
1388 fn tenant_header_parser_reads_valid_value() {
1389 let mut headers = HeaderMap::new();
1390 headers.insert(X_TENANT_ID.clone(), "tenant-1".parse().unwrap());
1391 assert_eq!(tenant_id_from_headers(&headers).as_deref(), Some("tenant-1"));
1392 }
1393
1394 #[test]
1395 fn tenant_header_parser_rejects_empty_values() {
1396 let mut headers = HeaderMap::new();
1397 headers.insert(X_TENANT_ID.clone(), " ".parse().unwrap());
1398 assert!(tenant_id_from_headers(&headers).is_none());
1399 }
1400
1401 #[test]
1402 fn tenant_routing_isolated_when_configured() {
1403 let tenant_dir =
1404 std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1405 let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()));
1406
1407 let tenant_a = state.commerce_for_tenant(Some("tenant-a")).expect("tenant-a commerce");
1408 let tenant_b = state.commerce_for_tenant(Some("tenant-b")).expect("tenant-b commerce");
1409
1410 tenant_a
1411 .customers()
1412 .create(CreateCustomer {
1413 email: "tenant-a@example.com".into(),
1414 first_name: "Tenant".into(),
1415 last_name: "A".into(),
1416 ..Default::default()
1417 })
1418 .unwrap();
1419
1420 let a_count = tenant_a.customers().list(Default::default()).unwrap().len();
1421 let b_count = tenant_b.customers().list(Default::default()).unwrap().len();
1422 assert_eq!(a_count, 1);
1423 assert_eq!(b_count, 0);
1424
1425 let _ = std::fs::remove_dir_all(tenant_dir);
1426 }
1427
1428 #[test]
1429 fn tenant_routing_requires_header_when_enabled() {
1430 let tenant_dir =
1431 std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1432 let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()));
1433
1434 let err = state.commerce_for_tenant(None).expect_err("expected missing tenant error");
1435 assert!(matches!(err, HttpError::BadRequest(_)));
1436
1437 let _ = std::fs::remove_dir_all(tenant_dir);
1438 }
1439
1440 #[test]
1441 fn tenant_routing_enforces_database_limit() {
1442 let tenant_dir =
1443 std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1444 let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()))
1445 .with_max_tenant_dbs(1);
1446
1447 let first = state.commerce_for_tenant(Some("tenant-a"));
1448 assert!(first.is_ok());
1449
1450 let second = state.commerce_for_tenant(Some("tenant-b"));
1451 assert!(matches!(second, Err(HttpError::TooManyRequests(_))));
1452
1453 let _ = std::fs::remove_dir_all(tenant_dir);
1454 }
1455
1456 #[test]
1457 fn tenant_routing_evicts_idle_lru_database_and_reopens_from_disk() {
1458 let tenant_dir =
1459 std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1460 let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()))
1461 .with_max_tenant_dbs(1);
1462
1463 let tenant_a = state.commerce_for_tenant(Some("tenant-a")).unwrap();
1464 tenant_a
1465 .customers()
1466 .create(CreateCustomer {
1467 email: "tenant-a@example.com".into(),
1468 first_name: "Tenant".into(),
1469 last_name: "A".into(),
1470 ..Default::default()
1471 })
1472 .unwrap();
1473 drop(tenant_a);
1474
1475 assert!(state.commerce_for_tenant(Some("tenant-b")).is_ok());
1476
1477 let tenant_a_reloaded = state.commerce_for_tenant(Some("tenant-a")).unwrap();
1478 let tenant_a_customer_count =
1479 tenant_a_reloaded.customers().list(Default::default()).unwrap().len();
1480 assert_eq!(tenant_a_customer_count, 1);
1481
1482 let _ = std::fs::remove_dir_all(tenant_dir);
1483 }
1484
1485 #[test]
1486 fn tenant_cache_metrics_track_hits_misses_evictions_and_rejections() {
1487 let tenant_dir =
1488 std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1489 let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()))
1490 .with_max_tenant_dbs(1);
1491
1492 let tenant_a = state.commerce_for_tenant(Some("tenant-a")).unwrap();
1493 let tenant_a_again = state.commerce_for_tenant(Some("tenant-a")).unwrap();
1494 drop(tenant_a_again);
1495
1496 let tenant_b_while_a_in_use = state.commerce_for_tenant(Some("tenant-b"));
1497 assert!(matches!(tenant_b_while_a_in_use, Err(HttpError::TooManyRequests(_))));
1498
1499 drop(tenant_a);
1500 let tenant_b_after_release = state.commerce_for_tenant(Some("tenant-b"));
1501 assert!(tenant_b_after_release.is_ok());
1502
1503 let metrics = state.tenant_cache_metrics();
1504 assert!(metrics.enabled);
1505 assert_eq!(metrics.max_cached_dbs, 1);
1506 assert_eq!(metrics.cached_dbs, 1);
1507 assert_eq!(metrics.hits, 1);
1508 assert_eq!(metrics.misses, 3);
1509 assert_eq!(metrics.evictions, 1);
1510 assert_eq!(metrics.rejections, 1);
1511
1512 let _ = std::fs::remove_dir_all(tenant_dir);
1513 }
1514}