diff --git a/changelogs/fragments/password_seed.yml b/changelogs/fragments/password_seed.yml new file mode 100644 index 00000000000..4bcf1e9cc5c --- /dev/null +++ b/changelogs/fragments/password_seed.yml @@ -0,0 +1,2 @@ +minor_changes: +- password - add new parameter ``seed`` in lookup plugin (https://github.com/ansible/ansible/pull/69775). diff --git a/lib/ansible/plugins/lookup/password.py b/lib/ansible/plugins/lookup/password.py index 10526391278..3eb48280ea5 100644 --- a/lib/ansible/plugins/lookup/password.py +++ b/lib/ansible/plugins/lookup/password.py @@ -57,6 +57,13 @@ DOCUMENTATION = """ description: The length of the generated password. default: 20 type: integer + seed: + version_added: "2.12" + description: + - A seed to initialize the random number generator. + - Identical seeds will yield identical passwords. + - Use this for random-but-idempotent password generation. + type: str notes: - A great alternative to the password lookup plugin, if you don't need to generate random passwords on a per-host basis, @@ -100,6 +107,10 @@ EXAMPLES = """ - name: create lowercase 8 character name for Kubernetes pod name set_fact: random_pod_name: "web-{{ lookup('password', '/dev/null chars=ascii_lowercase,digits length=8') }}" + +- name: create random but idempotent password + set_fact: + password: "{{ lookup('password', '/dev/null', seed=inventory_hostname) }}" """ RETURN = """ @@ -125,7 +136,7 @@ from ansible.utils.path import makedirs_safe DEFAULT_LENGTH = 20 -VALID_PARAMS = frozenset(('length', 'encrypt', 'chars', 'ident')) +VALID_PARAMS = frozenset(('length', 'encrypt', 'chars', 'ident', 'seed')) def _parse_parameters(term): @@ -164,6 +175,7 @@ def _parse_parameters(term): params['length'] = int(params.get('length', DEFAULT_LENGTH)) params['encrypt'] = params.get('encrypt', None) params['ident'] = params.get('ident', None) + params['seed'] = params.get('seed', None) params['chars'] = params.get('chars', None) if params['chars']: @@ -338,7 +350,7 @@ class LookupModule(LookupBase): content = _read_password_file(b_path) if content is None or b_path == to_bytes('/dev/null'): - plaintext_password = random_password(params['length'], chars) + plaintext_password = random_password(params['length'], chars, params['seed']) salt = None changed = True else: diff --git a/lib/ansible/utils/encrypt.py b/lib/ansible/utils/encrypt.py index f462ef53a4a..1579f779f9f 100644 --- a/lib/ansible/utils/encrypt.py +++ b/lib/ansible/utils/encrypt.py @@ -48,7 +48,7 @@ _LOCK = multiprocessing.Lock() DEFAULT_PASSWORD_LENGTH = 20 -def random_password(length=DEFAULT_PASSWORD_LENGTH, chars=C.DEFAULT_PASSWORD_CHARS): +def random_password(length=DEFAULT_PASSWORD_LENGTH, chars=C.DEFAULT_PASSWORD_CHARS, seed=None): '''Return a random password string of length containing only chars :kwarg length: The number of characters in the new password. Defaults to 20. @@ -58,7 +58,10 @@ def random_password(length=DEFAULT_PASSWORD_LENGTH, chars=C.DEFAULT_PASSWORD_CHA if not isinstance(chars, text_type): raise AnsibleAssertionError('%s (%s) is not a text_type' % (chars, type(chars))) - random_generator = random.SystemRandom() + if seed is None: + random_generator = random.SystemRandom() + else: + random_generator = random.Random(seed) return u''.join(random_generator.choice(chars) for dummy in range(length)) diff --git a/test/units/plugins/lookup/test_password.py b/test/units/plugins/lookup/test_password.py index cb9b3e887ea..4cd9f136c0a 100644 --- a/test/units/plugins/lookup/test_password.py +++ b/test/units/plugins/lookup/test_password.py @@ -50,7 +50,7 @@ old_style_params_data = ( dict( term=u'/path/to/file', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None), candidate_chars=DEFAULT_CANDIDATE_CHARS, ), @@ -58,38 +58,39 @@ old_style_params_data = ( dict( term=u'/path/with/embedded spaces and/file', filename=u'/path/with/embedded spaces and/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None), candidate_chars=DEFAULT_CANDIDATE_CHARS, ), dict( term=u'/path/with/equals/cn=com.ansible', filename=u'/path/with/equals/cn=com.ansible', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None), candidate_chars=DEFAULT_CANDIDATE_CHARS, ), dict( term=u'/path/with/unicode/くらとみ/file', filename=u'/path/with/unicode/くらとみ/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None), candidate_chars=DEFAULT_CANDIDATE_CHARS, ), + # Mix several special chars dict( term=u'/path/with/utf 8 and spaces/くらとみ/file', filename=u'/path/with/utf 8 and spaces/くらとみ/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None), candidate_chars=DEFAULT_CANDIDATE_CHARS, ), dict( term=u'/path/with/encoding=unicode/くらとみ/file', filename=u'/path/with/encoding=unicode/くらとみ/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None), candidate_chars=DEFAULT_CANDIDATE_CHARS, ), dict( term=u'/path/with/encoding=unicode/くらとみ/and spaces file', filename=u'/path/with/encoding=unicode/くらとみ/and spaces file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None), candidate_chars=DEFAULT_CANDIDATE_CHARS, ), @@ -97,39 +98,48 @@ old_style_params_data = ( dict( term=u'/path/to/file length=42', filename=u'/path/to/file', - params=dict(length=42, encrypt=None, ident=None, chars=DEFAULT_CHARS), + params=dict(length=42, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None), candidate_chars=DEFAULT_CANDIDATE_CHARS, ), dict( term=u'/path/to/file encrypt=pbkdf2_sha256', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt='pbkdf2_sha256', ident=None, chars=DEFAULT_CHARS), + params=dict(length=password.DEFAULT_LENGTH, encrypt='pbkdf2_sha256', ident=None, chars=DEFAULT_CHARS, seed=None), candidate_chars=DEFAULT_CANDIDATE_CHARS, ), dict( term=u'/path/to/file chars=abcdefghijklmnop', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abcdefghijklmnop']), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abcdefghijklmnop'], seed=None), candidate_chars=u'abcdefghijklmnop', ), dict( term=u'/path/to/file chars=digits,abc,def', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=sorted([u'digits', u'abc', u'def'])), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, + chars=sorted([u'digits', u'abc', u'def']), seed=None), candidate_chars=u'abcdef0123456789', ), + dict( + term=u'/path/to/file seed=1', + filename=u'/path/to/file', + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed='1'), + candidate_chars=DEFAULT_CANDIDATE_CHARS, + ), # Including comma in chars dict( term=u'/path/to/file chars=abcdefghijklmnop,,digits', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=sorted([u'abcdefghijklmnop', u',', u'digits'])), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, + chars=sorted([u'abcdefghijklmnop', u',', u'digits']), seed=None), candidate_chars=u',abcdefghijklmnop0123456789', ), dict( term=u'/path/to/file chars=,,', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u',']), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, + chars=[u','], seed=None), candidate_chars=u',', ), @@ -137,13 +147,15 @@ old_style_params_data = ( dict( term=u'/path/to/file chars=digits,=,,', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=sorted([u'digits', u'=', u','])), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, + chars=sorted([u'digits', u'=', u',']), seed=None), candidate_chars=u',=0123456789', ), dict( term=u'/path/to/file chars=digits,abc=def', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=sorted([u'digits', u'abc=def'])), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, + chars=sorted([u'digits', u'abc=def']), seed=None), candidate_chars=u'abc=def0123456789', ), @@ -151,14 +163,16 @@ old_style_params_data = ( dict( term=u'/path/to/file chars=digits,くらとみ,,', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=sorted([u'digits', u'くらとみ', u','])), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, + chars=sorted([u'digits', u'くらとみ', u',']), seed=None), candidate_chars=u',0123456789くらとみ', ), # Including only unicode in chars dict( term=u'/path/to/file chars=くらとみ', filename=u'/path/to/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=sorted([u'くらとみ'])), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, + chars=sorted([u'くらとみ']), seed=None), candidate_chars=u'くらとみ', ), @@ -166,7 +180,8 @@ old_style_params_data = ( dict( term=u'/path/to/file_with:colon chars=ascii_letters,digits', filename=u'/path/to/file_with:colon', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=sorted([u'ascii_letters', u'digits'])), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, + chars=sorted([u'ascii_letters', u'digits']), seed=None), candidate_chars=u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ), @@ -175,19 +190,19 @@ old_style_params_data = ( dict( term=u'/path/with/embedded spaces and/file chars=abc=def', filename=u'/path/with/embedded spaces and/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abc=def']), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abc=def'], seed=None), candidate_chars=u'abc=def', ), dict( term=u'/path/with/equals/cn=com.ansible chars=abc=def', filename=u'/path/with/equals/cn=com.ansible', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abc=def']), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abc=def'], seed=None), candidate_chars=u'abc=def', ), dict( term=u'/path/with/unicode/くらとみ/file chars=くらとみ', filename=u'/path/with/unicode/くらとみ/file', - params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'くらとみ']), + params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'くらとみ'], seed=None), candidate_chars=u'くらとみ', ), ) @@ -280,6 +295,13 @@ class TestRandomPassword(unittest.TestCase): self._assert_valid_chars(res, u'くらとみ') self.assertEqual(len(res), 11) + def test_seed(self): + pw1 = password.random_password(seed=1) + pw2 = password.random_password(seed=1) + pw3 = password.random_password(seed=2) + self.assertEqual(pw1, pw2) + self.assertNotEqual(pw1, pw3) + def test_gen_password(self): for testcase in old_style_params_data: params = testcase['params']