b10-cmdctl-usermgr.py.in 8.66 KB
Newer Older
1
2
#!@PYTHON@

Likun Zhang's avatar
Likun Zhang committed
3
4
5
6
7
8
9
10
11
12
13
# 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
Jelte Jansen's avatar
Jelte Jansen committed
14
15
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN COMMAND OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS COMMAND, ARISING OUT OF OR IN CONNECTION
Likun Zhang's avatar
Likun Zhang committed
16
17
18
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

'''
19
20
This tool implements user management for b10-cmdctl. It is used to
add and remove users from the accounts file.
Likun Zhang's avatar
Likun Zhang committed
21
'''
22
import sys; sys.path.append ('@@PYTHONPATH@@')
23
from bind10_config import SYSCONFPATH
24
from collections import OrderedDict
Likun Zhang's avatar
Likun Zhang committed
25
26
27
28
import random
from hashlib import sha1
import csv
import getpass
29
from optparse import OptionParser, OptionValueError
30
import os
31
import isc.util.process
Michal Vaner's avatar
Michal Vaner committed
32

33
isc.util.process.rename()
Likun Zhang's avatar
Likun Zhang committed
34

35
36
37
VERSION_STRING = "b10-cmdctl-usermgr @PACKAGE_VERSION@"
DEFAULT_FILE = SYSCONFPATH + "/cmdctl-accounts.csv"

38
39
# Actions that can be performed (used for argument parsing,
# code paths, and output)
Jelte Jansen's avatar
Jelte Jansen committed
40
41
COMMAND_ADD = "add"
COMMAND_DELETE = "delete"
42
43
44
45
46
47

# Non-zero return codes, used in tests
BAD_ARGUMENTS = 1
FILE_ERROR = 2
USER_EXISTS = 3
USER_DOES_NOT_EXIST = 4
Likun Zhang's avatar
Likun Zhang committed
48

49
class UserManager:
50
    def __init__(self, options, args):
51
        self.options = options
52
        self.args = args
53

54
    def __print(self, msg):
55
56
57
        if not self.options.quiet:
            print(msg)

58
59
60
    def __gen_password_hash(self, password):
        salt = "".join(chr(random.randint(ord('!'), ord('~')))\
                    for x in range(64))
61
62
63
        saltedpwd = sha1((password + salt).encode()).hexdigest()
        return salt, saltedpwd

64
    def __read_user_info(self):
65
66
67
68
69
70
71
72
        """
        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.
73
        self.user_info = OrderedDict()
74
75
76
        if os.path.exists(self.options.output_file):
            # Just let any file read error bubble up; it will
            # be caught in the run() method
77
            with open(self.options.output_file, newline='') as csvfile:
78
                reader = csv.reader(csvfile, strict=True)
79
                for row in reader:
80
                    self.user_info[row[0]] = row
81

82
    def __save_user_info(self):
83
84
85
86
87
88
        """
        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
89
90
        with open(self.options.output_file, 'w',
                  newline='') as csvfile:
91
            writer = csv.writer(csvfile)
92
93
            for row in self.user_info.values():
                writer.writerow(row)
94

95
    def __add_user(self, name, password):
96
        """
97
        Add the given username/password combination to the stored user_info.
98
99
100
        First checks if the username exists, and returns False if so.
        If not, it is added, and this method returns True.
        """
101
        if name in self.user_info:
102
            return False
103
104
        salt, pw = self.__gen_password_hash(password)
        self.user_info[name] = [name, pw, salt]
105
106
        return True

107
    def __delete_user(self, name):
108
109
110
111
112
        """
        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
        """
113
        if name not in self.user_info:
114
            return False
115
        del self.user_info[name]
116
117
        return True

118
119
120
121
122
123
124
    # 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):
125
        # Note, direct prints here are intentional
126
127
        while True:
            name = self._input("Username to " + command + ": ")
128
129
            if name == "":
                print("Error username can't be empty")
130
131
                continue

132
            if command == COMMAND_ADD and name in self.user_info:
133
134
                 print("user already exists")
                 continue
135
            elif command == COMMAND_DELETE and name not in self.user_info:
136
                 print("user does not exist")
Likun Zhang's avatar
Likun Zhang committed
137
138
                 continue

139
            return name
140

141
142
143
    # in essence this is private, but made 'protected' for ease
    # of testing
    def _prompt_for_password(self):
144
        # Note, direct prints here are intentional
145
146
147
148
149
150
151
152
153
154
        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
155

156
    def __verify_options_and_args(self):
157
        """
158
159
        Basic sanity checks on command line arguments.
        Returns False if there is a problem, True if everything seems OK.
160
161
        """
        if len(self.args) < 1:
162
            self.__print("Error: no command specified")
163
164
            return False
        if len(self.args) > 3:
165
            self.__print("Error: extraneous arguments")
166
            return False
Jelte Jansen's avatar
Jelte Jansen committed
167
        if self.args[0] not in [ COMMAND_ADD, COMMAND_DELETE ]:
168
            self.__print("Error: command must be either add or delete")
169
            return False
Jelte Jansen's avatar
Jelte Jansen committed
170
        if self.args[0] == COMMAND_DELETE and len(self.args) > 2:
171
            self.__print("Error: delete only needs username, not a password")
172
173
            return False
        return True
174

175
    def run(self):
176
        if not self.__verify_options_and_args():
177
178
179
            return BAD_ARGUMENTS

        try:
180
181
            self.__print("Using accounts file: " + self.options.output_file)
            self.__read_user_info()
182

Jelte Jansen's avatar
Jelte Jansen committed
183
            command = self.args[0]
184
185
186
187

            if len(self.args) > 1:
                username = self.args[1]
            else:
188
                username = self._prompt_for_username(command)
189

Jelte Jansen's avatar
Jelte Jansen committed
190
            if command == COMMAND_ADD:
191
192
193
                if len(self.args) > 2:
                    password = self.args[2]
                else:
194
                    password = self._prompt_for_password()
195
                if not self.__add_user(username, password):
196
197
                    print("Error: username exists")
                    return USER_EXISTS
Jelte Jansen's avatar
Jelte Jansen committed
198
            elif command == COMMAND_DELETE:
199
                if not self.__delete_user(username):
200
201
202
                    print("Error: username does not exist")
                    return USER_DOES_NOT_EXIST

203
            self.__save_user_info()
204
205
            return 0
        except IOError as ioe:
206
207
208
209
210
            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))
211
            return FILE_ERROR
212
213
214
215

def set_options(parser):
    parser.add_option("-f", "--file",
                      dest="output_file", default=DEFAULT_FILE,
216
                      help="Accounts file to modify"
217
218
219
220
221
222
223
                     )
    parser.add_option("-q", "--quiet",
                      dest="quiet", action="store_true", default=False,
                      help="Quiet mode, don't print any output"
                     )

def main():
Jelte Jansen's avatar
Jelte Jansen committed
224
    usage = "usage: %prog [options] <command> [username] [password]\n\n"\
225
            "Arguments:\n"\
Jelte Jansen's avatar
Jelte Jansen committed
226
            "  command\t\teither 'add' or 'delete'\n"\
227
228
229
230
            "  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"\
231
232
233
234
            "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."
235
    parser = OptionParser(usage=usage, version=VERSION_STRING)
236
    set_options(parser)
237
    (options, args) = parser.parse_args()
238

239
    usermgr = UserManager(options, args)
240
    return usermgr.run()
Likun Zhang's avatar
Likun Zhang committed
241
242

if __name__ == '__main__':
243
    sys.exit(main())
Likun Zhang's avatar
Likun Zhang committed
244