ansible-vars 1.0.10__tar.gz → 1.0.11__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.10 → ansible_vars-1.0.11}/PKG-INFO +8 -6
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/README.md +7 -5
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/pyproject.toml +1 -1
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/src/ansible_vars/cli.py +13 -7
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/src/ansible_vars/constants.py +1 -1
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/src/ansible_vars/vault.py +63 -60
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/.gitignore +0 -0
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/LICENSE +0 -0
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/src/ansible_vars/__init__.py +0 -0
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/src/ansible_vars/errors.py +0 -0
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/src/ansible_vars/util.py +0 -0
- {ansible_vars-1.0.10 → ansible_vars-1.0.11}/src/ansible_vars/vault_crypt.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.11
|
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
|
@@ -107,7 +107,7 @@ You can disable automatic key detection by flagging `--no-encrypt-keys|-D`. Use
|
|
107
107
|
|
108
108
|
#### Encryption salts
|
109
109
|
|
110
|
-
Each time you edit a vault or otherwise encrypt a value, a randomly generated salt is used so even identical plain values will result in unique ciphers. However, this also means that each time a single encrypted variable is edited, all other ciphers in the vault will change as well. You can avoid this by passing a fixed salt via `--fixed-salt|-S <salt
|
110
|
+
Each time you edit a vault or otherwise encrypt a value, a randomly generated salt is used so even identical plain values will result in unique ciphers. However, this also means that each time a single encrypted variable is edited, all other ciphers in the vault will change as well. You can avoid this by passing a fixed salt via `--fixed-salt|-S <salt>` or the environment. Note that it should be at least 32 characters long and sufficiently random for Ansible's AES-256 encryption, and that you won't benefit from unique ciphers for identical plaintexts anymore.
|
111
111
|
|
112
112
|
### Diff logging
|
113
113
|
|
@@ -123,7 +123,7 @@ As you cannot mix different encryption keys and/or plain logging in the same log
|
|
123
123
|
# Create the variable file `./host_vars/my_host/main.yml` without full encryption and open it for editing
|
124
124
|
ansible-vars create --make-parents --plain host_vars/my_host/main.yml
|
125
125
|
# Short version (see `Tips` section to learn about vault search paths)
|
126
|
-
ansible-vars create -mp h:my_host
|
126
|
+
ansible-vars create -mp h:my_host/
|
127
127
|
|
128
128
|
# Decrypt the vault file `./config/logging.yml` in-place
|
129
129
|
ansible-vars decrypt file config/logging.yml
|
@@ -146,6 +146,7 @@ ansible-vars get --json g:database_hosts 'my_key' '[4]' '133'
|
|
146
146
|
- When a command supports a `--json` flag, the command's help (`ansible-vars <command> -h`) will define the returned structure.
|
147
147
|
- The directories `host_vars`, `group_vars`, and `vars` are common vault locations. When in their parent directory, you can use the prefixes `h:`, `g:`, and `v:` in any vault path you specify, followed by a path relative to them. Wherever a directory is not expected as a path, supplying a directory path will also append a `main.yml` to the path automatically. In summary, this lets you type `h:my_host` when you actually mean `./host_vars/my_host/main.yml`. Shell completion for these prefixed paths is provided.
|
148
148
|
- These three directories are also default sources for the `file-daemon` command.
|
149
|
+
- For vault creation with the `--make-parents` flag, a path like `h:my_host` would be ambiguous as to the expanded path being `./host_vars/my_host` or `./host_vars/my_host/main.yml`, since the directory does not exist yet. `ansible-vars` will assume the first case, unless you end your search path with a / like `h:my_host/`.
|
149
150
|
- When referencing vault traversal keys, you can specify numbers to access lists and number-indexed dictionaries. However, just specifying `2` as a key segment will resolve into the string `'2'`. Instead, you should write `[2]` to mark it as a number index. If you need to specify the string `'[2]'` for some reason, you can escape it by adding another set of brackets (and so on).
|
150
151
|
|
151
152
|
### Commands
|
@@ -200,15 +201,15 @@ Compares two vaults or variable files and prints a tree structure showing differ
|
|
200
201
|
|
201
202
|
#### file-daemon
|
202
203
|
|
203
|
-
Starts a daemon which mirrors the decrypted contents of one or multiple vault or variable
|
204
|
+
Starts a daemon which mirrors the decrypted contents of one or multiple vault or variable files/directories to a target directory. By default, this includes the directories `./host_vars`, `./group_vars`, and `./vars`. Changes to the source files are reflected in the decrypted targets. Changes to the target files are ignored. For added security, consider syncing the files to a mounted ramdisk.
|
204
205
|
|
205
206
|
#### get
|
206
207
|
|
207
|
-
Displays the (decrypted) value of a specified key in a vault or variable file. Supports dictionary and list traversal, and JSON output.
|
208
|
+
Displays the (optionally recursively decrypted) value of a specified key in a vault or variable file. Supports dictionary and list traversal, and JSON output.
|
208
209
|
|
209
210
|
#### set, del (experimental)
|
210
211
|
|
211
|
-
Creates, updates, or deletes a key-value pair from a vault or variable file. When setting a value, you may provide a YAML string which will be parsed into the corresponding objects. Note that these are experimental features, as the current parser has difficulty preserving the metadata for programmatic variable changes. Comments and Jinja2 blocks between the affected key and the next key in the file may be lost.
|
212
|
+
Creates, updates, or deletes a key-value pair from a vault or variable file. When setting a value, you may provide a YAML string which will be parsed into the corresponding objects. When the `--encrypt` flag is set, the object's leaf string values will be encrypted. Note that these are experimental features, as the current parser has difficulty preserving the metadata for programmatic variable changes. Comments and Jinja2 blocks between the affected key and the next key in the file may be lost.
|
212
213
|
|
213
214
|
### Environment variables
|
214
215
|
|
@@ -276,6 +277,7 @@ When editing a file or creating a daemon, decrypted vaults are written to disk t
|
|
276
277
|
- Ansible only directly supports encrypted string values (although you can work around this with the `from_yaml` filter).
|
277
278
|
- Ansible-encrypted strings must include a newline between the envelope and the cipher.
|
278
279
|
- Ansible vault and variable file roots must be a dictionary.
|
280
|
+
- Due to parsing limitations in `ansible-vars`, a file with explicit JSON style '{}' as the outermost level is currently not supported.
|
279
281
|
- `grep` command:
|
280
282
|
- Will ignore files which cannot be parsed as an Ansible YAML file.
|
281
283
|
- `file-daemon` command:
|
@@ -85,7 +85,7 @@ You can disable automatic key detection by flagging `--no-encrypt-keys|-D`. Use
|
|
85
85
|
|
86
86
|
#### Encryption salts
|
87
87
|
|
88
|
-
Each time you edit a vault or otherwise encrypt a value, a randomly generated salt is used so even identical plain values will result in unique ciphers. However, this also means that each time a single encrypted variable is edited, all other ciphers in the vault will change as well. You can avoid this by passing a fixed salt via `--fixed-salt|-S <salt
|
88
|
+
Each time you edit a vault or otherwise encrypt a value, a randomly generated salt is used so even identical plain values will result in unique ciphers. However, this also means that each time a single encrypted variable is edited, all other ciphers in the vault will change as well. You can avoid this by passing a fixed salt via `--fixed-salt|-S <salt>` or the environment. Note that it should be at least 32 characters long and sufficiently random for Ansible's AES-256 encryption, and that you won't benefit from unique ciphers for identical plaintexts anymore.
|
89
89
|
|
90
90
|
### Diff logging
|
91
91
|
|
@@ -101,7 +101,7 @@ As you cannot mix different encryption keys and/or plain logging in the same log
|
|
101
101
|
# Create the variable file `./host_vars/my_host/main.yml` without full encryption and open it for editing
|
102
102
|
ansible-vars create --make-parents --plain host_vars/my_host/main.yml
|
103
103
|
# Short version (see `Tips` section to learn about vault search paths)
|
104
|
-
ansible-vars create -mp h:my_host
|
104
|
+
ansible-vars create -mp h:my_host/
|
105
105
|
|
106
106
|
# Decrypt the vault file `./config/logging.yml` in-place
|
107
107
|
ansible-vars decrypt file config/logging.yml
|
@@ -124,6 +124,7 @@ ansible-vars get --json g:database_hosts 'my_key' '[4]' '133'
|
|
124
124
|
- When a command supports a `--json` flag, the command's help (`ansible-vars <command> -h`) will define the returned structure.
|
125
125
|
- The directories `host_vars`, `group_vars`, and `vars` are common vault locations. When in their parent directory, you can use the prefixes `h:`, `g:`, and `v:` in any vault path you specify, followed by a path relative to them. Wherever a directory is not expected as a path, supplying a directory path will also append a `main.yml` to the path automatically. In summary, this lets you type `h:my_host` when you actually mean `./host_vars/my_host/main.yml`. Shell completion for these prefixed paths is provided.
|
126
126
|
- These three directories are also default sources for the `file-daemon` command.
|
127
|
+
- For vault creation with the `--make-parents` flag, a path like `h:my_host` would be ambiguous as to the expanded path being `./host_vars/my_host` or `./host_vars/my_host/main.yml`, since the directory does not exist yet. `ansible-vars` will assume the first case, unless you end your search path with a / like `h:my_host/`.
|
127
128
|
- When referencing vault traversal keys, you can specify numbers to access lists and number-indexed dictionaries. However, just specifying `2` as a key segment will resolve into the string `'2'`. Instead, you should write `[2]` to mark it as a number index. If you need to specify the string `'[2]'` for some reason, you can escape it by adding another set of brackets (and so on).
|
128
129
|
|
129
130
|
### Commands
|
@@ -178,15 +179,15 @@ Compares two vaults or variable files and prints a tree structure showing differ
|
|
178
179
|
|
179
180
|
#### file-daemon
|
180
181
|
|
181
|
-
Starts a daemon which mirrors the decrypted contents of one or multiple vault or variable
|
182
|
+
Starts a daemon which mirrors the decrypted contents of one or multiple vault or variable files/directories to a target directory. By default, this includes the directories `./host_vars`, `./group_vars`, and `./vars`. Changes to the source files are reflected in the decrypted targets. Changes to the target files are ignored. For added security, consider syncing the files to a mounted ramdisk.
|
182
183
|
|
183
184
|
#### get
|
184
185
|
|
185
|
-
Displays the (decrypted) value of a specified key in a vault or variable file. Supports dictionary and list traversal, and JSON output.
|
186
|
+
Displays the (optionally recursively decrypted) value of a specified key in a vault or variable file. Supports dictionary and list traversal, and JSON output.
|
186
187
|
|
187
188
|
#### set, del (experimental)
|
188
189
|
|
189
|
-
Creates, updates, or deletes a key-value pair from a vault or variable file. When setting a value, you may provide a YAML string which will be parsed into the corresponding objects. Note that these are experimental features, as the current parser has difficulty preserving the metadata for programmatic variable changes. Comments and Jinja2 blocks between the affected key and the next key in the file may be lost.
|
190
|
+
Creates, updates, or deletes a key-value pair from a vault or variable file. When setting a value, you may provide a YAML string which will be parsed into the corresponding objects. When the `--encrypt` flag is set, the object's leaf string values will be encrypted. Note that these are experimental features, as the current parser has difficulty preserving the metadata for programmatic variable changes. Comments and Jinja2 blocks between the affected key and the next key in the file may be lost.
|
190
191
|
|
191
192
|
### Environment variables
|
192
193
|
|
@@ -254,6 +255,7 @@ When editing a file or creating a daemon, decrypted vaults are written to disk t
|
|
254
255
|
- Ansible only directly supports encrypted string values (although you can work around this with the `from_yaml` filter).
|
255
256
|
- Ansible-encrypted strings must include a newline between the envelope and the cipher.
|
256
257
|
- Ansible vault and variable file roots must be a dictionary.
|
258
|
+
- Due to parsing limitations in `ansible-vars`, a file with explicit JSON style '{}' as the outermost level is currently not supported.
|
257
259
|
- `grep` command:
|
258
260
|
- Will ignore files which cannot be parsed as an Ansible YAML file.
|
259
261
|
- `file-daemon` command:
|
@@ -72,8 +72,9 @@ tips:
|
|
72
72
|
- When a command asks for a vault file path it actually accepts multiple kinds of search path:
|
73
73
|
- You can specify a full or relative path to a vault file just as usual. This path will always be tried first.
|
74
74
|
- If you specify `h:<path>`, `g:<path>`, or `v:<path>`, it looks in `./host_vars`, `./group_vars`, or `./vars`, respectively.
|
75
|
-
- If a resolved path is a directory instead of a file, it looks for a `main.yml` in that directory.
|
75
|
+
- If a resolved path is a directory instead of a file, it looks for or creates a `main.yml` in that directory.
|
76
76
|
- For example, to open the file `/ansible/host_vars/my_host/main.yml`, you may run the command in `/ansible` and specify `h:my_host`.
|
77
|
+
- For vault creation with the `--make-parents` flag, `<path>` will create the file `<path>`, while `<path>/` will create `<path>/main.yml`.
|
77
78
|
- Data keys are split into segments (e.g. `root['my_key'][0]` would become `'my_key', 0`) for easier parsing.
|
78
79
|
- When specifying a key segment which is a number (e.g. a list index), surround it in brackets (`[2]`) to differentiate it from a string.
|
79
80
|
- If you need to actually use the string `[2]`, add a set of brackets to escape it (`[[2]]` -> `'[2]'`, `[[[2]]]` -> `'[[2]]'`, ...).
|
@@ -182,14 +183,14 @@ The sync works as long as the command is running, after which the target root di
|
|
182
183
|
''',
|
183
184
|
'cmd_get': '''
|
184
185
|
Looks up the value of a key in a vault and displays it if it exists.
|
185
|
-
If the key resolves to a leaf value, the value is decrypted and displayed.
|
186
|
+
If the key resolves to a leaf value, the value is recursively decrypted and displayed.
|
186
187
|
For a list or dictionary, the full YAML code is printed, but child values are not automatically decrypted.
|
187
188
|
|
188
189
|
JSON mode formatting:
|
189
190
|
- [ ... ] or { ... } for lists/dictionaries, "<value>" for strings, <value> for numbers
|
190
191
|
''',
|
191
192
|
'cmd_set': '''
|
192
|
-
Creates or updates a node in a vault with a YAML value, optionally encrypting the value first using the configured encryption key.
|
193
|
+
Creates or updates a node in a vault with a YAML value, optionally encrypting the value('s string leaves) first using the configured encryption key.
|
193
194
|
For creating a new list entry, the last specified key segment has to equal the largest index of the list plus one (e.g. `[5]` for a list of length 5).
|
194
195
|
The value is interpreted as YAML code.
|
195
196
|
|
@@ -410,7 +411,7 @@ cmd_daemon.add_argument('--no-default-dirs', '-N', action='store_false', dest='i
|
|
410
411
|
cmd_daemon.add_argument('--force', '-f', action='store_true', help='if the target root already exists and is not empty, delete its contents')
|
411
412
|
|
412
413
|
cmd_get = commands.add_parser(
|
413
|
-
'get', help='get a key\'s (decrypted) value if it exists', description=HELP['cmd_get'],
|
414
|
+
'get', help='get a key\'s (recursively decrypted) value if it exists', description=HELP['cmd_get'],
|
414
415
|
formatter_class=RawDescriptionHelpFormatter
|
415
416
|
)
|
416
417
|
cmd_get.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to get value from') \
|
@@ -429,7 +430,7 @@ cmd_set.add_argument('vault_path', type=str, metavar='<vault path>', help='path
|
|
429
430
|
.completer = _prefixed_path_completer # type: ignore
|
430
431
|
cmd_set.add_argument('value', type=str, metavar='<value>', help='value to set (will be loaded as YAML)')
|
431
432
|
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)')
|
432
|
-
cmd_set.add_argument('--encrypt', '-e', action='store_true', dest='encrypt_value', help='encrypt the value if it is\'t encrypted yet')
|
433
|
+
cmd_set.add_argument('--encrypt', '-e', action='store_true', dest='encrypt_value', help='recursively encrypt the value(\'s leaves) if it is\'t encrypted yet')
|
433
434
|
|
434
435
|
cmd_del = commands.add_parser(
|
435
436
|
'del', help='delete a key and its value if they exist (experimental!)', description=HELP['cmd_del'],
|
@@ -548,7 +549,8 @@ def resolve_vault_path(search_path: str, create_mode: bool = False, allow_dirs:
|
|
548
549
|
- As an absolute path or a relative path from the PWD
|
549
550
|
- As a relative path with prefix `h:` / `g:` / `v:` to be treated as a subpath into `./host_vars` / `./group_vars` / `./vars`
|
550
551
|
- If the path is expected to be a file and the previous steps found a directory, append a `main.yml` to that path
|
551
|
-
If `create_mode` is set to True (i.e. the searched file doesn't exist yet), we test for
|
552
|
+
If `create_mode` is set to True (i.e. the searched file doesn't exist yet), we test for case 2 first, then cases 1 and 3.
|
553
|
+
Setting `create_mode` will also treat a path ending in a / as a folder to create a `main.yml` in.
|
552
554
|
'''
|
553
555
|
# Try the path as-is first
|
554
556
|
abspath: str = os.path.abspath(search_path)
|
@@ -560,7 +562,7 @@ def resolve_vault_path(search_path: str, create_mode: bool = False, allow_dirs:
|
|
560
562
|
if len(search_path) > 2:
|
561
563
|
abspath = os.path.join(abspath, search_path[2:].lstrip(os.path.sep))
|
562
564
|
# Check for main.yml in directory
|
563
|
-
if not allow_dirs and os.path.isdir(abspath):
|
565
|
+
if (not allow_dirs and os.path.isdir(abspath)) or (create_mode and search_path.endswith('/')):
|
564
566
|
abspath = os.path.join(abspath, 'main.yml')
|
565
567
|
# Debug output
|
566
568
|
debug(f"Resolved path { search_path } to { abspath }")
|
@@ -649,6 +651,8 @@ if config.command in [ 'create', 'edit' ]:
|
|
649
651
|
if config.command == 'create':
|
650
652
|
if config.make_parents:
|
651
653
|
os.makedirs(os.path.dirname(vault_path), mode=0o700, exist_ok=True)
|
654
|
+
if not vault_path.endswith('.yml'):
|
655
|
+
print(f"Treating path as a file, append a / to create a directory containing a main.yml instead", Color.MEH)
|
652
656
|
vault = VaultFile.create(vault_path, full_encryption=config.encrypt_vault, permissions=0o600, keyring=keyring)
|
653
657
|
print(f"Created { 'encrypted' if vault.full_encryption else 'plain' } vault at { vault_path }", Color.GOOD)
|
654
658
|
else:
|
@@ -1168,6 +1172,8 @@ if config.command in [ 'get', 'set', 'del' ]:
|
|
1168
1172
|
# Set command
|
1169
1173
|
if config.command == 'set':
|
1170
1174
|
value: Any = yaml.safe_load(config.value)
|
1175
|
+
if type(value) is not str and config.encrypt_value:
|
1176
|
+
print('Only plain string leaves will be encrypted, not the entire object.', Color.MEH)
|
1171
1177
|
vault.set(key, value, overwrite=True, create_parents=True, encrypt=config.encrypt_value)
|
1172
1178
|
vault.save()
|
1173
1179
|
print('Value has been set.\n', Color.GOOD)
|
@@ -39,7 +39,7 @@ ENCRYPTED_VAR_TAG: str = u'!enc'
|
|
39
39
|
# Will be searched for and removed on re-parsing
|
40
40
|
EDIT_MODE_HEADER: str = f"""
|
41
41
|
#~ DO NOT EDIT THIS HEADER
|
42
|
-
#~ Variables which should be encrypted are formatted
|
42
|
+
#~ Variables which should be encrypted are formatted as '{ ENCRYPTED_VAR_TAG } <value>'.
|
43
43
|
#~ Do not remove this prefix unless you want to convert them to plain variables.
|
44
44
|
#~ Add the prefix to any string variable you want to be encrypted.
|
45
45
|
|
@@ -4,8 +4,7 @@
|
|
4
4
|
import os, re, json
|
5
5
|
from io import StringIO
|
6
6
|
from functools import reduce
|
7
|
-
from typing import Type, Hashable, Callable, Any
|
8
|
-
from types import MappingProxyType
|
7
|
+
from typing import Type, Hashable, Callable, Any, cast
|
9
8
|
from difflib import unified_diff
|
10
9
|
|
11
10
|
# External library imports
|
@@ -26,6 +25,7 @@ class EncryptedVar():
|
|
26
25
|
The content should be a str, as Ansible does not directly support other data types in encrypted variables.
|
27
26
|
As this class has no `VaultKeyring` access, decryption must be performed externally.
|
28
27
|
Note that comparing `EncryptedVar` objects by their `cipher`s usually does not work, as Ansible ciphers contain a random salt.
|
28
|
+
This class should be treated as static. Do not change its values, replace it instead.
|
29
29
|
'''
|
30
30
|
|
31
31
|
def __init__(self, cipher: str, name: str | None = None) -> None:
|
@@ -54,7 +54,10 @@ class EncryptedVar():
|
|
54
54
|
return EncryptedVar(cipher, name=node.id)
|
55
55
|
|
56
56
|
class ProtoEncryptedVar():
|
57
|
-
'''
|
57
|
+
'''
|
58
|
+
A variable marked to be encrypted in a `Vault` editable.
|
59
|
+
This class should be treated as static. Do not change its values, replace it instead.
|
60
|
+
'''
|
58
61
|
|
59
62
|
def __init__(self, plaintext: str, name: str) -> None:
|
60
63
|
'''Initialize a plaintext value marked for encryption with a name for internal representation.'''
|
@@ -198,21 +201,9 @@ class Vault():
|
|
198
201
|
DictPath = Hashable | tuple[Hashable, ...]
|
199
202
|
|
200
203
|
@property
|
201
|
-
def decrypted_vars(self) ->
|
202
|
-
'''
|
203
|
-
|
204
|
-
Note that the state is frozen whenever you access this property, and not updated when the vault changes.
|
205
|
-
'''
|
206
|
-
copy: dict = Vault._copy_data(self._data)
|
207
|
-
copy.pop(SENTINEL_KEY, None)
|
208
|
-
def _traverse_and_decrypt(root: Any) -> Any:
|
209
|
-
if isinstance(root, dict):
|
210
|
-
return { k: _traverse_and_decrypt(v) for k, v in root.items() }
|
211
|
-
elif isinstance(root, list):
|
212
|
-
return [ _traverse_and_decrypt(v) for v in root ]
|
213
|
-
else:
|
214
|
-
return self.keyring.decrypt(root.cipher) if type(root) is EncryptedVar else root
|
215
|
-
return MappingProxyType(_traverse_and_decrypt(copy))
|
204
|
+
def decrypted_vars(self) -> dict:
|
205
|
+
'''A copy of the vault's variables, with any EncryptedVars already decrypted.'''
|
206
|
+
return self._decrypted_copy(remove_sentinel=True) # might not have correct YAML metadata, but the values are okay
|
216
207
|
|
217
208
|
def has(self, path: DictPath) -> bool:
|
218
209
|
'''Checks if the given key path is present in the vault's data.'''
|
@@ -222,23 +213,24 @@ class Vault():
|
|
222
213
|
|
223
214
|
def get(
|
224
215
|
self, path: DictPath, default: Any | Type[ThrowError] = ThrowError,
|
225
|
-
decrypt: bool = False, with_index: bool = False
|
216
|
+
decrypt: bool = False, with_index: bool = False, copy: bool = False
|
226
217
|
) -> Any | tuple[int, Any]:
|
227
218
|
'''
|
228
|
-
Retrieves a value from the vault's variables by its key path, optionally decrypting it
|
219
|
+
Retrieves a value from the vault's variables by its key path, optionally decrypting it recursively.
|
229
220
|
When `default` is set to `ThrowError`, a `KeyError` will be raised if the path does not exist.
|
230
221
|
Else, the default value is returned if the path does not exist.
|
231
|
-
When `with_index` is set to True, a tuple `(index_in_parent, value)` is returned (-1 if defaulted).
|
222
|
+
When `with_index` is set to True, a tuple `(index_in_parent, value)` is returned (index is -1 if defaulted).
|
223
|
+
When `copy` or `decrypt` is set to True, a deep copy of the value will be returned.
|
232
224
|
'''
|
233
225
|
path = Vault._to_path(path)
|
234
226
|
try:
|
235
|
-
value: Any = self._traverse(path, decrypt=decrypt)
|
227
|
+
value: Any = self._traverse(path, decrypt=decrypt, copy=copy)
|
236
228
|
if with_index:
|
237
|
-
parent: Indexable = self._traverse(path[:-1], decrypt=False)
|
229
|
+
parent: Indexable = self._data if not path else self._traverse(path[:-1], decrypt=False)
|
238
230
|
if isinstance(parent, dict):
|
239
231
|
key_index: int = next(index for index, key in enumerate(parent) if key == path[-1])
|
240
232
|
else:
|
241
|
-
key_index
|
233
|
+
key_index = cast(int, path[-1])
|
242
234
|
return (key_index, value)
|
243
235
|
return value
|
244
236
|
except:
|
@@ -267,19 +259,22 @@ class Vault():
|
|
267
259
|
- `True`: Create any nested dictionaries needed to traverse to the last key
|
268
260
|
- `False`: Abort silently if any but the last key in the path do not exist, returning False
|
269
261
|
- `ThrowError`: Raise a KeyError if any but the last key in the path do not exist
|
270
|
-
- `encrypt`: Controls if the value should be encrypted before storing it (
|
271
|
-
- `True`: Attempt to convert the value into an `EncryptedVar` before storing it
|
262
|
+
- `encrypt`: Controls if the value should be recursively encrypted before storing it (only plain `str`s get encrypted)
|
263
|
+
- `True`: Attempt to copy and convert the value('s leaf values) into an `EncryptedVar` before storing it
|
272
264
|
- `False`: Store the value as-is
|
273
265
|
'''
|
274
266
|
path = Vault._to_path(path)
|
275
|
-
#
|
276
|
-
if encrypt
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
267
|
+
# Encrypt value if necessary
|
268
|
+
if encrypt:
|
269
|
+
value = Vault._copy_data(value)
|
270
|
+
def _encrypt_leaf(_: tuple[Hashable, ...], _value: Any) -> Any:
|
271
|
+
'''Transforms strings into `EncryptedVar`s.'''
|
272
|
+
if type(_value) is not str:
|
273
|
+
return _value
|
274
|
+
if VaultKey.is_encrypted(_value):
|
275
|
+
return EncryptedVar(_value, name=str(path[-1]))
|
276
|
+
return EncryptedVar(self.keyring.encrypt(_value), name=str(path[-1]))
|
277
|
+
Vault._transform_leaves(value, _encrypt_leaf, tuple()) if isinstance(value, dict | list) else _encrypt_leaf(tuple(), value)
|
283
278
|
# Resolve chain and create parents if necessary, then set value for last item
|
284
279
|
parent: Any = self._data
|
285
280
|
par_path: str = ''
|
@@ -292,7 +287,7 @@ class Vault():
|
|
292
287
|
if isinstance(parent, list) and type(segment) is not int:
|
293
288
|
raise TypeError(f"Type of list index has to be int, got { type(segment) } ({ par_path }[{ segment }])")
|
294
289
|
# Check if the current segment has to be created in the parent
|
295
|
-
if (isinstance(parent, dict) and segment not in parent) or (isinstance(parent, list) and segment >= len(parent)):
|
290
|
+
if (isinstance(parent, dict) and segment not in parent) or (isinstance(parent, list) and cast(int, segment) >= len(parent)):
|
296
291
|
if not is_last:
|
297
292
|
if create_parents is ThrowError:
|
298
293
|
raise KeyError(f"Parents of { '.'.join(map(str, path)) } could not be resolved ({ segment } not in { par_path })")
|
@@ -467,18 +462,32 @@ class Vault():
|
|
467
462
|
new_parent.ca.items[new_path[-1]] = old_parent.ca.items.pop(old_path[-1])
|
468
463
|
"""
|
469
464
|
|
470
|
-
def _traverse(self, path: DictPath, decrypt: bool = False) -> Any:
|
471
|
-
'''
|
472
|
-
path
|
465
|
+
def _traverse(self, path: DictPath, decrypt: bool = False, copy: bool = False) -> Any:
|
466
|
+
'''
|
467
|
+
Gets the value of the specified key path from the `Vault`'s `_data`, optionally decrypting it.
|
468
|
+
When `copy` or `decrypt` is set to True, a deep copy of the value will be returned.
|
469
|
+
'''
|
470
|
+
path = cast(tuple[Hashable, ...], Vault._to_path(path))
|
471
|
+
data: dict = self._decrypted_copy() if decrypt else (Vault._copy_data(self._data) if copy else self._data)
|
473
472
|
def _get_child(parent: Indexable, index: Hashable) -> Any:
|
474
473
|
if not isinstance(parent, dict | list):
|
475
474
|
raise TypeError(f"Can only index into dict-like and list-like types, got { type(parent) } for index { index }")
|
476
475
|
is_dict: bool = isinstance(parent, dict)
|
477
|
-
if (is_dict and index not in parent) or (not is_dict and index > len(parent)):
|
476
|
+
if (is_dict and index not in parent) or (not is_dict and cast(int, index) > len(parent)):
|
478
477
|
raise KeyError(f"Key '{ index }' of path '{ '.'.join(map(str, path)) }' could not be resolved")
|
479
478
|
return parent[index] # type: ignore
|
480
|
-
|
481
|
-
|
479
|
+
return reduce(_get_child, path, data)
|
480
|
+
|
481
|
+
def _decrypted_copy(self, remove_sentinel: bool = False) -> dict:
|
482
|
+
'''Returns a recursively decrypted deep copy of the vault data. Removing the sentinel may break comments/Jinja2.'''
|
483
|
+
copy: CommentedMap = Vault._copy_data(self._data)
|
484
|
+
if remove_sentinel:
|
485
|
+
copy.pop(SENTINEL_KEY, None)
|
486
|
+
def _decrypt_leaf(_: tuple[Hashable, ...], value: Any) -> Any:
|
487
|
+
'''Transforms EncryptedVar leaves into decrypted strings.'''
|
488
|
+
return self.keyring.decrypt(value.cipher) if type(value) is EncryptedVar else value
|
489
|
+
Vault._transform_leaves(copy, _decrypt_leaf, tuple())
|
490
|
+
return copy
|
482
491
|
|
483
492
|
@staticmethod
|
484
493
|
def _to_path(path: DictPath) -> tuple[Hashable, ...]:
|
@@ -493,12 +502,7 @@ class Vault():
|
|
493
502
|
|
494
503
|
def as_plain(self) -> str:
|
495
504
|
'''Returns the vault in fully decrypted form as Jinja2 YAML code with the original metadata.'''
|
496
|
-
copy:
|
497
|
-
#copy.pop(SENTINEL_KEY, None) # <-- would break a file containing only metadata and the sentinel key
|
498
|
-
def _decrypt_leaf(_: tuple[Hashable, ...], value: Any) -> Any:
|
499
|
-
'''Transforms EncryptedVar leaves into decrypted strings.'''
|
500
|
-
return self.keyring.decrypt(value.cipher) if type(value) is EncryptedVar else value
|
501
|
-
Vault._transform_leaves(copy, _decrypt_leaf, tuple())
|
505
|
+
copy: dict = self._decrypted_copy()
|
502
506
|
yaml_content: str = self._dump_to_str(copy)
|
503
507
|
yaml_content = Vault._remove_sentinel(yaml_content)
|
504
508
|
return yaml_content
|
@@ -537,12 +541,11 @@ class Vault():
|
|
537
541
|
Runs the transform_fn on all leaves of the indexable object recursively, passing the current path and the leaf object as a tuple.
|
538
542
|
The leaves are replaced by the result of the function call.
|
539
543
|
'''
|
540
|
-
if isinstance(indexable, dict):
|
541
|
-
|
542
|
-
else
|
543
|
-
keys: list[Hashable] = list(range(len(indexable)))
|
544
|
+
if not isinstance(indexable, dict | list):
|
545
|
+
raise Exception(f"Calling _transform_leaves on a non-indexable object is not allowed, got { type(indexable) }")
|
546
|
+
keys: list[Hashable] = list(indexable.keys()) if isinstance(indexable, dict) else list(range(len(indexable)))
|
544
547
|
for key in keys:
|
545
|
-
_curr_path = curr_path + ( key, )
|
548
|
+
_curr_path: tuple[Hashable, ...] = curr_path + ( key, )
|
546
549
|
if isinstance(indexable[key], dict | list): # type: ignore
|
547
550
|
Vault._transform_leaves(indexable[key], transform_fn, _curr_path) # type: ignore
|
548
551
|
else:
|
@@ -575,8 +578,8 @@ class Vault():
|
|
575
578
|
Not thread-safe.
|
576
579
|
'''
|
577
580
|
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))
|
581
|
+
copy: Any = data.copy()
|
582
|
+
keys = cast(dict, copy).keys() if is_dict else range(len(copy))
|
580
583
|
for key in keys:
|
581
584
|
copy[key] = Vault._copy_data(copy[key])
|
582
585
|
return copy
|
@@ -653,11 +656,11 @@ class Vault():
|
|
653
656
|
_path: tuple[Hashable, ...] = path + ( key, )
|
654
657
|
# Check if a key has been added or removed
|
655
658
|
if (is_dict and key in old_node and key not in new_node) or \
|
656
|
-
(not is_dict and key < len(old_node) and key >= len(new_node)):
|
659
|
+
(not is_dict and cast(int, key) < len(old_node) and cast(int, key) >= len(new_node)):
|
657
660
|
removed_paths.append(_path)
|
658
661
|
continue
|
659
662
|
if (is_dict and key in new_node and key not in old_node) or \
|
660
|
-
(not is_dict and key < len(new_node) and key >= len(old_node)):
|
663
|
+
(not is_dict and cast(int, key) < len(new_node) and cast(int, key) >= len(old_node)):
|
661
664
|
added_paths.append(_path)
|
662
665
|
continue
|
663
666
|
# Traverse subtree of node
|
@@ -796,7 +799,7 @@ class VaultFile(Vault):
|
|
796
799
|
if full_encryption and not keyring:
|
797
800
|
raise NoVaultKeysError(f"No vault keys available to write encrypted content to { path }")
|
798
801
|
with open(path, 'w') as file:
|
799
|
-
file.write(keyring.encrypt(content) if full_encryption else content)
|
802
|
+
file.write(cast(VaultKeyring, keyring).encrypt(content) if full_encryption else content)
|
800
803
|
# Create VaultFile and write content to disk
|
801
804
|
vaultfile = VaultFile(path, keyring=keyring)
|
802
805
|
vaultfile.full_encryption = full_encryption
|
@@ -807,10 +810,10 @@ class VaultFile(Vault):
|
|
807
810
|
def from_editable(VaultFile: Type['VaultFile'], prev_vault_file: 'VaultFile', edited_content: str) -> 'VaultFile':
|
808
811
|
'''Converts a YAML vault edited from a `VaultFile.as_editable` template into a new `VaultFile`. Does not update the file on disk.'''
|
809
812
|
# Create vault from editable, then wrap with our class and copy relevant attributes over
|
810
|
-
vault:
|
813
|
+
vault: VaultFile = cast(VaultFile, Vault.from_editable(prev_vault_file, edited_content))
|
811
814
|
vault.__class__ = VaultFile
|
812
|
-
vault.vault_path = prev_vault_file.vault_path
|
813
|
-
return vault
|
815
|
+
vault.vault_path = prev_vault_file.vault_path
|
816
|
+
return vault
|
814
817
|
|
815
818
|
def save(self) -> None:
|
816
819
|
'''Saves the current `Vault` contents to the vault file attached to this `VaultFile`. '''
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|