vaulttool 2.0.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.
vaulttool/config.py ADDED
@@ -0,0 +1,264 @@
1
+ """Configuration loading and validation for VaultTool.
2
+
3
+ This module handles loading VaultTool configuration from YAML files,
4
+ with support for multiple configuration file locations, environment variable
5
+ overrides, and validation of required configuration keys.
6
+
7
+ Environment Variable Support:
8
+ Configuration can be overridden using environment variables with the
9
+ VAULTTOOL_ prefix. For list values, use comma-separated strings.
10
+
11
+ Examples:
12
+ VAULTTOOL_OPTIONS_KEY_FILE=/path/to/key
13
+ VAULTTOOL_OPTIONS_USE_SUFFIX_FALLBACK=true
14
+ VAULTTOOL_INCLUDE_PATTERNS="*.env,*.secret"
15
+ VAULTTOOL_EXCLUDE_DIRECTORIES=".git,.venv"
16
+ """
17
+
18
+ import os
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List
21
+
22
+ import yaml
23
+
24
+
25
+ def _parse_bool(value: str) -> bool:
26
+ """Parse boolean value from string.
27
+
28
+ Args:
29
+ value: String value to parse (case-insensitive)
30
+
31
+ Returns:
32
+ Boolean value
33
+
34
+ Example:
35
+ >>> _parse_bool("true")
36
+ True
37
+ >>> _parse_bool("false")
38
+ False
39
+ >>> _parse_bool("1")
40
+ True
41
+ """
42
+ return value.lower() in ("true", "yes", "1", "on")
43
+
44
+
45
+ def _parse_list(value: str) -> List[str]:
46
+ """Parse comma-separated list from string.
47
+
48
+ Args:
49
+ value: Comma-separated string
50
+
51
+ Returns:
52
+ List of stripped strings
53
+
54
+ Example:
55
+ >>> _parse_list("*.env, *.ini, *.secret")
56
+ ['*.env', '*.ini', '*.secret']
57
+ """
58
+ if not value:
59
+ return []
60
+ return [item.strip() for item in value.split(",") if item.strip()]
61
+
62
+
63
+ def _apply_env_overrides(config: Dict[str, Any]) -> Dict[str, Any]:
64
+ """Apply environment variable overrides to configuration.
65
+
66
+ Environment variables with VAULTTOOL_ prefix override config values.
67
+ Nested keys use double underscores, e.g., VAULTTOOL_OPTIONS_KEY_FILE.
68
+ Lists use comma-separated values.
69
+
70
+ Args:
71
+ config: Base configuration dictionary
72
+
73
+ Returns:
74
+ Configuration with environment overrides applied
75
+
76
+ Example:
77
+ export VAULTTOOL_OPTIONS_KEY_FILE=/custom/key
78
+ export VAULTTOOL_INCLUDE_PATTERNS="*.env,*.secret"
79
+ """
80
+ env_prefix = "VAULTTOOL_"
81
+
82
+ # Map of config keys to environment variable names
83
+ env_mappings = {
84
+ # Top-level lists
85
+ "include_directories": f"{env_prefix}INCLUDE_DIRECTORIES",
86
+ "exclude_directories": f"{env_prefix}EXCLUDE_DIRECTORIES",
87
+ "include_patterns": f"{env_prefix}INCLUDE_PATTERNS",
88
+ "exclude_patterns": f"{env_prefix}EXCLUDE_PATTERNS",
89
+
90
+ # Options (nested)
91
+ "options.suffix": f"{env_prefix}OPTIONS_SUFFIX",
92
+ "options.key_file": f"{env_prefix}OPTIONS_KEY_FILE",
93
+ "options.algorithm": f"{env_prefix}OPTIONS_ALGORITHM",
94
+ "options.openssl_path": f"{env_prefix}OPTIONS_OPENSSL_PATH",
95
+ "options.use_suffix_fallback": f"{env_prefix}OPTIONS_USE_SUFFIX_FALLBACK",
96
+ }
97
+
98
+ for config_key, env_var in env_mappings.items():
99
+ env_value = os.environ.get(env_var)
100
+ if env_value is None:
101
+ continue
102
+
103
+ # Parse the configuration key path
104
+ if "." in config_key:
105
+ # Nested key (e.g., "options.key_file")
106
+ parent_key, child_key = config_key.split(".", 1)
107
+
108
+ # Ensure parent exists
109
+ if parent_key not in config:
110
+ config[parent_key] = {}
111
+ if not isinstance(config[parent_key], dict):
112
+ config[parent_key] = {}
113
+
114
+ # Determine value type and parse
115
+ if child_key in ("use_suffix_fallback",):
116
+ # Boolean values
117
+ config[parent_key][child_key] = _parse_bool(env_value)
118
+ else:
119
+ # String values
120
+ config[parent_key][child_key] = env_value
121
+ else:
122
+ # Top-level key
123
+ if config_key.endswith(("_directories", "_patterns")):
124
+ # List values
125
+ config[config_key] = _parse_list(env_value)
126
+ else:
127
+ # String values
128
+ config[config_key] = env_value
129
+
130
+ return config
131
+
132
+
133
+ def load_config(path: str = ".vaulttool.yml") -> Dict[str, Any]:
134
+ """Load and validate the VaultTool configuration file.
135
+
136
+ Attempts to load configuration from the specified path. If the file doesn't
137
+ exist, searches for configuration files in standard locations:
138
+
139
+ 1. Current directory: .vaulttool.yml
140
+ 2. User config: ~/.vaulttool/.vaulttool.yml
141
+ 3. System config: /etc/vaulttool/config.yml
142
+
143
+ After loading, applies environment variable overrides with VAULTTOOL_ prefix.
144
+ Environment variables override file configuration.
145
+
146
+ Args:
147
+ path: Path to the configuration file. Defaults to ".vaulttool.yml".
148
+
149
+ Returns:
150
+ Dictionary containing the parsed and validated configuration.
151
+
152
+ Raises:
153
+ FileNotFoundError: If no configuration file is found in any location.
154
+ ValueError: If the configuration file is invalid, malformed, or
155
+ missing required keys.
156
+ yaml.YAMLError: If the YAML file cannot be parsed.
157
+
158
+ Example:
159
+ >>> config = load_config()
160
+ >>> print(config['options']['algorithm'])
161
+ 'aes-256-cbc'
162
+
163
+ >>> # With environment override:
164
+ >>> os.environ['VAULTTOOL_OPTIONS_KEY_FILE'] = '/custom/key'
165
+ >>> config = load_config()
166
+ >>> print(config['options']['key_file'])
167
+ '/custom/key'
168
+ """
169
+ config_path = Path(path)
170
+
171
+ if not config_path.exists():
172
+ # search for ~/.vaulttool/.vaulttool.yml
173
+ user_config_path = Path.home() / ".vaulttool" / ".vaulttool.yml"
174
+ if user_config_path.exists():
175
+ config_path = user_config_path
176
+ else:
177
+ etc_config_path = Path("/etc/vaulttool/config.yml")
178
+ if etc_config_path.exists():
179
+ config_path = etc_config_path
180
+
181
+ if not config_path.exists():
182
+ raise FileNotFoundError(
183
+ f"Configuration file not found at {path}. Please create a .vaulttool.yml file or specify the correct path."
184
+ )
185
+
186
+ with open(config_path, "r", encoding="utf-8") as f:
187
+ data = yaml.safe_load(f) or {}
188
+
189
+ # Support the documented structure with a top-level `vaulttool` key
190
+ cfg = data.get("vaulttool", data)
191
+
192
+ # Ensure the config has the required keys
193
+ if cfg is None or not isinstance(cfg, dict):
194
+ raise ValueError("Invalid configuration format. Expected a dictionary.")
195
+
196
+ # Apply environment variable overrides
197
+ cfg = _apply_env_overrides(cfg)
198
+
199
+ required_keys = [
200
+ "include_directories",
201
+ "exclude_directories",
202
+ "include_patterns",
203
+ "exclude_patterns",
204
+ "options",
205
+ ]
206
+ missing = [k for k in required_keys if k not in cfg]
207
+ if missing:
208
+ raise ValueError(f"Missing required configuration keys: {', '.join(missing)}")
209
+
210
+ # Validate types of required keys
211
+ if not isinstance(cfg.get("include_directories"), list):
212
+ raise ValueError("'include_directories' must be a list")
213
+ if not isinstance(cfg.get("exclude_directories"), list):
214
+ raise ValueError("'exclude_directories' must be a list")
215
+ if not isinstance(cfg.get("include_patterns"), list):
216
+ raise ValueError("'include_patterns' must be a list")
217
+ if not isinstance(cfg.get("exclude_patterns"), list):
218
+ raise ValueError("'exclude_patterns' must be a list")
219
+
220
+ # Validate include_patterns is not empty
221
+ if not cfg.get("include_patterns"):
222
+ raise ValueError("'include_patterns' cannot be empty - at least one pattern required")
223
+
224
+ # Basic validation of options block
225
+ options = cfg.get("options")
226
+ if not isinstance(options, dict):
227
+ raise ValueError("'options' must be a mapping/dictionary.")
228
+
229
+ # Validate key_file is specified
230
+ if "key_file" not in options:
231
+ raise ValueError("'options.key_file' is required")
232
+ if not isinstance(options.get("key_file"), str):
233
+ raise ValueError("'options.key_file' must be a string")
234
+ if not options.get("key_file"):
235
+ raise ValueError("'options.key_file' cannot be empty")
236
+
237
+ # Suffix validation logic
238
+ suffix = options.get("suffix")
239
+ if isinstance(suffix, str):
240
+ # Check if suffix contains a dot at all
241
+ if "." not in suffix:
242
+ raise ValueError("Suffix must contain a dot (e.g., .vault, .secret.vault)")
243
+
244
+ # If suffix comes from environment variable, enforce .vault ending
245
+ # Check if VAULTTOOL_OPTIONS_SUFFIX is set
246
+ if os.environ.get("VAULTTOOL_OPTIONS_SUFFIX"):
247
+ if not suffix.endswith(".vault"):
248
+ raise ValueError(f"'options.suffix' from environment must end with '.vault', got: {suffix}")
249
+
250
+ if not suffix.startswith("."):
251
+ # If a dot is present but doesn't start with dot, ensure it starts with underscore
252
+ if not suffix.startswith("_"):
253
+ # Prepend underscore
254
+ new_suffix = f"_{suffix}"
255
+ options["suffix"] = new_suffix
256
+
257
+ # Ensure exclude_patterns contains the suffix pattern
258
+ exclude_patterns = cfg.get("exclude_patterns", [])
259
+ suffix_pattern = f"*{suffix}" if suffix else "*.vault"
260
+ if suffix_pattern not in exclude_patterns:
261
+ # Add suffix pattern to exclude_patterns to avoid self-encryption
262
+ cfg["exclude_patterns"].append(suffix_pattern)
263
+
264
+ return cfg