def delete_user(username): """Delete a user, and return HTML success/failure message.""" user = User(username, False) if not user.user: return user_management_failure_message("No such user: "******"user", user.user) audit("User deleted: {}".format(user.user)) return user_management_success_message("User " + user.user + " deleted")
def set_password_directly(username, password): """If the user exists, set its password. Returns Boolean success.""" user = User(username, False) if not user.user: return False user.set_password(password) user.save() user.enable() audit("Password changed for user " + user.user, from_console=True) return True
def act_on_login_failure(username): """Record login failure and lock out user if necessary.""" audit("Failed login as user: {}".format(username)) record_login_failure(username) nfailures = how_many_login_failures(username) nlockouts = nfailures // pls.LOCKOUT_THRESHOLD nfailures_since_last_lockout = nfailures % pls.LOCKOUT_THRESHOLD if nlockouts >= 1 and nfailures_since_last_lockout == 0: # new lockout required lockout_minutes = nlockouts * pls.LOCKOUT_DURATION_INCREMENT_MINUTES lock_user_out(username, lockout_minutes)
def lock_user_out(username, lockout_minutes): """Lock user out for a specified number of minutes.""" lock_until = pls.NOW_UTC_NO_TZ + datetime.timedelta( minutes=lockout_minutes) pls.db.db_exec( "INSERT INTO " + SECURITY_ACCOUNT_LOCKOUT_TABLENAME + " (user, locked_until) VALUES (?, ?)", username, lock_until) audit("Account {} locked out for {} minutes".format(username, lockout_minutes))
def get_multiple_views_data_as_tsv_zip(tables): """Returns the data from multiple views, as multiple TSV files in a ZIP.""" tables = validate_table_list(tables) if not tables: return None memfile = io.BytesIO() z = zipfile.ZipFile(memfile, "w") for t in tables: result = get_view_data_as_tsv(t, prevalidated=True, audit_individually=False) z.writestr(t + ".tsv", result.encode("utf-8")) z.close() audit("dump as TSV ZIP: " + " ".join(tables)) return memfile.getvalue()
def change_password(username, form, as_manager=False): """Change password, and return success/failure HTML.""" user = User(username, False) if not user.user: return user_management_failure_message( "Problem: can't find user " + username, as_manager) old_password = ws.get_cgi_parameter_str(form, PARAM.OLD_PASSWORD) new_password_1 = ws.get_cgi_parameter_str(form, PARAM.NEW_PASSWORD_1) new_password_2 = ws.get_cgi_parameter_str(form, PARAM.NEW_PASSWORD_2) must_change_password = ws.get_cgi_parameter_bool( form, PARAM.MUST_CHANGE_PASSWORD) if new_password_1 != new_password_2: return user_management_failure_message("New passwords don't match", as_manager) if len(new_password_1) < MINIMUM_PASSWORD_LENGTH: return user_management_failure_message( "New password must be at least {} characters; not changed.".format( MINIMUM_PASSWORD_LENGTH ), as_manager ) if old_password == new_password_1 and not as_manager: return user_management_failure_message( "Old/new passwords are the same", as_manager ) if (not as_manager) and (not user.is_password_valid(old_password)): return user_management_failure_message("Old password incorrect", as_manager) # OK user.set_password(new_password_1) user.save() if not as_manager: must_change_password = False if must_change_password: user.force_password_change() audit("Password changed for user " + user.user) return user_management_success_message( "Password updated for user {}.".format(user.user), as_manager, """<div class="important"> If you store your password in your CamCOPS tablet application, remember to change it there as well. </div>""" )
def get_database_dump_as_sql(tables=[]): """Returns a database dump of all the tables requested, in SQL format.""" tables = validate_table_list(tables) if not tables: return NOTHING_VALID_SPECIFIED # We'll need to re-fetch the database password, # since we don't store it (for security reasons). config = ConfigParser.ConfigParser() config.read(pls.CAMCOPS_CONFIG_FILE) # ------------------------------------------------------------------------- # SECURITY: from this point onwards, consider the possibility of a # password leaking via a debugging exception handler # ------------------------------------------------------------------------- try: DB_PASSWORD = config.get(CONFIG_FILE_MAIN_SECTION, "DB_PASSWORD") except Exception as e: # deliberately conceal details for security raise RuntimeError( "Problem reading DB_PASSWORD from config: {}".format(e)) if DB_PASSWORD is None: raise RuntimeError("No database password specified") # OK from a security perspective: if there's no password, there's no # password to leak via a debugging exception handler # Database: try: audit("dump as SQL: " + " ".join(tables)) return subprocess.check_output([ pls.MYSQLDUMP, "-h", pls.DB_SERVER, # rather than --host=X "-P", str(pls.DB_PORT), # rather than --port=X "-u", pls.DB_USER, # rather than --user=X "-p{}".format(DB_PASSWORD), # neither -pPASSWORD nor --password=PASSWORD accept spaces "--opt", "--hex-blob", "--default-character-set=utf8", pls.DB_NAME, ] + tables).decode('utf8') except: # deliberately conceal details for security raise RuntimeError("Problem opening or reading from database; " "details concealed for security reasons") finally: # Executed whether an exception is raised or not. DB_PASSWORD = None
def get_view_data_as_tsv(view, prevalidated=False, audit_individually=True): """Returns the data from the view specified, in TSV format.""" # Views need special handling: mysqldump will provide the view-generating # SQL, not the contents. If the output is saved as .XLS, Excel will open it # without prompting for conversion. if not prevalidated: view = validate_single_table(view) if not view: return "Invalid table or view" # Special blob handling... if view == cc_blob.Blob.TABLENAME: query = ( "SELECT " + ",".join(cc_blob.Blob.FIELDS_WITHOUT_BLOB) + ",HEX(theblob) FROM " + cc_blob.Blob.TABLENAME ) else: query = "SELECT * FROM " + view if audit_individually: audit("dump as TSV: " + view) return get_query_as_tsv(query)
def create_superuser(username, password): """Create a superuser.""" user = User(username, False) if user.user: # already exists! return False user = User(username, True) user.may_upload = True user.may_register_devices = True user.may_use_webstorage = True user.may_use_webviewer = True user.may_view_other_users_records = True user.view_all_patients_when_unfiltered = True user.superuser = True user.may_dump_data = True user.may_run_reports = True user.may_add_notes = True user.set_password(password) user.save() audit("SUPERUSER CREATED: " + user.user, from_console=True) return True
def add_user(form): """Add a user, and return HTML success/failure message.""" username = ws.get_cgi_parameter_str(form, PARAM.USERNAME) password_1 = ws.get_cgi_parameter_str(form, PARAM.PASSWORD_1) password_2 = ws.get_cgi_parameter_str(form, PARAM.PASSWORD_2) must_change_password = ws.get_cgi_parameter_bool( form, PARAM.MUST_CHANGE_PASSWORD) may_use_webviewer = ws.get_cgi_parameter_bool( form, PARAM.MAY_USE_WEBVIEWER) may_view_other_users_records = ws.get_cgi_parameter_bool( form, PARAM.MAY_VIEW_OTHER_USERS_RECORDS) view_all_patients_when_unfiltered = ws.get_cgi_parameter_bool( form, PARAM.VIEW_ALL_PTS_WHEN_UNFILTERED) may_upload = ws.get_cgi_parameter_bool(form, PARAM.MAY_UPLOAD) superuser = ws.get_cgi_parameter_bool(form, PARAM.SUPERUSER) may_register_devices = ws.get_cgi_parameter_bool( form, PARAM.MAY_REGISTER_DEVICES) may_use_webstorage = ws.get_cgi_parameter_bool( form, PARAM.MAY_USE_WEBSTORAGE) may_dump_data = ws.get_cgi_parameter_bool(form, PARAM.MAY_DUMP_DATA) may_run_reports = ws.get_cgi_parameter_bool(form, PARAM.MAY_RUN_REPORTS) may_add_notes = ws.get_cgi_parameter_bool(form, PARAM.MAY_ADD_NOTES) user = User(username, False) if user.user: return user_management_failure_message( "User already exists: " + username) if not is_username_permissible(username): return user_management_failure_message( "Invalid username: "******"Passwords don't mach") if len(password_1) < MINIMUM_PASSWORD_LENGTH: return user_management_failure_message( "Password must be at least {} characters".format( MINIMUM_PASSWORD_LENGTH )) user = User(username, True) user.set_password(password_1) user.may_use_webviewer = may_use_webviewer user.may_view_other_users_records = may_view_other_users_records user.view_all_patients_when_unfiltered = view_all_patients_when_unfiltered user.may_upload = may_upload user.superuser = superuser user.may_register_devices = may_register_devices user.may_use_webstorage = may_use_webstorage user.may_dump_data = may_dump_data user.may_run_reports = may_run_reports user.may_add_notes = may_add_notes user.save() if must_change_password: user.force_password_change() audit( ( "User created: {}: " "may_use_webviewer={}, " "may_view_other_users_records={}, " "view_all_patients_when_unfiltered={}, " "may_upload={}, " "superuser={}, " "may_register_devices={}, " "may_use_webstorage={}, " "may_dump_data={}, " "may_run_reports={}, " "may_add_notes={}, " "must_change_password={}" ).format( user.user, may_use_webviewer, may_view_other_users_records, view_all_patients_when_unfiltered, may_upload, superuser, may_register_devices, may_use_webstorage, may_dump_data, may_run_reports, may_add_notes, must_change_password ) ) return user_management_success_message("User " + user.user + " created")
def change_user(form): """Apply changes to a user, and return success/failure HTML.""" username = ws.get_cgi_parameter_str(form, PARAM.USERNAME) may_use_webviewer = ws.get_cgi_parameter_bool( form, PARAM.MAY_USE_WEBVIEWER) may_view_other_users_records = ws.get_cgi_parameter_bool( form, PARAM.MAY_VIEW_OTHER_USERS_RECORDS) view_all_patients_when_unfiltered = ws.get_cgi_parameter_bool( form, PARAM.VIEW_ALL_PTS_WHEN_UNFILTERED) may_upload = ws.get_cgi_parameter_bool(form, PARAM.MAY_UPLOAD) superuser = ws.get_cgi_parameter_bool(form, PARAM.SUPERUSER) may_register_devices = ws.get_cgi_parameter_bool( form, PARAM.MAY_REGISTER_DEVICES) may_use_webstorage = ws.get_cgi_parameter_bool( form, PARAM.MAY_USE_WEBSTORAGE) may_dump_data = ws.get_cgi_parameter_bool(form, PARAM.MAY_DUMP_DATA) may_run_reports = ws.get_cgi_parameter_bool(form, PARAM.MAY_RUN_REPORTS) may_add_notes = ws.get_cgi_parameter_bool(form, PARAM.MAY_ADD_NOTES) user = User(username, False) if not user.user: return user_management_failure_message("Invalid user: "******"User permissions edited for user {}: " "may_use_webviewer={}, " "may_view_other_users_records={}, " "view_all_patients_when_unfiltered={}, " "may_upload={}, " "superuser={}, " "may_register_devices={}, " "may_use_webstorage={}, " "may_dump_data={}, " "may_run_reports={}, " "may_add_notes={} " ).format( user.user, may_use_webviewer, may_view_other_users_records, view_all_patients_when_unfiltered, may_upload, superuser, may_register_devices, may_use_webstorage, may_dump_data, may_run_reports, may_add_notes, ) ) return user_management_success_message( "Details updated for user " + user.user)
def enable_user(username): """Unlock user and clear login failures.""" unlock_user(username) clear_login_failures(username) audit("User {} re-enabled".format(username))