Coverage for nlp_webserver/manage_users.py: 18%

96 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-08-27 10:34 -0500

1#!/usr/bin/env python 

2 

3r""" 

4crate_anon/nlp_webserver/manage_users.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2015, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CRATE. 

12 

13 CRATE is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CRATE is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CRATE. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28Manages the user authentication file for CRATE's implementation of an NLPRP 

29server. 

30 

31""" 

32 

33import argparse 

34import logging 

35from shutil import copyfile 

36from typing import Dict 

37 

38from cardinal_pythonlib.logs import main_only_quicksetup_rootlogger 

39from rich_argparse import RichHelpFormatter 

40 

41from crate_anon.nlp_webserver.constants import NlpServerConfigKeys 

42from crate_anon.nlp_webserver.security import hash_password 

43from crate_anon.nlp_webserver.settings import SETTINGS, SETTINGS_PATH 

44 

45log = logging.getLogger(__name__) 

46 

47USERS_FILENAME = SETTINGS[NlpServerConfigKeys.USERS_FILE] 

48 

49 

50def get_users() -> Dict[str, str]: 

51 """ 

52 Reads the user file and returns a dictionary mapping usernames to hashed 

53 passwords. 

54 """ 

55 with open(USERS_FILENAME, "r") as user_file: 

56 user_lines = user_file.readlines() 

57 user_elements = [x.split(",") for x in user_lines] 

58 users = {x[0]: x[1].strip() for x in user_elements} 

59 return users 

60 

61 

62def add_user(username: str, password: str) -> None: 

63 """ 

64 Adds a username/password combination to the users file, hashing the 

65 password en route. 

66 """ 

67 users = get_users() 

68 if username in users: 

69 proceed = input( 

70 f"User {username} already exists. " 

71 f"Overwrite (change password)? [yes/no] " 

72 ) 

73 if proceed.lower() == "yes": 

74 change_password(username, password) 

75 return 

76 else: 

77 return 

78 with open(USERS_FILENAME, "a") as user_file: 

79 user_file.write(f"{username},{hash_password(password)}\n") 

80 log.info(f"User {username} added.") 

81 

82 

83def rm_user(username: str) -> None: 

84 """ 

85 Removes a user from the user file. 

86 """ 

87 user_found = False 

88 # Create a backup in case something goes wrong during writing 

89 backup_filename = USERS_FILENAME + "~" 

90 copyfile(USERS_FILENAME, backup_filename) 

91 users = get_users() 

92 try: 

93 with open(USERS_FILENAME, "w") as user_file: 

94 for user in users: 

95 if user != username: 

96 user_file.write(f"{user},{users[user]}\n") 

97 else: 

98 user_found = True 

99 except IOError: 

100 log.error( 

101 f"An error occured in opening the file {USERS_FILENAME}. If the " 

102 f"integrity of this file is compromised, the backup is " 

103 f"{backup_filename}." 

104 ) 

105 raise 

106 if user_found: 

107 log.info(f"User {username} removed.") 

108 else: 

109 log.info(f"User {username} not found.") 

110 

111 

112def change_password(username: str, password: str) -> None: 

113 """ 

114 Changes a user's password by rewriting the user file. 

115 """ 

116 user_found = False 

117 # Create a backup in case something goes wrong during writing 

118 backup_filename = USERS_FILENAME + "~" 

119 copyfile(USERS_FILENAME, backup_filename) 

120 users = get_users() 

121 try: 

122 with open(USERS_FILENAME, "w") as user_file: 

123 for user in users: 

124 if user != username: 

125 user_file.write(f"{user},{users[user]}\n") 

126 else: 

127 user_found = True 

128 user_file.write(f"{username},{hash_password(password)}\n") 

129 except IOError: 

130 log.error( 

131 f"An error occured in opening the file {USERS_FILENAME}. If the " 

132 f"integrity of this file is compromised, the backup is " 

133 f"{backup_filename}." 

134 ) 

135 raise 

136 if user_found: 

137 log.info(f"Password changed for user {username}.") 

138 else: 

139 log.info(f"User {username} not found.") 

140 

141 

142def main() -> None: 

143 """ 

144 Command-line entry point. 

145 """ 

146 description = "Manage users for the CRATE nlp_web server." 

147 

148 # noinspection PyTypeChecker 

149 parser = argparse.ArgumentParser( 

150 description=description, 

151 formatter_class=RichHelpFormatter, 

152 ) 

153 arg_group = parser.add_mutually_exclusive_group() 

154 arg_group.add_argument( 

155 "--adduser", 

156 nargs=2, 

157 metavar=("USERNAME", "PASSWORD"), 

158 help="Add a user and associated password.", 

159 ) 

160 arg_group.add_argument( 

161 "--rmuser", 

162 nargs=1, 

163 metavar="USERNAME", 

164 help="Remove a user by specifying their username.", 

165 ) 

166 arg_group.add_argument( 

167 "--changepw", 

168 nargs=2, 

169 metavar=("USERNAME", "PASSWORD"), 

170 help="Change a user's password.", 

171 ) 

172 args = parser.parse_args() 

173 

174 main_only_quicksetup_rootlogger() 

175 

176 if not args.adduser and not args.rmuser and not args.changepw: 

177 log.error( 

178 "One option required: '--adduser', '--rmuser' or '--changepw'." 

179 ) 

180 return 

181 

182 log.debug(f"Settings file: {SETTINGS_PATH}") 

183 log.debug(f"Users file: {USERS_FILENAME}") 

184 if args.rmuser: 

185 username = args.rmuser[0] 

186 proceed = input(f"Confirm remove user: {username} ? [yes/no] ") 

187 if proceed.lower() == "yes": 

188 rm_user(username) 

189 else: 

190 log.info("User remove aborted.") 

191 elif args.adduser: 

192 username = args.adduser[0] 

193 password = args.adduser[1] 

194 add_user(username, password) 

195 elif args.changepw: 

196 username = args.changepw[0] 

197 new_password = args.changepw[1] 

198 proceed = input( 

199 f"Confirm change password for user: {username} ? [yes/no] " 

200 ) 

201 if proceed.lower() == "yes": 

202 change_password(username, new_password) 

203 else: 

204 log.info("Password change aborted.") 

205 

206 

207if __name__ == "__main__": 

208 main()