ansible-vars 1.0.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ansible-vars
3
- Version: 1.0.9
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>`. 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.
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
@@ -178,7 +179,7 @@ Shows the amounts of encrypted and decrypted variables in a vault file. Supports
178
179
 
179
180
  En-/Decrypts or checks the encryption status of a file or string value. Note that only full file encryption is considered in file mode, a hybrid vault with individually encrypted variables will be counted as plain.
180
181
 
181
- ### rekey
182
+ #### rekey
182
183
 
183
184
  Re-encrypts a vault file with a different encryption key and/or salt. The key specified in the global `--encryption-key|-K <identifier>` flag is used for encryption, along with an optional fixed salt set via the global `--fixed-salt|-S <salt>` flag.
184
185
 
@@ -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 file(s)/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
+ 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>`. 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.
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
@@ -156,7 +157,7 @@ Shows the amounts of encrypted and decrypted variables in a vault file. Supports
156
157
 
157
158
  En-/Decrypts or checks the encryption status of a file or string value. Note that only full file encryption is considered in file mode, a hybrid vault with individually encrypted variables will be counted as plain.
158
159
 
159
- ### rekey
160
+ #### rekey
160
161
 
161
162
  Re-encrypts a vault file with a different encryption key and/or salt. The key specified in the global `--encryption-key|-K <identifier>` flag is used for encryption, along with an optional fixed salt set via the global `--fixed-salt|-S <salt>` flag.
162
163
 
@@ -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 file(s)/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
+ 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:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ansible-vars"
7
- version = "1.0.9"
7
+ version = "1.0.11"
8
8
  authors = [
9
9
  { name="xorwow", email="pip@xorwow.de" },
10
10
  ]
@@ -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 option 2 first, then option 1 and option 3.
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:
@@ -776,12 +780,12 @@ if config.command == 'info':
776
780
  if encrypted_leaves:
777
781
  print('\n'.join([ f"- { format_key_path(key) }" for key in encrypted_leaves ]))
778
782
  else:
779
- print('No encrypted vars')
783
+ print('None', Color.MEH)
780
784
  print('\nPlain leaf values:', Color.GOOD)
781
- if encrypted_leaves:
785
+ if plain_leaves:
782
786
  print('\n'.join([ f"- { format_key_path(key) }" for key in plain_leaves ]))
783
787
  else:
784
- print('No plain vars')
788
+ print('None', Color.MEH)
785
789
 
786
790
  # Encrypt & Decrypt & Is-Encrypted commands
787
791
 
@@ -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 like '{ ENCRYPTED_VAR_TAG } <value>'.
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
- '''A variable marked to be encrypted in a `Vault` editable.'''
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) -> MappingProxyType:
202
- '''
203
- A read-only dictionary of the vault's variables, with any EncryptedVars already decrypted.
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 (but not its children).
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: int = path[-1] # type: ignore
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 (value has to be a str)
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
- # Convert value to EncryptedVar if neccessary
276
- if encrypt and type(value) is not EncryptedVar:
277
- if type(value) is not str:
278
- raise TypeError(f"Ansible only supports encrypted str values, got { type(value) } for { '.'.join(map(str, path)) }")
279
- if VaultKey.is_encrypted(value):
280
- value = EncryptedVar(value, name=str(path[-1]))
281
- else:
282
- value = EncryptedVar(self.keyring.encrypt(value), name=str(path[-1]))
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)): # type: ignore
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
- '''Gets the value of the specified key path from the `Vault`'s `_data`, optionally decrypting it.'''
472
- path = Vault._to_path(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)): # type: ignore
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
- leaf: Any = reduce(_get_child, path, self._data)
481
- return self.keyring.decrypt(leaf.cipher) if (decrypt and type(leaf) is EncryptedVar) else leaf
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: CommentedMap = Vault._copy_data(self._data)
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
- keys: list[Hashable] = list(indexable.keys())
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, ) # type: ignore
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)) # type: ignore
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)): # type: ignore
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)): # type: ignore
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) # type: ignore
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: Vault = Vault.from_editable(prev_vault_file, edited_content)
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 # type: ignore
813
- return vault # type: ignore
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