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/__init__.py +2 -2
- ostruct/cli/cli.py +466 -604
- ostruct/cli/click_options.py +257 -0
- ostruct/cli/errors.py +234 -183
- ostruct/cli/file_info.py +154 -50
- ostruct/cli/file_list.py +189 -64
- ostruct/cli/file_utils.py +95 -67
- ostruct/cli/path_utils.py +58 -77
- ostruct/cli/security/__init__.py +32 -0
- ostruct/cli/security/allowed_checker.py +47 -0
- ostruct/cli/security/case_manager.py +75 -0
- ostruct/cli/security/errors.py +184 -0
- ostruct/cli/security/normalization.py +161 -0
- ostruct/cli/security/safe_joiner.py +211 -0
- ostruct/cli/security/security_manager.py +353 -0
- ostruct/cli/security/symlink_resolver.py +483 -0
- ostruct/cli/security/types.py +108 -0
- ostruct/cli/security/windows_paths.py +404 -0
- ostruct/cli/template_filters.py +8 -5
- ostruct/cli/template_io.py +4 -2
- {ostruct_cli-0.2.0.dist-info → ostruct_cli-0.4.0.dist-info}/METADATA +9 -6
- ostruct_cli-0.4.0.dist-info/RECORD +36 -0
- ostruct/cli/security.py +0 -323
- ostruct/cli/security_types.py +0 -49
- ostruct_cli-0.2.0.dist-info/RECORD +0 -27
- {ostruct_cli-0.2.0.dist-info → ostruct_cli-0.4.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.2.0.dist-info → ostruct_cli-0.4.0.dist-info}/WHEEL +0 -0
- {ostruct_cli-0.2.0.dist-info → ostruct_cli-0.4.0.dist-info}/entry_points.txt +0 -0
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)
|
ostruct/cli/security_types.py
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|