Skip to content

Commit abe685e

Browse files
authored
Merge pull request #89 from mbaldessari/v1-branch2
Update v1
2 parents 8cb9a99 + 2f106df commit abe685e

15 files changed

Lines changed: 763 additions & 499 deletions

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ super-linter: ## Runs super linter locally
1818
-e VALIDATE_JSON_PRETTIER=false \
1919
-e VALIDATE_MARKDOWN_PRETTIER=false \
2020
-e VALIDATE_PYTHON_PYLINT=false \
21+
-e VALIDATE_PYTHON_PYINK=false \
2122
-e VALIDATE_PYTHON_RUFF_FORMAT=false \
2223
-e VALIDATE_SHELL_SHFMT=false \
2324
-e VALIDATE_YAML=false \
@@ -34,12 +35,12 @@ ansible-lint: ## run ansible lint on ansible/ folder
3435

3536
.PHONY: ansible-sanitytest
3637
ansible-sanitytest: ## run ansible unit tests
37-
ansible-test sanity --docker default
38+
ansible-test sanity --docker default --python 3.11 --python 3.12
3839

3940
.PHONY: ansible-unittest
4041
ansible-unittest: ## run ansible unit tests
4142
rm -rf tests/output
42-
ansible-test units --docker
43+
ansible-test units --docker --python 3.11 --python 3.12
4344

4445
.PHONY: test
4546
test: ansible-sanitytest ansible-unittest

playbooks/process_secrets.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@
4848
kubernetes_secret_objects: "{{ secrets_results['kubernetes_secret_objects'] }}"
4949
vault_policies: "{{ secrets_results['vault_policies'] }}"
5050
parsed_secrets: "{{ secrets_results['parsed_secrets'] }}"
51+
unique_vault_prefixes: "{{ secrets_results['unique_vault_prefixes'] | default([]) }}"

plugins/module_utils/load_secrets_common.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,20 @@
2121
__metaclass__ = type
2222

2323
import configparser
24+
import getpass
25+
import os
2426
from collections.abc import MutableMapping
2527

28+
default_vp_vault_policies = {
29+
"validatedPatternDefaultPolicy": (
30+
"length=20\n"
31+
'rule "charset" { charset = "abcdefghijklmnopqrstuvwxyz" min-chars = 1 }\n'
32+
'rule "charset" { charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" min-chars = 1 }\n'
33+
'rule "charset" { charset = "0123456789" min-chars = 1 }\n'
34+
'rule "charset" { charset = "!@#%^&*" min-chars = 1 }\n'
35+
)
36+
}
37+
2638

2739
def find_dupes(array):
2840
"""
@@ -149,3 +161,247 @@ def filter_module_args(arg_spec):
149161
pass
150162

151163
return arg_spec
164+
165+
166+
class SecretsV2Base:
167+
"""
168+
Base class with common functionality for V2 secrets handling
169+
"""
170+
171+
def __init__(self, module, syaml):
172+
self.module = module
173+
self.syaml = syaml
174+
175+
def _get_vault_policies(self, enable_default_vp_policies=True):
176+
# We start off with the hard-coded default VP policy and add the user-defined ones
177+
policies = (
178+
default_vp_vault_policies.copy() if enable_default_vp_policies else {}
179+
)
180+
policies.update(self.syaml.get("vaultPolicies", {}))
181+
return policies
182+
183+
def _get_secrets(self):
184+
return self.syaml.get("secrets", [])
185+
186+
def _get_field_on_missing_value(self, f):
187+
# By default if 'onMissingValue' is missing we assume we need to
188+
# error out whenever the value is missing
189+
return f.get("onMissingValue", "error")
190+
191+
def _get_field_value(self, f):
192+
return f.get("value", None)
193+
194+
def _get_field_path(self, f):
195+
return f.get("path", None)
196+
197+
def _get_field_ini_file(self, f):
198+
return f.get("ini_file", None)
199+
200+
def _get_field_kind(self, f):
201+
# value: null will be interpreted with None, so let's just
202+
# check for the existence of the field, as we use 'value: null' to say
203+
# "we want a value/secret and not a file path"
204+
found = []
205+
for i in ["value", "path", "ini_file"]:
206+
if i in f:
207+
found.append(i)
208+
209+
if len(found) > 1: # you can only have one of value, path and ini_file
210+
self.module.fail_json(f"Both '{found[0]}' and '{found[1]}' cannot be used")
211+
212+
if len(found) == 0:
213+
return ""
214+
return found[0]
215+
216+
def _get_field_prompt(self, f):
217+
return f.get("prompt", None)
218+
219+
def _get_field_base64(self, f):
220+
return bool(f.get("base64", False))
221+
222+
def _get_field_override(self, f):
223+
return bool(f.get("override", False))
224+
225+
def _validate_field(self, f):
226+
# Check mandatory fields
227+
if "name" not in f:
228+
return (False, f"Field {f} is missing name")
229+
230+
# Validate field structure and types
231+
result = self._validate_field_structure(f)
232+
if not result[0]:
233+
return result
234+
235+
# Validate vault policy
236+
result = self._validate_vault_policy(f)
237+
if not result[0]:
238+
return result
239+
240+
on_missing_value = self._get_field_on_missing_value(f)
241+
# Validate based on onMissingValue type
242+
match on_missing_value:
243+
case "error":
244+
return self._validate_error_mode(f)
245+
case "generate":
246+
return self._validate_generate_mode(f)
247+
case "prompt":
248+
return self._validate_prompt_mode(f)
249+
250+
return (False, f"onMissingValue: {on_missing_value} is invalid")
251+
252+
def _validate_field_structure(self, f):
253+
"""Validate field structure and basic types"""
254+
kind = self._get_field_kind(f)
255+
if kind == "ini_file":
256+
ini_key = f.get("ini_key", None)
257+
if ini_key is None:
258+
return (False, "ini_file requires at least ini_key to be defined")
259+
260+
# Test if base64 and override are correct booleans
261+
self._get_field_base64(f)
262+
self._get_field_override(f)
263+
264+
return (True, "")
265+
266+
def _validate_vault_policy(self, f):
267+
"""Validate vault policy exists if specified"""
268+
vault_policy = f.get("vaultPolicy", None)
269+
if vault_policy is not None and vault_policy not in self._get_vault_policies():
270+
return (
271+
False,
272+
f"Secret has vaultPolicy set to {vault_policy} but no such policy exists",
273+
)
274+
return (True, "")
275+
276+
def _validate_error_mode(self, f):
277+
"""Validate fields when onMissingValue is 'error'"""
278+
value = self._get_field_value(f)
279+
path = self._get_field_path(f)
280+
ini_file = self._get_field_ini_file(f)
281+
282+
# Check that at least one source is provided and not empty
283+
if (
284+
(value is None or len(value) < 1)
285+
and (path is None or len(path) < 1)
286+
and (ini_file is None or len(ini_file) < 1)
287+
):
288+
return (
289+
False,
290+
"Secret has onMissingValue set to 'error' and has neither value nor path nor ini_file set",
291+
)
292+
293+
# Validate file paths exist
294+
if path is not None and not os.path.isfile(os.path.expanduser(path)):
295+
return (False, f"Field has non-existing path: {path}")
296+
297+
if ini_file is not None and not os.path.isfile(os.path.expanduser(ini_file)):
298+
return (False, f"Field has non-existing ini_file: {ini_file}")
299+
300+
# Override not allowed in error mode
301+
if "override" in f:
302+
return (
303+
False,
304+
"'override' attribute requires 'onMissingValue' to be set to 'generate'",
305+
)
306+
307+
return (True, "")
308+
309+
def _validate_generate_mode(self, f):
310+
"""Validate fields when onMissingValue is 'generate'"""
311+
value = self._get_field_value(f)
312+
path = self._get_field_path(f)
313+
vault_policy = f.get("vaultPolicy", None)
314+
315+
if value is not None:
316+
return (
317+
False,
318+
"Secret has onMissingValue set to 'generate' but has a value set",
319+
)
320+
321+
if path is not None:
322+
return (
323+
False,
324+
"Secret has onMissingValue set to 'generate' but has a path set",
325+
)
326+
327+
if vault_policy is None:
328+
return (
329+
False,
330+
"Secret has no vaultPolicy but onMissingValue is set to 'generate'",
331+
)
332+
333+
return (True, "")
334+
335+
def _validate_prompt_mode(self, f):
336+
"""Validate fields when onMissingValue is 'prompt'"""
337+
# When we prompt, the user needs to set one of the following:
338+
# - value: null # prompt for a secret without a default value
339+
# - value: 123 # prompt for a secret but use a default value
340+
# - path: null # prompt for a file path without a default value
341+
# - path: /tmp/ca.crt # prompt for a file path with a default value
342+
if "value" not in f and "path" not in f:
343+
return (
344+
False,
345+
"Secret has onMissingValue set to 'prompt' but has no value nor path fields",
346+
)
347+
348+
# Override not allowed in prompt mode
349+
if "override" in f:
350+
return (
351+
False,
352+
"'override' attribute requires 'onMissingValue' to be set to 'generate'",
353+
)
354+
355+
return (True, "")
356+
357+
def _get_secret_value(self, name, field):
358+
on_missing_value = self._get_field_on_missing_value(field)
359+
# We checked for errors in _validate_secrets() already
360+
match on_missing_value:
361+
case "error":
362+
value = field.get("value")
363+
# Allow subclasses to override value processing
364+
return self._process_secret_value(value)
365+
case "prompt":
366+
prompt = self._get_field_prompt(field)
367+
if prompt is None:
368+
prompt = f"Type secret for {name}/{field['name']}: "
369+
value = self._get_field_value(field)
370+
if value is not None:
371+
prompt += f" [{value}]"
372+
prompt += ": "
373+
return getpass.getpass(prompt)
374+
case _:
375+
return None
376+
377+
def _process_secret_value(self, value):
378+
"""
379+
Process a secret value. Can be overridden by subclasses.
380+
"""
381+
return value
382+
383+
def _get_file_path(self, name, field):
384+
on_missing_value = self._get_field_on_missing_value(field)
385+
match on_missing_value:
386+
case "error":
387+
return os.path.expanduser(field.get("path"))
388+
case "prompt":
389+
prompt = self._get_field_prompt(field)
390+
path = self._get_field_path(field)
391+
if path is None:
392+
path = ""
393+
394+
if prompt is None:
395+
text = f"Type path for file {name}/{field['name']} [{path}]: "
396+
else:
397+
text = f"{prompt} [{path}]: "
398+
399+
newpath = getpass.getpass(text)
400+
if newpath == "": # Set the default if no string was entered
401+
newpath = path
402+
403+
if os.path.isfile(os.path.expanduser(newpath)):
404+
return newpath
405+
self.module.fail_json(f"File {newpath} not found, exiting")
406+
case _:
407+
self.module.fail_json("File with wrong onMissingValue")

0 commit comments

Comments
 (0)