uni_common/config.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4use std::path::{Path, PathBuf};
5use std::thread;
6use std::time::Duration;
7
8#[derive(Clone, Debug)]
9pub struct CompactionConfig {
10 /// Enable background compaction (default: true)
11 pub enabled: bool,
12
13 /// Max L1 runs before triggering compaction (default: 4)
14 pub max_l1_runs: usize,
15
16 /// Max L1 size in bytes before compaction (default: 256MB)
17 pub max_l1_size_bytes: u64,
18
19 /// Max age of oldest L1 run before compaction (default: 1 hour)
20 pub max_l1_age: Duration,
21
22 /// Background check interval (default: 30s)
23 pub check_interval: Duration,
24
25 /// Number of compaction worker threads (default: 1)
26 pub worker_threads: usize,
27}
28
29impl Default for CompactionConfig {
30 fn default() -> Self {
31 Self {
32 enabled: true,
33 max_l1_runs: 4,
34 max_l1_size_bytes: 256 * 1024 * 1024,
35 max_l1_age: Duration::from_secs(3600),
36 check_interval: Duration::from_secs(30),
37 worker_threads: 1,
38 }
39 }
40}
41
42/// Configuration for background index rebuilding.
43#[derive(Clone, Debug)]
44pub struct IndexRebuildConfig {
45 /// Maximum number of retry attempts for failed index builds (default: 3).
46 pub max_retries: u32,
47
48 /// Delay between retry attempts (default: 60s).
49 pub retry_delay: Duration,
50
51 /// How often to check for pending index rebuild tasks (default: 5s).
52 pub worker_check_interval: Duration,
53
54 /// Row growth ratio to trigger rebuild (default: 0.5 = 50%). Set 0.0 to disable.
55 pub growth_trigger_ratio: f64,
56
57 /// Max index age before rebuild. `None` disables the time-based trigger.
58 pub max_index_age: Option<Duration>,
59
60 /// Enable post-flush automatic rebuild scheduling (default: false).
61 pub auto_rebuild_enabled: bool,
62}
63
64impl Default for IndexRebuildConfig {
65 fn default() -> Self {
66 Self {
67 max_retries: 3,
68 retry_delay: Duration::from_secs(60),
69 worker_check_interval: Duration::from_secs(5),
70 growth_trigger_ratio: 0.5,
71 max_index_age: None,
72 auto_rebuild_enabled: false,
73 }
74 }
75}
76
77#[derive(Clone, Copy, Debug)]
78pub struct WriteThrottleConfig {
79 /// L1 run count to start throttling (default: 8)
80 pub soft_limit: usize,
81
82 /// L1 run count to stop writes entirely (default: 16)
83 pub hard_limit: usize,
84
85 /// Base delay when throttling (default: 10ms)
86 pub base_delay: Duration,
87}
88
89impl Default for WriteThrottleConfig {
90 fn default() -> Self {
91 Self {
92 soft_limit: 8,
93 hard_limit: 16,
94 base_delay: Duration::from_millis(10),
95 }
96 }
97}
98
99#[derive(Clone, Debug)]
100pub struct ObjectStoreConfig {
101 pub connect_timeout: Duration,
102 pub read_timeout: Duration,
103 pub write_timeout: Duration,
104 pub max_retries: u32,
105 pub retry_backoff_base: Duration,
106 pub retry_backoff_max: Duration,
107}
108
109impl Default for ObjectStoreConfig {
110 fn default() -> Self {
111 Self {
112 connect_timeout: Duration::from_secs(10),
113 read_timeout: Duration::from_secs(30),
114 write_timeout: Duration::from_secs(60),
115 max_retries: 3,
116 retry_backoff_base: Duration::from_millis(100),
117 retry_backoff_max: Duration::from_secs(10),
118 }
119 }
120}
121
122/// Security configuration for file system operations.
123/// Controls which paths can be accessed by BACKUP, COPY, and EXPORT commands.
124///
125/// Disabled by default for backward compatibility in embedded mode.
126/// MUST be enabled for server mode with untrusted clients.
127#[derive(Clone, Debug, Default)]
128pub struct FileSandboxConfig {
129 /// If true, file operations are restricted to allowed_paths.
130 /// If false, all paths are allowed (NOT RECOMMENDED for server mode).
131 pub enabled: bool,
132
133 /// List of allowed base directories for file operations.
134 /// Paths must be absolute and canonical.
135 /// File operations are only allowed within these directories.
136 pub allowed_paths: Vec<PathBuf>,
137}
138
139/// Deployment mode for the database.
140///
141/// Used to determine appropriate security defaults.
142#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
143pub enum DeploymentMode {
144 /// Embedded/library mode where the host application controls access.
145 /// File sandbox is disabled by default for backward compatibility.
146 #[default]
147 Embedded,
148 /// Server mode with untrusted clients.
149 /// File sandbox is enabled by default with restricted paths.
150 Server,
151}
152
153/// HTTP server configuration.
154///
155/// Controls CORS, authentication, and other HTTP-related security settings.
156///
157/// # Security
158///
159/// **CWE-942 (Overly Permissive CORS)**, **CWE-306 (Missing Authentication)**:
160/// Production deployments should configure explicit `allowed_origins` and
161/// enable API key authentication.
162#[derive(Clone, Debug)]
163pub struct ServerConfig {
164 /// Allowed CORS origins.
165 ///
166 /// - Empty vector: No CORS headers (most restrictive)
167 /// - `["*"]`: Allow all origins (NOT RECOMMENDED for production)
168 /// - Explicit list: Only allow specified origins (RECOMMENDED)
169 ///
170 /// # Security
171 ///
172 /// **CWE-942**: Using `["*"]` allows any website to make requests to
173 /// your server, potentially exposing sensitive data.
174 pub allowed_origins: Vec<String>,
175
176 /// Optional API key for request authentication.
177 ///
178 /// When set, all API requests must include the header:
179 /// `X-API-Key: <key>`
180 ///
181 /// # Security
182 ///
183 /// **CWE-306**: Without authentication, any client can execute queries.
184 /// Enable this for any deployment accessible beyond localhost.
185 pub api_key: Option<String>,
186
187 /// Whether to require API key for metrics endpoint.
188 ///
189 /// Default: false (metrics are public for observability tooling)
190 pub require_auth_for_metrics: bool,
191}
192
193impl Default for ServerConfig {
194 fn default() -> Self {
195 Self {
196 // Default to localhost-only origin for development safety
197 allowed_origins: vec!["http://localhost:3000".to_string()],
198 api_key: None,
199 require_auth_for_metrics: false,
200 }
201 }
202}
203
204impl ServerConfig {
205 /// Create a permissive config for local development only.
206 ///
207 /// # Security
208 ///
209 /// **WARNING**: Do not use in production. This config allows all CORS origins
210 /// and has no authentication.
211 #[must_use]
212 pub fn development() -> Self {
213 Self {
214 allowed_origins: vec!["*".to_string()],
215 api_key: None,
216 require_auth_for_metrics: false,
217 }
218 }
219
220 /// Create a production config with explicit origins and required API key.
221 ///
222 /// # Panics
223 ///
224 /// Panics if `api_key` is empty.
225 #[must_use]
226 pub fn production(allowed_origins: Vec<String>, api_key: String) -> Self {
227 assert!(
228 !api_key.is_empty(),
229 "API key must not be empty for production"
230 );
231 Self {
232 allowed_origins,
233 api_key: Some(api_key),
234 require_auth_for_metrics: true,
235 }
236 }
237
238 /// Returns a security warning if the config is insecure.
239 pub fn security_warning(&self) -> Option<&'static str> {
240 if self.allowed_origins.contains(&"*".to_string()) && self.api_key.is_none() {
241 Some(
242 "Server config has permissive CORS (allow all origins) and no API key. \
243 This is insecure for production deployments.",
244 )
245 } else if self.allowed_origins.contains(&"*".to_string()) {
246 Some(
247 "Server config has permissive CORS (allow all origins). \
248 Consider restricting to specific origins for production.",
249 )
250 } else if self.api_key.is_none() {
251 Some(
252 "Server config has no API key authentication. \
253 Enable api_key for production deployments.",
254 )
255 } else {
256 None
257 }
258 }
259}
260
261impl FileSandboxConfig {
262 /// Creates a sandboxed config that only allows operations in the specified directories.
263 pub fn sandboxed(paths: Vec<PathBuf>) -> Self {
264 Self {
265 enabled: true,
266 allowed_paths: paths,
267 }
268 }
269
270 /// Creates a config with appropriate defaults for the deployment mode.
271 ///
272 /// # Security
273 ///
274 /// - **Embedded mode**: Sandbox disabled (host application controls access)
275 /// - **Server mode**: Sandbox enabled with default paths `/var/lib/uni/data` and
276 /// `/var/lib/uni/backups`
277 ///
278 /// **CWE-22 (Path Traversal)**: Server deployments MUST enable the sandbox to
279 /// prevent arbitrary file read/write via BACKUP, COPY, and EXPORT commands.
280 pub fn default_for_mode(mode: DeploymentMode) -> Self {
281 match mode {
282 DeploymentMode::Embedded => Self {
283 enabled: false,
284 allowed_paths: vec![],
285 },
286 DeploymentMode::Server => Self {
287 enabled: true,
288 allowed_paths: vec![
289 PathBuf::from("/var/lib/uni/data"),
290 PathBuf::from("/var/lib/uni/backups"),
291 ],
292 },
293 }
294 }
295
296 /// Returns a security warning message if the sandbox is disabled.
297 ///
298 /// Call this at startup to alert administrators about potential security risks.
299 /// Returns `Some(message)` if a warning should be displayed, `None` otherwise.
300 ///
301 /// # Security
302 ///
303 /// **CWE-22 (Path Traversal)**, **CWE-73 (External Control of File Name)**:
304 /// Disabled sandbox allows unrestricted filesystem access for BACKUP, COPY,
305 /// and EXPORT commands, which can lead to:
306 /// - Arbitrary file read/write in server deployments
307 /// - Data exfiltration to attacker-controlled paths
308 /// - Potential privilege escalation via file overwrites
309 ///
310 /// # Example
311 ///
312 /// ```ignore
313 /// if let Some(warning) = config.file_sandbox.security_warning() {
314 /// tracing::warn!(target: "uni_db::security", "{}", warning);
315 /// }
316 /// ```
317 pub fn security_warning(&self) -> Option<&'static str> {
318 if !self.enabled {
319 Some(
320 "File sandbox is DISABLED. This allows unrestricted filesystem access \
321 for BACKUP, COPY, and EXPORT commands. Enable sandbox for server \
322 deployments: file_sandbox.enabled = true",
323 )
324 } else {
325 None
326 }
327 }
328
329 /// Returns whether the sandbox is in a potentially insecure state.
330 ///
331 /// Returns `true` if the sandbox is disabled or enabled with no allowed paths.
332 pub fn is_potentially_insecure(&self) -> bool {
333 !self.enabled || self.allowed_paths.is_empty()
334 }
335
336 /// Validate that a path is within the allowed sandbox.
337 /// Returns Ok(canonical_path) if allowed, Err if not.
338 pub fn validate_path(&self, path: &str) -> Result<PathBuf, String> {
339 if !self.enabled {
340 // Sandbox disabled - allow all paths
341 return Ok(PathBuf::from(path));
342 }
343
344 if self.allowed_paths.is_empty() {
345 return Err("File sandbox is enabled but no allowed paths configured".to_string());
346 }
347
348 // Resolve the path to canonical form to prevent traversal attacks
349 let input_path = Path::new(path);
350
351 // For paths that don't exist yet (e.g., export destinations), we need to
352 // check their parent directory exists and is within allowed paths
353 let canonical = if input_path.exists() {
354 input_path
355 .canonicalize()
356 .map_err(|e| format!("Failed to canonicalize path: {}", e))?
357 } else {
358 // Path doesn't exist - check parent
359 let parent = input_path
360 .parent()
361 .ok_or_else(|| "Invalid path: no parent directory".to_string())?;
362 if !parent.exists() {
363 return Err(format!(
364 "Parent directory does not exist: {}",
365 parent.display()
366 ));
367 }
368 let canonical_parent = parent
369 .canonicalize()
370 .map_err(|e| format!("Failed to canonicalize parent: {}", e))?;
371 // Reconstruct with canonical parent + original filename
372 let filename = input_path
373 .file_name()
374 .ok_or_else(|| "Invalid path: no filename".to_string())?;
375 canonical_parent.join(filename)
376 };
377
378 // Check if the canonical path is within any allowed directory
379 for allowed in &self.allowed_paths {
380 // Ensure allowed path is canonical too
381 let canonical_allowed = if allowed.exists() {
382 allowed.canonicalize().unwrap_or_else(|_| allowed.clone())
383 } else {
384 allowed.clone()
385 };
386
387 if canonical.starts_with(&canonical_allowed) {
388 return Ok(canonical);
389 }
390 }
391
392 Err(format!(
393 "Path '{}' is outside allowed sandbox directories. Allowed: {:?}",
394 path, self.allowed_paths
395 ))
396 }
397}
398
399#[derive(Clone, Debug)]
400pub struct UniConfig {
401 /// Maximum adjacency cache size in bytes (default: 1GB)
402 pub cache_size: usize,
403
404 /// Number of worker threads for parallel execution
405 pub parallelism: usize,
406
407 /// Size of each data morsel/batch (number of rows)
408 pub batch_size: usize,
409
410 /// Maximum size of traversal frontier before pruning
411 pub max_frontier_size: usize,
412
413 /// Auto-flush threshold for L0 buffer (default: 10_000 mutations)
414 pub auto_flush_threshold: usize,
415
416 /// Auto-flush interval for L0 buffer (default: 5 seconds).
417 /// Flush triggers if time elapsed AND mutation count >= auto_flush_min_mutations.
418 /// Set to None to disable time-based flush.
419 pub auto_flush_interval: Option<Duration>,
420
421 /// Minimum mutations required before time-based flush triggers (default: 1).
422 /// Prevents unnecessary flushes when there's minimal activity.
423 pub auto_flush_min_mutations: usize,
424
425 /// Enable write-ahead logging (default: true)
426 pub wal_enabled: bool,
427
428 /// Compaction configuration
429 pub compaction: CompactionConfig,
430
431 /// Write throttling configuration
432 pub throttle: WriteThrottleConfig,
433
434 /// File sandbox configuration for BACKUP/COPY/EXPORT commands.
435 /// MUST be enabled with allowed paths in server mode to prevent arbitrary file access.
436 pub file_sandbox: FileSandboxConfig,
437
438 /// Default query execution timeout (default: 30s)
439 pub query_timeout: Duration,
440
441 /// Default maximum memory per query (default: 1GB)
442 pub max_query_memory: usize,
443
444 /// Maximum transaction buffer memory in bytes (default: 1GB).
445 /// Limits memory usage during transactions to prevent OOM.
446 pub max_transaction_memory: usize,
447
448 /// Maximum rows for in-memory compaction (default: 5M, ~725MB at 145 bytes/row).
449 /// Configurable OOM guard to prevent memory exhaustion during compaction.
450 pub max_compaction_rows: usize,
451
452 /// Enable in-memory VID-to-labels index for O(1) lookups (default: true).
453 /// Memory cost: ~42 bytes per vertex (1M vertices ≈ 42MB).
454 pub enable_vid_labels_index: bool,
455
456 /// Object store resilience configuration
457 pub object_store: ObjectStoreConfig,
458
459 /// Background index rebuild configuration
460 pub index_rebuild: IndexRebuildConfig,
461}
462
463impl Default for UniConfig {
464 fn default() -> Self {
465 let parallelism = thread::available_parallelism()
466 .map(|n| n.get())
467 .unwrap_or(4);
468
469 Self {
470 cache_size: 1024 * 1024 * 1024, // 1GB
471 parallelism,
472 batch_size: 1024, // Default morsel size
473 max_frontier_size: 1_000_000,
474 auto_flush_threshold: 10_000,
475 auto_flush_interval: Some(Duration::from_secs(5)),
476 auto_flush_min_mutations: 1,
477 wal_enabled: true,
478 compaction: CompactionConfig::default(),
479 throttle: WriteThrottleConfig::default(),
480 file_sandbox: FileSandboxConfig::default(),
481 query_timeout: Duration::from_secs(30),
482 max_query_memory: 1024 * 1024 * 1024, // 1GB
483 max_transaction_memory: 1024 * 1024 * 1024, // 1GB
484 max_compaction_rows: 5_000_000, // 5M rows
485 enable_vid_labels_index: true, // Enable by default
486 object_store: ObjectStoreConfig::default(),
487 index_rebuild: IndexRebuildConfig::default(),
488 }
489 }
490}
491
492/// Cloud storage backend configuration.
493///
494/// Supports Amazon S3, Google Cloud Storage, and Azure Blob Storage.
495/// Each variant contains the credentials and connection parameters for
496/// its respective cloud provider.
497///
498/// # Examples
499///
500/// ```ignore
501/// // Create S3 configuration from environment variables
502/// let config = CloudStorageConfig::s3_from_env("my-bucket");
503///
504/// // Create explicit S3 configuration for LocalStack testing
505/// let config = CloudStorageConfig::S3 {
506/// bucket: "test-bucket".to_string(),
507/// region: Some("us-east-1".to_string()),
508/// endpoint: Some("http://localhost:4566".to_string()),
509/// access_key_id: Some("test".to_string()),
510/// secret_access_key: Some("test".to_string()),
511/// session_token: None,
512/// virtual_hosted_style: false,
513/// };
514/// ```
515#[derive(Clone, Debug)]
516pub enum CloudStorageConfig {
517 /// Amazon S3 storage configuration.
518 S3 {
519 /// S3 bucket name.
520 bucket: String,
521 /// AWS region (e.g., "us-east-1"). Uses AWS_REGION env var if None.
522 region: Option<String>,
523 /// Custom endpoint URL for S3-compatible services (MinIO, LocalStack).
524 endpoint: Option<String>,
525 /// AWS access key ID. Uses AWS_ACCESS_KEY_ID env var if None.
526 access_key_id: Option<String>,
527 /// AWS secret access key. Uses AWS_SECRET_ACCESS_KEY env var if None.
528 secret_access_key: Option<String>,
529 /// AWS session token for temporary credentials.
530 session_token: Option<String>,
531 /// Use virtual-hosted-style requests (bucket.s3.region.amazonaws.com).
532 virtual_hosted_style: bool,
533 },
534 /// Google Cloud Storage configuration.
535 Gcs {
536 /// GCS bucket name.
537 bucket: String,
538 /// Path to service account JSON key file.
539 service_account_path: Option<String>,
540 /// Service account JSON key content (alternative to path).
541 service_account_key: Option<String>,
542 },
543 /// Azure Blob Storage configuration.
544 Azure {
545 /// Azure container name.
546 container: String,
547 /// Azure storage account name.
548 account: String,
549 /// Azure storage account access key.
550 access_key: Option<String>,
551 /// Azure SAS token for limited access.
552 sas_token: Option<String>,
553 },
554}
555
556impl CloudStorageConfig {
557 /// Creates an S3 configuration using environment variables.
558 ///
559 /// Reads credentials from standard AWS environment variables:
560 /// - `AWS_ACCESS_KEY_ID`
561 /// - `AWS_SECRET_ACCESS_KEY`
562 /// - `AWS_SESSION_TOKEN` (optional)
563 /// - `AWS_REGION` or `AWS_DEFAULT_REGION`
564 /// - `AWS_ENDPOINT_URL` (optional, for S3-compatible services)
565 #[must_use]
566 pub fn s3_from_env(bucket: &str) -> Self {
567 Self::S3 {
568 bucket: bucket.to_string(),
569 region: std::env::var("AWS_REGION")
570 .or_else(|_| std::env::var("AWS_DEFAULT_REGION"))
571 .ok(),
572 endpoint: std::env::var("AWS_ENDPOINT_URL").ok(),
573 access_key_id: std::env::var("AWS_ACCESS_KEY_ID").ok(),
574 secret_access_key: std::env::var("AWS_SECRET_ACCESS_KEY").ok(),
575 session_token: std::env::var("AWS_SESSION_TOKEN").ok(),
576 virtual_hosted_style: false,
577 }
578 }
579
580 /// Creates a GCS configuration using environment variables.
581 ///
582 /// Reads service account path from `GOOGLE_APPLICATION_CREDENTIALS`.
583 #[must_use]
584 pub fn gcs_from_env(bucket: &str) -> Self {
585 Self::Gcs {
586 bucket: bucket.to_string(),
587 service_account_path: std::env::var("GOOGLE_APPLICATION_CREDENTIALS").ok(),
588 service_account_key: None,
589 }
590 }
591
592 /// Creates an Azure configuration using environment variables.
593 ///
594 /// Reads credentials from Azure environment variables:
595 /// - `AZURE_STORAGE_ACCOUNT`
596 /// - `AZURE_STORAGE_ACCESS_KEY` (optional)
597 /// - `AZURE_STORAGE_SAS_TOKEN` (optional)
598 ///
599 /// # Panics
600 ///
601 /// Panics if `AZURE_STORAGE_ACCOUNT` is not set.
602 #[must_use]
603 pub fn azure_from_env(container: &str) -> Self {
604 Self::Azure {
605 container: container.to_string(),
606 account: std::env::var("AZURE_STORAGE_ACCOUNT")
607 .expect("AZURE_STORAGE_ACCOUNT environment variable required"),
608 access_key: std::env::var("AZURE_STORAGE_ACCESS_KEY").ok(),
609 sas_token: std::env::var("AZURE_STORAGE_SAS_TOKEN").ok(),
610 }
611 }
612
613 /// Returns the bucket/container name for this configuration.
614 #[must_use]
615 pub fn bucket_name(&self) -> &str {
616 match self {
617 Self::S3 { bucket, .. } => bucket,
618 Self::Gcs { bucket, .. } => bucket,
619 Self::Azure { container, .. } => container,
620 }
621 }
622
623 /// Returns a URL-style identifier for this storage location.
624 #[must_use]
625 pub fn to_url(&self) -> String {
626 match self {
627 Self::S3 { bucket, .. } => format!("s3://{bucket}"),
628 Self::Gcs { bucket, .. } => format!("gs://{bucket}"),
629 Self::Azure {
630 container, account, ..
631 } => format!("az://{account}/{container}"),
632 }
633 }
634}
635
636#[cfg(test)]
637mod security_tests {
638 use super::*;
639
640 /// Tests for CWE-22 (Path Traversal) prevention in file sandbox.
641 mod file_sandbox {
642 use super::*;
643
644 #[test]
645 fn test_sandbox_disabled_allows_all_paths() {
646 let config = FileSandboxConfig::default();
647 assert!(!config.enabled);
648 // When disabled, all paths are allowed
649 assert!(config.validate_path("/tmp/test").is_ok());
650 }
651
652 #[test]
653 fn test_sandbox_enabled_with_no_paths_rejects() {
654 let config = FileSandboxConfig {
655 enabled: true,
656 allowed_paths: vec![],
657 };
658 let result = config.validate_path("/tmp/test");
659 assert!(result.is_err());
660 assert!(result.unwrap_err().contains("no allowed paths configured"));
661 }
662
663 #[test]
664 fn test_sandbox_rejects_outside_path() {
665 let config = FileSandboxConfig {
666 enabled: true,
667 allowed_paths: vec![PathBuf::from("/var/lib/uni")],
668 };
669 let result = config.validate_path("/etc/passwd");
670 assert!(result.is_err());
671 assert!(result.unwrap_err().contains("outside allowed sandbox"));
672 }
673
674 #[test]
675 fn test_is_potentially_insecure() {
676 // Disabled is insecure
677 let disabled = FileSandboxConfig::default();
678 assert!(disabled.is_potentially_insecure());
679
680 // Enabled with no paths is insecure
681 let no_paths = FileSandboxConfig {
682 enabled: true,
683 allowed_paths: vec![],
684 };
685 assert!(no_paths.is_potentially_insecure());
686
687 // Enabled with paths is secure
688 let secure = FileSandboxConfig::sandboxed(vec![PathBuf::from("/data")]);
689 assert!(!secure.is_potentially_insecure());
690 }
691
692 #[test]
693 fn test_security_warning_when_disabled() {
694 let disabled = FileSandboxConfig::default();
695 assert!(disabled.security_warning().is_some());
696
697 let enabled = FileSandboxConfig::sandboxed(vec![PathBuf::from("/data")]);
698 assert!(enabled.security_warning().is_none());
699 }
700
701 #[test]
702 fn test_deployment_mode_defaults() {
703 let embedded = FileSandboxConfig::default_for_mode(DeploymentMode::Embedded);
704 assert!(!embedded.enabled);
705
706 let server = FileSandboxConfig::default_for_mode(DeploymentMode::Server);
707 assert!(server.enabled);
708 assert!(!server.allowed_paths.is_empty());
709 }
710 }
711}