def delete_user(args: argparse.Namespace, context: Optional[AppContext] = None) -> int: """Deletes a journalist or admin from the application.""" with context or app_context(): username = _get_username_to_delete() try: selected_user = Journalist.query.filter_by(username=username).one() except NoResultFound: print("ERROR: That user was not found!") return 0 # Confirm deletion if user is found if not _get_delete_confirmation(selected_user.username): return 0 # Try to delete user from the database try: db.session.delete(selected_user) db.session.commit() except Exception as e: # If the user was deleted between the user selection and # confirmation, (e.g., through the web app), we don't report any # errors. If the user is still there, but there was a error # deleting them from the database, we do report it. try: Journalist.query.filter_by(username=username).one() except NoResultFound: pass else: raise e print('User "{}" successfully deleted'.format(username)) return 0
def delete_disconnected_fs_submissions(args): """ Delete files without a corresponding Submission record in the database. """ with app_context(): disconnected_files = find_disconnected_fs_submissions(args.store_dir) bytes_deleted = 0 time_elapsed = 0.0 rate = 1.0 filecount = len(disconnected_files) eta = 1.0 eta_msg = "" for i, f in enumerate(disconnected_files, 1): remove = args.force if not args.force: remove = input("Enter 'y' to delete {}: ".format(f)) == "y" if remove: filesize = os.stat(f).st_size if i > 1: eta = filesize / rate eta_msg = " (ETA to remove {:d} bytes: {:.0f}s )".format(filesize, eta) print("Securely removing file {}/{} {}{}...".format(i, filecount, f, eta_msg)) start = time.time() secure_delete(f) file_elapsed = time.time() - start bytes_deleted += filesize time_elapsed += file_elapsed rate = bytes_deleted / time_elapsed print( "elapsed: {:.2f}s rate: {:.1f} MB/s overall rate: {:.1f} MB/s".format( file_elapsed, filesize / 1048576 / file_elapsed, rate / 1048576 ) ) else: print("Not removing {}.".format(f))
def were_there_submissions_today(args, context=None): with context or app_context(): something = (db.session.query(Source).filter( Source.last_updated > datetime.datetime.utcnow() - datetime.timedelta(hours=24)).count() > 0) count_file = os.path.join(args.data_root, "submissions_today.txt") open(count_file, "w").write(something and "1" or "0")
def list_disconnected_db_submissions(args): """ List the IDs of Submission records whose files are missing. """ with app_context(): disconnected_submissions = find_disconnected_db_submissions(args.store_dir) if disconnected_submissions: print( 'Run "manage.py delete-disconnected-db-submissions" to delete these records.', file=sys.stderr, ) for s in disconnected_submissions: print(s.id)
def check_for_disconnected_db_submissions(args): """ Check for Submission records whose files are missing. """ with app_context(): disconnected = find_disconnected_db_submissions(args.store_dir) if disconnected: print( "There are submissions in the database with no corresponding files. " 'Run "manage.py list-disconnected-db-submissions" for details.' ) else: print("No problems were found. All submissions' files are present.")
def list_disconnected_fs_submissions(args): """ List files without a corresponding Submission or Reply record in the database. """ with app_context(): disconnected_files = find_disconnected_fs_submissions(args.store_dir) if disconnected_files: print( 'Run "manage.py delete-disconnected-fs-submissions" to delete these files.', file=sys.stderr, ) for f in disconnected_files: print(f)
def check_for_disconnected_fs_submissions(args): """ Check for files without a corresponding Submission or Reply record in the database. """ with app_context(): disconnected = find_disconnected_fs_submissions(args.store_dir) if disconnected: print( "There are files in the submission area with no corresponding records in the " 'database. Run "manage.py list-disconnected-fs-submissions" for details.' ) else: print("No unexpected files were found in the store.")
def delete_disconnected_db_submissions(args): """ Delete Submission records whose files are missing. """ with app_context(): disconnected_submissions = find_disconnected_db_submissions(args.store_dir) ids = [s.id for s in disconnected_submissions] remove = args.force if not args.force: remove = input("Enter 'y' to delete all submissions missing files: ") == "y" if remove: print("Removing submission IDs {}...".format(ids)) db.session.query(Submission).filter(Submission.id.in_(ids)).delete( synchronize_session="fetch" ) db.session.commit() else: print("Not removing disconnected submissions in database.")
def reset(args: argparse.Namespace, context: Optional[AppContext] = None) -> int: """Clears the SecureDrop development applications' state, restoring them to the way they were immediately after running `setup_dev.sh`. This command: 1. Erases the development sqlite database file. 2. Regenerates the database. 3. Erases stored submissions and replies from the store dir. """ # Erase the development db file if not hasattr(config, "DATABASE_FILE"): raise Exception("./manage.py doesn't know how to clear the db " "if the backend is not sqlite") # we need to save some data about the old DB file so we can recreate it # with the same state try: stat_res = os.stat(config.DATABASE_FILE) uid = stat_res.st_uid gid = stat_res.st_gid except OSError: uid = os.getuid() gid = os.getgid() try: os.remove(config.DATABASE_FILE) except OSError: pass # Regenerate the database # 1. Create it subprocess.check_call(["sqlite3", config.DATABASE_FILE, ".databases"]) # 2. Set permissions on it os.chown(config.DATABASE_FILE, uid, gid) os.chmod(config.DATABASE_FILE, 0o0640) if os.environ.get("SECUREDROP_ENV") == "dev": # 3. Create the DB from the metadata directly when in 'dev' so # developers can test application changes without first writing # alembic migration. with context or app_context(): db.create_all() else: # We have to override the hardcoded .ini file because during testing # the value in the .ini doesn't exist. ini_dir = os.path.dirname( getattr(config, "TEST_ALEMBIC_INI", "alembic.ini")) # 3. Migrate it to 'head' # nosemgrep: python.lang.security.audit.subprocess-shell-true.subprocess-shell-true subprocess.check_call("cd {} && alembic upgrade head".format(ini_dir), shell=True) # nosec # Clear submission/reply storage try: os.stat(args.store_dir) except OSError: pass else: for source_dir in os.listdir(args.store_dir): try: # Each entry in STORE_DIR is a directory corresponding # to a source shutil.rmtree(os.path.join(args.store_dir, source_dir)) except OSError: pass return 0
def _add_user(is_admin: bool = False, context: Optional[AppContext] = None) -> int: with context or app_context(): username = _get_username() first_name = _get_first_name() last_name = _get_last_name() print("Note: Passwords are now autogenerated.") password = PassphraseGenerator.get_default().generate_passphrase() print("This user's password is: {}".format(password)) is_hotp = _get_yubikey_usage() otp_secret = None if is_hotp: while True: otp_secret = obtain_input( "Please configure this user's YubiKey and enter the " "secret: ") if otp_secret: tmp_str = otp_secret.replace(" ", "") if len(tmp_str) != 40: print("The length of the secret is not correct. " "Expected 40 characters, but received {0}. " "Try again.".format(len(tmp_str))) continue if otp_secret: break try: user = Journalist( username=username, first_name=first_name, last_name=last_name, password=password, is_admin=is_admin, otp_secret=otp_secret, ) db.session.add(user) db.session.commit() except Exception as exc: db.session.rollback() if "UNIQUE constraint failed: journalists.username" in str(exc): print("ERROR: That username is already taken!") else: exc_type, exc_value, exc_traceback = sys.exc_info() print( repr( traceback.format_exception(exc_type, exc_value, exc_traceback))) return 1 else: print('User "{}" successfully added'.format(username)) if not otp_secret: # Print the QR code for FreeOTP print("\nScan the QR code below with FreeOTP:\n") uri = user.totp.provisioning_uri(username, issuer_name="SecureDrop") qr = qrcode.QRCode() qr.add_data(uri) qr.print_ascii(tty=sys.stdout.isatty()) print("\nIf the barcode does not render correctly, try " "changing your terminal's font (Monospace for Linux, " "Menlo for OS X). If you are using iTerm on Mac OS X, " 'you will need to change the "Non-ASCII Font", which ' "is your profile's Text settings.\n\nCan't scan the " "barcode? Enter following shared secret manually:" "\n{}\n".format(user.formatted_otp_secret)) return 0