Parcourir la source

[2713] Change the new options into arguments

And added a number of tests
Jelte Jansen il y a 12 ans
Parent
commit
44fab7f71b

+ 111 - 54
src/bin/usermgr/b10-cmdctl-usermgr.py.in

@@ -31,12 +31,22 @@ import isc.util.process
 
 
 isc.util.process.rename()
 isc.util.process.rename()
 
 
-VERSION_STRING = 'b10-cmdctl-usermgr @PACKAGE_VERSION@'
-DEFAULT_FILE = SYSCONFPATH + '/cmdctl-accounts.csv'
+VERSION_STRING = "b10-cmdctl-usermgr @PACKAGE_VERSION@"
+DEFAULT_FILE = SYSCONFPATH + "/cmdctl-accounts.csv"
+
+ACTION_ADD = "add"
+ACTION_DELETE = "delete"
+
+# Non-zero return codes, used in tests
+BAD_ARGUMENTS = 1
+FILE_ERROR = 2
+USER_EXISTS = 3
+USER_DOES_NOT_EXIST = 4
 
 
 class UserManager:
 class UserManager:
-    def __init__(self, options):
+    def __init__(self, options, args):
         self.options = options
         self.options = options
+        self.args = args
 
 
     def print(self, msg):
     def print(self, msg):
         if not self.options.quiet:
         if not self.options.quiet:
@@ -73,6 +83,7 @@ class UserManager:
         Add the given username/password combination to the file.
         Add the given username/password combination to the file.
         First checks if the username exists, and returns False if so.
         First checks if the username exists, and returns False if so.
         If not, it is added, and this method returns True.
         If not, it is added, and this method returns True.
+        (if it cannot write to the file it raises an exception)
         """
         """
         if self.username_exists(name):
         if self.username_exists(name):
             return False
             return False
@@ -80,66 +91,105 @@ class UserManager:
         self.save_userinfo(name, pw, salt, self.options.output_file)
         self.save_userinfo(name, pw, salt, self.options.output_file)
         return True
         return True
 
 
-    def interactive_mode(self):
+    def delete_user(self, name):
+        if not self.username_exists(name):
+            return False
+        rows = []
+        with open(self.options.output_file) as csvfile:
+            reader = csv.reader(csvfile)
+            for row in reader:
+                if row[0] != name:
+                    rows.append(row)
+        with open(self.options.output_file, 'w') as csvfile:
+            writer = csv.writer(csvfile)
+            writer.writerows(rows)
+        return True
+
+    def prompt_for_username(self, action):
         while True :
         while True :
-            name = input("Username to add: ")
-            if name == '':
-                print("error, username can't be empty")
+            name = input("Username to " + action + ": ")
+            if name == "":
+                # Note, direct print here is intentional
+                print("Error username can't be empty")
                 continue
                 continue
 
 
-            if self.username_exists(name):
-                 print("user already exists!")
+            if action == ACTION_ADD and self.username_exists(name):
+                 print("user already exists")
+                 continue
+            elif action == ACTION_DELETE and not self.username_exists(name):
+                 print("user does not exist")
                  continue
                  continue
 
 
-            while True:
-                pwd1 = getpass.getpass("Choose a password: ")
-                pwd2 = getpass.getpass("Re-enter password: ")
-                if pwd1 != pwd2:
-                    print("passwords do not match, try again")
-                else:
-                    break;
+            return name
 
 
-            if not self.add_user(name, pwd1):
-                self.print("Error: username exists")
+    def prompt_for_password(self):
+        while True:
+            pwd1 = getpass.getpass("Choose a password: ")
+            if pwd1 == "":
+                # Note, direct print here is intentional
+                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
 
 
-            inputdata = input('Add another user (y/n)? ')
-            if inputdata not in ['y', 'Y']:
-                break
+    def verify_options_and_args(self):
+        """
+        Basic sanity checks on command line arguments
+        """
+        if len(self.args) < 1:
+            self.print("Error: must specify an action")
+            return False
+        if len(self.args) > 3:
+            self.print("Error: extraneous arguments")
+            return False
+        if self.args[0] not in [ ACTION_ADD, ACTION_DELETE ]:
+            self.print("Error: action must be either add or delete")
+            return False
+        if self.args[0] == ACTION_DELETE and len(self.args) > 2:
+            self.print("Error: delete only needs username, not a password")
+            return False
+        return True
 
 
     def run(self):
     def run(self):
-        self.print("Using accounts file: " + self.options.output_file)
-        filename = self.options.output_file
-
-        if self.options.username is not None or\
-           self.options.password is not None:
-            if self.options.username is None:
-                self.print("Error: password but no username given")
-                return 1
-            if self.options.password is None:
-                self.print("Error: username but no password given")
-                return 1
-            if not self.add_user(self.options.username,
-                                 self.options.password):
-                self.print("Error: username exists")
-        else:
-            try:
-                self.interactive_mode()
-            except KeyboardInterrupt:
-                pass
-        return 0
+        if not self.verify_options_and_args():
+            return BAD_ARGUMENTS
+
+        try:
+            self.print("Using accounts file: " + self.options.output_file)
+
+            action = self.args[0]
+
+            if len(self.args) > 1:
+                username = self.args[1]
+            else:
+                username = self.prompt_for_username(action)
+
+            if action == ACTION_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 action == ACTION_DELETE:
+                if not self.delete_user(username):
+                    print("Error: username does not exist")
+                    return USER_DOES_NOT_EXIST
+
+            return 0
+        except IOError as ioe:
+            self.print("Error accessing " + ioe.filename +\
+                       ": " + str(ioe.strerror))
+            return FILE_ERROR
 
 
 def set_options(parser):
 def set_options(parser):
     parser.add_option("-f", "--file",
     parser.add_option("-f", "--file",
                       dest="output_file", default=DEFAULT_FILE,
                       dest="output_file", default=DEFAULT_FILE,
-                      help="Specify the file to append user name and password"
-                     )
-    parser.add_option("-u", "--username",
-                      dest="username", default=None,
-                      help="Specify username to add"
-                     )
-    parser.add_option("-p", "--password",
-                      dest="password", default=None,
-                      help="Specify password to add"
+                      help="Accounts file to modify"
                      )
                      )
     parser.add_option("-q", "--quiet",
     parser.add_option("-q", "--quiet",
                       dest="quiet", action="store_true", default=False,
                       dest="quiet", action="store_true", default=False,
@@ -147,14 +197,21 @@ def set_options(parser):
                      )
                      )
 
 
 def main():
 def main():
-    parser = OptionParser(version = VERSION_STRING)
+    usage = "usage: %prog [options] <action> [username] [password]\n\n"\
+            "Arguments:\n"\
+            "  action\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."
+    parser = OptionParser(usage=usage, version=VERSION_STRING)
     set_options(parser)
     set_options(parser)
-    (options, _) = parser.parse_args()
+    (options, args) = parser.parse_args()
 
 
-    usermgr = UserManager(options)
+    usermgr = UserManager(options, args)
     return usermgr.run()
     return usermgr.run()
 
 
-
 if __name__ == '__main__':
 if __name__ == '__main__':
     result = main()
     result = main()
     sys.exit(result)
     sys.exit(result)

+ 151 - 17
src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py

@@ -18,16 +18,18 @@ from hashlib import sha1
 import imp
 import imp
 import os
 import os
 import subprocess
 import subprocess
+import stat
 import unittest
 import unittest
 from bind10_config import SYSCONFPATH
 from bind10_config import SYSCONFPATH
 
 
 def run(command):
 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).
+    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).
     """
     """
-    subp = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    subp = subprocess.Popen(command, stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE)
     (stdout, stderr) = subp.communicate()
     (stdout, stderr) = subp.communicate()
     return (subp.returncode, stdout, stderr)
     return (subp.returncode, stdout, stderr)
 
 
@@ -89,23 +91,27 @@ class TestUserMgr(unittest.TestCase):
 
 
     def test_help(self):
     def test_help(self):
         self.run_check(0,
         self.run_check(0,
-'''Usage: b10-cmdctl-usermgr [options]
+'''Usage: b10-cmdctl-usermgr [options] <action> [username] [password]
+
+Arguments:
+  action		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.
 
 
 Options:
 Options:
   --version             show program's version number and exit
   --version             show program's version number and exit
   -h, --help            show this help message and exit
   -h, --help            show this help message and exit
   -f OUTPUT_FILE, --file=OUTPUT_FILE
   -f OUTPUT_FILE, --file=OUTPUT_FILE
-                        Specify the file to append user name and password
-  -u USERNAME, --username=USERNAME
-                        Specify username to add
-  -p PASSWORD, --password=PASSWORD
-                        Specify password to add
+                        Accounts file to modify
   -q, --quiet           Quiet mode, don't print any output
   -q, --quiet           Quiet mode, don't print any output
 ''',
 ''',
                        '',
                        '',
                        [self.TOOL, '-h'])
                        [self.TOOL, '-h'])
 
 
-    def test_create_users(self):
+    def test_add_delete_users_ok(self):
         """
         """
         Test that a file is created, and users are added.
         Test that a file is created, and users are added.
         Also tests quiet mode for adding a user to an existing file.
         Also tests quiet mode for adding a user to an existing file.
@@ -120,8 +126,8 @@ Options:
                        '',
                        '',
                        [ self.TOOL,
                        [ self.TOOL,
                          '-f', self.OUTPUT_FILE,
                          '-f', self.OUTPUT_FILE,
-                         '-u', 'user1',
-                         '-p', 'pass1' ])
+                         'add', 'user1', 'pass1'
+                       ])
         expected_content.append(('user1', 'pass1'))
         expected_content.append(('user1', 'pass1'))
         self.check_output_file(expected_content)
         self.check_output_file(expected_content)
 
 
@@ -132,8 +138,8 @@ Options:
                        '',
                        '',
                        [ self.TOOL,
                        [ self.TOOL,
                          '-f', self.OUTPUT_FILE,
                          '-f', self.OUTPUT_FILE,
-                         '-u', 'user2',
-                         '-p', 'pass2' ])
+                         'add', 'user2', 'pass2'
+                       ])
         expected_content.append(('user2', 'pass2'))
         expected_content.append(('user2', 'pass2'))
         self.check_output_file(expected_content)
         self.check_output_file(expected_content)
 
 
@@ -143,11 +149,80 @@ Options:
                        '',
                        '',
                        [ self.TOOL, '-q',
                        [ self.TOOL, '-q',
                          '-f', self.OUTPUT_FILE,
                          '-f', self.OUTPUT_FILE,
-                         '-u', 'user3',
-                         '-p', 'pass3' ])
+                         'add', 'user3', 'pass3'
+                       ])
         expected_content.append(('user3', 'pass3'))
         expected_content.append(('user3', 'pass3'))
         self.check_output_file(expected_content)
         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: must specify an action\n',
+                       '',
+                       [ self.TOOL ])
+        self.run_check(1,
+                       'Error: action 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):
     def test_default_file(self):
         """
         """
         Check the default file is the correct one.
         Check the default file is the correct one.
@@ -159,6 +234,65 @@ Options:
         self.assertEqual(SYSCONFPATH + '/cmdctl-accounts.csv',
         self.assertEqual(SYSCONFPATH + '/cmdctl-accounts.csv',
                          usermgr.DEFAULT_FILE)
                          usermgr.DEFAULT_FILE)
 
 
+    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'
+                       ])
+
+
 if __name__== '__main__':
 if __name__== '__main__':
     unittest.main()
     unittest.main()