ostruct-cli 0.2.0__py3-none-any.whl → 0.4.0__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.
ostruct/cli/security.py DELETED
@@ -1,323 +0,0 @@
1
- """Security management for file access.
2
-
3
- This module provides security checks for file access, including:
4
- - Base directory restrictions
5
- - Allowed directory validation
6
- - Path traversal prevention
7
- - Temporary directory handling
8
- """
9
-
10
- import logging
11
- import os
12
- import tempfile
13
- from pathlib import Path
14
- from typing import List, Optional, Set
15
-
16
- from .errors import DirectoryNotFoundError, PathSecurityError
17
- from .security_types import SecurityManagerProtocol
18
-
19
-
20
- def is_temp_file(path: str) -> bool:
21
- """Check if a file is in a temporary directory.
22
-
23
- Args:
24
- path: Path to check (will be converted to absolute path)
25
-
26
- Returns:
27
- True if the path is in a temporary directory, False otherwise
28
-
29
- Note:
30
- This function handles platform-specific path normalization, including symlinks
31
- (e.g., on macOS where /var is symlinked to /private/var).
32
- """
33
- # Normalize the input path (resolve symlinks)
34
- abs_path = os.path.realpath(path)
35
-
36
- # Get all potential temp directories and normalize them
37
- temp_dirs = set()
38
- # System temp dir (platform independent)
39
- temp_dirs.add(os.path.realpath(tempfile.gettempdir()))
40
-
41
- # Common Unix/Linux/macOS temp locations
42
- unix_temp_dirs = ["/tmp", "/var/tmp", "/var/folders"]
43
- for temp_dir in unix_temp_dirs:
44
- if os.path.exists(temp_dir):
45
- temp_dirs.add(os.path.realpath(temp_dir))
46
-
47
- # Windows temp locations (if on Windows)
48
- if os.name == "nt":
49
- if "TEMP" in os.environ:
50
- temp_dirs.add(os.path.realpath(os.environ["TEMP"]))
51
- if "TMP" in os.environ:
52
- temp_dirs.add(os.path.realpath(os.environ["TMP"]))
53
-
54
- # Check if file is in any temp directory using normalized paths
55
- abs_path_parts = os.path.normpath(abs_path).split(os.sep)
56
- for temp_dir in temp_dirs:
57
- temp_dir_parts = os.path.normpath(temp_dir).split(os.sep)
58
- # Check if the path starts with the temp directory components
59
- if len(abs_path_parts) >= len(temp_dir_parts) and all(
60
- a == b
61
- for a, b in zip(
62
- abs_path_parts[: len(temp_dir_parts)], temp_dir_parts
63
- )
64
- ):
65
- return True
66
-
67
- return False
68
-
69
-
70
- class SecurityManager(SecurityManagerProtocol):
71
- """Manages security for file access.
72
-
73
- Validates all file access against a base directory and optional
74
- allowed directories. Prevents unauthorized access and directory
75
- traversal attacks.
76
-
77
- The security model is based on:
78
- 1. A base directory that serves as the root for all file operations
79
- 2. A set of explicitly allowed directories that can be accessed outside the base directory
80
- 3. Special handling for temporary directories that are always allowed
81
-
82
- All paths are normalized using realpath() to handle symlinks consistently across platforms.
83
- """
84
-
85
- def __init__(
86
- self,
87
- base_dir: Optional[str] = None,
88
- allowed_dirs: Optional[List[str]] = None,
89
- ):
90
- """Initialize security manager.
91
-
92
- Args:
93
- base_dir: Base directory for file access. Defaults to current working directory.
94
- allowed_dirs: Optional list of additional allowed directories
95
-
96
- All paths are normalized using realpath to handle symlinks
97
- and relative paths consistently across platforms.
98
- """
99
- logger = logging.getLogger("ostruct")
100
- logger.debug("Initializing SecurityManager")
101
- self._base_dir = Path(os.path.realpath(base_dir or os.getcwd()))
102
- logger.debug("Base directory set to: %s", self._base_dir)
103
-
104
- self._allowed_dirs: Set[Path] = set()
105
- if allowed_dirs:
106
- for directory in allowed_dirs:
107
- logger.debug("Adding allowed directory: %s", directory)
108
- self.add_allowed_dir(directory)
109
-
110
- @property
111
- def base_dir(self) -> Path:
112
- """Get the base directory."""
113
- return self._base_dir
114
-
115
- @property
116
- def allowed_dirs(self) -> List[Path]:
117
- """Get the list of allowed directories."""
118
- return sorted(self._allowed_dirs) # Sort for consistent ordering
119
-
120
- def add_allowed_dir(self, directory: str) -> None:
121
- """Add a directory to the set of allowed directories.
122
-
123
- Args:
124
- directory: Directory to allow access to
125
-
126
- Raises:
127
- DirectoryNotFoundError: If directory does not exist
128
- """
129
- logger = logging.getLogger("ostruct")
130
- logger.debug("Adding allowed directory: %s", directory)
131
- real_path = Path(os.path.realpath(directory))
132
- logger.debug("Resolved real path: %s", real_path)
133
-
134
- if not real_path.exists():
135
- logger.debug("Directory not found: %s", directory)
136
- raise DirectoryNotFoundError(f"Directory not found: {directory}")
137
- if not real_path.is_dir():
138
- logger.debug("Path is not a directory: %s", directory)
139
- raise DirectoryNotFoundError(
140
- f"Path is not a directory: {directory}"
141
- )
142
- self._allowed_dirs.add(real_path)
143
- logger.debug("Successfully added allowed directory: %s", real_path)
144
-
145
- def add_allowed_dirs_from_file(self, file_path: str) -> None:
146
- """Add allowed directories from a file.
147
-
148
- Args:
149
- file_path: Path to file containing allowed directories (one per line)
150
-
151
- Raises:
152
- PathSecurityError: If file_path is outside allowed directories
153
- FileNotFoundError: If file does not exist
154
- ValueError: If file contains invalid directories
155
- """
156
- real_path = Path(os.path.realpath(file_path))
157
-
158
- # First validate the file path itself
159
- try:
160
- self.validate_path(
161
- str(real_path), purpose="read allowed directories"
162
- )
163
- except PathSecurityError:
164
- raise PathSecurityError.from_expanded_paths(
165
- original_path=file_path,
166
- expanded_path=str(real_path),
167
- error_logged=True,
168
- base_dir=str(self._base_dir),
169
- allowed_dirs=[str(d) for d in self._allowed_dirs],
170
- )
171
-
172
- if not real_path.exists():
173
- raise FileNotFoundError(f"File not found: {file_path}")
174
-
175
- with open(real_path) as f:
176
- for line in f:
177
- directory = line.strip()
178
- if directory and not directory.startswith("#"):
179
- self.add_allowed_dir(directory)
180
-
181
- def is_path_allowed(self, path: str) -> bool:
182
- """Check if a path is allowed.
183
-
184
- A path is allowed if it is:
185
- 1. Under the normalized base directory
186
- 2. Under any normalized allowed directory
187
-
188
- The path must also exist.
189
-
190
- Args:
191
- path: Path to check
192
-
193
- Returns:
194
- bool: True if path exists and is allowed, False otherwise
195
- """
196
- logger = logging.getLogger("ostruct")
197
- logger.debug("Checking if path is allowed: %s", path)
198
- logger.debug("Base directory: %s", self._base_dir)
199
- logger.debug("Allowed directories: %s", self._allowed_dirs)
200
-
201
- try:
202
- real_path = Path(os.path.realpath(path))
203
- logger.debug("Resolved real path: %s", real_path)
204
-
205
- # Check if the path exists
206
- if not real_path.exists():
207
- logger.debug("Path does not exist")
208
- return False
209
-
210
- except (ValueError, OSError) as e:
211
- logger.debug("Failed to resolve real path: %s", e)
212
- return False
213
-
214
- try:
215
- if real_path.is_relative_to(self._base_dir):
216
- logger.debug("Path is relative to base directory")
217
- return True
218
- except ValueError:
219
- logger.debug("Path is not relative to base directory")
220
-
221
- for allowed_dir in self._allowed_dirs:
222
- try:
223
- if real_path.is_relative_to(allowed_dir):
224
- logger.debug(
225
- "Path is relative to allowed directory: %s",
226
- allowed_dir,
227
- )
228
- return True
229
- except ValueError:
230
- logger.debug(
231
- "Path is not relative to allowed directory: %s",
232
- allowed_dir,
233
- )
234
- continue
235
-
236
- logger.debug("Path is not allowed")
237
- return False
238
-
239
- def validate_path(self, path: str, purpose: str = "access") -> Path:
240
- """Validate and normalize a path.
241
-
242
- Args:
243
- path: Path to validate
244
- purpose: Description of intended access (for error messages)
245
-
246
- Returns:
247
- Path: Normalized path if valid
248
-
249
- Raises:
250
- PathSecurityError: If path is not allowed
251
- """
252
- logger = logging.getLogger("ostruct")
253
- logger.debug("Validating path for %s: %s", purpose, path)
254
-
255
- try:
256
- real_path = Path(os.path.realpath(path))
257
- logger.debug("Resolved real path: %s", real_path)
258
- except (ValueError, OSError) as e:
259
- logger.error("Invalid path format: %s", e)
260
- raise PathSecurityError(
261
- f"Invalid path format: {e}", error_logged=True
262
- )
263
-
264
- if not self.is_path_allowed(str(real_path)):
265
- logger.error(
266
- "Access denied: %s is outside base directory and not in allowed directories",
267
- path,
268
- )
269
- raise PathSecurityError.from_expanded_paths(
270
- original_path=path,
271
- expanded_path=str(real_path),
272
- base_dir=str(self._base_dir),
273
- allowed_dirs=[str(d) for d in self._allowed_dirs],
274
- error_logged=True,
275
- )
276
-
277
- logger.debug("Path validation successful")
278
- return real_path
279
-
280
- def is_allowed_file(self, path: str) -> bool:
281
- """Check if file access is allowed.
282
-
283
- Args:
284
- path: Path to check
285
-
286
- Returns:
287
- bool: True if file exists and is allowed
288
- """
289
- try:
290
- real_path = Path(os.path.realpath(path))
291
- return self.is_path_allowed(str(real_path)) and real_path.is_file()
292
- except (ValueError, OSError):
293
- return False
294
-
295
- def is_allowed_path(self, path_str: str) -> bool:
296
- """Check if string path is allowed.
297
-
298
- Args:
299
- path_str: Path string to check
300
-
301
- Returns:
302
- bool: True if path is allowed
303
- """
304
- try:
305
- return self.is_path_allowed(path_str)
306
- except (ValueError, OSError):
307
- return False
308
-
309
- def resolve_path(self, path: str) -> Path:
310
- """Resolve and validate a path.
311
-
312
- This is an alias for validate_path() for backward compatibility.
313
-
314
- Args:
315
- path: Path to resolve and validate
316
-
317
- Returns:
318
- Path: Normalized path if valid
319
-
320
- Raises:
321
- PathSecurityError: If path is not allowed
322
- """
323
- return self.validate_path(path)
@@ -1,49 +0,0 @@
1
- """Security type definitions and protocols."""
2
-
3
- from pathlib import Path
4
- from typing import List, Protocol
5
-
6
-
7
- class SecurityManagerProtocol(Protocol):
8
- """Protocol defining the interface for security managers."""
9
-
10
- @property
11
- def base_dir(self) -> Path:
12
- """Get the base directory."""
13
- ...
14
-
15
- @property
16
- def allowed_dirs(self) -> List[Path]:
17
- """Get the list of allowed directories."""
18
- ...
19
-
20
- def add_allowed_dir(self, directory: str) -> None:
21
- """Add a directory to the set of allowed directories."""
22
- ...
23
-
24
- def add_allowed_dirs_from_file(self, file_path: str) -> None:
25
- """Add allowed directories from a file."""
26
- ...
27
-
28
- def is_path_allowed(self, path: str) -> bool:
29
- """Check if a path is allowed."""
30
- ...
31
-
32
- def validate_path(self, path: str, purpose: str = "access") -> Path:
33
- """Validate and normalize a path."""
34
- ...
35
-
36
- def is_allowed_file(self, path: str) -> bool:
37
- """Check if file access is allowed."""
38
- ...
39
-
40
- def is_allowed_path(self, path_str: str) -> bool:
41
- """Check if string path is allowed."""
42
- ...
43
-
44
- def resolve_path(self, path: str) -> Path:
45
- """Resolve and validate a path.
46
-
47
- This is an alias for validate_path() for backward compatibility.
48
- """
49
- ...
@@ -1,27 +0,0 @@
1
- ostruct/__init__.py,sha256=X6zo6V7ZNMv731Wi388aTVQngD1410ExGwGx4J6lpyo,187
2
- ostruct/cli/__init__.py,sha256=dbdUfKRT4ARSHgjsZOmQ-TJT1mHwL7L3xMuilPkhG4Q,411
3
- ostruct/cli/cache_manager.py,sha256=ej3KrRfkKKZ_lEp2JswjbJ5bW2ncsvna9NeJu81cqqs,5192
4
- ostruct/cli/cli.py,sha256=jJ60txGOA4ju-4Z057FA8eRnrpxTHDLBn6bZLNsJy9Q,70170
5
- ostruct/cli/errors.py,sha256=vxfjXpUbsarYJoTBaGFpjpa8wXqOMGQryTh-roUhRuU,9454
6
- ostruct/cli/file_info.py,sha256=SdU_V5g4R0o41tOTlobOF5cibZfb9qS12fK7G2XNfRs,10481
7
- ostruct/cli/file_list.py,sha256=srrouvhimoklNo69nWjrKyUN-5zTOmtVj_swdBZPBTk,7105
8
- ostruct/cli/file_utils.py,sha256=uqEvRoWskzKUd5akA-NtdKtLWByKMTXOiVJiiO7uXus,21153
9
- ostruct/cli/path_utils.py,sha256=jhJsvmxsq0_FU5cWwB-JoEymGbvB2JX9Q0a-dUf0s1w,4461
10
- ostruct/cli/progress.py,sha256=rj9nVEco5UeZORMbzd7mFJpFGJjbH9KbBFh5oTE5Anw,3415
11
- ostruct/cli/security.py,sha256=j7oRb7UM_Mndl_CvnQlql8eyudcAVgfCkeQuXIQyME4,10945
12
- ostruct/cli/security_types.py,sha256=oSNbaKjhZVHB6pQEG_WOUhUYKlw9cl4uGOBx2R9BxRk,1341
13
- ostruct/cli/template_env.py,sha256=S2ZvxuMQMicodSVqUhrw0kOzbNmlpQjSHtWlOwjXCms,1538
14
- ostruct/cli/template_extensions.py,sha256=tJN3HGAS2yzGI8Up6STPday8NVL0VV6UCClBrtDKYr0,1623
15
- ostruct/cli/template_filters.py,sha256=SNp7PR4ZbuC9BVUlEgwzd6VZYjI0lsobTabLiJe_sZM,19030
16
- ostruct/cli/template_io.py,sha256=6rDw2Wx6czK1VntKGUM6cvyMbMWojt41hUlYRpfQuoc,8749
17
- ostruct/cli/template_rendering.py,sha256=GrQAcKpGe6QEjSVQkOjpegMcor9LzVUikGmmEVgiWCE,12391
18
- ostruct/cli/template_schema.py,sha256=ckH4rUZnEgfm_BHS9LnMGr8LtDxRmZ0C6UBVrSp8KTc,19604
19
- ostruct/cli/template_utils.py,sha256=QGgewxU_Tgn81J5U-Y4xfi67CkN2dEqXI7PsaNiI9es,7812
20
- ostruct/cli/template_validation.py,sha256=q3ACw4TscdekJb3Z3CTYw0YPEYttqjKjm74ap4lWtU4,11737
21
- ostruct/cli/utils.py,sha256=1UCl4rHjBWKR5EKugvlVGHiHjO3XXmqvkgeAUSyIPDU,831
22
- ostruct/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- ostruct_cli-0.2.0.dist-info/LICENSE,sha256=QUOY6QCYVxAiH8vdrUTDqe3i9hQ5bcNczppDSVpLTjk,1068
24
- ostruct_cli-0.2.0.dist-info/METADATA,sha256=uduwQEF87qBeSBhzFg9AWdFqnDM1TdRyvdRwq-IKqZQ,5300
25
- ostruct_cli-0.2.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
26
- ostruct_cli-0.2.0.dist-info/entry_points.txt,sha256=NFq9IuqHVTem0j9zKjV8C1si_zGcP1RL6Wbvt9fUDXw,48
27
- ostruct_cli-0.2.0.dist-info/RECORD,,