[passwordstore] Use builtin _random_password function instead of pwgen (#25843)

* [password] _random_password -> random_password and moved to util/encrypt.py
* [passwordstore] Use built-in random_password instead of pwgen utility
* [passwordstore] Add integration tests
This commit is contained in:
3onyc 2017-08-15 00:19:40 +02:00 committed by Toshio Kuratomi
commit 554496c404
21 changed files with 217 additions and 49 deletions

View file

@ -21,15 +21,12 @@ __metaclass__ = type
import os
import string
import random
from ansible import constants as C
from ansible.errors import AnsibleError
from ansible.module_utils.six import text_type
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.parsing.splitter import parse_kv
from ansible.plugins.lookup import LookupBase
from ansible.utils.encrypt import do_encrypt
from ansible.utils.encrypt import do_encrypt, random_password
from ansible.utils.path import makedirs_safe
@ -136,35 +133,13 @@ def _gen_candidate_chars(characters):
return chars
def _random_password(length=DEFAULT_LENGTH, chars=C.DEFAULT_PASSWORD_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 chars: The characters to choose from. The default is all ascii
letters, ascii digits, and these symbols ``.,:-_``
.. note: this was moved from the old ansible utils code, as nothing
else appeared to use it.
'''
assert isinstance(chars, text_type), '%s (%s) is not a text_type' % (chars, type(chars))
random_generator = random.SystemRandom()
password = []
while len(password) < length:
new_char = random_generator.choice(chars)
password.append(new_char)
return u''.join(password)
def _random_salt():
"""Return a text string suitable for use as a salt for the hash functions we use to encrypt passwords.
"""
# Note passlib salt values must be pure ascii so we can't let the user
# configure this
salt_chars = _gen_candidate_chars(['ascii_letters', 'digits', './'])
return _random_password(length=8, chars=salt_chars)
return random_password(length=8, chars=salt_chars)
def _parse_content(content):
@ -234,7 +209,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)
salt = None
changed = True
else:

View file

@ -20,8 +20,11 @@ __metaclass__ = type
import os
import subprocess
import time
from distutils import util
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.utils.encrypt import random_password
from ansible.plugins.lookup import LookupBase
@ -35,25 +38,31 @@ def check_output2(*popenargs, **kwargs):
if 'input' in kwargs:
if 'stdin' in kwargs:
raise ValueError('stdin and input arguments may not both be used.')
inputdata = kwargs['input']
b_inputdata = to_bytes(kwargs['input'], errors='surrogate_or_strict')
del kwargs['input']
kwargs['stdin'] = subprocess.PIPE
else:
inputdata = None
b_inputdata = None
process = subprocess.Popen(*popenargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
try:
out, err = process.communicate(inputdata)
b_out, b_err = process.communicate(b_inputdata)
except:
process.kill()
process.wait()
raise
retcode = process.poll()
if retcode:
if retcode != 0 or \
b'encryption failed: Unusable public key' in b_out or \
b'encryption failed: Unusable public key' in b_err:
cmd = kwargs.get("args")
if cmd is None:
cmd = popenargs[0]
raise subprocess.CalledProcessError(retcode, cmd, out + err)
return out
raise subprocess.CalledProcessError(
retcode,
cmd,
to_native(b_out + b_err, errors='surrogate_or_strict')
)
return b_out
class LookupModule(LookupBase):
@ -95,11 +104,14 @@ class LookupModule(LookupBase):
def check_pass(self):
try:
self.passoutput = check_output2(["pass", self.passname]).splitlines()
self.passoutput = to_text(
check_output2(["pass", self.passname]),
errors='surrogate_or_strict'
).splitlines()
self.password = self.passoutput[0]
self.passdict = {}
for line in self.passoutput[1:]:
if ":" in line:
if ':' in line:
name, value = line.split(':', 1)
self.passdict[name.strip()] = value.strip()
except (subprocess.CalledProcessError) as e:
@ -118,10 +130,7 @@ class LookupModule(LookupBase):
if self.paramvals['userpass']:
newpass = self.paramvals['userpass']
else:
try:
newpass = check_output2(['pwgen', '-cns', str(self.paramvals['length']), '1']).rstrip()
except (subprocess.CalledProcessError) as e:
raise AnsibleError(e)
newpass = random_password(length=self.paramvals['length'])
return newpass
def update_password(self):
@ -131,7 +140,7 @@ class LookupModule(LookupBase):
msg = newpass + '\n' + '\n'.join(self.passoutput[1:])
msg += "\nlookup_pass: old password was {} (Updated on {})\n".format(self.password, datetime)
try:
generate = check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg)
check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg)
except (subprocess.CalledProcessError) as e:
raise AnsibleError(e)
return newpass
@ -143,7 +152,7 @@ class LookupModule(LookupBase):
datetime = time.strftime("%d/%m/%Y %H:%M:%S")
msg = newpass + '\n' + "lookup_pass: First generated by ansible on {}\n".format(datetime)
try:
generate = check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg)
check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg)
except (subprocess.CalledProcessError) as e:
raise AnsibleError(e)
return newpass

View file

@ -23,11 +23,14 @@ import stat
import tempfile
import time
import warnings
import random
from ansible import constants as C
from ansible.errors import AnsibleError
from ansible.module_utils.six import text_type
from ansible.module_utils._text import to_text, to_bytes
PASSLIB_AVAILABLE = False
try:
import passlib.hash
@ -162,3 +165,19 @@ def keyczar_decrypt(key, msg):
return key.Decrypt(msg)
except key_errors.InvalidSignatureError:
raise AnsibleError("decryption failed")
DEFAULT_PASSWORD_LENGTH = 20
def random_password(length=DEFAULT_PASSWORD_LENGTH, chars=C.DEFAULT_PASSWORD_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 chars: The characters to choose from. The default is all ascii
letters, ascii digits, and these symbols ``.,:-_``
'''
assert isinstance(chars, text_type), '%s (%s) is not a text_type' % (chars, type(chars))
random_generator = random.SystemRandom()
return u''.join(random_generator.choice(chars) for dummy in range(length))