Add seed parameter to password lookup (#69775)

This commit is contained in:
s-hamann 2021-06-04 16:20:44 +00:00 committed by GitHub
parent ffa548503d
commit e2658801f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 64 additions and 25 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- password - add new parameter ``seed`` in lookup plugin (https://github.com/ansible/ansible/pull/69775).

View file

@ -57,6 +57,13 @@ DOCUMENTATION = """
description: The length of the generated password. description: The length of the generated password.
default: 20 default: 20
type: integer 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: notes:
- A great alternative to the password lookup plugin, - A great alternative to the password lookup plugin,
if you don't need to generate random passwords on a per-host basis, 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 - name: create lowercase 8 character name for Kubernetes pod name
set_fact: set_fact:
random_pod_name: "web-{{ lookup('password', '/dev/null chars=ascii_lowercase,digits length=8') }}" 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 = """ RETURN = """
@ -125,7 +136,7 @@ from ansible.utils.path import makedirs_safe
DEFAULT_LENGTH = 20 DEFAULT_LENGTH = 20
VALID_PARAMS = frozenset(('length', 'encrypt', 'chars', 'ident')) VALID_PARAMS = frozenset(('length', 'encrypt', 'chars', 'ident', 'seed'))
def _parse_parameters(term): def _parse_parameters(term):
@ -164,6 +175,7 @@ def _parse_parameters(term):
params['length'] = int(params.get('length', DEFAULT_LENGTH)) params['length'] = int(params.get('length', DEFAULT_LENGTH))
params['encrypt'] = params.get('encrypt', None) params['encrypt'] = params.get('encrypt', None)
params['ident'] = params.get('ident', None) params['ident'] = params.get('ident', None)
params['seed'] = params.get('seed', None)
params['chars'] = params.get('chars', None) params['chars'] = params.get('chars', None)
if params['chars']: if params['chars']:
@ -338,7 +350,7 @@ class LookupModule(LookupBase):
content = _read_password_file(b_path) content = _read_password_file(b_path)
if content is None or b_path == to_bytes('/dev/null'): 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 salt = None
changed = True changed = True
else: else:

View file

@ -48,7 +48,7 @@ _LOCK = multiprocessing.Lock()
DEFAULT_PASSWORD_LENGTH = 20 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 '''Return a random password string of length containing only chars
:kwarg length: The number of characters in the new password. Defaults to 20. :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): if not isinstance(chars, text_type):
raise AnsibleAssertionError('%s (%s) is not a text_type' % (chars, type(chars))) 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)) return u''.join(random_generator.choice(chars) for dummy in range(length))

View file

@ -50,7 +50,7 @@ old_style_params_data = (
dict( dict(
term=u'/path/to/file', term=u'/path/to/file',
filename=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, candidate_chars=DEFAULT_CANDIDATE_CHARS,
), ),
@ -58,38 +58,39 @@ old_style_params_data = (
dict( dict(
term=u'/path/with/embedded spaces and/file', term=u'/path/with/embedded spaces and/file',
filename=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, candidate_chars=DEFAULT_CANDIDATE_CHARS,
), ),
dict( dict(
term=u'/path/with/equals/cn=com.ansible', term=u'/path/with/equals/cn=com.ansible',
filename=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, candidate_chars=DEFAULT_CANDIDATE_CHARS,
), ),
dict( dict(
term=u'/path/with/unicode/くらとみ/file', term=u'/path/with/unicode/くらとみ/file',
filename=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, candidate_chars=DEFAULT_CANDIDATE_CHARS,
), ),
# Mix several special chars # Mix several special chars
dict( dict(
term=u'/path/with/utf 8 and spaces/くらとみ/file', term=u'/path/with/utf 8 and spaces/くらとみ/file',
filename=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, candidate_chars=DEFAULT_CANDIDATE_CHARS,
), ),
dict( dict(
term=u'/path/with/encoding=unicode/くらとみ/file', term=u'/path/with/encoding=unicode/くらとみ/file',
filename=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, candidate_chars=DEFAULT_CANDIDATE_CHARS,
), ),
dict( dict(
term=u'/path/with/encoding=unicode/くらとみ/and spaces file', term=u'/path/with/encoding=unicode/くらとみ/and spaces file',
filename=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, candidate_chars=DEFAULT_CANDIDATE_CHARS,
), ),
@ -97,39 +98,48 @@ old_style_params_data = (
dict( dict(
term=u'/path/to/file length=42', term=u'/path/to/file length=42',
filename=u'/path/to/file', 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, candidate_chars=DEFAULT_CANDIDATE_CHARS,
), ),
dict( dict(
term=u'/path/to/file encrypt=pbkdf2_sha256', term=u'/path/to/file encrypt=pbkdf2_sha256',
filename=u'/path/to/file', 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, candidate_chars=DEFAULT_CANDIDATE_CHARS,
), ),
dict( dict(
term=u'/path/to/file chars=abcdefghijklmnop', term=u'/path/to/file chars=abcdefghijklmnop',
filename=u'/path/to/file', 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', candidate_chars=u'abcdefghijklmnop',
), ),
dict( dict(
term=u'/path/to/file chars=digits,abc,def', term=u'/path/to/file chars=digits,abc,def',
filename=u'/path/to/file', 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', 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 # Including comma in chars
dict( dict(
term=u'/path/to/file chars=abcdefghijklmnop,,digits', term=u'/path/to/file chars=abcdefghijklmnop,,digits',
filename=u'/path/to/file', 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', candidate_chars=u',abcdefghijklmnop0123456789',
), ),
dict( dict(
term=u'/path/to/file chars=,,', term=u'/path/to/file chars=,,',
filename=u'/path/to/file', 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',', candidate_chars=u',',
), ),
@ -137,13 +147,15 @@ old_style_params_data = (
dict( dict(
term=u'/path/to/file chars=digits,=,,', term=u'/path/to/file chars=digits,=,,',
filename=u'/path/to/file', 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', candidate_chars=u',=0123456789',
), ),
dict( dict(
term=u'/path/to/file chars=digits,abc=def', term=u'/path/to/file chars=digits,abc=def',
filename=u'/path/to/file', 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', candidate_chars=u'abc=def0123456789',
), ),
@ -151,14 +163,16 @@ old_style_params_data = (
dict( dict(
term=u'/path/to/file chars=digits,くらとみ,,', term=u'/path/to/file chars=digits,くらとみ,,',
filename=u'/path/to/file', 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くらとみ', candidate_chars=u',0123456789くらとみ',
), ),
# Including only unicode in chars # Including only unicode in chars
dict( dict(
term=u'/path/to/file chars=くらとみ', term=u'/path/to/file chars=くらとみ',
filename=u'/path/to/file', 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'くらとみ', candidate_chars=u'くらとみ',
), ),
@ -166,7 +180,8 @@ old_style_params_data = (
dict( dict(
term=u'/path/to/file_with:colon chars=ascii_letters,digits', term=u'/path/to/file_with:colon chars=ascii_letters,digits',
filename=u'/path/to/file_with:colon', 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', candidate_chars=u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
), ),
@ -175,19 +190,19 @@ old_style_params_data = (
dict( dict(
term=u'/path/with/embedded spaces and/file chars=abc=def', term=u'/path/with/embedded spaces and/file chars=abc=def',
filename=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=[u'abc=def']), params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abc=def'], seed=None),
candidate_chars=u'abc=def', candidate_chars=u'abc=def',
), ),
dict( dict(
term=u'/path/with/equals/cn=com.ansible chars=abc=def', term=u'/path/with/equals/cn=com.ansible chars=abc=def',
filename=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=[u'abc=def']), params=dict(length=password.DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abc=def'], seed=None),
candidate_chars=u'abc=def', candidate_chars=u'abc=def',
), ),
dict( dict(
term=u'/path/with/unicode/くらとみ/file chars=くらとみ', term=u'/path/with/unicode/くらとみ/file chars=くらとみ',
filename=u'/path/with/unicode/くらとみ/file', 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'くらとみ', candidate_chars=u'くらとみ',
), ),
) )
@ -280,6 +295,13 @@ class TestRandomPassword(unittest.TestCase):
self._assert_valid_chars(res, u'くらとみ') self._assert_valid_chars(res, u'くらとみ')
self.assertEqual(len(res), 11) 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): def test_gen_password(self):
for testcase in old_style_params_data: for testcase in old_style_params_data:
params = testcase['params'] params = testcase['params']