#!@PYTHON@ # Copyright (C) 2010 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. ''' 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 from optparse import OptionParser, OptionValueError import os import sys; sys.path.append ('@@PYTHONPATH@@') import isc.util.process isc.util.process.rename() VERSION_STRING = "b10-cmdctl-usermgr @PACKAGE_VERSION@" DEFAULT_FILE = SYSCONFPATH + "/cmdctl-accounts.csv" # Actions that can be performed (used for argument parsing, # code paths, and output) COMMAND_ADD = "add" COMMAND_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: def __init__(self, options, args): self.options = options self.args = args def __print(self, msg): if not self.options.quiet: print(msg) 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 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 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] [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__': sys.exit(main())