2
0
mirror of https://gitlab.isc.org/isc-projects/kea synced 2025-08-30 21:45:37 +00:00

[master] Merge branch 'trac2713'

This commit is contained in:
Jelte Jansen
2013-02-28 15:45:51 +01:00
8 changed files with 754 additions and 103 deletions

View File

@@ -1204,6 +1204,7 @@ AC_CONFIG_FILES([Makefile
src/bin/stats/tests/Makefile
src/bin/stats/tests/testdata/Makefile
src/bin/usermgr/Makefile
src/bin/usermgr/tests/Makefile
src/bin/tests/Makefile
src/lib/Makefile
src/lib/asiolink/Makefile

View File

@@ -1,8 +1,11 @@
SUBDIRS = tests
sbin_SCRIPTS = b10-cmdctl-usermgr
noinst_SCRIPTS = run_b10-cmdctl-usermgr.sh
b10_cmdctl_usermgrdir = $(pkgdatadir)
CLEANFILES= b10-cmdctl-usermgr
CLEANFILES= b10-cmdctl-usermgr b10-cmdctl-usermgr.pyc
man_MANS = b10-cmdctl-usermgr.8
DISTCLEANFILES = $(man_MANS)
@@ -25,3 +28,7 @@ endif
b10-cmdctl-usermgr: b10-cmdctl-usermgr.py
$(SED) "s|@@PYTHONPATH@@|@pyexecdir@|" b10-cmdctl-usermgr.py >$@
chmod a+x $@
CLEANDIRS = __pycache__
clean-local:
rm -rf $(CLEANDIRS)

291
src/bin/usermgr/b10-cmdctl-usermgr.py.in Normal file → Executable file
View File

@@ -11,115 +11,234 @@
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN COMMAND OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS COMMAND, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
'''
This file implements user management program. The user name and
its password is appended to csv file.
This tool implements user management for b10-cmdctl. It is used to
add and remove users from the accounts file.
'''
from bind10_config import SYSCONFPATH
from collections import OrderedDict
import random
from hashlib import sha1
import csv
import getpass
import getopt
from optparse import OptionParser, OptionValueError
import os
import sys; sys.path.append ('@@PYTHONPATH@@')
import isc.util.process
isc.util.process.rename()
VERSION_NUMBER = 'bind10'
DEFAULT_FILE = 'cmdctl-accounts.csv'
VERSION_STRING = "b10-cmdctl-usermgr @PACKAGE_VERSION@"
DEFAULT_FILE = SYSCONFPATH + "/cmdctl-accounts.csv"
def gen_password_hash(password):
salt = "".join(chr(random.randint(33, 127)) for x in range(64))
saltedpwd = sha1((password + salt).encode()).hexdigest()
return salt, saltedpwd
# Actions that can be performed (used for argument parsing,
# code paths, and output)
COMMAND_ADD = "add"
COMMAND_DELETE = "delete"
def username_exist(name, filename):
# The file may doesn't exist.
exist = False
csvfile = None
try:
csvfile = open(filename)
reader = csv.reader(csvfile)
for row in reader:
if name == row[0]:
exist = True
break
except Exception:
pass
if csvfile:
csvfile.close()
return exist
# Non-zero return codes, used in tests
BAD_ARGUMENTS = 1
FILE_ERROR = 2
USER_EXISTS = 3
USER_DOES_NOT_EXIST = 4
def save_userinfo(username, pw, salt, filename):
csvfile = open(filename, 'a')
writer = csv.writer(csvfile)
writer.writerow([username, pw, salt])
csvfile.close()
print("\n create new account successfully! \n")
class UserManager:
def __init__(self, options, args):
self.options = options
self.args = args
def usage():
print('''Usage: usermgr [options]
-h, --help \t Show this help message and exit
-f, --file \t Specify the file to append user name and password
-v, --version\t Get version number
''')
def __print(self, msg):
if not self.options.quiet:
print(msg)
def main():
filename = DEFAULT_FILE
try:
opts, args = getopt.getopt(sys.argv[1:], 'f:hv',
['file=', 'help', 'version'])
except getopt.GetoptError as err:
print(err)
usage()
sys.exit(2)
for op, param in opts:
if op in ('-h', '--help'):
usage()
sys.exit()
elif op in ('-v', '--version'):
print(VERSION_NUMBER)
sys.exit()
elif op in ('-f', "--file"):
filename = param
else:
assert False, 'unknown option'
usage()
try:
while True :
name = input("Desired Login Name:")
if name == '':
print("error, user name can't be empty")
def __gen_password_hash(self, password):
salt = "".join(chr(random.randint(ord('!'), ord('~')))\
for x in range(64))
saltedpwd = sha1((password + salt).encode()).hexdigest()
return salt, saltedpwd
def __read_user_info(self):
"""
Read the existing user info
Raises an IOError if the file can't be read
"""
# Currently, this is done quite naively (there is no
# check that the file is modified between read and write)
# But taking multiple simultaneous users of this tool on the
# same file seems unnecessary at this point.
self.user_info = OrderedDict()
if os.path.exists(self.options.output_file):
# Just let any file read error bubble up; it will
# be caught in the run() method
with open(self.options.output_file, newline='') as csvfile:
reader = csv.reader(csvfile)
for row in reader:
self.user_info[row[0]] = row
def __save_user_info(self):
"""
Write out the (modified) user info
Raises an IOError if the file can't be written
"""
# Just let any file write error bubble up; it will
# be caught in the run() method
with open(self.options.output_file, 'w',
newline='') as csvfile:
writer = csv.writer(csvfile)
for row in self.user_info.values():
writer.writerow(row)
def __add_user(self, name, password):
"""
Add the given username/password combination to the stored user_info.
First checks if the username exists, and returns False if so.
If not, it is added, and this method returns True.
"""
if name in self.user_info:
return False
salt, pw = self.__gen_password_hash(password)
self.user_info[name] = [name, pw, salt]
return True
def __delete_user(self, name):
"""
Removes the row with the given name from the stored user_info
First checks if the username exists, and returns False if not.
Otherwise, it is removed, and this mehtod returns True
"""
if name not in self.user_info:
return False
del self.user_info[name]
return True
# overridable input() call, used in testing
def _input(self, prompt):
return input(prompt)
# in essence this is private, but made 'protected' for ease
# of testing
def _prompt_for_username(self, command):
# Note, direct prints here are intentional
while True:
name = self._input("Username to " + command + ": ")
if name == "":
print("Error username can't be empty")
continue
if username_exist(name, filename):
print("user name already exists!")
if command == COMMAND_ADD and name in self.user_info:
print("user already exists")
continue
elif command == COMMAND_DELETE and name not in self.user_info:
print("user does not exist")
continue
while True:
pwd1 = getpass.getpass("Choose a password:")
pwd2 = getpass.getpass("Re-enter password:")
if pwd1 != pwd2:
print("password is not same, please input again")
else:
break;
salt, pw = gen_password_hash(pwd1)
save_userinfo(name, pw, salt, filename)
inputdata = input('continue to create new account by input \'y\' or \'Y\':')
if inputdata not in ['y', 'Y']:
break
except KeyboardInterrupt:
pass
return name
# in essence this is private, but made 'protected' for ease
# of testing
def _prompt_for_password(self):
# Note, direct prints here are intentional
while True:
pwd1 = getpass.getpass("Choose a password: ")
if pwd1 == "":
print("Error: password cannot be empty")
continue
pwd2 = getpass.getpass("Re-enter password: ")
if pwd1 != pwd2:
print("passwords do not match, try again")
continue
return pwd1
def __verify_options_and_args(self):
"""
Basic sanity checks on command line arguments.
Returns False if there is a problem, True if everything seems OK.
"""
if len(self.args) < 1:
self.__print("Error: no command specified")
return False
if len(self.args) > 3:
self.__print("Error: extraneous arguments")
return False
if self.args[0] not in [ COMMAND_ADD, COMMAND_DELETE ]:
self.__print("Error: command must be either add or delete")
return False
if self.args[0] == COMMAND_DELETE and len(self.args) > 2:
self.__print("Error: delete only needs username, not a password")
return False
return True
def run(self):
if not self.__verify_options_and_args():
return BAD_ARGUMENTS
try:
self.__print("Using accounts file: " + self.options.output_file)
self.__read_user_info()
command = self.args[0]
if len(self.args) > 1:
username = self.args[1]
else:
username = self._prompt_for_username(command)
if command == COMMAND_ADD:
if len(self.args) > 2:
password = self.args[2]
else:
password = self._prompt_for_password()
if not self.__add_user(username, password):
print("Error: username exists")
return USER_EXISTS
elif command == COMMAND_DELETE:
if not self.__delete_user(username):
print("Error: username does not exist")
return USER_DOES_NOT_EXIST
self.__save_user_info()
return 0
except IOError as ioe:
self.__print("Error accessing " + ioe.filename +\
": " + str(ioe.strerror))
return FILE_ERROR
except csv.Error as csve:
self.__print("Error parsing csv file: " + str(csve))
return FILE_ERROR
def set_options(parser):
parser.add_option("-f", "--file",
dest="output_file", default=DEFAULT_FILE,
help="Accounts file to modify"
)
parser.add_option("-q", "--quiet",
dest="quiet", action="store_true", default=False,
help="Quiet mode, don't print any output"
)
def main():
usage = "usage: %prog [options] <command> [username] [password]\n\n"\
"Arguments:\n"\
" command\t\teither 'add' or 'delete'\n"\
" username\t\tthe username to add or delete\n"\
" password\t\tthe password to set for the added user\n"\
"\n"\
"If username or password are not specified, %prog will\n"\
"prompt for them. It is recommended practice to let the\n"\
"tool prompt for the password, as command-line\n"\
"arguments can be visible through history or process\n"\
"viewers."
parser = OptionParser(usage=usage, version=VERSION_STRING)
set_options(parser)
(options, args) = parser.parse_args()
usermgr = UserManager(options, args)
return usermgr.run()
if __name__ == '__main__':
main()
sys.exit(main())

View File

@@ -12,8 +12,8 @@
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN COMMAND OF CONTRACT, NEGLIGENCE
- OR OTHER TORTIOUS COMMAND, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- PERFORMANCE OF THIS SOFTWARE.
-->
@@ -47,10 +47,12 @@
<arg><option>-f <replaceable>filename</replaceable></option></arg>
<arg><option>-h</option></arg>
<arg><option>-v</option></arg>
<arg><option>--file <replaceable>filename</replaceable></option></arg>
<arg><option>--file=<replaceable>filename</replaceable></option></arg>
<arg><option>--help</option></arg>
<arg><option>--version</option></arg>
<arg choice="plain"><replaceable>command</replaceable></arg>
<arg><option>username</option></arg>
<arg><option>password</option></arg>
</cmdsynopsis>
</refsynopsisdiv>
@@ -58,24 +60,22 @@
<refsect1>
<title>DESCRIPTION</title>
<para>The <command>b10-cmdctl-usermgr</command> tool may be used
to add accounts with passwords for the
to add and remove accounts with passwords for the
<citerefentry><refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum></citerefentry>
daemon.
</para>
<para>
By default, the accounts are saved in the
<filename>cmdctl-accounts.csv</filename> file in the current directory,
unless the <option>--filename</option> switch is used.
The entry is appended to the file.
<!-- TODO: default should have full path? -->
<filename>cmdctl-accounts.csv</filename> file in the system config
directory, unless the <option>--filename</option> switch is used.
The entry is appended to or removed from the file.
</para>
<para>
The tool can't remove or replace existing entries.
The tool can't replace existing entries, but this can easily be
accomplished by removing the entry and adding a new one.
</para>
<!-- TODO: the tool can't remove or replace existing entries -->
</refsect1>
<refsect1>
@@ -83,6 +83,18 @@
<para>The arguments are as follows:</para>
<para>
command is either 'add' or 'delete', respectively to add or delete users.
</para>
<para>
If a username and password are given (or just a username in case of
deletion), these are used. Otherwise, the tool shall prompt for a
username and/or password. It is recommended practice to let the
tool prompt for the password, as command-line arguments can be
visible through history or process viewers.
</para>
<variablelist>
<varlistentry>
@@ -97,14 +109,13 @@
<term><option>-f <replaceable>filename</replaceable></option></term>
<term><option>--file <replaceable>filename</replaceable></option></term>
<listitem><para>
Define the filename to append the account to. The default
is <filename>cmdctl-accounts.csv</filename> in the current directory.
<!-- TODO: default should have full path? -->
Specify the accounts file to update. The default is
<filename>cmdctl-accounts.csv</filename> in the system config
directory.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>-v</option></term>
<term><option>--version</option></term>
<listitem><para>
Report the version and exit.

3
src/bin/usermgr/run_b10-cmdctl-usermgr.sh.in Normal file → Executable file
View File

@@ -18,6 +18,9 @@
PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
export PYTHON_EXEC
PYTHONPATH=@abs_top_builddir@/src/lib/python
export PYTHONPATH
MYPATH_PATH=@abs_top_builddir@/src/bin/usermgr
BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket

View File

@@ -0,0 +1,22 @@
PYCOVERAGE_RUN=@PYCOVERAGE_RUN@
PYTESTS = b10-cmdctl-usermgr_test.py
EXTRA_DIST = $(PYTESTS)
CLEANFILES = *.csv
# test using command-line arguments, so use check-local target instead of TESTS
check-local:
if ENABLE_PYTHON_COVERAGE
touch $(abs_top_srcdir)/.coverage
rm -f .coverage
${LN_S} $(abs_top_srcdir)/.coverage .coverage
endif
for pytest in $(PYTESTS) ; do \
echo Running test: $$pytest ; \
$(LIBRARY_PATH_PLACEHOLDER) \
PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/cmdctl \
CMDCTL_BUILD_PATH=$(abs_top_builddir)/src/bin/cmdctl \
CMDCTL_SRC_PATH=$(abs_top_srcdir)/src/bin/cmdctl \
B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
done

View File

@@ -0,0 +1,483 @@
# Copyright (C) 2013 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN COMMAND OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS COMMAND, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import csv
from hashlib import sha1
import getpass
import imp
import os
import subprocess
import stat
import sys
import unittest
from bind10_config import SYSCONFPATH
class PrintCatcher:
def __init__(self):
self.stdout_lines = []
def __enter__(self):
self.__orig_stdout_write = sys.stdout.write
def new_write(line):
self.stdout_lines.append(line)
sys.stdout.write = new_write
return self
def __exit__(self, type, value, traceback):
sys.stdout.write = self.__orig_stdout_write
class OverrideGetpass:
def __init__(self, new_getpass):
self.__new_getpass = new_getpass
self.__orig_getpass = getpass.getpass
def __enter__(self):
getpass.getpass = self.__new_getpass
return self
def __exit__(self, type, value, traceback):
getpass.getpass = self.__orig_getpass
# input() is a built-in function and not easily overridable
# so this one uses usermgr for that
class OverrideInput:
def __init__(self, usermgr, new_getpass):
self.__usermgr = usermgr
self.__new_input = new_getpass
self.__orig_input = usermgr._input
def __enter__(self):
self.__usermgr._input = self.__new_input
return self
def __exit__(self, type, value, traceback):
self.__usermgr._input = self.__orig_input
def run(command):
"""
Small helper function that returns a tuple of (rcode, stdout, stderr)
after running the given command (an array of command and arguments, as
passed on to subprocess).
Parameters:
command: an array of command and argument strings, which will be
passed to subprocess.Popen()
"""
subp = subprocess.Popen(command, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(stdout, stderr) = subp.communicate()
return (subp.returncode, stdout, stderr)
class TestUserMgr(unittest.TestCase):
TOOL = '../b10-cmdctl-usermgr'
OUTPUT_FILE = 'test_users.csv'
def setUp(self):
self.delete_output_file()
# For access to the actual module, we load it directly
self.usermgr_module = imp.load_source('usermgr',
'../b10-cmdctl-usermgr.py')
# And instantiate 1 instance (with fake options/args)
self.usermgr = self.usermgr_module.UserManager(object(), object())
def tearDown(self):
self.delete_output_file()
def delete_output_file(self):
if os.path.exists(self.OUTPUT_FILE):
os.remove(self.OUTPUT_FILE)
def check_output_file(self, expected_content):
self.assertTrue(os.path.exists(self.OUTPUT_FILE))
csv_entries = []
with open(self.OUTPUT_FILE, newline='') as csvfile:
reader = csv.reader(csvfile)
csv_entries = [row for row in reader]
self.assertEqual(len(expected_content), len(csv_entries))
csv_entries.reverse()
for expected_entry in expected_content:
expected_name = expected_entry[0]
expected_pass = expected_entry[1]
csv_entry = csv_entries.pop()
entry_name = csv_entry[0]
entry_salt = csv_entry[2]
entry_hash = csv_entry[1]
self.assertEqual(expected_name, entry_name)
expected_hash =\
sha1((expected_pass + entry_salt).encode()).hexdigest()
self.assertEqual(expected_hash, entry_hash)
def run_check(self, expected_returncode, expected_stdout, expected_stderr,
command):
"""
Runs the given command, and checks return code, and outputs (if provided).
Arguments:
expected_returncode, return code of the command
expected_stdout, (multiline) string that is checked against stdout.
May be None, in which case the check is skipped.
expected_stderr, (multiline) string that is checked against stderr.
May be None, in which case the check is skipped.
"""
(returncode, stdout, stderr) = run(command)
if expected_stderr is not None:
self.assertEqual(expected_stderr, stderr.decode())
if expected_stdout is not None:
self.assertEqual(expected_stdout, stdout.decode())
self.assertEqual(expected_returncode, returncode, " ".join(command))
def test_help(self):
self.run_check(0,
'''Usage: b10-cmdctl-usermgr [options] <command> [username] [password]
Arguments:
command either 'add' or 'delete'
username the username to add or delete
password the password to set for the added user
If username or password are not specified, b10-cmdctl-usermgr will
prompt for them. It is recommended practice to let the
tool prompt for the password, as command-line
arguments can be visible through history or process
viewers.
Options:
--version show program's version number and exit
-h, --help show this help message and exit
-f OUTPUT_FILE, --file=OUTPUT_FILE
Accounts file to modify
-q, --quiet Quiet mode, don't print any output
''',
'',
[self.TOOL, '-h'])
def test_add_delete_users_ok(self):
"""
Test that a file is created, and users are added.
Also tests quiet mode for adding a user to an existing file.
"""
# content is a list of (user, pass) tuples
expected_content = []
# Creating a file
self.run_check(0,
'Using accounts file: test_users.csv\n',
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user1', 'pass1'
])
expected_content.append(('user1', 'pass1'))
self.check_output_file(expected_content)
# Add to existing file
self.run_check(0,
'Using accounts file: test_users.csv\n',
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user2', 'pass2'
])
expected_content.append(('user2', 'pass2'))
self.check_output_file(expected_content)
# Quiet mode
self.run_check(0,
'',
'',
[ self.TOOL, '-q',
'-f', self.OUTPUT_FILE,
'add', 'user3', 'pass3'
])
expected_content.append(('user3', 'pass3'))
self.check_output_file(expected_content)
# Delete a user (let's pick the middle one)
self.run_check(0,
'',
'',
[ self.TOOL, '-q',
'-f', self.OUTPUT_FILE,
'delete', 'user2'
])
del expected_content[1]
self.check_output_file(expected_content)
def test_add_delete_users_bad(self):
"""
More add/delete tests, this time for some error scenarios
"""
# content is a list of (user, pass) tuples
expected_content = []
# First add one
self.run_check(0, None, None,
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user', 'pass'
])
expected_content.append(('user', 'pass'))
self.check_output_file(expected_content)
# Adding it again should error
self.run_check(3,
'Using accounts file: test_users.csv\n'
'Error: username exists\n',
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user', 'pass'
])
self.check_output_file(expected_content)
# Deleting a non-existent one should fail too
self.run_check(4,
'Using accounts file: test_users.csv\n'
'Error: username does not exist\n',
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'delete', 'nosuchuser'
])
self.check_output_file(expected_content)
def test_bad_arguments(self):
"""
Assorted tests with bad command-line arguments
"""
self.run_check(1,
'Error: no command specified\n',
'',
[ self.TOOL ])
self.run_check(1,
'Error: command must be either add or delete\n',
'',
[ self.TOOL, 'foo' ])
self.run_check(1,
'Error: extraneous arguments\n',
'',
[ self.TOOL, 'add', 'user', 'pass', 'toomuch' ])
self.run_check(1,
'Error: delete only needs username, not a password\n',
'',
[ self.TOOL, 'delete', 'user', 'pass' ])
def test_default_file(self):
"""
Check the default file is the correct one.
"""
# Hardcoded path .. should be ok since this is run from make check
self.assertEqual(SYSCONFPATH + '/cmdctl-accounts.csv',
self.usermgr_module.DEFAULT_FILE)
def test_prompt_for_password_different(self):
"""
Check that the method that prompts for a password verifies that
the same value is entered twice
"""
# returns a different string (the representation of the number
# of times it has been called), until it has been called
# over 10 times, in which case it will always return "11"
getpass_different_called = 0
def getpass_different(question):
nonlocal getpass_different_called
getpass_different_called += 1
if getpass_different_called > 10:
return "11"
else:
return str(getpass_different_called)
with PrintCatcher() as pc:
with OverrideGetpass(getpass_different):
pwd = self.usermgr._prompt_for_password()
self.assertEqual(12, getpass_different_called)
self.assertEqual("11", pwd)
# stdout should be 5 times the no match string;
expected_output = "passwords do not match, try again\n"*5
self.assertEqual(expected_output, ''.join(pc.stdout_lines))
def test_prompt_for_password_empty(self):
"""
Check that the method that prompts for a password verifies that
the value entered is not empty
"""
# returns an empty string until it has been called over 10
# times
getpass_empty_called = 0
def getpass_empty(prompt):
nonlocal getpass_empty_called
getpass_empty_called += 1
if getpass_empty_called > 10:
return "nonempty"
else:
return ""
with PrintCatcher() as pc:
with OverrideGetpass(getpass_empty):
pwd = self.usermgr._prompt_for_password()
self.assertEqual("nonempty", pwd)
self.assertEqual(12, getpass_empty_called)
# stdout should be 10 times the 'cannot be empty' string
expected_output = "Error: password cannot be empty\n"*10
self.assertEqual(expected_output, ''.join(pc.stdout_lines))
def test_prompt_for_user(self):
"""
Test that the method that prompts for a username verifies that
is not empty, and that it exists (or does not, depending on the
action that is specified)
"""
new_input_called = 0
input_results = [ '', '', 'existinguser', 'nonexistinguser',
'', '', 'nonexistinguser', 'existinguser' ]
def new_input(prompt):
nonlocal new_input_called
if new_input_called < len(input_results):
result = input_results[new_input_called]
else:
result = 'empty'
new_input_called += 1
return result
# add fake user (value doesn't matter, method only checks for key)
self.usermgr.user_info = { 'existinguser': None }
expected_output = ''
with PrintCatcher() as pc:
with OverrideInput(self.usermgr, new_input):
# should skip the first three since empty or existing
# are not allowed, then return 'nonexistinguser'
username = self.usermgr._prompt_for_username(
self.usermgr_module.COMMAND_ADD)
self.assertEqual('nonexistinguser', username)
expected_output += "Error username can't be empty\n"*2
expected_output += "user already exists\n"
self.assertEqual(expected_output, ''.join(pc.stdout_lines))
# For delete, should again not accept empty (in a while true
# loop), and this time should not accept nonexisting users
username = self.usermgr._prompt_for_username(
self.usermgr_module.COMMAND_DELETE)
self.assertEqual('existinguser', username)
expected_output += "Error username can't be empty\n"*2
expected_output += "user does not exist\n"
self.assertEqual(expected_output, ''.join(pc.stdout_lines))
def test_bad_file(self):
"""
Check for graceful handling of bad file argument
"""
self.run_check(2,
'Using accounts file: /\n'
'Error accessing /: Is a directory\n',
'',
[ self.TOOL, '-f', '/', 'add', 'user', 'pass' ])
# Make sure we can initially write to the test file
self.run_check(0, None, None,
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user1', 'pass1'
])
# Make it non-writable (don't worry about cleanup, the
# file should be deleted after each test anyway
os.chmod(self.OUTPUT_FILE, stat.S_IRUSR)
self.run_check(2,
'Using accounts file: test_users.csv\n'
'Error accessing test_users.csv: Permission denied\n',
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user2', 'pass1'
])
self.run_check(2,
'Using accounts file: test_users.csv\n'
'Error accessing test_users.csv: Permission denied\n',
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'delete', 'user1'
])
# Making it write-only should have the same effect
os.chmod(self.OUTPUT_FILE, stat.S_IWUSR)
self.run_check(2,
'Using accounts file: test_users.csv\n'
'Error accessing test_users.csv: Permission denied\n',
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user2', 'pass1'
])
self.run_check(2,
'Using accounts file: test_users.csv\n'
'Error accessing test_users.csv: Permission denied\n',
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'delete', 'user1'
])
def test_missing_fields(self):
"""
Test that an invalid csv file is handled gracefully
"""
# Valid but incomplete csv; should be handled
# correctly
with open(self.OUTPUT_FILE, 'w', newline='') as f:
f.write('onlyuserfield\n')
f.write('userfield,saltfield\n')
f.write(',emptyuserfield,passwordfield\n')
self.run_check(0, None, None,
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user1', 'pass1'
])
self.run_check(0, None, None,
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'delete', 'onlyuserfield'
])
self.run_check(0, None, None,
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'delete', ''
])
def test_bad_data(self):
# I can only think of one invalid format, an unclosed string
with open(self.OUTPUT_FILE, 'w', newline='') as f:
f.write('a,"\n')
self.run_check(2,
'Using accounts file: test_users.csv\n'
'Error parsing csv file: newline inside string\n',
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user1', 'pass1'
])
if __name__== '__main__':
unittest.main()

View File

@@ -24,11 +24,13 @@ def reload():
global PLUGIN_PATHS
global PREFIX
global LIBEXECPATH
global SYSCONFPATH
BIND10_MSGQ_SOCKET_FILE = os.path.join("@localstatedir@",
"@PACKAGE_NAME@",
"msgq_socket").replace("${prefix}",
"@prefix@")
PREFIX = "@prefix@"
SYSCONFPATH="@sysconfdir@/@PACKAGE@".replace('${prefix}', PREFIX)
# B10_FROM_SOURCE is set in the environment for internal tests and
# an experimental run without installagion. In that case we need to
@@ -49,6 +51,9 @@ def reload():
# When "FROM_SOURCE", it lists the directories where the programs are
# built so that when BIND 10 is experimentally started on the source
# tree the programs in the tree (not installed ones) will be used.
# SYSCONFPATH: Path where the system-wide configuration files are
# stored (e.g. <prefix>/var/<package name>). This value is *not*
# overwritten if B10_FROM_SOURCE is specified.
#
# B10_FROM_SOURCE_LOCALSTATEDIR is specifically intended to be used for
# tests where we want to use various types of configuration within the test