ansible-vars 1.0.5__tar.gz → 1.0.7__tar.gz
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.
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/PKG-INFO +8 -6
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/README.md +7 -5
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/pyproject.toml +1 -1
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/src/ansible_vars/cli.py +156 -46
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/src/ansible_vars/errors.py +8 -0
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/src/ansible_vars/util.py +1 -1
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/src/ansible_vars/vault.py +27 -12
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/src/ansible_vars/vault_crypt.py +1 -1
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/.gitignore +0 -0
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/LICENSE +0 -0
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/src/ansible_vars/__init__.py +0 -0
- {ansible_vars-1.0.5 → ansible_vars-1.0.7}/src/ansible_vars/constants.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ansible-vars
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.7
|
4
4
|
Summary: Manage vaults and variable files for Ansible
|
5
5
|
Project-URL: Homepage, https://github.com/xorwow/ansible-vars
|
6
6
|
Project-URL: Issues, https://github.com/xorwow/ansible-vars/issues
|
@@ -254,21 +254,23 @@ When editing a file or creating a daemon, decrypted vaults are written to disk t
|
|
254
254
|
|
255
255
|
## Known issues and limitations
|
256
256
|
|
257
|
-
- YAML round-trip parser
|
257
|
+
- YAML round-trip parser:
|
258
258
|
- Trailing comments and Jinja2 blocks may be misaligned and a trailing newline may be inserted/removed when switching between folded (`|`, `>`) and non-foldes values.
|
259
259
|
- The `set` and `del` commands may remove trailing comments and Jinja2 blocks.
|
260
260
|
- Explicit start/end markers (`---`, `...`) are not preserved.
|
261
261
|
- Supports lists, dictionaries, and scalar values.
|
262
262
|
- Does not support custom YAML tags (`!tag`).
|
263
|
-
- Ansible
|
263
|
+
- Ansible:
|
264
264
|
- Ansible only directly supports encrypted string values (although you can work around this with the `from_yaml` filter).
|
265
265
|
- Ansible-encrypted strings must include a newline between the envelope and the cipher.
|
266
266
|
- Ansible vault and variable file roots must be a dictionary.
|
267
|
-
- `grep` command
|
267
|
+
- `grep` command:
|
268
268
|
- Will ignore files which cannot be parsed as an Ansible YAML file.
|
269
|
-
- `file-daemon` command
|
269
|
+
- `file-daemon` command:
|
270
270
|
- Changes to file metadata (permissions, ...) are not mirrored.
|
271
|
-
- `ansible-vars`
|
271
|
+
- `ansible-vars` does not support files which are not (Jinja2) YAML dictionaries, except for limited support in these commands:
|
272
|
+
- `edit`, `view` (without `--json` support), `encrypt`, `decrypt`, `is-encrypted`
|
273
|
+
- When a vault is modified, all of its ciphers will change due to new salts, even if only one encrypted variable has been changed.
|
272
274
|
|
273
275
|
## Extension plans
|
274
276
|
|
@@ -232,21 +232,23 @@ When editing a file or creating a daemon, decrypted vaults are written to disk t
|
|
232
232
|
|
233
233
|
## Known issues and limitations
|
234
234
|
|
235
|
-
- YAML round-trip parser
|
235
|
+
- YAML round-trip parser:
|
236
236
|
- Trailing comments and Jinja2 blocks may be misaligned and a trailing newline may be inserted/removed when switching between folded (`|`, `>`) and non-foldes values.
|
237
237
|
- The `set` and `del` commands may remove trailing comments and Jinja2 blocks.
|
238
238
|
- Explicit start/end markers (`---`, `...`) are not preserved.
|
239
239
|
- Supports lists, dictionaries, and scalar values.
|
240
240
|
- Does not support custom YAML tags (`!tag`).
|
241
|
-
- Ansible
|
241
|
+
- Ansible:
|
242
242
|
- Ansible only directly supports encrypted string values (although you can work around this with the `from_yaml` filter).
|
243
243
|
- Ansible-encrypted strings must include a newline between the envelope and the cipher.
|
244
244
|
- Ansible vault and variable file roots must be a dictionary.
|
245
|
-
- `grep` command
|
245
|
+
- `grep` command:
|
246
246
|
- Will ignore files which cannot be parsed as an Ansible YAML file.
|
247
|
-
- `file-daemon` command
|
247
|
+
- `file-daemon` command:
|
248
248
|
- Changes to file metadata (permissions, ...) are not mirrored.
|
249
|
-
- `ansible-vars`
|
249
|
+
- `ansible-vars` does not support files which are not (Jinja2) YAML dictionaries, except for limited support in these commands:
|
250
|
+
- `edit`, `view` (without `--json` support), `encrypt`, `decrypt`, `is-encrypted`
|
251
|
+
- When a vault is modified, all of its ciphers will change due to new salts, even if only one encrypted variable has been changed.
|
250
252
|
|
251
253
|
## Extension plans
|
252
254
|
|
@@ -4,7 +4,7 @@
|
|
4
4
|
# CLI entry point for ansible-vars
|
5
5
|
|
6
6
|
# Standard library imports
|
7
|
-
import
|
7
|
+
import os, re, sys, json, atexit, signal
|
8
8
|
from glob import glob
|
9
9
|
from time import sleep
|
10
10
|
from enum import StrEnum
|
@@ -39,7 +39,7 @@ from .vault import VaultFile, EncryptedVar, ProtoEncryptedVar
|
|
39
39
|
from .vault_crypt import VaultKey, VaultKeyring
|
40
40
|
from .util import DiffFileLogger, VaultDaemon
|
41
41
|
from .constants import Unset, MatchLocation, SENTINEL_KEY
|
42
|
-
from .errors import YAMLFormatError
|
42
|
+
from .errors import YAMLFormatError, UnsupportedGenericFileOperation
|
43
43
|
|
44
44
|
## CLI argument parsing
|
45
45
|
|
@@ -195,7 +195,7 @@ Deletes a node from a vault if it exists.
|
|
195
195
|
}
|
196
196
|
|
197
197
|
DEFAULT_EDITOR: str = os.environ.get('EDITOR', 'notepad.exe' if os.name == 'nt' else 'vi')
|
198
|
-
DEFAULT_COLOR_MODE: str = os.environ.get('AV_COLOR_MODE', '256' if
|
198
|
+
DEFAULT_COLOR_MODE: str = os.environ.get('AV_COLOR_MODE', '256' if sys.stdout.isatty() else 'none')
|
199
199
|
DEFAULT_TEMP_DIR: str = os.environ.get('AV_TEMP_DIR', gettempdir())
|
200
200
|
DEFAULT_CREATE_PLAIN: bool = os.environ.get('AV_CREATE_PLAIN', 'no').lower() in [ 'yes', 'y', 'true', 't', '1' ]
|
201
201
|
|
@@ -253,11 +253,17 @@ log_args.add_argument('--logging-key', '-Q', type=str, metavar='<identifier>', h
|
|
253
253
|
# Commands
|
254
254
|
commands = args.add_subparsers(dest='command', metavar='<command>', required=True)
|
255
255
|
|
256
|
-
cmd_keyring = commands.add_parser(
|
256
|
+
cmd_keyring = commands.add_parser(
|
257
|
+
'keyring', help='show available vault keys and their passphrases', description=HELP['cmd_keyring'],
|
258
|
+
formatter_class=RawDescriptionHelpFormatter
|
259
|
+
)
|
257
260
|
cmd_keyring.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the vault keys as JSON and nothing else')
|
258
261
|
cmd_keyring.add_argument('--keys-only', '-o', action='store_false', dest='show_passphrases', help='show only the vault keys, not the passphrases')
|
259
262
|
|
260
|
-
cmd_create = commands.add_parser(
|
263
|
+
cmd_create = commands.add_parser(
|
264
|
+
'create', help=f"create a new vault ({ 'hybrid/plain' if DEFAULT_CREATE_PLAIN else 'fully encrypted' } by default)",
|
265
|
+
description=HELP['cmd_create'], formatter_class=RawDescriptionHelpFormatter
|
266
|
+
)
|
261
267
|
cmd_create.add_argument('vault_path', type=str, metavar='<vault path>', help='path to create a new vault at') \
|
262
268
|
.completer = _prefixed_path_completer # type: ignore
|
263
269
|
# Invert flag if the user wants plain mode by default
|
@@ -266,50 +272,74 @@ if DEFAULT_CREATE_PLAIN:
|
|
266
272
|
else:
|
267
273
|
cmd_create.add_argument('--plain', '-p', action='store_false', dest='encrypt_vault', help='create without full file encryption')
|
268
274
|
cmd_create.add_argument('--make-parents', '-m', action='store_true', help='create all directories in the given path')
|
269
|
-
|
270
|
-
|
271
|
-
|
275
|
+
create_mutex = cmd_create.add_mutually_exclusive_group()
|
276
|
+
create_mutex.add_argument('--no-edit', '-n', action='store_false', dest='open_edit_mode', help='just create the file, don\'t open it for editing')
|
277
|
+
create_mutex.add_argument(
|
272
278
|
'--edit-command', '-e', type=str, default=DEFAULT_EDITOR, help=f"editor command to use (runs as <command> <some path>) (default: { DEFAULT_EDITOR })"
|
273
279
|
)
|
274
280
|
|
275
|
-
cmd_edit = commands.add_parser(
|
281
|
+
cmd_edit = commands.add_parser(
|
282
|
+
'edit', help='edit a vault', description=HELP['cmd_edit'], formatter_class=RawDescriptionHelpFormatter
|
283
|
+
)
|
276
284
|
cmd_edit.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to edit') \
|
277
285
|
.completer = _prefixed_path_completer # type: ignore
|
278
286
|
cmd_edit.add_argument(
|
279
287
|
'--edit-command', '-e', type=str, default=DEFAULT_EDITOR, help=f"editor command to use (runs as <command> <some path>) (default: { DEFAULT_EDITOR })"
|
280
288
|
)
|
281
289
|
|
282
|
-
cmd_view = commands.add_parser(
|
290
|
+
cmd_view = commands.add_parser(
|
291
|
+
'view', help='show the decrypted contents of a vault', formatter_class=RawDescriptionHelpFormatter
|
292
|
+
)
|
283
293
|
cmd_view.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to dump') \
|
284
294
|
.completer = _prefixed_path_completer # type: ignore
|
285
295
|
cmd_view.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the vault data as JSON and nothing else')
|
286
296
|
|
287
|
-
cmd_info = commands.add_parser(
|
297
|
+
cmd_info = commands.add_parser(
|
298
|
+
'info', help='show information about a vault\'s variables', description=HELP['cmd_info'],
|
299
|
+
formatter_class=RawDescriptionHelpFormatter
|
300
|
+
)
|
288
301
|
cmd_info.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to analyze') \
|
289
302
|
.completer = _prefixed_path_completer # type: ignore
|
290
303
|
cmd_info.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the information as JSON and nothing else')
|
291
304
|
|
292
|
-
cmd_encrypt = commands.add_parser(
|
305
|
+
cmd_encrypt = commands.add_parser(
|
306
|
+
'encrypt', help='encrypt a file in-place or a string with the encryption key', description=HELP['cmd_encrypt'],
|
307
|
+
formatter_class=RawDescriptionHelpFormatter
|
308
|
+
)
|
293
309
|
cmd_encrypt.add_argument('target_type', type=str, choices=['file', 'string'], help='select if target is a file path or a string')
|
294
310
|
cmd_encrypt.add_argument('target', type=str, metavar='<vault path | string>', help='path of vault or string value to encrypt') \
|
295
311
|
.completer = _prefixed_path_completer # type: ignore
|
312
|
+
cmd_encrypt.add_argument('--quiet', '-q', action='store_true', help='only output the encrypted value (ignored in file mode)')
|
296
313
|
|
297
|
-
cmd_decrypt = commands.add_parser(
|
314
|
+
cmd_decrypt = commands.add_parser(
|
315
|
+
'decrypt', help='decrypt a file in-place or a string', description=HELP['cmd_decrypt'],
|
316
|
+
formatter_class=RawDescriptionHelpFormatter
|
317
|
+
)
|
298
318
|
cmd_decrypt.add_argument('target_type', type=str, choices=['file', 'string'], help='select if target is a file path or a string')
|
299
319
|
cmd_decrypt.add_argument('target', type=str, metavar='<vault path | string>', help='path of vault or string value to decrypt') \
|
300
320
|
.completer = _prefixed_path_completer # type: ignore
|
321
|
+
cmd_decrypt.add_argument('--quiet', '-q', action='store_true', help='only output the decrypted value (ignored in file mode)')
|
301
322
|
|
302
|
-
cmd_is_enc = commands.add_parser(
|
323
|
+
cmd_is_enc = commands.add_parser(
|
324
|
+
'is-encrypted', help='check if a file or string is vault-encrypted', description=HELP['cmd_is_enc'],
|
325
|
+
formatter_class=RawDescriptionHelpFormatter
|
326
|
+
)
|
303
327
|
cmd_is_enc.add_argument('target_type', type=str, choices=['file', 'string'], help='select if target is a file path or a string')
|
304
328
|
cmd_is_enc.add_argument('target', type=str, metavar='<vault path | string>', help='path of vault or string value to check') \
|
305
329
|
.completer = _prefixed_path_completer # type: ignore
|
306
330
|
cmd_is_enc.add_argument('--quiet', '-q', action='store_true', help='no output, only set the rc to 0 if encrypted or 100 if unencrypted')
|
307
331
|
|
308
|
-
cmd_convert = commands.add_parser(
|
332
|
+
cmd_convert = commands.add_parser(
|
333
|
+
'convert', help='switch vault between outer (file) and inner (vars) encryption', description=HELP['cmd_convert'],
|
334
|
+
formatter_class=RawDescriptionHelpFormatter
|
335
|
+
)
|
309
336
|
cmd_convert.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to convert') \
|
310
337
|
.completer = _prefixed_path_completer # type: ignore
|
311
338
|
|
312
|
-
cmd_grep = commands.add_parser(
|
339
|
+
cmd_grep = commands.add_parser(
|
340
|
+
'grep', help='search a file or folder for a pattern', description=HELP['cmd_grep'],
|
341
|
+
formatter_class=RawDescriptionHelpFormatter
|
342
|
+
)
|
313
343
|
cmd_grep.add_argument('query', type=str, metavar='<pattern>', help='regex query to match with targets')
|
314
344
|
cmd_grep.add_argument('targets', type=str, nargs='+', metavar='[<target> ...]', help='file(s) or folder(s) to search recursively') \
|
315
345
|
.completer = _prefixed_path_completer # type: ignore
|
@@ -324,21 +354,30 @@ grep_mutex_type.add_argument('--multiline', '-m', action='store_true', help='mak
|
|
324
354
|
cmd_grep.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the matches as JSON and nothing else')
|
325
355
|
cmd_grep.add_argument('--quiet', '-q', action='store_true', help='no output, only set the rc to 0 if any matches found or 100 if none found')
|
326
356
|
|
327
|
-
cmd_diff = commands.add_parser(
|
357
|
+
cmd_diff = commands.add_parser(
|
358
|
+
'diff', help='show line differences between two vaults', description=HELP['cmd_diff'],
|
359
|
+
formatter_class=RawDescriptionHelpFormatter
|
360
|
+
)
|
328
361
|
cmd_diff.add_argument('old_vault', type=str, metavar='<old vault vault path>', help='path of "old"/base vault') \
|
329
362
|
.completer = _prefixed_path_completer # type: ignore
|
330
363
|
cmd_diff.add_argument('new_vault', type=str, metavar='<new vault vault path>', help='path of "new"/changed vault') \
|
331
364
|
.completer = _prefixed_path_completer # type: ignore
|
332
365
|
cmd_diff.add_argument('--context-lines', '-c', type=int, metavar='<amount>', default=3, help='show <amount> lines of context around changed lines (default: 3)')
|
333
366
|
|
334
|
-
cmd_changes = commands.add_parser(
|
367
|
+
cmd_changes = commands.add_parser(
|
368
|
+
'changes', help='show var changes between vaults', description=HELP['cmd_changes'],
|
369
|
+
formatter_class=RawDescriptionHelpFormatter
|
370
|
+
)
|
335
371
|
cmd_changes.add_argument('old_vault', type=str, metavar='<old vault vault path>', help='path of "old"/base vault') \
|
336
372
|
.completer = _prefixed_path_completer # type: ignore
|
337
373
|
cmd_changes.add_argument('new_vault', type=str, metavar='<new vault vault path>', help='path of "new"/changed vault') \
|
338
374
|
.completer = _prefixed_path_completer # type: ignore
|
339
375
|
cmd_changes.add_argument('--json', '-j', action='store_true', dest='as_json', help='print added/changed/removed/decrypted vars as JSON and nothing else')
|
340
376
|
|
341
|
-
cmd_daemon = commands.add_parser(
|
377
|
+
cmd_daemon = commands.add_parser(
|
378
|
+
'file-daemon', help='sync decrypted vault copies into a folder', description=HELP['cmd_daemon'],
|
379
|
+
formatter_class=RawDescriptionHelpFormatter
|
380
|
+
)
|
342
381
|
cmd_daemon.add_argument('target_root', type=str, metavar='<target root path>', help='root folder the decrypted files and folders should be synced into (non-existent or empty)') \
|
343
382
|
.completer = _prefixed_path_completer # type: ignore
|
344
383
|
# This arg can be repeated (results in [ [source, rel_target], ... ])
|
@@ -350,7 +389,10 @@ cmd_daemon.add_argument('--no-recurse', '-n', action='store_false', dest='recurs
|
|
350
389
|
cmd_daemon.add_argument('--no-default-dirs', '-N', action='store_false', dest='include_default_dirs', help='don\'t include default sync sources')
|
351
390
|
cmd_daemon.add_argument('--force', '-f', action='store_true', help='if the target root already exists and is not empty, delete its contents')
|
352
391
|
|
353
|
-
cmd_get = commands.add_parser(
|
392
|
+
cmd_get = commands.add_parser(
|
393
|
+
'get', help='get a key\'s (decrypted) value if it exists', description=HELP['cmd_get'],
|
394
|
+
formatter_class=RawDescriptionHelpFormatter
|
395
|
+
)
|
354
396
|
cmd_get.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to get value from') \
|
355
397
|
.completer = _prefixed_path_completer # type: ignore
|
356
398
|
cmd_get.add_argument('key_segments', type=str, nargs='+', metavar='<key segment> [<key segment> ...]', help='segment(s) of the key to look up (`[<num>]` for numbers)')
|
@@ -359,14 +401,20 @@ get_mutex_format = cmd_get.add_mutually_exclusive_group()
|
|
359
401
|
get_mutex_format.add_argument('--quiet', '-q', action='store_true', help='only output the raw YAML value or set the rc to 100 if the key doesn\'t exist')
|
360
402
|
get_mutex_format.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the value as JSON or set the rc to 100 if the key doesn\'t exist')
|
361
403
|
|
362
|
-
cmd_set = commands.add_parser(
|
404
|
+
cmd_set = commands.add_parser(
|
405
|
+
'set', help='update a key\'s value or add a new key (experimental!)', description=HELP['cmd_set'],
|
406
|
+
formatter_class=RawDescriptionHelpFormatter
|
407
|
+
)
|
363
408
|
cmd_set.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to set value in') \
|
364
409
|
.completer = _prefixed_path_completer # type: ignore
|
365
410
|
cmd_set.add_argument('value', type=str, metavar='<value>', help='value to set (will be loaded as YAML)')
|
366
411
|
cmd_set.add_argument('key_segments', type=str, nargs='+', metavar='<key segment> [<key segment> ...]', help='segment(s) of the key to look up (`[<num>]` for numbers)')
|
367
412
|
cmd_set.add_argument('--encrypt', '-e', action='store_true', dest='encrypt_value', help='encrypt the value if it is\'t encrypted yet')
|
368
413
|
|
369
|
-
cmd_del = commands.add_parser(
|
414
|
+
cmd_del = commands.add_parser(
|
415
|
+
'del', help='delete a key and its value if they exist (experimental!)', description=HELP['cmd_del'],
|
416
|
+
formatter_class=RawDescriptionHelpFormatter
|
417
|
+
)
|
370
418
|
cmd_del.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to delete key from') \
|
371
419
|
.completer = _prefixed_path_completer # type: ignore
|
372
420
|
cmd_del.add_argument('key_segments', type=str, nargs='+', metavar='<key segment> [<key segment> ...]', help='segment(s) of the key to look up (`[<num>]` for numbers)')
|
@@ -582,7 +630,30 @@ if config.command in [ 'create', 'edit' ]:
|
|
582
630
|
vault = VaultFile.create(vault_path, full_encryption=config.encrypt_vault, permissions=0o600, keyring=keyring)
|
583
631
|
print(f"Created { 'encrypted' if vault.full_encryption else 'plain' } vault at { vault_path }", Color.GOOD)
|
584
632
|
else:
|
585
|
-
|
633
|
+
try:
|
634
|
+
vault = VaultFile(vault_path, keyring=keyring)
|
635
|
+
except YAMLFormatError:
|
636
|
+
print('Invalid vault format, will be treated as a generic file', Color.MEH)
|
637
|
+
with NamedTemporaryFile(mode='w+', dir=config.temp_dir, prefix='vaultlike_') as edit_file:
|
638
|
+
with open(vault_path, 'r+') as file:
|
639
|
+
# Load and decrypt file
|
640
|
+
content: str = file.read()
|
641
|
+
if (is_enc := VaultKey.is_encrypted(content)):
|
642
|
+
content = keyring.decrypt(content)
|
643
|
+
# Let user edit the content in a temporary file
|
644
|
+
edit_file.write(content)
|
645
|
+
edit_file.flush()
|
646
|
+
sys_command(f"{ config.edit_command } { edit_file.name }", shell=True)
|
647
|
+
edit_file.seek(0)
|
648
|
+
# Encrypt the new content and write it back
|
649
|
+
new_content: str = edit_file.read()
|
650
|
+
if is_enc:
|
651
|
+
new_content = keyring.encrypt(new_content)
|
652
|
+
file.seek(0)
|
653
|
+
file.truncate()
|
654
|
+
file.write(new_content)
|
655
|
+
print(f"Saved changes!", Color.GOOD)
|
656
|
+
exit()
|
586
657
|
# Open vault for edit mode
|
587
658
|
if getattr(config, 'open_edit_mode', True):
|
588
659
|
print(f"Editing vault at { vault_path }")
|
@@ -591,6 +662,7 @@ if config.command in [ 'create', 'edit' ]:
|
|
591
662
|
# Write vault contents to temp file
|
592
663
|
editable: str = vault.as_editable()
|
593
664
|
edit_file.write(editable)
|
665
|
+
edit_file.flush()
|
594
666
|
while True:
|
595
667
|
# Open editor and wait for it to close
|
596
668
|
edit_file.seek(0)
|
@@ -613,18 +685,23 @@ if config.command in [ 'create', 'edit' ]:
|
|
613
685
|
break
|
614
686
|
new_vault.save()
|
615
687
|
print(f"Saved changes!", Color.GOOD)
|
616
|
-
|
617
|
-
if (changes := new_vault.changes(vault))[0]:
|
618
|
-
print(f"\n[!] The following vars have been decrypted in this edit:", Color.MEH)
|
619
|
-
print('\n'.join([ f"- { format_key_path(path) }" for path in changes[0] ]))
|
620
|
-
# Inform about new plain leaf variables
|
688
|
+
decrypted_vars: list[tuple[Hashable, ...]] = []
|
621
689
|
new_plain_leaves: list[tuple[Hashable, ...]] = []
|
622
690
|
def _find_new_plain_vars(path: tuple[Hashable, ...], value: Any) -> Any:
|
623
691
|
if path != ( SENTINEL_KEY, ) and type(value) is not EncryptedVar:
|
624
|
-
if vault.get(path, default=Unset) != value:
|
625
|
-
|
692
|
+
if (old_value := vault.get(path, default=Unset)) != value:
|
693
|
+
std_print(path[-1], type(old_value), type(value))
|
694
|
+
if type(old_value) is EncryptedVar:
|
695
|
+
decrypted_vars.append(path)
|
696
|
+
else:
|
697
|
+
new_plain_leaves.append(path)
|
626
698
|
return value
|
627
699
|
vault._transform_leaves(new_vault._data, _find_new_plain_vars, tuple())
|
700
|
+
# Warn about decrypted variables
|
701
|
+
if decrypted_vars:
|
702
|
+
print(f"\n[!] The following vars have been decrypted in this edit:", Color.MEH)
|
703
|
+
print('\n'.join([ f"- { format_key_path(path) }" for path in decrypted_vars ]))
|
704
|
+
# Warn about new plain leaf variables
|
628
705
|
if not new_vault.full_encryption and new_plain_leaves:
|
629
706
|
print(f"\n[!] The following plain vars have been added in this edit:", Color.MEH)
|
630
707
|
print('\n'.join([ f"- { format_key_path(path) }" for path in new_plain_leaves ]))
|
@@ -639,11 +716,18 @@ if config.command in [ 'create', 'edit' ]:
|
|
639
716
|
|
640
717
|
if config.command == 'view':
|
641
718
|
vault_path: str = resolve_vault_path(config.vault_path)
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
719
|
+
try:
|
720
|
+
vault = VaultFile(vault_path, keyring=keyring)
|
721
|
+
if config.as_json:
|
722
|
+
print_json(vault.as_json())
|
723
|
+
else:
|
724
|
+
print_yaml(vault.as_plain())
|
725
|
+
except YAMLFormatError:
|
726
|
+
if config.as_json:
|
727
|
+
raise UnsupportedGenericFileOperation(operation='--json')
|
728
|
+
with open(vault_path) as file:
|
729
|
+
content: str = file.read()
|
730
|
+
print(keyring.decrypt(content) if VaultKey.is_encrypted(content) else content)
|
647
731
|
|
648
732
|
# Info command
|
649
733
|
|
@@ -684,27 +768,53 @@ if config.command in [ 'encrypt', 'decrypt', 'is-encrypted' ]:
|
|
684
768
|
# File target
|
685
769
|
if config.target_type == 'file':
|
686
770
|
vault_path: str = resolve_vault_path(config.target)
|
687
|
-
|
771
|
+
is_generic: bool = False
|
772
|
+
try:
|
773
|
+
vault = VaultFile(vault_path, keyring=keyring)
|
774
|
+
is_enc: bool = vault.full_encryption
|
775
|
+
except YAMLFormatError as e:
|
776
|
+
print('Invalid vault format, will be treated as a generic file', Color.MEH)
|
777
|
+
with open(vault_path) as file:
|
778
|
+
is_enc = VaultKey.is_encrypted(file.read())
|
779
|
+
is_generic = True
|
688
780
|
if config.command in [ 'encrypt', 'decrypt' ]:
|
689
|
-
if
|
690
|
-
print(f"Vault is already { 'en' if
|
781
|
+
if is_enc == (config.command == 'encrypt'):
|
782
|
+
print(f"Vault is already { 'en' if is_enc else 'de' }crypted.", Color.GOOD)
|
691
783
|
else:
|
692
|
-
|
693
|
-
|
694
|
-
|
784
|
+
is_enc = (config.command == 'encrypt')
|
785
|
+
# Generic file
|
786
|
+
if is_generic:
|
787
|
+
with open(vault_path, 'r+') as file:
|
788
|
+
content: str = file.read()
|
789
|
+
file.seek(0)
|
790
|
+
file.truncate()
|
791
|
+
file.write(keyring.encrypt(content) if is_enc else keyring.decrypt(content))
|
792
|
+
# Vault file
|
793
|
+
else:
|
794
|
+
vault.full_encryption = is_enc # type: ignore
|
795
|
+
vault.save() # type: ignore
|
796
|
+
print(f"Vault { 'en' if is_enc else 'de' }crypted.", Color.GOOD)
|
695
797
|
else:
|
696
798
|
if config.quiet:
|
697
|
-
exit(0 if
|
799
|
+
exit(0 if is_enc else 100)
|
698
800
|
else:
|
699
|
-
print(f"Vault is { 'encrypted' if
|
801
|
+
print(f"Vault is { 'fully encrypted' if is_enc else 'plain or hybrid' }.", Color.GOOD if is_enc else Color.MEH)
|
700
802
|
# String target
|
701
803
|
else:
|
702
804
|
is_encrypted: bool = VaultKey.is_encrypted(config.target)
|
805
|
+
# The key may not be passed properly, in which case we auto-convert literal '\n' to newlines
|
806
|
+
# We can assume an encrypted value should not contain any literal backslashes
|
807
|
+
if is_encrypted:
|
808
|
+
config.target = config.target.replace('\\n', '\n')
|
703
809
|
if config.command in [ 'encrypt', 'decrypt' ]:
|
704
810
|
if is_encrypted == (config.command == 'encrypt'):
|
705
|
-
|
811
|
+
if not config.quiet:
|
812
|
+
print(f"Value is already { 'en' if is_encrypted else 'de' }crypted.", Color.GOOD)
|
813
|
+
else:
|
814
|
+
print(config.target)
|
706
815
|
else:
|
707
|
-
|
816
|
+
if not config.quiet:
|
817
|
+
print(f"{ 'En' if not is_encrypted else 'De' }crypted value:", Color.GOOD)
|
708
818
|
print(keyring.encrypt(config.target) if (config.command == 'encrypt') else keyring.decrypt(config.target))
|
709
819
|
else:
|
710
820
|
if config.quiet:
|
@@ -732,7 +842,7 @@ if config.command == 'convert':
|
|
732
842
|
vault.save()
|
733
843
|
print(f"Vault converted to { 'outer' if vault.full_encryption else 'inner' } encryption.", Color.GOOD)
|
734
844
|
if not vault.full_encryption:
|
735
|
-
print('Please check the vault to make sure all secrets have been encrypted', Color.MEH)
|
845
|
+
print('Please check the vault to make sure all secrets have been encrypted!', Color.MEH)
|
736
846
|
_convert()
|
737
847
|
|
738
848
|
# Grep command
|
@@ -2,6 +2,14 @@
|
|
2
2
|
|
3
3
|
# YAML parsing
|
4
4
|
|
5
|
+
class UnsupportedGenericFileOperation(Exception):
|
6
|
+
'''The requested operation or flag is not available for generic (i.e. non-YAML-dictionary) files.'''
|
7
|
+
|
8
|
+
def __init__(self, *args: object, operation: str | None = None) -> None:
|
9
|
+
super().__init__(
|
10
|
+
f"The requested operation or flag is not available for generic files{ f': { operation }' * bool(operation) }", *args
|
11
|
+
)
|
12
|
+
|
5
13
|
class YAMLFormatError(Exception):
|
6
14
|
'''The supplied content is not a valid Ansible YAML file. Supports passing the triggering parent exception.'''
|
7
15
|
|
@@ -282,7 +282,7 @@ class VaultDaemon(FileSystemEventHandler):
|
|
282
282
|
content: str = src.read()
|
283
283
|
with open(target_path, 'w') as tgt:
|
284
284
|
try:
|
285
|
-
tgt.write(Vault(content, keyring=self.keyring).
|
285
|
+
tgt.write(Vault(content, keyring=self.keyring).as_plain())
|
286
286
|
self.debug(f"Created/Updated file { target_path } from vault contents")
|
287
287
|
except:
|
288
288
|
tgt.write(content)
|
@@ -6,16 +6,14 @@ from io import StringIO
|
|
6
6
|
from functools import reduce
|
7
7
|
from typing import Type, Hashable, Callable, Any
|
8
8
|
from types import MappingProxyType
|
9
|
-
from collections import OrderedDict
|
10
9
|
from difflib import unified_diff
|
11
10
|
|
12
11
|
# External library imports
|
13
12
|
from ruamel.yaml import YAML
|
14
13
|
from ruamel.yaml.nodes import ScalarNode
|
15
|
-
from ruamel.yaml.parser import CommentToken
|
16
14
|
from ruamel.yaml.representer import Representer
|
17
15
|
from ruamel.yaml.constructor import Constructor
|
18
|
-
from ruamel.yaml.comments import CommentedMap
|
16
|
+
from ruamel.yaml.comments import CommentedMap
|
19
17
|
|
20
18
|
# Internal module imports
|
21
19
|
from .constants import ThrowError, octal, Indexable, ChangeList, MatchLocation, SENTINEL_KEY, EDIT_MODE_HEADER, ENCRYPTED_VAR_TAG
|
@@ -53,7 +51,7 @@ class EncryptedVar():
|
|
53
51
|
cipher: Any = constructor.construct_scalar(node)
|
54
52
|
if not isinstance(cipher, str):
|
55
53
|
raise TypeError(f"Expected encrypted value to be a str, but got { type(cipher) }")
|
56
|
-
return EncryptedVar(
|
54
|
+
return EncryptedVar(cipher, name=node.id)
|
57
55
|
|
58
56
|
class ProtoEncryptedVar():
|
59
57
|
'''A variable marked to be encrypted in a `Vault` editable.'''
|
@@ -77,14 +75,14 @@ class ProtoEncryptedVar():
|
|
77
75
|
|
78
76
|
@classmethod
|
79
77
|
def to_yaml(ProtoEncryptedVar: Type['ProtoEncryptedVar'], representer: Representer, var: 'ProtoEncryptedVar') -> Any:
|
80
|
-
return representer.represent_scalar(ENCRYPTED_VAR_TAG, var.plaintext, style='\'')
|
78
|
+
return representer.represent_scalar(ENCRYPTED_VAR_TAG, var.plaintext, style=('|' if '\n' in var.plaintext else ''))
|
81
79
|
|
82
80
|
@classmethod
|
83
81
|
def from_yaml(ProtoEncryptedVar: Type['ProtoEncryptedVar'], constructor: Constructor, node: ScalarNode) -> 'ProtoEncryptedVar':
|
84
82
|
plaintext: Any = constructor.construct_scalar(node)
|
85
83
|
if not isinstance(plaintext, str):
|
86
84
|
raise TypeError(f"Expected decrypted value to be a str, but got { type(plaintext) }")
|
87
|
-
return ProtoEncryptedVar(
|
85
|
+
return ProtoEncryptedVar(plaintext.rstrip('\n'), name=node.id)
|
88
86
|
|
89
87
|
class Vault():
|
90
88
|
'''
|
@@ -205,7 +203,7 @@ class Vault():
|
|
205
203
|
A read-only dictionary of the vault's variables, with any EncryptedVars already decrypted.
|
206
204
|
Note that the state is frozen whenever you access this property, and not updated when the vault changes.
|
207
205
|
'''
|
208
|
-
copy: dict = self._data
|
206
|
+
copy: dict = Vault._copy_data(self._data)
|
209
207
|
copy.pop(SENTINEL_KEY, None)
|
210
208
|
def _traverse_and_decrypt(root: Any) -> Any:
|
211
209
|
if isinstance(root, dict):
|
@@ -495,7 +493,7 @@ class Vault():
|
|
495
493
|
|
496
494
|
def as_plain(self) -> str:
|
497
495
|
'''Returns the vault in fully decrypted form as Jinja2 YAML code with the original metadata.'''
|
498
|
-
copy: CommentedMap = self._data
|
496
|
+
copy: CommentedMap = Vault._copy_data(self._data)
|
499
497
|
#copy.pop(SENTINEL_KEY, None) # <-- would break a file containing only metadata and the sentinel key
|
500
498
|
def _decrypt_leaf(_: tuple[Hashable, ...], value: Any) -> Any:
|
501
499
|
'''Transforms EncryptedVar leaves into decrypted strings.'''
|
@@ -510,7 +508,7 @@ class Vault():
|
|
510
508
|
Returns the vault as Jinja2 YAML code with the original metadata.
|
511
509
|
It is prepared for editing and later re-encryption with YAML tags and a static explanatory header.
|
512
510
|
'''
|
513
|
-
copy: CommentedMap = self._data
|
511
|
+
copy: CommentedMap = Vault._copy_data(self._data)
|
514
512
|
def _convert_to_proto(path: tuple[Hashable, ...], value: Any) -> Any:
|
515
513
|
'''Marks encrypted leaves for encryption after editing.'''
|
516
514
|
if type(value) is not EncryptedVar:
|
@@ -526,7 +524,7 @@ class Vault():
|
|
526
524
|
|
527
525
|
def as_encrypted(self) -> str:
|
528
526
|
'''Returns the vault as Jinja2 YAML code with the original metadata, with encrypted variables and full encryption if enabled.'''
|
529
|
-
copy: CommentedMap = self._data
|
527
|
+
copy: CommentedMap = Vault._copy_data(self._data)
|
530
528
|
yaml_content: str = self._dump_to_str(copy)
|
531
529
|
yaml_content = Vault._remove_sentinel(yaml_content)
|
532
530
|
if self.full_encryption:
|
@@ -567,6 +565,23 @@ class Vault():
|
|
567
565
|
self._parser.dump(data, builder)
|
568
566
|
return builder.getvalue().strip('\n') + '\n'
|
569
567
|
|
568
|
+
@staticmethod
|
569
|
+
def _copy_data(data: Any) -> Any:
|
570
|
+
'''
|
571
|
+
Create a deep copy of any data, using the object's `copy()` method for dicts and lists.
|
572
|
+
Parser dicts and lists contain special data, and their copy function will preserve that.
|
573
|
+
The built-in `copy.deepcopy()` would require a `__deepcopy__` method for this to work.
|
574
|
+
Copying works for dicts and lists. Everything else is referenced normally.
|
575
|
+
Not thread-safe.
|
576
|
+
'''
|
577
|
+
if (is_dict := isinstance(data, dict)) or isinstance(data, list):
|
578
|
+
copy = data.copy()
|
579
|
+
keys = copy.keys() if is_dict else range(len(copy)) # type: ignore
|
580
|
+
for key in keys:
|
581
|
+
copy[key] = Vault._copy_data(copy[key])
|
582
|
+
return copy
|
583
|
+
return data
|
584
|
+
|
570
585
|
# Comparing to older versions of this vault
|
571
586
|
|
572
587
|
def diff(self, prev_vault: 'Vault', context_lines: int = 3, show_filenames: bool = True) -> str:
|
@@ -653,7 +668,7 @@ class Vault():
|
|
653
668
|
def copy(self) -> 'Vault':
|
654
669
|
'''Create a copy of this `Vault` instance.'''
|
655
670
|
copy = Vault('', self.keyring)
|
656
|
-
copy._data = self._data
|
671
|
+
copy._data = Vault._copy_data(self._data)
|
657
672
|
copy.full_encryption = self.full_encryption
|
658
673
|
copy.keyring = self.keyring
|
659
674
|
copy._parser = self._parser
|
@@ -804,7 +819,7 @@ class VaultFile(Vault):
|
|
804
819
|
def copy(self) -> 'VaultFile':
|
805
820
|
'''Create a copy of this `VaultFile` instance.'''
|
806
821
|
copy: VaultFile = self.from_editable(self, '')
|
807
|
-
copy._data = self._data
|
822
|
+
copy._data = VaultFile._copy_data(self._data)
|
808
823
|
copy.full_encryption = self.full_encryption
|
809
824
|
copy.keyring = self.keyring
|
810
825
|
copy._parser = self._parser
|
@@ -70,7 +70,7 @@ class VaultKey():
|
|
70
70
|
vault_cipher = VaultKey._strip_vault_tag(vault_cipher)
|
71
71
|
try:
|
72
72
|
decrypted: bytes = self._vaultlib.decrypt(vault_cipher)
|
73
|
-
return decrypted.decode('utf-8')
|
73
|
+
return decrypted.decode('utf-8')
|
74
74
|
except AnsibleVaultError as e:
|
75
75
|
if e.message.startswith('Decryption failed (no vault secrets were found that could decrypt)'):
|
76
76
|
raise VaultKeyMatchError(f"Could not match cipher with { self }")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|