1use std::{
4 collections::HashSet,
5 fmt,
6 net::{IpAddr, SocketAddr},
7 path::PathBuf,
8};
9
10use axum::Router;
11use stateset_authz::AuthzEngine;
12use stateset_embedded::Commerce;
13use uuid::Uuid;
14
15use crate::error::HttpError;
16use crate::middleware::{self, AuthzConfig, BearerAuthBinding, RateLimitConfig};
17use crate::routes::{self, DEFAULT_REQUEST_BODY_LIMIT_BYTES};
18use crate::state::{AppState, IpCidr, MetricsHeaderLimits};
19
20const DEFAULT_ADDR: ([u8; 4], u16) = ([127, 0, 0, 1], 3000);
22const METRICS_IP_ALLOWLIST_ENV: &str = "STATESET_HTTP_METRICS_IP_ALLOWLIST";
23const METRICS_IP_CIDR_ALLOWLIST_ENV: &str = "STATESET_HTTP_METRICS_IP_CIDR_ALLOWLIST";
24const METRICS_TRUSTED_PROXIES_ENV: &str = "STATESET_HTTP_METRICS_TRUSTED_PROXIES";
25const METRICS_FORWARDED_HEADER_MAX_BYTES_ENV: &str = "STATESET_HTTP_METRICS_FORWARDED_MAX_BYTES";
26const METRICS_X_FORWARDED_FOR_HEADER_MAX_BYTES_ENV: &str =
27 "STATESET_HTTP_METRICS_X_FORWARDED_FOR_MAX_BYTES";
28const METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV: &str = "STATESET_HTTP_METRICS_X_REAL_IP_MAX_BYTES";
29const METRICS_AUTHORIZATION_HEADER_MAX_BYTES_ENV: &str =
30 "STATESET_HTTP_METRICS_AUTHORIZATION_MAX_BYTES";
31const REQUEST_BODY_MAX_BYTES_ENV: &str = "STATESET_HTTP_MAX_BODY_BYTES";
32const ALLOW_UNAUTHENTICATED_ENV: &str = "STATESET_HTTP_ALLOW_UNAUTHENTICATED";
33const REQUIRE_IDEMPOTENCY_KEYS_ENV: &str = "STATESET_HTTP_REQUIRE_IDEMPOTENCY_KEYS";
34
35#[derive(Clone, Debug, PartialEq, Eq)]
36struct AdditionalApiBearerBinding {
37 token: String,
38 bound_tenant_id: Option<String>,
39 bound_actor_id: Option<String>,
40}
41
42impl AdditionalApiBearerBinding {
43 const fn new(
44 token: String,
45 bound_tenant_id: Option<String>,
46 bound_actor_id: Option<String>,
47 ) -> Self {
48 Self { token, bound_tenant_id, bound_actor_id }
49 }
50}
51
52fn parse_ip_allowlist_csv(env_var: &str, value: &str) -> Result<Vec<IpAddr>, HttpError> {
53 let mut ips = value
54 .split(',')
55 .map(str::trim)
56 .filter(|entry| !entry.is_empty())
57 .map(|entry| {
58 entry.parse::<IpAddr>().map_err(|error| {
59 HttpError::BadRequest(format!("invalid IP '{entry}' in {env_var}: {error}"))
60 })
61 })
62 .collect::<Result<Vec<_>, _>>()?;
63 ips.sort_unstable();
64 ips.dedup();
65 Ok(ips)
66}
67
68fn parse_ip_cidr_allowlist_csv(env_var: &str, value: &str) -> Result<Vec<IpCidr>, HttpError> {
69 let mut cidrs = value
70 .split(',')
71 .map(str::trim)
72 .filter(|entry| !entry.is_empty())
73 .map(|entry| {
74 entry.parse::<IpCidr>().map_err(|error| {
75 HttpError::BadRequest(format!("invalid CIDR '{entry}' in {env_var}: {error}"))
76 })
77 })
78 .collect::<Result<Vec<_>, _>>()?;
79 cidrs.sort_unstable();
80 cidrs.dedup();
81 Ok(cidrs)
82}
83
84fn parse_positive_usize_env(env_var: &str, value: &str) -> Result<usize, HttpError> {
85 let trimmed = value.trim();
86 if trimmed.is_empty() {
87 return Err(HttpError::BadRequest(format!(
88 "{env_var} must be a positive integer greater than zero"
89 )));
90 }
91 let parsed = trimmed.parse::<usize>().map_err(|error| {
92 HttpError::BadRequest(format!("invalid value '{trimmed}' in {env_var}: {error}"))
93 })?;
94 if parsed == 0 {
95 return Err(HttpError::BadRequest(format!("{env_var} must be greater than zero")));
96 }
97 Ok(parsed)
98}
99
100fn parse_bool_env(env_var: &str, value: &str) -> Result<bool, HttpError> {
101 match value.trim().to_ascii_lowercase().as_str() {
102 "1" | "true" | "yes" => Ok(true),
103 "0" | "false" | "no" | "" => Ok(false),
104 other => Err(HttpError::BadRequest(format!(
105 "invalid value '{other}' in {env_var}: expected true/false"
106 ))),
107 }
108}
109
110pub struct ServerBuilder {
129 state: AppState,
130 addr: SocketAddr,
131 enable_cors: bool,
132 enable_request_id: bool,
133 api_bearer_token: Option<String>,
134 bound_tenant_id: Option<String>,
135 bound_actor_id: Option<String>,
136 additional_api_bearer_bindings: Vec<AdditionalApiBearerBinding>,
137 generated_default_token: bool,
138 max_request_body_bytes: usize,
139 authz_config: Option<AuthzConfig>,
140 trust_actor_headers_for_authz: bool,
141 authz_strict: bool,
142 allow_unauthenticated: bool,
143 rate_limit: Option<RateLimitConfig>,
144 require_idempotency_keys: bool,
145}
146
147impl fmt::Debug for ServerBuilder {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 f.debug_struct("ServerBuilder")
150 .field("state", &"AppState { .. }")
151 .field("addr", &self.addr)
152 .field("enable_cors", &self.enable_cors)
153 .field("enable_request_id", &self.enable_request_id)
154 .field("api_bearer_token", &self.api_bearer_token.as_ref().map(|_| "<redacted>"))
155 .field(
156 "metrics_bearer_token",
157 &self.state.metrics_bearer_auth_token().map(|_| "<redacted>"),
158 )
159 .field("bound_tenant_id", &self.bound_tenant_id.as_ref().map(|_| "<redacted>"))
160 .field("bound_actor_id", &self.bound_actor_id.as_ref().map(|_| "<redacted>"))
161 .field("additional_api_bearer_bindings", &self.additional_api_bearer_bindings.len())
162 .field("generated_default_token", &self.generated_default_token)
163 .field("max_request_body_bytes", &self.max_request_body_bytes)
164 .field("authz_enabled", &self.authz_config.is_some())
165 .field("trust_actor_headers_for_authz", &self.trust_actor_headers_for_authz)
166 .field("authz_strict", &self.authz_strict)
167 .field("allow_unauthenticated", &self.allow_unauthenticated)
168 .field("rate_limit", &self.rate_limit)
169 .field("require_idempotency_keys", &self.require_idempotency_keys)
170 .finish()
171 }
172}
173
174impl ServerBuilder {
175 fn api_bearer_bindings(&self) -> Vec<AdditionalApiBearerBinding> {
176 let mut bindings = Vec::with_capacity(
177 usize::from(self.api_bearer_token.is_some())
178 + self.additional_api_bearer_bindings.len(),
179 );
180 if let Some(token) = self.api_bearer_token.clone() {
181 bindings.push(AdditionalApiBearerBinding::new(
182 token,
183 self.bound_tenant_id.clone(),
184 self.bound_actor_id.clone(),
185 ));
186 }
187 bindings.extend(self.additional_api_bearer_bindings.iter().cloned());
188 bindings
189 }
190
191 fn tenant_routing_auth_error(&self) -> Option<&'static str> {
192 self.state.tenant_db_dir()?;
193 let bindings = self.api_bearer_bindings();
194 if bindings.is_empty() {
195 return Some("per-tenant database routing requires API auth to remain enabled");
196 }
197 if bindings.iter().any(|binding| binding.bound_tenant_id.is_none()) {
198 return Some(
199 "per-tenant database routing requires binding every API bearer token to a single tenant",
200 );
201 }
202 None
203 }
204
205 fn api_auth_error(&self) -> Option<String> {
206 if let Some(message) = self.tenant_routing_auth_error() {
207 return Some(message.to_string());
208 }
209 let bindings = self.api_bearer_bindings();
210 if bindings.is_empty() {
211 if self.authz_config.is_some() && !self.trust_actor_headers_for_authz {
212 return Some(
213 "request authorization requires actor-bound API authentication or explicitly trusted x-actor-id headers"
214 .to_string(),
215 );
216 }
217 if self.bound_actor_id.is_some() {
218 return Some(
219 "actor-bound API authentication requires API auth to remain enabled"
220 .to_string(),
221 );
222 }
223 return None;
224 }
225
226 let mut seen_tokens = HashSet::with_capacity(bindings.len());
227 for binding in &bindings {
228 if !seen_tokens.insert(binding.token.clone()) {
229 return Some("duplicate API bearer tokens are not allowed".to_string());
230 }
231 if let Some(bound_actor_id) = binding.bound_actor_id.as_deref() {
232 if !middleware::is_valid_actor_id(bound_actor_id) {
233 return Some("bound actor ID is invalid".to_string());
234 }
235 }
236 }
237 if self.authz_config.is_some()
238 && !self.trust_actor_headers_for_authz
239 && bindings.iter().any(|binding| binding.bound_actor_id.is_none())
240 {
241 return Some(
242 "request authorization requires binding every API bearer token to a single actor or explicitly trusting x-actor-id headers"
243 .to_string(),
244 );
245 }
246 None
247 }
248
249 #[must_use]
251 pub fn new(commerce: Commerce) -> Self {
252 let generated_token = Uuid::new_v4().to_string();
253 Self {
254 state: AppState::new(commerce).with_metrics_bearer_auth(generated_token.clone()),
255 addr: SocketAddr::from(DEFAULT_ADDR),
256 enable_cors: false,
257 enable_request_id: false,
258 api_bearer_token: Some(generated_token),
261 bound_tenant_id: None,
262 bound_actor_id: None,
263 additional_api_bearer_bindings: Vec::new(),
264 generated_default_token: true,
265 max_request_body_bytes: DEFAULT_REQUEST_BODY_LIMIT_BYTES,
266 authz_config: None,
267 trust_actor_headers_for_authz: false,
268 authz_strict: false,
269 allow_unauthenticated: false,
270 rate_limit: None,
271 require_idempotency_keys: true,
274 }
275 }
276
277 pub fn new_from_env(commerce: Commerce) -> Result<Self, HttpError> {
289 Self::new(commerce)
290 .with_metrics_network_policy_from_env()?
291 .with_metrics_header_limits_from_env()?
292 .with_request_body_limit_from_env()?
293 .with_allow_unauthenticated_from_env()?
294 .with_require_idempotency_keys_from_env()
295 }
296
297 pub fn with_allow_unauthenticated_from_env(mut self) -> Result<Self, HttpError> {
302 if let Ok(raw) = std::env::var(ALLOW_UNAUTHENTICATED_ENV) {
303 self.allow_unauthenticated = parse_bool_env(ALLOW_UNAUTHENTICATED_ENV, &raw)?;
304 }
305 Ok(self)
306 }
307
308 #[must_use]
314 pub const fn require_idempotency_keys(mut self, require: bool) -> Self {
315 self.require_idempotency_keys = require;
316 self
317 }
318
319 pub fn with_require_idempotency_keys_from_env(self) -> Result<Self, HttpError> {
325 let raw = std::env::var(REQUIRE_IDEMPOTENCY_KEYS_ENV).ok();
326 self.with_require_idempotency_keys_from_value(raw.as_deref())
327 }
328
329 fn with_require_idempotency_keys_from_value(
331 mut self,
332 raw: Option<&str>,
333 ) -> Result<Self, HttpError> {
334 if let Some(raw) = raw {
335 self.require_idempotency_keys = parse_bool_env(REQUIRE_IDEMPOTENCY_KEYS_ENV, raw)?;
336 }
337 Ok(self)
338 }
339
340 #[must_use]
342 pub const fn bind(mut self, addr: SocketAddr) -> Self {
343 self.addr = addr;
344 self
345 }
346
347 #[must_use]
352 pub const fn with_cors(mut self) -> Self {
353 self.enable_cors = true;
354 self
355 }
356
357 #[must_use]
359 pub const fn with_request_id(mut self) -> Self {
360 self.enable_request_id = true;
361 self
362 }
363
364 #[must_use]
368 pub fn with_bearer_auth(mut self, token: impl Into<String>) -> Self {
369 self.api_bearer_token = Some(token.into());
370 self.generated_default_token = false;
371 self
372 }
373
374 fn push_bearer_auth_binding(
375 &mut self,
376 token: impl Into<String>,
377 bound_tenant_id: Option<String>,
378 bound_actor_id: Option<String>,
379 ) {
380 self.additional_api_bearer_bindings.push(AdditionalApiBearerBinding::new(
381 token.into(),
382 bound_tenant_id,
383 bound_actor_id,
384 ));
385 }
386
387 #[must_use]
389 pub fn with_metrics_bearer_auth(mut self, token: impl Into<String>) -> Self {
390 self.state = self.state.with_metrics_bearer_auth(token);
391 self
392 }
393
394 #[must_use]
396 pub fn without_metrics_auth(mut self) -> Self {
397 self.state = self.state.without_metrics_auth();
398 self
399 }
400
401 #[must_use]
403 pub fn with_metrics_ip_allowlist<I>(mut self, ips: I) -> Self
404 where
405 I: IntoIterator<Item = IpAddr>,
406 {
407 self.state = self.state.with_metrics_ip_allowlist(ips);
408 self
409 }
410
411 #[must_use]
413 pub fn without_metrics_ip_allowlist(mut self) -> Self {
414 self.state = self.state.without_metrics_ip_allowlist();
415 self
416 }
417
418 #[must_use]
420 pub fn with_metrics_ip_cidr_allowlist<I>(mut self, cidrs: I) -> Self
421 where
422 I: IntoIterator<Item = IpCidr>,
423 {
424 self.state = self.state.with_metrics_ip_cidr_allowlist(cidrs);
425 self
426 }
427
428 #[must_use]
430 pub fn without_metrics_ip_cidr_allowlist(mut self) -> Self {
431 self.state = self.state.without_metrics_ip_cidr_allowlist();
432 self
433 }
434
435 #[must_use]
437 pub fn with_metrics_trusted_proxies<I>(mut self, cidrs: I) -> Self
438 where
439 I: IntoIterator<Item = IpCidr>,
440 {
441 self.state = self.state.with_metrics_trusted_proxies(cidrs);
442 self
443 }
444
445 #[must_use]
447 pub fn with_metrics_header_limits(mut self, limits: MetricsHeaderLimits) -> Self {
448 self.state = self.state.with_metrics_header_limits(limits);
449 self
450 }
451
452 pub fn with_metrics_network_policy_from_env(self) -> Result<Self, HttpError> {
461 let ip_allowlist = std::env::var(METRICS_IP_ALLOWLIST_ENV).ok();
462 let ip_cidr_allowlist = std::env::var(METRICS_IP_CIDR_ALLOWLIST_ENV).ok();
463 let trusted_proxies = std::env::var(METRICS_TRUSTED_PROXIES_ENV).ok();
464 self.with_metrics_network_policy_from_values(
465 ip_allowlist.as_deref(),
466 ip_cidr_allowlist.as_deref(),
467 trusted_proxies.as_deref(),
468 )
469 }
470
471 pub fn with_metrics_header_limits_from_env(self) -> Result<Self, HttpError> {
479 let forwarded = std::env::var(METRICS_FORWARDED_HEADER_MAX_BYTES_ENV).ok();
480 let x_forwarded_for = std::env::var(METRICS_X_FORWARDED_FOR_HEADER_MAX_BYTES_ENV).ok();
481 let x_real_ip = std::env::var(METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV).ok();
482 let authorization = std::env::var(METRICS_AUTHORIZATION_HEADER_MAX_BYTES_ENV).ok();
483 self.with_metrics_header_limits_from_values(
484 forwarded.as_deref(),
485 x_forwarded_for.as_deref(),
486 x_real_ip.as_deref(),
487 authorization.as_deref(),
488 )
489 }
490
491 fn with_metrics_network_policy_from_values(
492 mut self,
493 ip_allowlist: Option<&str>,
494 ip_cidr_allowlist: Option<&str>,
495 trusted_proxies: Option<&str>,
496 ) -> Result<Self, HttpError> {
497 if let Some(raw) = ip_allowlist {
498 let ips = parse_ip_allowlist_csv(METRICS_IP_ALLOWLIST_ENV, raw)?;
499 self = if ips.is_empty() {
500 self.without_metrics_ip_allowlist()
501 } else {
502 self.with_metrics_ip_allowlist(ips)
503 };
504 }
505
506 if let Some(raw) = ip_cidr_allowlist {
507 let cidrs = parse_ip_cidr_allowlist_csv(METRICS_IP_CIDR_ALLOWLIST_ENV, raw)?;
508 self = if cidrs.is_empty() {
509 self.without_metrics_ip_cidr_allowlist()
510 } else {
511 self.with_metrics_ip_cidr_allowlist(cidrs)
512 };
513 }
514
515 if let Some(raw) = trusted_proxies {
516 let cidrs = parse_ip_cidr_allowlist_csv(METRICS_TRUSTED_PROXIES_ENV, raw)?;
517 self = if cidrs.is_empty() {
518 self.without_metrics_trusted_proxies()
519 } else {
520 self.with_metrics_trusted_proxies(cidrs)
521 };
522 }
523
524 Ok(self)
525 }
526
527 fn with_metrics_header_limits_from_values(
528 self,
529 forwarded: Option<&str>,
530 x_forwarded_for: Option<&str>,
531 x_real_ip: Option<&str>,
532 authorization: Option<&str>,
533 ) -> Result<Self, HttpError> {
534 let current = self.metrics_header_limits();
535 let forwarded = if let Some(raw) = forwarded {
536 parse_positive_usize_env(METRICS_FORWARDED_HEADER_MAX_BYTES_ENV, raw)?
537 } else {
538 current.forwarded_header_value_bytes()
539 };
540 let x_forwarded_for = if let Some(raw) = x_forwarded_for {
541 parse_positive_usize_env(METRICS_X_FORWARDED_FOR_HEADER_MAX_BYTES_ENV, raw)?
542 } else {
543 current.x_forwarded_for_header_value_bytes()
544 };
545 let x_real_ip = if let Some(raw) = x_real_ip {
546 parse_positive_usize_env(METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV, raw)?
547 } else {
548 current.x_real_ip_header_value_bytes()
549 };
550 let authorization = if let Some(raw) = authorization {
551 parse_positive_usize_env(METRICS_AUTHORIZATION_HEADER_MAX_BYTES_ENV, raw)?
552 } else {
553 current.authorization_header_value_bytes()
554 };
555 let limits = MetricsHeaderLimits::new_with_authorization(
556 forwarded,
557 x_forwarded_for,
558 x_real_ip,
559 authorization,
560 )?;
561 Ok(self.with_metrics_header_limits(limits))
562 }
563
564 pub fn with_request_body_limit_from_env(self) -> Result<Self, HttpError> {
566 let max_body_bytes = std::env::var(REQUEST_BODY_MAX_BYTES_ENV).ok();
567 self.with_request_body_limit_from_value(max_body_bytes.as_deref())
568 }
569
570 fn with_request_body_limit_from_value(
571 mut self,
572 max_body_bytes: Option<&str>,
573 ) -> Result<Self, HttpError> {
574 if let Some(raw) = max_body_bytes {
575 self.max_request_body_bytes =
576 parse_positive_usize_env(REQUEST_BODY_MAX_BYTES_ENV, raw)?;
577 }
578 Ok(self)
579 }
580
581 #[must_use]
583 pub fn without_metrics_trusted_proxies(mut self) -> Self {
584 self.state = self.state.without_metrics_trusted_proxies();
585 self
586 }
587
588 #[must_use]
592 pub fn bind_auth_tenant(mut self, tenant_id: impl Into<String>) -> Self {
593 self.bound_tenant_id = Some(tenant_id.into());
594 self
595 }
596
597 #[must_use]
602 pub fn bind_auth_actor(mut self, actor_id: impl Into<String>) -> Self {
603 self.bound_actor_id = Some(actor_id.into());
604 self
605 }
606
607 #[must_use]
609 pub fn with_bearer_auth_for_tenant(
610 self,
611 token: impl Into<String>,
612 tenant_id: impl Into<String>,
613 ) -> Self {
614 self.with_bearer_auth(token).bind_auth_tenant(tenant_id)
615 }
616
617 #[must_use]
619 pub fn with_bearer_auth_for_actor(
620 self,
621 token: impl Into<String>,
622 actor_id: impl Into<String>,
623 ) -> Self {
624 self.with_bearer_auth(token).bind_auth_actor(actor_id)
625 }
626
627 #[must_use]
631 pub fn add_bearer_auth(mut self, token: impl Into<String>) -> Self {
632 self.push_bearer_auth_binding(token, None, None);
633 self
634 }
635
636 #[must_use]
638 pub fn add_bearer_auth_for_tenant(
639 mut self,
640 token: impl Into<String>,
641 tenant_id: impl Into<String>,
642 ) -> Self {
643 self.push_bearer_auth_binding(token, Some(tenant_id.into()), None);
644 self
645 }
646
647 #[must_use]
649 pub fn add_bearer_auth_for_actor(
650 mut self,
651 token: impl Into<String>,
652 actor_id: impl Into<String>,
653 ) -> Self {
654 self.push_bearer_auth_binding(token, None, Some(actor_id.into()));
655 self
656 }
657
658 #[must_use]
660 pub fn add_bearer_auth_for_actor_and_tenant(
661 mut self,
662 token: impl Into<String>,
663 actor_id: impl Into<String>,
664 tenant_id: impl Into<String>,
665 ) -> Self {
666 self.push_bearer_auth_binding(token, Some(tenant_id.into()), Some(actor_id.into()));
667 self
668 }
669
670 #[must_use]
672 pub fn with_tenant_db_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
673 self.state = self.state.with_tenant_db_dir(base_dir);
674 self
675 }
676
677 #[must_use]
684 pub fn with_ignore_tenant_header(mut self) -> Self {
685 self.state = self.state.with_ignore_tenant_header();
686 self
687 }
688
689 #[must_use]
691 pub fn with_max_tenant_dbs(mut self, max_tenant_dbs: usize) -> Self {
692 self.state = self.state.with_max_tenant_dbs(max_tenant_dbs);
693 self
694 }
695
696 #[must_use]
698 pub const fn with_max_request_body_bytes(mut self, max_request_body_bytes: usize) -> Self {
699 self.max_request_body_bytes =
700 if max_request_body_bytes == 0 { 1 } else { max_request_body_bytes };
701 self
702 }
703
704 #[must_use]
710 pub fn with_authz_engine(mut self, engine: AuthzEngine) -> Self {
711 self.authz_config = Some(AuthzConfig::new(engine));
712 self
713 }
714
715 #[must_use]
720 pub const fn trust_actor_headers_for_authz(mut self) -> Self {
721 self.trust_actor_headers_for_authz = true;
722 self
723 }
724
725 #[must_use]
732 pub const fn allow_unauthenticated(mut self) -> Self {
733 self.allow_unauthenticated = true;
734 self
735 }
736
737 #[must_use]
745 pub const fn with_strict_authz(mut self) -> Self {
746 self.authz_strict = true;
747 self
748 }
749
750 #[must_use]
752 pub fn without_auth(mut self) -> Self {
753 self.api_bearer_token = None;
754 self.bound_tenant_id = None;
755 self.bound_actor_id = None;
756 self.additional_api_bearer_bindings.clear();
757 self.generated_default_token = false;
758 self
759 }
760
761 #[must_use]
767 pub const fn with_rate_limit(mut self, requests_per_second: u64, burst_size: u64) -> Self {
768 self.rate_limit = Some(RateLimitConfig { requests_per_second, burst_size });
769 self
770 }
771
772 #[must_use]
774 pub fn bearer_auth_token(&self) -> Option<&str> {
775 self.api_bearer_token.as_deref()
776 }
777
778 #[must_use]
780 pub fn metrics_bearer_auth_token(&self) -> Option<&str> {
781 self.state.metrics_bearer_auth_token()
782 }
783
784 #[must_use]
786 pub fn metrics_ip_allowlist(&self) -> Option<Vec<IpAddr>> {
787 self.state.metrics_ip_allowlist()
788 }
789
790 #[must_use]
792 pub fn metrics_ip_cidr_allowlist(&self) -> Option<Vec<IpCidr>> {
793 self.state.metrics_ip_cidr_allowlist()
794 }
795
796 #[must_use]
798 pub fn metrics_trusted_proxies(&self) -> Option<Vec<IpCidr>> {
799 self.state.metrics_trusted_proxies()
800 }
801
802 #[must_use]
804 pub const fn metrics_header_limits(&self) -> MetricsHeaderLimits {
805 self.state.metrics_header_limits()
806 }
807
808 pub fn build(self) -> Router {
812 let misconfigured_auth_message = self.api_auth_error();
813 let auth_bindings = self
814 .api_bearer_bindings()
815 .into_iter()
816 .map(|binding| {
817 BearerAuthBinding::new(
818 binding.token,
819 binding.bound_tenant_id,
820 binding.bound_actor_id,
821 )
822 })
823 .collect::<Vec<_>>();
824 let auth_config = if auth_bindings.is_empty() { None } else { Some(auth_bindings) };
825 let trust_actor_headers_for_authz = self.trust_actor_headers_for_authz;
826 let authz_strict = self.authz_strict;
827 let authz_config = self.authz_config.map(|config| {
828 let config = if trust_actor_headers_for_authz {
829 config.with_trusted_actor_headers()
830 } else {
831 config
832 };
833 if authz_strict { config.with_strict_path_mapping() } else { config }
834 });
835 let mut idempotency_layer = crate::idempotency::IdempotencyLayer::new()
839 .with_required_keys(self.require_idempotency_keys);
840 if let Ok(commerce) = self.state.commerce_for_tenant(None) {
841 idempotency_layer = idempotency_layer.with_durable_store(commerce);
842 }
843 let router =
844 routes::api_router_with_idempotency(self.max_request_body_bytes, idempotency_layer)
845 .with_state(self.state);
846 middleware::apply_middleware(
847 router,
848 self.enable_cors,
849 self.enable_request_id,
850 auth_config,
851 authz_config,
852 misconfigured_auth_message,
853 self.rate_limit,
854 )
855 }
856
857 pub async fn serve(self) -> Result<(), HttpError> {
861 let token = self.api_bearer_token.clone();
862 let api_token_count = self.api_bearer_bindings().len();
863 let metrics_token = self.state.metrics_bearer_auth_token().map(ToOwned::to_owned);
864 let trusted_proxy_count = self.state.metrics_trusted_proxies().map_or(0, |v| v.len());
865 let metrics_header_limits = self.state.metrics_header_limits();
866 let bound_tenant_id = self.bound_tenant_id.clone();
867 let bound_actor_id = self.bound_actor_id.clone();
868 let generated_default_token = self.generated_default_token;
869 let authz_enabled = self.authz_config.is_some();
870 let rate_limit_enabled = self.rate_limit.is_some();
871 let trust_actor_headers_for_authz = self.trust_actor_headers_for_authz;
872 let addr = self.addr;
873
874 if api_token_count == 0 && !self.allow_unauthenticated {
875 if addr.ip().is_loopback() {
876 tracing::warn!(
877 "No API authentication configured on a loopback bind. Configure a bearer \
878 token with ServerBuilder::with_bearer_auth before exposing this server."
879 );
880 } else {
881 return Err(HttpError::BadRequest(format!(
882 "Refusing to start without API authentication on non-loopback address \
883 {addr}. Configure a bearer token with ServerBuilder::with_bearer_auth (or \
884 add_bearer_auth_for_actor/add_bearer_auth_for_tenant), or explicitly opt \
885 out with ServerBuilder::allow_unauthenticated() or \
886 {ALLOW_UNAUTHENTICATED_ENV}=true."
887 )));
888 }
889 }
890
891 if let Some(message) = self.api_auth_error() {
892 return Err(HttpError::BadRequest(format!("Refusing to start: {message}")));
893 }
894
895 let app = self.build();
896
897 tracing::info!("StateSet HTTP listening on {addr}");
898 if let Some(token) = token.as_deref() {
899 tracing::info!("API bearer authentication is enabled for /api/v1/*");
900 if api_token_count > 1 {
901 tracing::info!(api_token_count, "Multiple API bearer tokens are configured");
902 }
903 if let Some(bound_tenant_id) = bound_tenant_id.as_deref() {
904 tracing::info!(
905 tenant_id = %bound_tenant_id,
906 "API token is bound to a specific tenant"
907 );
908 }
909 if let Some(bound_actor_id) = bound_actor_id.as_deref() {
910 tracing::info!(
911 actor_id = %bound_actor_id,
912 "API token is bound to a specific actor"
913 );
914 }
915 if generated_default_token {
916 tracing::warn!(
917 "Using generated bearer token. Persist it and rotate for production deployments."
918 );
919 let preview: String = token.chars().take(8).collect();
920 tracing::info!(
921 token_preview = %preview,
922 token_length = token.len(),
923 "Generated API bearer token (redacted preview)"
924 );
925 }
926 } else if api_token_count > 0 {
927 tracing::info!("API bearer authentication is enabled for /api/v1/*");
928 tracing::info!(api_token_count, "Multiple API bearer tokens are configured");
929 } else {
930 tracing::warn!("API authentication is disabled for /api/v1/*");
931 }
932 if authz_enabled && bound_actor_id.is_none() && trust_actor_headers_for_authz {
933 tracing::warn!(
934 "Request authorization is enabled for /api/v1/*; ensure x-actor-id is set by a trusted upstream"
935 );
936 }
937 if !addr.ip().is_loopback() {
938 if !authz_enabled {
941 tracing::warn!(
942 "No authorization (RBAC) configured on a non-loopback bind: any valid \
943 bearer token has full access to every /api/v1 route. Configure \
944 ServerBuilder::with_authz for production deployments."
945 );
946 }
947 if !rate_limit_enabled {
948 tracing::warn!(
949 "No rate limiting configured on a non-loopback bind. Configure \
950 ServerBuilder::with_rate_limit for production deployments."
951 );
952 }
953 }
954
955 if metrics_token.is_some() {
956 tracing::info!("Metrics bearer authentication is enabled for /metrics");
957 } else if !addr.ip().is_loopback() {
958 tracing::warn!(
959 "Metrics authentication is disabled for /metrics on a non-loopback bind"
960 );
961 }
962 if trusted_proxy_count > 0 {
963 tracing::info!(
964 trusted_proxy_count,
965 "Metrics forwarded headers are trusted only for configured proxy CIDRs"
966 );
967 }
968 tracing::info!(
969 forwarded_header_max_bytes = metrics_header_limits.forwarded_header_value_bytes(),
970 x_forwarded_for_header_max_bytes =
971 metrics_header_limits.x_forwarded_for_header_value_bytes(),
972 x_real_ip_header_max_bytes = metrics_header_limits.x_real_ip_header_value_bytes(),
973 authorization_header_max_bytes =
974 metrics_header_limits.authorization_header_value_bytes(),
975 "Metrics header byte limits configured"
976 );
977
978 let listener = tokio::net::TcpListener::bind(addr)
979 .await
980 .map_err(|e| HttpError::InternalError(format!("Failed to bind: {e}")))?;
981
982 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
983 .await
984 .map_err(|e| HttpError::InternalError(format!("Server error: {e}")))?;
985
986 Ok(())
987 }
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993
994 use axum::body::Body;
995 use axum::http::{Request, StatusCode};
996 use stateset_authz::{AuthzEngineBuilder, Role};
997 use tower::ServiceExt;
998 use uuid::Uuid;
999
1000 fn test_commerce() -> Commerce {
1001 Commerce::new(":memory:").expect("in-memory Commerce")
1002 }
1003
1004 #[test]
1005 fn builder_default_addr() {
1006 let builder = ServerBuilder::new(test_commerce());
1007 assert_eq!(builder.addr, SocketAddr::from(DEFAULT_ADDR));
1008 assert!(!builder.enable_cors);
1009 assert!(!builder.enable_request_id);
1010 assert!(builder.api_bearer_token.is_some());
1011 assert!(builder.metrics_bearer_auth_token().is_some());
1012 assert_eq!(builder.metrics_header_limits(), MetricsHeaderLimits::default());
1013 assert!(builder.bound_tenant_id.is_none());
1014 assert!(builder.bound_actor_id.is_none());
1015 assert!(builder.additional_api_bearer_bindings.is_empty());
1016 assert_eq!(builder.max_request_body_bytes, DEFAULT_REQUEST_BODY_LIMIT_BYTES);
1017 assert!(builder.authz_config.is_none());
1018 assert!(!builder.trust_actor_headers_for_authz);
1019 }
1020
1021 #[test]
1022 fn builder_with_bind() {
1023 let addr: SocketAddr = "0.0.0.0:8080".parse().unwrap();
1024 let builder = ServerBuilder::new(test_commerce()).bind(addr);
1025 assert_eq!(builder.addr, addr);
1026 }
1027
1028 #[test]
1029 fn builder_with_cors() {
1030 let builder = ServerBuilder::new(test_commerce()).with_cors();
1031 assert!(builder.enable_cors);
1032 }
1033
1034 #[test]
1035 fn builder_with_request_id() {
1036 let builder = ServerBuilder::new(test_commerce()).with_request_id();
1037 assert!(builder.enable_request_id);
1038 }
1039
1040 #[test]
1041 fn builder_with_bearer_auth() {
1042 let builder = ServerBuilder::new(test_commerce());
1043 let default_metrics_token = builder
1044 .metrics_bearer_auth_token()
1045 .expect("builder should start with metrics auth")
1046 .to_string();
1047 let builder = builder.with_bearer_auth("test-token");
1048 assert_eq!(builder.bearer_auth_token(), Some("test-token"));
1049 assert_eq!(builder.metrics_bearer_auth_token(), Some(default_metrics_token.as_str()));
1050 assert!(builder.bound_tenant_id.is_none());
1051 assert!(builder.bound_actor_id.is_none());
1052 }
1053
1054 #[test]
1055 fn builder_with_metrics_bearer_auth() {
1056 let builder = ServerBuilder::new(test_commerce()).with_metrics_bearer_auth("metrics-token");
1057 assert_eq!(builder.metrics_bearer_auth_token(), Some("metrics-token"));
1058 }
1059
1060 #[test]
1061 fn builder_with_bearer_auth_keeps_explicit_metrics_token() {
1062 let builder = ServerBuilder::new(test_commerce())
1063 .with_metrics_bearer_auth("metrics-token")
1064 .with_bearer_auth("api-token");
1065 assert_eq!(builder.bearer_auth_token(), Some("api-token"));
1066 assert_eq!(builder.metrics_bearer_auth_token(), Some("metrics-token"));
1067 }
1068
1069 #[test]
1070 fn builder_with_metrics_ip_allowlist() {
1071 let builder = ServerBuilder::new(test_commerce())
1072 .with_metrics_ip_allowlist(["127.0.0.1".parse().unwrap(), "10.0.0.1".parse().unwrap()]);
1073
1074 let allowlist = builder.metrics_ip_allowlist().unwrap();
1075 assert_eq!(allowlist.len(), 2);
1076 }
1077
1078 #[test]
1079 fn parse_ip_allowlist_csv_parses_and_sorts_unique_values() {
1080 let parsed =
1081 parse_ip_allowlist_csv(METRICS_IP_ALLOWLIST_ENV, "127.0.0.1, 10.0.0.1,127.0.0.1")
1082 .unwrap();
1083 assert_eq!(
1084 parsed,
1085 vec!["10.0.0.1".parse::<IpAddr>().unwrap(), "127.0.0.1".parse::<IpAddr>().unwrap()]
1086 );
1087 }
1088
1089 #[test]
1090 fn parse_ip_cidr_allowlist_csv_rejects_invalid_entries() {
1091 let err = parse_ip_cidr_allowlist_csv(METRICS_IP_CIDR_ALLOWLIST_ENV, "10.0.0.0/8,bogus")
1092 .expect_err("should reject invalid CIDR");
1093 assert!(err.to_string().contains(METRICS_IP_CIDR_ALLOWLIST_ENV));
1094 }
1095
1096 #[test]
1097 fn builder_with_metrics_ip_cidr_allowlist() {
1098 let builder = ServerBuilder::new(test_commerce()).with_metrics_ip_cidr_allowlist([
1099 "10.0.0.0/8".parse().unwrap(),
1100 "127.0.0.1".parse().unwrap(),
1101 ]);
1102
1103 let cidrs = builder.metrics_ip_cidr_allowlist().unwrap();
1104 assert_eq!(cidrs, vec!["10.0.0.0/8".parse().unwrap(), "127.0.0.1/32".parse().unwrap()]);
1105 }
1106
1107 #[test]
1108 fn builder_with_metrics_trusted_proxies() {
1109 let builder = ServerBuilder::new(test_commerce())
1110 .with_metrics_trusted_proxies(["10.0.0.0/8".parse().unwrap()]);
1111 let proxies = builder.metrics_trusted_proxies().unwrap();
1112 assert_eq!(proxies, vec!["10.0.0.0/8".parse().unwrap()]);
1113 }
1114
1115 #[test]
1116 fn builder_with_metrics_header_limits() {
1117 let limits = MetricsHeaderLimits::new(1024, 1536, 256).unwrap();
1118 let builder = ServerBuilder::new(test_commerce()).with_metrics_header_limits(limits);
1119 assert_eq!(builder.metrics_header_limits(), limits);
1120 }
1121
1122 #[test]
1123 fn builder_with_metrics_network_policy_from_env() {
1124 let builder = ServerBuilder::new(test_commerce())
1125 .with_metrics_network_policy_from_values(
1126 Some("127.0.0.1"),
1127 Some("10.0.0.0/8"),
1128 Some("10.1.0.0/16"),
1129 )
1130 .expect("policy should parse");
1131
1132 assert_eq!(builder.metrics_ip_allowlist(), Some(vec!["127.0.0.1".parse().unwrap()]));
1133 assert_eq!(builder.metrics_ip_cidr_allowlist(), Some(vec!["10.0.0.0/8".parse().unwrap()]));
1134 assert_eq!(builder.metrics_trusted_proxies(), Some(vec!["10.1.0.0/16".parse().unwrap()]));
1135 }
1136
1137 #[test]
1138 fn builder_with_metrics_network_policy_from_env_can_disable_lists_with_empty_values() {
1139 let builder = ServerBuilder::new(test_commerce())
1140 .with_metrics_ip_allowlist(["127.0.0.1".parse().unwrap()])
1141 .with_metrics_ip_cidr_allowlist(["10.0.0.0/8".parse().unwrap()])
1142 .with_metrics_trusted_proxies(["10.1.0.0/16".parse().unwrap()])
1143 .with_metrics_network_policy_from_values(Some(" "), Some(","), Some(" , "))
1144 .expect("empty policy values should disable lists");
1145
1146 assert!(builder.metrics_ip_allowlist().is_none());
1147 assert!(builder.metrics_ip_cidr_allowlist().is_none());
1148 assert!(builder.metrics_trusted_proxies().is_none());
1149 }
1150
1151 #[test]
1152 fn parse_positive_usize_env_rejects_zero_and_empty_values() {
1153 let zero =
1154 parse_positive_usize_env(METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV, "0").unwrap_err();
1155 assert!(zero.to_string().contains("greater than zero"));
1156
1157 let empty =
1158 parse_positive_usize_env(METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV, " ").unwrap_err();
1159 assert!(empty.to_string().contains("positive integer"));
1160 }
1161
1162 #[test]
1163 fn builder_with_metrics_header_limits_from_values() {
1164 let builder = ServerBuilder::new(test_commerce())
1165 .with_metrics_header_limits_from_values(
1166 Some("1024"),
1167 Some("1536"),
1168 Some("256"),
1169 Some("768"),
1170 )
1171 .expect("limits should parse");
1172 let limits = builder.metrics_header_limits();
1173 assert_eq!(limits.forwarded_header_value_bytes(), 1024);
1174 assert_eq!(limits.x_forwarded_for_header_value_bytes(), 1536);
1175 assert_eq!(limits.x_real_ip_header_value_bytes(), 256);
1176 assert_eq!(limits.authorization_header_value_bytes(), 768);
1177 }
1178
1179 #[test]
1180 fn builder_with_metrics_header_limits_from_values_applies_partial_overrides() {
1181 let builder = ServerBuilder::new(test_commerce())
1182 .with_metrics_header_limits_from_values(None, Some("4096"), None, None)
1183 .expect("partial limits should parse");
1184 let limits = builder.metrics_header_limits();
1185 assert_eq!(
1186 limits.forwarded_header_value_bytes(),
1187 MetricsHeaderLimits::DEFAULT_FORWARDED_HEADER_VALUE_BYTES
1188 );
1189 assert_eq!(limits.x_forwarded_for_header_value_bytes(), 4096);
1190 assert_eq!(
1191 limits.x_real_ip_header_value_bytes(),
1192 MetricsHeaderLimits::DEFAULT_X_REAL_IP_HEADER_VALUE_BYTES
1193 );
1194 assert_eq!(
1195 limits.authorization_header_value_bytes(),
1196 MetricsHeaderLimits::DEFAULT_AUTHORIZATION_HEADER_VALUE_BYTES
1197 );
1198 }
1199
1200 #[test]
1201 fn builder_with_request_body_limit() {
1202 let builder = ServerBuilder::new(test_commerce()).with_max_request_body_bytes(4096);
1203 assert_eq!(builder.max_request_body_bytes, 4096);
1204 }
1205
1206 #[test]
1207 fn builder_with_request_body_limit_from_value() {
1208 let builder = ServerBuilder::new(test_commerce())
1209 .with_request_body_limit_from_value(Some("8192"))
1210 .expect("request body limit should parse");
1211 assert_eq!(builder.max_request_body_bytes, 8192);
1212 }
1213
1214 #[test]
1215 fn builder_with_authz_engine() {
1216 let engine = AuthzEngineBuilder::new()
1217 .add_role(Role::viewer())
1218 .assign_role("viewer-1", "viewer")
1219 .build();
1220 let builder = ServerBuilder::new(test_commerce()).with_authz_engine(engine);
1221 assert!(builder.authz_config.is_some());
1222 }
1223
1224 #[test]
1225 fn builder_trusts_actor_headers_for_authz() {
1226 let builder = ServerBuilder::new(test_commerce()).trust_actor_headers_for_authz();
1227 assert!(builder.trust_actor_headers_for_authz);
1228 }
1229
1230 #[test]
1231 fn builder_with_bearer_auth_for_actor() {
1232 let builder = ServerBuilder::new(test_commerce())
1233 .with_bearer_auth_for_actor("actor-token", "admin-1");
1234 assert_eq!(builder.bearer_auth_token(), Some("actor-token"));
1235 assert_eq!(builder.bound_actor_id.as_deref(), Some("admin-1"));
1236 }
1237
1238 #[test]
1239 fn builder_adds_additional_actor_bound_token() {
1240 let builder = ServerBuilder::new(test_commerce())
1241 .without_auth()
1242 .add_bearer_auth_for_actor("viewer-token", "viewer-1");
1243 assert!(builder.bearer_auth_token().is_none());
1244 assert_eq!(builder.additional_api_bearer_bindings.len(), 1);
1245 assert_eq!(builder.additional_api_bearer_bindings[0].token, "viewer-token");
1246 assert_eq!(
1247 builder.additional_api_bearer_bindings[0].bound_actor_id.as_deref(),
1248 Some("viewer-1")
1249 );
1250 }
1251
1252 #[test]
1253 fn builder_with_bearer_auth_for_tenant() {
1254 let builder = ServerBuilder::new(test_commerce())
1255 .with_bearer_auth_for_tenant("tenant-token", "tenant-1");
1256 assert_eq!(builder.bearer_auth_token(), Some("tenant-token"));
1257 assert_eq!(builder.bound_tenant_id.as_deref(), Some("tenant-1"));
1258 }
1259
1260 #[test]
1261 fn builder_without_auth() {
1262 let builder = ServerBuilder::new(test_commerce())
1263 .with_bearer_auth_for_tenant("token", "tenant-1")
1264 .bind_auth_actor("admin-1")
1265 .without_auth();
1266 assert!(builder.bearer_auth_token().is_none());
1267 assert!(builder.metrics_bearer_auth_token().is_some());
1268 assert!(builder.bound_tenant_id.is_none());
1269 assert!(builder.bound_actor_id.is_none());
1270 }
1271
1272 #[test]
1273 fn builder_allow_unauthenticated_flag() {
1274 let builder = ServerBuilder::new(test_commerce());
1275 assert!(!builder.allow_unauthenticated, "secure default must be false");
1276 let builder = builder.allow_unauthenticated();
1277 assert!(builder.allow_unauthenticated);
1278 }
1279
1280 #[test]
1281 fn builder_require_idempotency_keys_flag() {
1282 let builder = ServerBuilder::new(test_commerce());
1283 assert!(builder.require_idempotency_keys, "secure default must require keys");
1284 let builder = builder.require_idempotency_keys(false);
1285 assert!(!builder.require_idempotency_keys);
1286 }
1287
1288 #[test]
1289 fn builder_require_idempotency_keys_from_value() {
1290 let builder = ServerBuilder::new(test_commerce())
1291 .with_require_idempotency_keys_from_value(Some("false"))
1292 .expect("false should parse");
1293 assert!(!builder.require_idempotency_keys);
1294
1295 let builder = ServerBuilder::new(test_commerce())
1296 .with_require_idempotency_keys_from_value(None)
1297 .expect("unset keeps default");
1298 assert!(builder.require_idempotency_keys, "unset must keep the secure default");
1299
1300 assert!(
1301 ServerBuilder::new(test_commerce())
1302 .with_require_idempotency_keys_from_value(Some("banana"))
1303 .is_err(),
1304 "invalid values must be rejected"
1305 );
1306 }
1307
1308 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1309 async fn built_server_returns_428_on_keyless_payment_create_by_default() {
1310 use tower::ServiceExt as _;
1311 let app = ServerBuilder::new(test_commerce()).without_auth().build();
1312 let response = app
1313 .oneshot(
1314 axum::http::Request::post("/api/v1/payments")
1315 .header("content-type", "application/json")
1316 .body(axum::body::Body::from("{}"))
1317 .unwrap(),
1318 )
1319 .await
1320 .unwrap();
1321 assert_eq!(response.status(), axum::http::StatusCode::PRECONDITION_REQUIRED);
1322 }
1323
1324 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1325 async fn built_server_allows_keyless_payment_create_when_opted_out() {
1326 use tower::ServiceExt as _;
1327 let app = ServerBuilder::new(test_commerce())
1328 .without_auth()
1329 .require_idempotency_keys(false)
1330 .build();
1331 let response = app
1332 .oneshot(
1333 axum::http::Request::post("/api/v1/payments")
1334 .header("content-type", "application/json")
1335 .body(axum::body::Body::from("{}"))
1336 .unwrap(),
1337 )
1338 .await
1339 .unwrap();
1340 assert_ne!(
1341 response.status(),
1342 axum::http::StatusCode::PRECONDITION_REQUIRED,
1343 "opt-out must disable the 428 gate"
1344 );
1345 }
1346
1347 #[test]
1348 fn builder_strict_authz_flag() {
1349 let builder = ServerBuilder::new(test_commerce());
1350 assert!(!builder.authz_strict, "default must preserve existing behavior");
1351 let builder = builder.with_strict_authz();
1352 assert!(builder.authz_strict);
1353 }
1354
1355 #[test]
1356 fn parse_bool_env_accepts_expected_values() {
1357 for raw in ["1", "true", "TRUE", "yes"] {
1358 assert!(matches!(parse_bool_env("TEST_ENV", raw), Ok(true)), "raw={raw}");
1359 }
1360 for raw in ["0", "false", "no", "", " "] {
1361 assert!(matches!(parse_bool_env("TEST_ENV", raw), Ok(false)), "raw={raw}");
1362 }
1363 assert!(parse_bool_env("TEST_ENV", "maybe").is_err());
1364 }
1365
1366 #[tokio::test]
1367 async fn serve_refuses_non_loopback_bind_without_auth() {
1368 let err = ServerBuilder::new(test_commerce())
1369 .without_auth()
1370 .bind("192.0.2.1:0".parse().expect("socket addr"))
1371 .serve()
1372 .await
1373 .expect_err("must refuse to start without auth on a non-loopback bind");
1374 match err {
1375 HttpError::BadRequest(message) => {
1376 assert!(message.contains("Refusing to start"), "message: {message}");
1377 assert!(message.contains("allow_unauthenticated"), "message: {message}");
1378 assert!(message.contains("STATESET_HTTP_ALLOW_UNAUTHENTICATED"));
1379 }
1380 other => panic!("unexpected error: {other:?}"),
1381 }
1382 }
1383
1384 #[tokio::test]
1385 async fn allow_unauthenticated_bypasses_non_loopback_refusal() {
1386 let err = ServerBuilder::new(test_commerce())
1389 .without_auth()
1390 .allow_unauthenticated()
1391 .bind("192.0.2.1:0".parse().expect("socket addr"))
1392 .serve()
1393 .await
1394 .expect_err("bind to TEST-NET-1 must fail");
1395 match err {
1396 HttpError::InternalError(message) => {
1397 assert!(message.contains("Failed to bind"), "message: {message}");
1398 }
1399 other => panic!("expected bind failure, got: {other:?}"),
1400 }
1401 }
1402
1403 #[tokio::test]
1404 async fn serve_allows_loopback_bind_without_auth() {
1405 let serve_future = ServerBuilder::new(test_commerce())
1406 .without_auth()
1407 .bind("127.0.0.1:0".parse().expect("socket addr"))
1408 .serve();
1409 let result =
1412 tokio::time::timeout(std::time::Duration::from_millis(500), serve_future).await;
1413 assert!(result.is_err(), "loopback bind without auth must start and keep serving");
1414 }
1415
1416 #[test]
1417 fn builder_without_metrics_auth() {
1418 let builder = ServerBuilder::new(test_commerce()).without_metrics_auth();
1419 assert!(builder.bearer_auth_token().is_some());
1420 assert!(builder.metrics_bearer_auth_token().is_none());
1421 }
1422
1423 #[test]
1424 fn builder_without_metrics_ip_allowlist() {
1425 let builder = ServerBuilder::new(test_commerce())
1426 .with_metrics_ip_allowlist(["127.0.0.1".parse().unwrap()])
1427 .without_metrics_ip_allowlist();
1428 assert!(builder.metrics_ip_allowlist().is_none());
1429 }
1430
1431 #[test]
1432 fn builder_without_metrics_ip_cidr_allowlist() {
1433 let builder = ServerBuilder::new(test_commerce())
1434 .with_metrics_ip_cidr_allowlist(["10.0.0.0/8".parse().unwrap()])
1435 .without_metrics_ip_cidr_allowlist();
1436 assert!(builder.metrics_ip_cidr_allowlist().is_none());
1437 }
1438
1439 #[test]
1440 fn builder_without_metrics_trusted_proxies() {
1441 let builder = ServerBuilder::new(test_commerce())
1442 .with_metrics_trusted_proxies(["10.0.0.0/8".parse().unwrap()])
1443 .without_metrics_trusted_proxies();
1444 assert!(builder.metrics_trusted_proxies().is_none());
1445 }
1446
1447 #[test]
1448 fn builder_chaining() {
1449 let addr: SocketAddr = "0.0.0.0:9090".parse().unwrap();
1450 let builder = ServerBuilder::new(test_commerce())
1451 .bind(addr)
1452 .with_cors()
1453 .with_request_id()
1454 .with_bearer_auth("chain-token")
1455 .bind_auth_tenant("chain-tenant")
1456 .bind_auth_actor("chain-actor");
1457 assert_eq!(builder.addr, addr);
1458 assert!(builder.enable_cors);
1459 assert!(builder.enable_request_id);
1460 assert_eq!(builder.bearer_auth_token(), Some("chain-token"));
1461 assert_eq!(builder.bound_tenant_id.as_deref(), Some("chain-tenant"));
1462 assert_eq!(builder.bound_actor_id.as_deref(), Some("chain-actor"));
1463 }
1464
1465 #[test]
1466 fn builder_with_max_tenant_dbs() {
1467 let tenant_dir =
1468 std::env::temp_dir().join(format!("stateset-http-builder-{}", Uuid::new_v4()));
1469 let builder = ServerBuilder::new(test_commerce())
1470 .with_tenant_db_dir(tenant_dir.clone())
1471 .with_max_tenant_dbs(1);
1472
1473 let tenant_a = builder.state.commerce_for_tenant(Some("tenant-a")).unwrap();
1474 let second_while_in_use = builder.state.commerce_for_tenant(Some("tenant-b"));
1475 assert!(matches!(second_while_in_use, Err(HttpError::TooManyRequests(_))));
1476
1477 drop(tenant_a);
1478 let second_after_release = builder.state.commerce_for_tenant(Some("tenant-b"));
1479 assert!(second_after_release.is_ok());
1480
1481 let _ = std::fs::remove_dir_all(tenant_dir);
1482 }
1483
1484 #[test]
1485 fn builder_builds_router() {
1486 let _router = ServerBuilder::new(test_commerce()).build();
1487 }
1488
1489 #[tokio::test]
1490 async fn built_router_fails_closed_for_unbound_tenant_routing() {
1491 let tenant_dir =
1492 std::env::temp_dir().join(format!("stateset-http-misconfig-{}", Uuid::new_v4()));
1493 let builder = ServerBuilder::new(test_commerce()).with_tenant_db_dir(tenant_dir.clone());
1494 let token =
1495 builder.bearer_auth_token().expect("default auth token should be present").to_string();
1496 let router = builder.build();
1497
1498 let resp = router
1499 .oneshot(
1500 Request::get("/api/v1/orders")
1501 .header("authorization", format!("Bearer {token}"))
1502 .header("x-tenant-id", "tenant-1")
1503 .body(Body::empty())
1504 .unwrap(),
1505 )
1506 .await
1507 .unwrap();
1508 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1509
1510 let _ = std::fs::remove_dir_all(tenant_dir);
1511 }
1512
1513 #[tokio::test]
1514 async fn built_router_fails_closed_for_invalid_bound_actor() {
1515 let builder = ServerBuilder::new(test_commerce()).bind_auth_actor("invalid actor");
1516 let token =
1517 builder.bearer_auth_token().expect("default auth token should be present").to_string();
1518 let router = builder.build();
1519
1520 let resp = router
1521 .oneshot(
1522 Request::get("/api/v1/orders")
1523 .header("authorization", format!("Bearer {token}"))
1524 .header("x-tenant-id", "tenant-1")
1525 .body(Body::empty())
1526 .unwrap(),
1527 )
1528 .await
1529 .unwrap();
1530 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1531 }
1532
1533 #[tokio::test]
1534 async fn built_router_fails_closed_for_actor_binding_without_auth() {
1535 let router =
1536 ServerBuilder::new(test_commerce()).without_auth().bind_auth_actor("admin-1").build();
1537
1538 let resp = router
1539 .oneshot(
1540 Request::get("/api/v1/orders")
1541 .header("x-tenant-id", "tenant-1")
1542 .body(Body::empty())
1543 .unwrap(),
1544 )
1545 .await
1546 .unwrap();
1547 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1548 }
1549
1550 #[tokio::test]
1551 async fn built_router_fails_closed_for_duplicate_api_tokens() {
1552 let builder = ServerBuilder::new(test_commerce())
1553 .with_bearer_auth("duplicate-token")
1554 .add_bearer_auth_for_actor("duplicate-token", "viewer-1");
1555 let router = builder.build();
1556
1557 let resp = router
1558 .oneshot(
1559 Request::get("/api/v1/orders")
1560 .header("authorization", "Bearer duplicate-token")
1561 .header("x-tenant-id", "tenant-1")
1562 .body(Body::empty())
1563 .unwrap(),
1564 )
1565 .await
1566 .unwrap();
1567 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1568 }
1569
1570 #[tokio::test]
1571 async fn built_router_fails_closed_for_authz_without_actor_bound_tokens_or_trusted_headers() {
1572 let engine = AuthzEngineBuilder::new()
1573 .add_role(Role::viewer())
1574 .assign_role("viewer-1", "viewer")
1575 .build();
1576 let builder = ServerBuilder::new(test_commerce()).with_authz_engine(engine);
1577 let token =
1578 builder.bearer_auth_token().expect("default auth token should be present").to_string();
1579 let router = builder.build();
1580
1581 let resp = router
1582 .oneshot(
1583 Request::get("/api/v1/orders")
1584 .header("authorization", format!("Bearer {token}"))
1585 .header("x-tenant-id", "tenant-1")
1586 .header("x-actor-id", "viewer-1")
1587 .body(Body::empty())
1588 .unwrap(),
1589 )
1590 .await
1591 .unwrap();
1592 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1593 }
1594
1595 #[tokio::test]
1596 async fn built_router_fails_closed_for_tenant_routing_with_unbound_additional_token() {
1597 let tenant_dir =
1598 std::env::temp_dir().join(format!("stateset-http-extra-token-{}", Uuid::new_v4()));
1599 let builder = ServerBuilder::new(test_commerce())
1600 .with_bearer_auth_for_tenant("primary-token", "tenant-1")
1601 .add_bearer_auth_for_actor("viewer-token", "viewer-1")
1602 .with_tenant_db_dir(tenant_dir.clone());
1603 let router = builder.build();
1604
1605 let resp = router
1606 .oneshot(
1607 Request::get("/api/v1/orders")
1608 .header("authorization", "Bearer viewer-token")
1609 .header("x-tenant-id", "tenant-1")
1610 .body(Body::empty())
1611 .unwrap(),
1612 )
1613 .await
1614 .unwrap();
1615 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1616
1617 let _ = std::fs::remove_dir_all(tenant_dir);
1618 }
1619
1620 #[tokio::test]
1621 async fn built_router_serves_health() {
1622 let router = ServerBuilder::new(test_commerce()).with_cors().with_request_id().build();
1623
1624 let resp =
1625 router.oneshot(Request::get("/health").body(Body::empty()).unwrap()).await.unwrap();
1626 assert_eq!(resp.status(), StatusCode::OK);
1627 }
1628
1629 #[tokio::test]
1630 async fn built_router_serves_api_orders() {
1631 let router = ServerBuilder::new(test_commerce()).without_auth().build();
1632
1633 let resp = router
1634 .oneshot(Request::get("/api/v1/orders").body(Body::empty()).unwrap())
1635 .await
1636 .unwrap();
1637 assert_eq!(resp.status(), StatusCode::OK);
1638 }
1639
1640 #[tokio::test]
1641 async fn built_router_serves_api_customers() {
1642 let router = ServerBuilder::new(test_commerce()).without_auth().build();
1643
1644 let resp = router
1645 .oneshot(Request::get("/api/v1/customers").body(Body::empty()).unwrap())
1646 .await
1647 .unwrap();
1648 assert_eq!(resp.status(), StatusCode::OK);
1649 }
1650
1651 #[tokio::test]
1652 async fn built_router_serves_api_products() {
1653 let router = ServerBuilder::new(test_commerce()).without_auth().build();
1654
1655 let resp = router
1656 .oneshot(Request::get("/api/v1/products").body(Body::empty()).unwrap())
1657 .await
1658 .unwrap();
1659 assert_eq!(resp.status(), StatusCode::OK);
1660 }
1661
1662 #[tokio::test]
1663 async fn built_router_404_for_unknown_path() {
1664 let router = ServerBuilder::new(test_commerce()).without_auth().build();
1665
1666 let resp = router
1667 .oneshot(Request::get("/api/v1/nonexistent").body(Body::empty()).unwrap())
1668 .await
1669 .unwrap();
1670 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1671 }
1672
1673 #[test]
1674 fn builder_debug_impl() {
1675 let builder = ServerBuilder::new(test_commerce());
1676 let dbg = format!("{builder:?}");
1677 assert!(dbg.contains("ServerBuilder"));
1678 assert!(dbg.contains("<redacted>"));
1679 }
1680
1681 #[tokio::test]
1682 async fn built_router_blocks_api_without_token_by_default() {
1683 let router = ServerBuilder::new(test_commerce()).build();
1684
1685 let resp = router
1686 .oneshot(Request::get("/api/v1/orders").body(Body::empty()).unwrap())
1687 .await
1688 .unwrap();
1689 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1690 }
1691
1692 #[tokio::test]
1693 async fn built_router_allows_api_with_token() {
1694 let builder = ServerBuilder::new(test_commerce()).with_ignore_tenant_header();
1697 let token =
1698 builder.bearer_auth_token().expect("default auth token should be present").to_string();
1699 let router = builder.build();
1700
1701 let resp = router
1702 .oneshot(
1703 Request::get("/api/v1/orders")
1704 .header("authorization", format!("Bearer {token}"))
1705 .header("x-tenant-id", "tenant-1")
1706 .body(Body::empty())
1707 .unwrap(),
1708 )
1709 .await
1710 .unwrap();
1711 assert_eq!(resp.status(), StatusCode::OK);
1712 }
1713
1714 #[tokio::test]
1715 async fn built_router_blocks_metrics_without_token_by_default() {
1716 let router = ServerBuilder::new(test_commerce()).build();
1717
1718 let resp =
1719 router.oneshot(Request::get("/metrics").body(Body::empty()).unwrap()).await.unwrap();
1720 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1721 }
1722
1723 #[tokio::test]
1724 async fn built_router_allows_metrics_with_token() {
1725 let builder = ServerBuilder::new(test_commerce());
1726 let token = builder
1727 .metrics_bearer_auth_token()
1728 .expect("default metrics token should be present")
1729 .to_string();
1730 let router = builder.build();
1731
1732 let resp = router
1733 .oneshot(
1734 Request::get("/metrics")
1735 .header("authorization", format!("Bearer {token}"))
1736 .body(Body::empty())
1737 .unwrap(),
1738 )
1739 .await
1740 .unwrap();
1741 assert_eq!(resp.status(), StatusCode::OK);
1742 }
1743
1744 #[tokio::test]
1745 async fn built_router_allows_metrics_without_token_when_disabled() {
1746 let router = ServerBuilder::new(test_commerce()).without_metrics_auth().build();
1747
1748 let resp =
1749 router.oneshot(Request::get("/metrics").body(Body::empty()).unwrap()).await.unwrap();
1750 assert_eq!(resp.status(), StatusCode::OK);
1751 }
1752
1753 #[tokio::test]
1754 async fn built_router_keeps_metrics_protected_after_without_auth() {
1755 let router = ServerBuilder::new(test_commerce()).without_auth().build();
1756
1757 let resp =
1758 router.oneshot(Request::get("/metrics").body(Body::empty()).unwrap()).await.unwrap();
1759 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1760 }
1761
1762 #[tokio::test]
1763 async fn built_router_rejects_mismatched_tenant_for_bound_token() {
1764 let router = ServerBuilder::new(test_commerce())
1765 .with_bearer_auth_for_tenant("bound-token", "tenant-1")
1766 .build();
1767
1768 let resp = router
1769 .oneshot(
1770 Request::get("/api/v1/orders")
1771 .header("authorization", "Bearer bound-token")
1772 .header("x-tenant-id", "tenant-2")
1773 .body(Body::empty())
1774 .unwrap(),
1775 )
1776 .await
1777 .unwrap();
1778 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
1779 }
1780
1781 #[tokio::test]
1782 async fn serve_refuses_public_bind_without_auth() {
1783 let addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
1784 let err = ServerBuilder::new(test_commerce())
1785 .bind(addr)
1786 .without_auth()
1787 .serve()
1788 .await
1789 .expect_err("should reject public bind without auth");
1790
1791 assert!(err.to_string().contains("Refusing to start without API auth"));
1792 }
1793
1794 #[tokio::test]
1795 async fn serve_refuses_tenant_routing_without_tenant_binding() {
1796 let tenant_dir =
1797 std::env::temp_dir().join(format!("stateset-http-serve-misconfig-{}", Uuid::new_v4()));
1798 let err = ServerBuilder::new(test_commerce())
1799 .with_tenant_db_dir(tenant_dir.clone())
1800 .serve()
1801 .await
1802 .expect_err("should reject unbound tenant routing");
1803
1804 assert!(err.to_string().contains("binding every API bearer token"));
1805 let _ = std::fs::remove_dir_all(tenant_dir);
1806 }
1807
1808 #[tokio::test]
1809 async fn serve_refuses_tenant_routing_without_auth() {
1810 let tenant_dir =
1811 std::env::temp_dir().join(format!("stateset-http-serve-no-auth-{}", Uuid::new_v4()));
1812 let err = ServerBuilder::new(test_commerce())
1813 .with_tenant_db_dir(tenant_dir.clone())
1814 .without_auth()
1815 .serve()
1816 .await
1817 .expect_err("should reject tenant routing without auth");
1818
1819 assert!(err.to_string().contains("requires API auth"));
1820 let _ = std::fs::remove_dir_all(tenant_dir);
1821 }
1822
1823 #[tokio::test]
1824 async fn serve_refuses_authz_without_actor_bound_tokens_or_trusted_headers() {
1825 let engine = AuthzEngineBuilder::new()
1826 .add_role(Role::viewer())
1827 .assign_role("viewer-1", "viewer")
1828 .build();
1829 let err = ServerBuilder::new(test_commerce())
1830 .with_authz_engine(engine)
1831 .serve()
1832 .await
1833 .expect_err("should reject authz configuration without actor identity source");
1834
1835 assert!(err.to_string().contains("binding every API bearer token"));
1836 }
1837}