spatial-memory-mcp 1.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spatial_memory/__init__.py +97 -0
- spatial_memory/__main__.py +271 -0
- spatial_memory/adapters/__init__.py +7 -0
- spatial_memory/adapters/lancedb_repository.py +880 -0
- spatial_memory/config.py +769 -0
- spatial_memory/core/__init__.py +118 -0
- spatial_memory/core/cache.py +317 -0
- spatial_memory/core/circuit_breaker.py +297 -0
- spatial_memory/core/connection_pool.py +220 -0
- spatial_memory/core/consolidation_strategies.py +401 -0
- spatial_memory/core/database.py +3072 -0
- spatial_memory/core/db_idempotency.py +242 -0
- spatial_memory/core/db_indexes.py +576 -0
- spatial_memory/core/db_migrations.py +588 -0
- spatial_memory/core/db_search.py +512 -0
- spatial_memory/core/db_versioning.py +178 -0
- spatial_memory/core/embeddings.py +558 -0
- spatial_memory/core/errors.py +317 -0
- spatial_memory/core/file_security.py +701 -0
- spatial_memory/core/filesystem.py +178 -0
- spatial_memory/core/health.py +289 -0
- spatial_memory/core/helpers.py +79 -0
- spatial_memory/core/import_security.py +433 -0
- spatial_memory/core/lifecycle_ops.py +1067 -0
- spatial_memory/core/logging.py +194 -0
- spatial_memory/core/metrics.py +192 -0
- spatial_memory/core/models.py +660 -0
- spatial_memory/core/rate_limiter.py +326 -0
- spatial_memory/core/response_types.py +500 -0
- spatial_memory/core/security.py +588 -0
- spatial_memory/core/spatial_ops.py +430 -0
- spatial_memory/core/tracing.py +300 -0
- spatial_memory/core/utils.py +110 -0
- spatial_memory/core/validation.py +406 -0
- spatial_memory/factory.py +444 -0
- spatial_memory/migrations/__init__.py +40 -0
- spatial_memory/ports/__init__.py +11 -0
- spatial_memory/ports/repositories.py +630 -0
- spatial_memory/py.typed +0 -0
- spatial_memory/server.py +1214 -0
- spatial_memory/services/__init__.py +70 -0
- spatial_memory/services/decay_manager.py +411 -0
- spatial_memory/services/export_import.py +1031 -0
- spatial_memory/services/lifecycle.py +1139 -0
- spatial_memory/services/memory.py +412 -0
- spatial_memory/services/spatial.py +1152 -0
- spatial_memory/services/utility.py +429 -0
- spatial_memory/tools/__init__.py +5 -0
- spatial_memory/tools/definitions.py +695 -0
- spatial_memory/verify.py +140 -0
- spatial_memory_mcp-1.9.1.dist-info/METADATA +509 -0
- spatial_memory_mcp-1.9.1.dist-info/RECORD +55 -0
- spatial_memory_mcp-1.9.1.dist-info/WHEEL +4 -0
- spatial_memory_mcp-1.9.1.dist-info/entry_points.txt +2 -0
- spatial_memory_mcp-1.9.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
"""Consolidated security module for file path operations.
|
|
2
|
+
|
|
3
|
+
This module provides a unified security facade for all file path validation
|
|
4
|
+
operations in the spatial-memory-mcp project. It consolidates path traversal
|
|
5
|
+
prevention, symlink attack prevention, file size limits, and allowed directory
|
|
6
|
+
configuration into a single, production-ready interface.
|
|
7
|
+
|
|
8
|
+
Security Architecture:
|
|
9
|
+
----------------------
|
|
10
|
+
The security system implements defense-in-depth with multiple layers:
|
|
11
|
+
|
|
12
|
+
1. **Input Validation Layer**
|
|
13
|
+
- Pattern-based detection of known attack vectors
|
|
14
|
+
- URL decoding to catch encoded attacks
|
|
15
|
+
- Null byte injection prevention
|
|
16
|
+
|
|
17
|
+
2. **Path Canonicalization Layer**
|
|
18
|
+
- Resolution of symbolic path elements
|
|
19
|
+
- Detection of traversal after normalization
|
|
20
|
+
- UNC path blocking (Windows network shares)
|
|
21
|
+
|
|
22
|
+
3. **Access Control Layer**
|
|
23
|
+
- Directory allowlist enforcement
|
|
24
|
+
- Sensitive directory blocking
|
|
25
|
+
- File extension validation
|
|
26
|
+
|
|
27
|
+
4. **Runtime Protection Layer**
|
|
28
|
+
- Symlink detection and optional blocking
|
|
29
|
+
- File size limit enforcement
|
|
30
|
+
- TOCTOU (Time-of-Check-Time-of-Use) prevention
|
|
31
|
+
|
|
32
|
+
Threat Model:
|
|
33
|
+
-------------
|
|
34
|
+
This module defends against:
|
|
35
|
+
|
|
36
|
+
- **Path Traversal Attacks**: Attempts to access files outside allowed
|
|
37
|
+
directories using sequences like ../, URL encoding (%2e%2e), or
|
|
38
|
+
double encoding (%252e%252e).
|
|
39
|
+
|
|
40
|
+
- **Symlink Attacks**: Creating symlinks in allowed directories that
|
|
41
|
+
point to sensitive files elsewhere.
|
|
42
|
+
|
|
43
|
+
- **UNC Path Attacks**: Accessing network shares on Windows using
|
|
44
|
+
paths like \\\\server\\share.
|
|
45
|
+
|
|
46
|
+
- **Large File DoS**: Uploading excessively large files to exhaust
|
|
47
|
+
disk space or memory.
|
|
48
|
+
|
|
49
|
+
- **Extension Spoofing**: Attempting to import/export executable or
|
|
50
|
+
script files by disguising extensions.
|
|
51
|
+
|
|
52
|
+
- **TOCTOU Race Conditions**: Swapping files between validation and
|
|
53
|
+
actual file operations.
|
|
54
|
+
|
|
55
|
+
Usage Example:
|
|
56
|
+
--------------
|
|
57
|
+
from spatial_memory.core.security import (
|
|
58
|
+
FileSecurityManager,
|
|
59
|
+
SecurityConfig,
|
|
60
|
+
validate_export_path,
|
|
61
|
+
validate_import_file,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Using the high-level API
|
|
65
|
+
config = SecurityConfig(
|
|
66
|
+
export_allowed_paths=["./exports", "./backups"],
|
|
67
|
+
import_allowed_paths=["./imports"],
|
|
68
|
+
max_import_size_mb=100.0,
|
|
69
|
+
allow_symlinks=False,
|
|
70
|
+
)
|
|
71
|
+
manager = FileSecurityManager(config)
|
|
72
|
+
|
|
73
|
+
# Validate export path
|
|
74
|
+
safe_path = manager.validate_export_path("./exports/backup.parquet")
|
|
75
|
+
|
|
76
|
+
# Validate and open import file atomically (TOCTOU-safe)
|
|
77
|
+
path, handle = manager.validate_and_open_import("./imports/data.json")
|
|
78
|
+
try:
|
|
79
|
+
content = handle.read()
|
|
80
|
+
finally:
|
|
81
|
+
handle.close()
|
|
82
|
+
|
|
83
|
+
# Or use convenience functions with default settings
|
|
84
|
+
safe_path = validate_export_path("./exports/backup.parquet", config)
|
|
85
|
+
|
|
86
|
+
Module Dependencies:
|
|
87
|
+
--------------------
|
|
88
|
+
- spatial_memory.core.errors: Custom exception types
|
|
89
|
+
- spatial_memory.core.file_security: PathValidator implementation
|
|
90
|
+
- spatial_memory.core.import_security: Import record validation
|
|
91
|
+
- spatial_memory.config: Application configuration
|
|
92
|
+
|
|
93
|
+
Author: Spatial Memory MCP Team
|
|
94
|
+
Security Review: Phase 5 Implementation
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
from __future__ import annotations
|
|
98
|
+
|
|
99
|
+
import logging
|
|
100
|
+
from dataclasses import dataclass, field
|
|
101
|
+
from pathlib import Path
|
|
102
|
+
from typing import BinaryIO
|
|
103
|
+
|
|
104
|
+
from spatial_memory.core.errors import (
|
|
105
|
+
DimensionMismatchError,
|
|
106
|
+
FileSizeLimitError,
|
|
107
|
+
PathSecurityError,
|
|
108
|
+
SchemaValidationError,
|
|
109
|
+
ValidationError,
|
|
110
|
+
)
|
|
111
|
+
from spatial_memory.core.file_security import (
|
|
112
|
+
PATH_TRAVERSAL_PATTERNS,
|
|
113
|
+
SENSITIVE_DIRECTORIES,
|
|
114
|
+
VALID_EXTENSIONS,
|
|
115
|
+
PathValidator,
|
|
116
|
+
)
|
|
117
|
+
from spatial_memory.core.import_security import (
|
|
118
|
+
BatchValidationResult,
|
|
119
|
+
ImportValidationConfig,
|
|
120
|
+
ImportValidator,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
# Configuration
|
|
125
|
+
"SecurityConfig",
|
|
126
|
+
"DEFAULT_SECURITY_CONFIG",
|
|
127
|
+
# Manager class
|
|
128
|
+
"FileSecurityManager",
|
|
129
|
+
# Convenience functions
|
|
130
|
+
"validate_export_path",
|
|
131
|
+
"validate_import_path",
|
|
132
|
+
"validate_import_file",
|
|
133
|
+
"validate_import_records",
|
|
134
|
+
# Error types (re-exported for convenience)
|
|
135
|
+
"PathSecurityError",
|
|
136
|
+
"FileSizeLimitError",
|
|
137
|
+
"DimensionMismatchError",
|
|
138
|
+
"SchemaValidationError",
|
|
139
|
+
"ValidationError",
|
|
140
|
+
# Constants (re-exported for inspection)
|
|
141
|
+
"PATH_TRAVERSAL_PATTERNS",
|
|
142
|
+
"SENSITIVE_DIRECTORIES",
|
|
143
|
+
"VALID_EXTENSIONS",
|
|
144
|
+
# Validation result types
|
|
145
|
+
"BatchValidationResult",
|
|
146
|
+
"ImportValidationConfig",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
logger = logging.getLogger(__name__)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# =============================================================================
|
|
153
|
+
# Configuration
|
|
154
|
+
# =============================================================================
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class SecurityConfig:
|
|
159
|
+
"""Immutable security configuration for file operations.
|
|
160
|
+
|
|
161
|
+
This configuration is intentionally immutable (frozen=True) to prevent
|
|
162
|
+
runtime modification of security settings after initialization.
|
|
163
|
+
|
|
164
|
+
Attributes:
|
|
165
|
+
export_allowed_paths: Directories where exports are permitted.
|
|
166
|
+
Paths can be absolute or relative. Relative paths are resolved
|
|
167
|
+
against the current working directory.
|
|
168
|
+
|
|
169
|
+
import_allowed_paths: Directories where imports are permitted.
|
|
170
|
+
Same resolution rules as export_allowed_paths.
|
|
171
|
+
|
|
172
|
+
max_import_size_mb: Maximum file size for imports in megabytes.
|
|
173
|
+
Files exceeding this size will be rejected to prevent DoS
|
|
174
|
+
attacks through disk/memory exhaustion.
|
|
175
|
+
|
|
176
|
+
allow_symlinks: Whether to allow following symlinks. Default False
|
|
177
|
+
for security - symlinks can escape allowed directories.
|
|
178
|
+
|
|
179
|
+
max_import_records: Maximum number of records in an import file.
|
|
180
|
+
Prevents memory exhaustion from very large files.
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
config = SecurityConfig(
|
|
184
|
+
export_allowed_paths=["./exports", "/data/backups"],
|
|
185
|
+
import_allowed_paths=["./imports"],
|
|
186
|
+
max_import_size_mb=100.0,
|
|
187
|
+
allow_symlinks=False,
|
|
188
|
+
max_import_records=100_000,
|
|
189
|
+
)
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
export_allowed_paths: tuple[str, ...] = field(
|
|
193
|
+
default_factory=lambda: ("./exports", "./backups")
|
|
194
|
+
)
|
|
195
|
+
import_allowed_paths: tuple[str, ...] = field(
|
|
196
|
+
default_factory=lambda: ("./imports", "./backups")
|
|
197
|
+
)
|
|
198
|
+
max_import_size_mb: float = 100.0
|
|
199
|
+
allow_symlinks: bool = False
|
|
200
|
+
max_import_records: int = 100_000
|
|
201
|
+
|
|
202
|
+
def __post_init__(self) -> None:
|
|
203
|
+
"""Validate configuration values."""
|
|
204
|
+
# Convert lists to tuples if needed (for immutability)
|
|
205
|
+
if isinstance(self.export_allowed_paths, list):
|
|
206
|
+
object.__setattr__(
|
|
207
|
+
self, "export_allowed_paths", tuple(self.export_allowed_paths)
|
|
208
|
+
)
|
|
209
|
+
if isinstance(self.import_allowed_paths, list):
|
|
210
|
+
object.__setattr__(
|
|
211
|
+
self, "import_allowed_paths", tuple(self.import_allowed_paths)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Validate ranges
|
|
215
|
+
if self.max_import_size_mb <= 0:
|
|
216
|
+
raise ValueError("max_import_size_mb must be positive")
|
|
217
|
+
if self.max_import_size_mb > 10_000:
|
|
218
|
+
raise ValueError("max_import_size_mb cannot exceed 10GB")
|
|
219
|
+
if self.max_import_records <= 0:
|
|
220
|
+
raise ValueError("max_import_records must be positive")
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def max_import_size_bytes(self) -> int:
|
|
224
|
+
"""Return maximum import size in bytes."""
|
|
225
|
+
return int(self.max_import_size_mb * 1024 * 1024)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# Default security configuration for general use
|
|
229
|
+
DEFAULT_SECURITY_CONFIG = SecurityConfig()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# =============================================================================
|
|
233
|
+
# File Security Manager
|
|
234
|
+
# =============================================================================
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class FileSecurityManager:
|
|
238
|
+
"""Unified security manager for all file path operations.
|
|
239
|
+
|
|
240
|
+
This class provides a thread-safe, production-ready interface for
|
|
241
|
+
validating file paths used in export/import operations. It encapsulates
|
|
242
|
+
all security checks into a single interface.
|
|
243
|
+
|
|
244
|
+
Thread Safety:
|
|
245
|
+
This class is thread-safe. The underlying PathValidator is stateless
|
|
246
|
+
and only reads from immutable configuration.
|
|
247
|
+
|
|
248
|
+
Example:
|
|
249
|
+
manager = FileSecurityManager(config)
|
|
250
|
+
|
|
251
|
+
# Export validation (file doesn't need to exist)
|
|
252
|
+
safe_path = manager.validate_export_path("./exports/backup.parquet")
|
|
253
|
+
|
|
254
|
+
# Import validation (file must exist, size checked)
|
|
255
|
+
safe_path = manager.validate_import_path("./imports/data.json")
|
|
256
|
+
|
|
257
|
+
# TOCTOU-safe import (validates and opens atomically)
|
|
258
|
+
path, handle = manager.validate_and_open_import("./imports/data.json")
|
|
259
|
+
try:
|
|
260
|
+
data = handle.read()
|
|
261
|
+
finally:
|
|
262
|
+
handle.close()
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(self, config: SecurityConfig | None = None) -> None:
|
|
266
|
+
"""Initialize the security manager.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
config: Security configuration. Uses DEFAULT_SECURITY_CONFIG
|
|
270
|
+
if not provided.
|
|
271
|
+
"""
|
|
272
|
+
self._config = config or DEFAULT_SECURITY_CONFIG
|
|
273
|
+
|
|
274
|
+
# Initialize the underlying PathValidator
|
|
275
|
+
self._validator = PathValidator(
|
|
276
|
+
allowed_export_paths=list(self._config.export_allowed_paths),
|
|
277
|
+
allowed_import_paths=list(self._config.import_allowed_paths),
|
|
278
|
+
allow_symlinks=self._config.allow_symlinks,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
logger.debug(
|
|
282
|
+
"FileSecurityManager initialized with config: "
|
|
283
|
+
f"export_paths={self._config.export_allowed_paths}, "
|
|
284
|
+
f"import_paths={self._config.import_allowed_paths}, "
|
|
285
|
+
f"max_size_mb={self._config.max_import_size_mb}, "
|
|
286
|
+
f"allow_symlinks={self._config.allow_symlinks}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def config(self) -> SecurityConfig:
|
|
291
|
+
"""Return the security configuration (read-only)."""
|
|
292
|
+
return self._config
|
|
293
|
+
|
|
294
|
+
def validate_export_path(self, path: str | Path) -> Path:
|
|
295
|
+
"""Validate a path for export operations.
|
|
296
|
+
|
|
297
|
+
Performs all security checks without requiring the file to exist.
|
|
298
|
+
Parent directories will be created during export if needed.
|
|
299
|
+
|
|
300
|
+
Security checks performed:
|
|
301
|
+
1. Path traversal pattern detection
|
|
302
|
+
2. URL decoding and re-checking
|
|
303
|
+
3. UNC path blocking
|
|
304
|
+
4. Path canonicalization
|
|
305
|
+
5. Extension validation (.parquet, .json, .csv only)
|
|
306
|
+
6. Symlink detection (if file exists and symlinks disabled)
|
|
307
|
+
7. Allowlist validation
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
path: The path to validate. Can be absolute or relative.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Canonicalized Path object that is safe to use.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
PathSecurityError: If the path fails any security check.
|
|
317
|
+
ValueError: If the path is empty or contains null bytes.
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
safe_path = manager.validate_export_path("./exports/backup.parquet")
|
|
321
|
+
# Use safe_path for actual file writing
|
|
322
|
+
"""
|
|
323
|
+
return self._validator.validate_export_path(path)
|
|
324
|
+
|
|
325
|
+
def validate_import_path(self, path: str | Path) -> Path:
|
|
326
|
+
"""Validate a path for import operations.
|
|
327
|
+
|
|
328
|
+
Performs all export validation checks plus:
|
|
329
|
+
- File existence verification
|
|
330
|
+
- Directory rejection (must be a file)
|
|
331
|
+
- File size limit enforcement
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
path: The path to validate. Can be absolute or relative.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Canonicalized Path object that is safe to use.
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
PathSecurityError: If the path fails any security check.
|
|
341
|
+
FileSizeLimitError: If the file exceeds the size limit.
|
|
342
|
+
ValueError: If the path is empty or contains null bytes.
|
|
343
|
+
|
|
344
|
+
Example:
|
|
345
|
+
safe_path = manager.validate_import_path("./imports/data.json")
|
|
346
|
+
# Use safe_path for actual file reading
|
|
347
|
+
"""
|
|
348
|
+
return self._validator.validate_import_path(
|
|
349
|
+
path, max_size_bytes=self._config.max_import_size_bytes
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def validate_and_open_import(
|
|
353
|
+
self, path: str | Path
|
|
354
|
+
) -> tuple[Path, BinaryIO]:
|
|
355
|
+
"""Atomically validate and open a file for import.
|
|
356
|
+
|
|
357
|
+
This method prevents TOCTOU (Time-of-Check-Time-of-Use) race
|
|
358
|
+
conditions by opening the file FIRST, then validating properties
|
|
359
|
+
on the open file descriptor.
|
|
360
|
+
|
|
361
|
+
IMPORTANT: The caller MUST use the returned file handle for reading.
|
|
362
|
+
DO NOT re-open the file by path after this call.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
path: The path to validate and open.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Tuple of (canonical_path, file_handle). The file handle is
|
|
369
|
+
opened in binary read mode ('rb'). Caller is responsible
|
|
370
|
+
for closing it.
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
PathSecurityError: If the path fails any security check.
|
|
374
|
+
FileSizeLimitError: If the file exceeds the size limit.
|
|
375
|
+
ValueError: If the path is empty or contains null bytes.
|
|
376
|
+
|
|
377
|
+
Example:
|
|
378
|
+
path, handle = manager.validate_and_open_import("./imports/data.json")
|
|
379
|
+
try:
|
|
380
|
+
data = handle.read()
|
|
381
|
+
# Process data...
|
|
382
|
+
finally:
|
|
383
|
+
handle.close()
|
|
384
|
+
"""
|
|
385
|
+
return self._validator.validate_and_open_import_file(
|
|
386
|
+
path, max_size_bytes=self._config.max_import_size_bytes
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def validate_import_records(
|
|
390
|
+
self,
|
|
391
|
+
records: list[dict[str, object]],
|
|
392
|
+
expected_vector_dim: int,
|
|
393
|
+
fail_fast: bool = False,
|
|
394
|
+
) -> BatchValidationResult:
|
|
395
|
+
"""Validate a batch of import records for schema compliance.
|
|
396
|
+
|
|
397
|
+
Validates each record against the import schema:
|
|
398
|
+
- Required fields (content)
|
|
399
|
+
- Optional field types (namespace, tags, importance, metadata, vector)
|
|
400
|
+
- Vector dimension matching
|
|
401
|
+
- Value constraints (importance range, etc.)
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
records: List of record dictionaries to validate.
|
|
405
|
+
expected_vector_dim: Expected vector dimensions.
|
|
406
|
+
fail_fast: If True, stop on first error.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
BatchValidationResult with validation statistics and errors.
|
|
410
|
+
|
|
411
|
+
Example:
|
|
412
|
+
result = manager.validate_import_records(records, expected_dim=384)
|
|
413
|
+
if not result.is_valid:
|
|
414
|
+
for idx, errors in result.errors:
|
|
415
|
+
print(f"Record {idx} errors: {errors}")
|
|
416
|
+
"""
|
|
417
|
+
config = ImportValidationConfig(
|
|
418
|
+
expected_vector_dim=expected_vector_dim,
|
|
419
|
+
fail_fast=fail_fast,
|
|
420
|
+
max_errors=100,
|
|
421
|
+
)
|
|
422
|
+
validator = ImportValidator(config)
|
|
423
|
+
return validator.validate_batch(records, expected_vector_dim)
|
|
424
|
+
|
|
425
|
+
def is_path_safe(self, path: str | Path, operation: str = "export") -> bool:
|
|
426
|
+
"""Check if a path passes security validation.
|
|
427
|
+
|
|
428
|
+
Convenience method that returns a boolean instead of raising.
|
|
429
|
+
Useful for pre-flight checks in UI code.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
path: The path to check.
|
|
433
|
+
operation: Either "export" or "import".
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
True if the path passes all security checks, False otherwise.
|
|
437
|
+
|
|
438
|
+
Example:
|
|
439
|
+
if manager.is_path_safe(user_input, "export"):
|
|
440
|
+
# Proceed with export
|
|
441
|
+
pass
|
|
442
|
+
else:
|
|
443
|
+
# Show error to user
|
|
444
|
+
pass
|
|
445
|
+
"""
|
|
446
|
+
try:
|
|
447
|
+
if operation == "export":
|
|
448
|
+
self.validate_export_path(path)
|
|
449
|
+
elif operation == "import":
|
|
450
|
+
self.validate_import_path(path)
|
|
451
|
+
else:
|
|
452
|
+
raise ValueError(f"Unknown operation: {operation}")
|
|
453
|
+
return True
|
|
454
|
+
except (PathSecurityError, FileSizeLimitError, ValueError):
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# =============================================================================
|
|
459
|
+
# Convenience Functions
|
|
460
|
+
# =============================================================================
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def validate_export_path(
|
|
464
|
+
path: str | Path,
|
|
465
|
+
config: SecurityConfig | None = None,
|
|
466
|
+
) -> Path:
|
|
467
|
+
"""Convenience function to validate an export path.
|
|
468
|
+
|
|
469
|
+
Creates a FileSecurityManager with the provided config and validates
|
|
470
|
+
the path. For repeated validations, prefer creating a manager instance.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
path: The path to validate.
|
|
474
|
+
config: Security configuration (uses defaults if not provided).
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Canonicalized Path object that is safe to use.
|
|
478
|
+
|
|
479
|
+
Raises:
|
|
480
|
+
PathSecurityError: If the path fails security checks.
|
|
481
|
+
"""
|
|
482
|
+
manager = FileSecurityManager(config)
|
|
483
|
+
return manager.validate_export_path(path)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def validate_import_path(
|
|
487
|
+
path: str | Path,
|
|
488
|
+
config: SecurityConfig | None = None,
|
|
489
|
+
) -> Path:
|
|
490
|
+
"""Convenience function to validate an import path.
|
|
491
|
+
|
|
492
|
+
Creates a FileSecurityManager with the provided config and validates
|
|
493
|
+
the path. For repeated validations, prefer creating a manager instance.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
path: The path to validate.
|
|
497
|
+
config: Security configuration (uses defaults if not provided).
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Canonicalized Path object that is safe to use.
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
PathSecurityError: If the path fails security checks.
|
|
504
|
+
FileSizeLimitError: If the file exceeds size limits.
|
|
505
|
+
"""
|
|
506
|
+
manager = FileSecurityManager(config)
|
|
507
|
+
return manager.validate_import_path(path)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def validate_import_file(
|
|
511
|
+
path: str | Path,
|
|
512
|
+
config: SecurityConfig | None = None,
|
|
513
|
+
) -> tuple[Path, BinaryIO]:
|
|
514
|
+
"""Convenience function to validate and open an import file atomically.
|
|
515
|
+
|
|
516
|
+
This is the TOCTOU-safe way to open import files. The returned file
|
|
517
|
+
handle MUST be used for reading - do not re-open by path.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
path: The path to validate and open.
|
|
521
|
+
config: Security configuration (uses defaults if not provided).
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Tuple of (canonical_path, file_handle).
|
|
525
|
+
|
|
526
|
+
Raises:
|
|
527
|
+
PathSecurityError: If the path fails security checks.
|
|
528
|
+
FileSizeLimitError: If the file exceeds size limits.
|
|
529
|
+
"""
|
|
530
|
+
manager = FileSecurityManager(config)
|
|
531
|
+
return manager.validate_and_open_import(path)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def validate_import_records(
|
|
535
|
+
records: list[dict[str, object]],
|
|
536
|
+
expected_vector_dim: int,
|
|
537
|
+
fail_fast: bool = False,
|
|
538
|
+
) -> BatchValidationResult:
|
|
539
|
+
"""Convenience function to validate import records.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
records: List of record dictionaries to validate.
|
|
543
|
+
expected_vector_dim: Expected vector dimensions.
|
|
544
|
+
fail_fast: If True, stop on first error.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
BatchValidationResult with validation statistics.
|
|
548
|
+
"""
|
|
549
|
+
config = ImportValidationConfig(
|
|
550
|
+
expected_vector_dim=expected_vector_dim,
|
|
551
|
+
fail_fast=fail_fast,
|
|
552
|
+
max_errors=100,
|
|
553
|
+
)
|
|
554
|
+
validator = ImportValidator(config)
|
|
555
|
+
return validator.validate_batch(records, expected_vector_dim)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# =============================================================================
|
|
559
|
+
# Factory Function for Settings Integration
|
|
560
|
+
# =============================================================================
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def create_security_manager_from_settings() -> FileSecurityManager:
|
|
564
|
+
"""Create a FileSecurityManager from application settings.
|
|
565
|
+
|
|
566
|
+
Reads security configuration from the application Settings object
|
|
567
|
+
(environment variables or .env file).
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
Configured FileSecurityManager instance.
|
|
571
|
+
|
|
572
|
+
Example:
|
|
573
|
+
manager = create_security_manager_from_settings()
|
|
574
|
+
safe_path = manager.validate_export_path(user_path)
|
|
575
|
+
"""
|
|
576
|
+
from spatial_memory.config import get_settings
|
|
577
|
+
|
|
578
|
+
settings = get_settings()
|
|
579
|
+
|
|
580
|
+
config = SecurityConfig(
|
|
581
|
+
export_allowed_paths=tuple(settings.export_allowed_paths),
|
|
582
|
+
import_allowed_paths=tuple(settings.import_allowed_paths),
|
|
583
|
+
max_import_size_mb=settings.import_max_file_size_mb,
|
|
584
|
+
allow_symlinks=settings.export_allow_symlinks or settings.import_allow_symlinks,
|
|
585
|
+
max_import_records=settings.import_max_records,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
return FileSecurityManager(config)
|