Source code for crate_anon.nlp_webserver.manage_users

#!/usr/bin/env python

r"""
crate_anon/nlp_webserver/manage_users.py

===============================================================================

    Copyright (C) 2015, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CRATE.

    CRATE is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CRATE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CRATE. If not, see <https://www.gnu.org/licenses/>.

===============================================================================

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

"""

import argparse
import logging
from shutil import copyfile
from typing import Dict

from cardinal_pythonlib.logs import main_only_quicksetup_rootlogger
from rich_argparse import RichHelpFormatter

from crate_anon.nlp_webserver.constants import NlpServerConfigKeys
from crate_anon.nlp_webserver.security import hash_password
from crate_anon.nlp_webserver.settings import SETTINGS, SETTINGS_PATH

log = logging.getLogger(__name__)

USERS_FILENAME = SETTINGS[NlpServerConfigKeys.USERS_FILE]


[docs]def get_users() -> Dict[str, str]: """ Reads the user file and returns a dictionary mapping usernames to hashed passwords. """ with open(USERS_FILENAME, "r") as user_file: user_lines = user_file.readlines() user_elements = [x.split(",") for x in user_lines] users = {x[0]: x[1].strip() for x in user_elements} return users
[docs]def add_user(username: str, password: str) -> None: """ Adds a username/password combination to the users file, hashing the password en route. """ users = get_users() if username in users: proceed = input( f"User {username} already exists. " f"Overwrite (change password)? [yes/no] " ) if proceed.lower() == "yes": change_password(username, password) return else: return with open(USERS_FILENAME, "a") as user_file: user_file.write(f"{username},{hash_password(password)}\n") log.info(f"User {username} added.")
[docs]def rm_user(username: str) -> None: """ Removes a user from the user file. """ user_found = False # Create a backup in case something goes wrong during writing backup_filename = USERS_FILENAME + "~" copyfile(USERS_FILENAME, backup_filename) users = get_users() try: with open(USERS_FILENAME, "w") as user_file: for user in users: if user != username: user_file.write(f"{user},{users[user]}\n") else: user_found = True except IOError: log.error( f"An error occured in opening the file {USERS_FILENAME}. If the " f"integrity of this file is compromised, the backup is " f"{backup_filename}." ) raise if user_found: log.info(f"User {username} removed.") else: log.info(f"User {username} not found.")
[docs]def change_password(username: str, password: str) -> None: """ Changes a user's password by rewriting the user file. """ user_found = False # Create a backup in case something goes wrong during writing backup_filename = USERS_FILENAME + "~" copyfile(USERS_FILENAME, backup_filename) users = get_users() try: with open(USERS_FILENAME, "w") as user_file: for user in users: if user != username: user_file.write(f"{user},{users[user]}\n") else: user_found = True user_file.write(f"{username},{hash_password(password)}\n") except IOError: log.error( f"An error occured in opening the file {USERS_FILENAME}. If the " f"integrity of this file is compromised, the backup is " f"{backup_filename}." ) raise if user_found: log.info(f"Password changed for user {username}.") else: log.info(f"User {username} not found.")
[docs]def main() -> None: """ Command-line entry point. """ description = "Manage users for the CRATE nlp_web server." # noinspection PyTypeChecker parser = argparse.ArgumentParser( description=description, formatter_class=RichHelpFormatter, ) arg_group = parser.add_mutually_exclusive_group() arg_group.add_argument( "--adduser", nargs=2, metavar=("USERNAME", "PASSWORD"), help="Add a user and associated password.", ) arg_group.add_argument( "--rmuser", nargs=1, metavar="USERNAME", help="Remove a user by specifying their username.", ) arg_group.add_argument( "--changepw", nargs=2, metavar=("USERNAME", "PASSWORD"), help="Change a user's password.", ) args = parser.parse_args() main_only_quicksetup_rootlogger() if not args.adduser and not args.rmuser and not args.changepw: log.error( "One option required: '--adduser', '--rmuser' or '--changepw'." ) return log.debug(f"Settings file: {SETTINGS_PATH}") log.debug(f"Users file: {USERS_FILENAME}") if args.rmuser: username = args.rmuser[0] proceed = input(f"Confirm remove user: {username} ? [yes/no] ") if proceed.lower() == "yes": rm_user(username) else: log.info("User remove aborted.") elif args.adduser: username = args.adduser[0] password = args.adduser[1] add_user(username, password) elif args.changepw: username = args.changepw[0] new_password = args.changepw[1] proceed = input( f"Confirm change password for user: {username} ? [yes/no] " ) if proceed.lower() == "yes": change_password(username, new_password) else: log.info("Password change aborted.")
if __name__ == "__main__": main()