Exemple #1
0
    def reply() -> werkzeug.Response:
        """Attempt to send a Reply from a Journalist to a Source. Empty
        messages are rejected, and an informative error message is flashed
        on the client. In the case of unexpected errors involving database
        transactions (potentially caused by racing request threads that
        modify the same the database object) logging is done in such a way
        so as not to write potentially sensitive information to disk, and a
        generic error message is flashed on the client.

        Returns:
           flask.Response: The user is redirected to the same Source
               collection view, regardless if the Reply is created
               successfully.
        """
        form = ReplyForm()
        if not form.validate_on_submit():
            for error in form.message.errors:
                flash(error, "error")
            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))

        g.source.interaction_count += 1
        filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count,
                                              g.source.journalist_filename)
        current_app.crypto_util.encrypt(
            form.message.data,
            [
                current_app.crypto_util.get_fingerprint(g.filesystem_id),
                config.JOURNALIST_KEY
            ],
            output=current_app.storage.path(g.filesystem_id, filename),
        )

        try:
            reply = Reply(g.user, g.source, filename)
            db.session.add(reply)
            db.session.flush()
            seen_reply = SeenReply(reply_id=reply.id, journalist_id=g.user.id)
            db.session.add(seen_reply)
            db.session.commit()
            store.async_add_checksum_for_file(reply)
        except Exception as exc:
            flash(
                gettext("An unexpected error occurred! Please "
                        "inform your admin."), "error")
            # We take a cautious approach to logging here because we're dealing
            # with responses to sources. It's possible the exception message
            # could contain information we don't want to write to disk.
            current_app.logger.error(
                "Reply from '{}' (ID {}) failed: {}!".format(
                    g.user.username, g.user.id, exc.__class__))
        else:

            flash(
                Markup("<b>{}</b> {}".format(
                    # Translators: Precedes a message confirming the success of an operation.
                    escape(gettext("Success!")),
                    escape(gettext("Your reply has been stored.")))),
                'success')
        finally:
            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
def test_async_add_checksum_for_file(config, app_storage, db_model):
    """
    Check that when we execute the `add_checksum_for_file` function, the database object is
    correctly updated with the actual hash of the file.

    We have to create our own app in order to have more control over the SQLAlchemy sessions. The
    fixture pushes a single app context that forces us to work within a single transaction.
    """
    app = create_app(config)

    with app.app_context():
        db.create_all()
        source_user = create_source_user(
            db_session=db.session,
            source_passphrase=PassphraseGenerator.get_default().
            generate_passphrase(),
            source_app_storage=app_storage,
        )
        source = source_user.get_db_record()
        target_file_path = app_storage.path(source.filesystem_id,
                                            "1-foo-msg.gpg")
        test_message = b"hash me!"
        expected_hash = "f1df4a6d8659471333f7f6470d593e0911b4d487856d88c83d2d187afa195927"

        with open(target_file_path, "wb") as f:
            f.write(test_message)

        if db_model == Submission:
            db_obj = Submission(source, target_file_path, app_storage)
        else:
            journalist, _ = utils.db_helper.init_journalist()
            db_obj = Reply(journalist, source, target_file_path, app_storage)

        db.session.add(db_obj)
        db.session.commit()
        db_obj_id = db_obj.id

        job = async_add_checksum_for_file(db_obj, app_storage)

    utils.asynchronous.wait_for_redis_worker(job, timeout=5)

    with app.app_context():
        # requery to get a new object
        db_obj = db_model.query.filter_by(id=db_obj_id).one()
        assert db_obj.checksum == "sha256:" + expected_hash
Exemple #3
0
def test_async_add_checksum_for_file(config, db_model):
    '''
    Check that when we execute the `add_checksum_for_file` function, the database object is
    correctly updated with the actual hash of the file.

    We have to create our own app in order to have more control over the SQLAlchemy sessions. The
    fixture pushes a single app context that forces us to work within a single transaction.
    '''
    app = create_app(config)

    with app.app_context():
        db.create_all()
        source, _ = utils.db_helper.init_source_without_keypair()
        target_file_path = app.storage.path(source.filesystem_id, '1-foo-msg.gpg')
        test_message = b'hash me!'
        expected_hash = 'f1df4a6d8659471333f7f6470d593e0911b4d487856d88c83d2d187afa195927'

        with open(target_file_path, 'wb') as f:
            f.write(test_message)

        if db_model == Submission:
            db_obj = Submission(source, target_file_path)
        else:
            journalist, _ = utils.db_helper.init_journalist()
            db_obj = Reply(journalist, source, target_file_path)

        db.session.add(db_obj)
        db.session.commit()
        db_obj_id = db_obj.id

        job = async_add_checksum_for_file(db_obj)

    utils.asynchronous.wait_for_redis_worker(job, timeout=5)

    with app.app_context():
        # requery to get a new object
        db_obj = db_model.query.filter_by(id=db_obj_id).one()
        assert db_obj.checksum == 'sha256:' + expected_hash
Exemple #4
0
    def submit() -> werkzeug.Response:
        allow_document_uploads = current_app.instance_config.allow_document_uploads
        form = SubmissionForm()
        if not form.validate():
            for field, errors in form.errors.items():
                for error in errors:
                    flash(error, "error")
            return redirect(url_for('main.lookup'))

        msg = request.form['msg']
        fh = None
        if allow_document_uploads and 'fh' in request.files:
            fh = request.files['fh']

        # Don't submit anything if it was an "empty" submission. #878
        if not (msg or fh):
            if allow_document_uploads:
                flash(
                    gettext(
                        "You must enter a message or choose a file to submit."
                    ), "error")
            else:
                flash(gettext("You must enter a message."), "error")
            return redirect(url_for('main.lookup'))

        fnames = []
        journalist_filename = g.source.journalist_filename
        first_submission = g.source.interaction_count == 0

        if msg:
            g.source.interaction_count += 1
            fnames.append(
                current_app.storage.save_message_submission(
                    g.filesystem_id, g.source.interaction_count,
                    journalist_filename, msg))
        if fh:
            g.source.interaction_count += 1
            fnames.append(
                current_app.storage.save_file_submission(
                    g.filesystem_id, g.source.interaction_count,
                    journalist_filename, fh.filename, fh.stream))

        if first_submission:
            flash_message = render_template(
                'first_submission_flashed_message.html')
            flash(Markup(flash_message), "success")

        else:
            if msg and not fh:
                html_contents = gettext('Thanks! We received your message.')
            elif fh and not msg:
                html_contents = gettext('Thanks! We received your document.')
            else:
                html_contents = gettext('Thanks! We received your message and '
                                        'document.')

            flash_message = render_template(
                'next_submission_flashed_message.html',
                html_contents=html_contents)
            flash(Markup(flash_message), "success")

        new_submissions = []
        for fname in fnames:
            submission = Submission(g.source, fname)
            db.session.add(submission)
            new_submissions.append(submission)

        if g.source.pending:
            g.source.pending = False

            # Generate a keypair now, if there's enough entropy (issue #303)
            # (gpg reads 300 bytes from /dev/random)
            entropy_avail = get_entropy_estimate()
            if entropy_avail >= 2400:
                db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']

                async_genkey(current_app.crypto_util, db_uri, g.filesystem_id,
                             g.codename)
                current_app.logger.info(
                    "generating key, entropy: {}".format(entropy_avail))
            else:
                current_app.logger.warning(
                    "skipping key generation. entropy: {}".format(
                        entropy_avail))

        g.source.last_updated = datetime.utcnow()
        db.session.commit()

        for sub in new_submissions:
            store.async_add_checksum_for_file(sub)

        normalize_timestamps(g.filesystem_id)

        return redirect(url_for('main.lookup'))
Exemple #5
0
    def submit(logged_in_source: SourceUser) -> werkzeug.Response:
        allow_document_uploads = InstanceConfig.get_default().allow_document_uploads
        form = SubmissionForm()
        if not form.validate():
            for field, errors in form.errors.items():
                for error in errors:
                    flash_msg("error", None, error)
            return redirect(url_for("main.lookup"))

        msg = request.form["msg"]
        fh = None
        if allow_document_uploads and "fh" in request.files:
            fh = request.files["fh"]

        # Don't submit anything if it was an "empty" submission. #878
        if not (msg or fh):
            if allow_document_uploads:
                html_contents = gettext("You must enter a message or choose a file to submit.")
            else:
                html_contents = gettext("You must enter a message.")

            flash_msg("error", None, html_contents)
            return redirect(url_for("main.lookup"))

        fnames = []
        logged_in_source_in_db = logged_in_source.get_db_record()
        first_submission = logged_in_source_in_db.interaction_count == 0

        if first_submission:
            min_len = InstanceConfig.get_default().initial_message_min_len
            if (min_len > 0) and (msg and not fh) and (len(msg) < min_len):
                flash_msg(
                    "error",
                    None,
                    gettext("Your first message must be at least {} characters long.").format(
                        min_len
                    ),
                )
                return redirect(url_for("main.lookup"))

            # if the new_user_codename key is not present in the session, this is
            # not a first session
            new_codename = session.get("new_user_codename", None)

            codenames_rejected = InstanceConfig.get_default().reject_message_with_codename
            if new_codename is not None:
                if codenames_rejected and codename_detected(msg, new_codename):
                    flash_msg(
                        "error",
                        None,
                        gettext("Please do not submit your codename!"),
                        gettext(
                            "Keep your codename secret, and use it to log in later to "
                            "check for replies."
                        ),
                    )
                    return redirect(url_for("main.lookup"))

        if not os.path.exists(Storage.get_default().path(logged_in_source.filesystem_id)):
            current_app.logger.debug(
                "Store directory not found for source '{}', creating one.".format(
                    logged_in_source_in_db.journalist_designation
                )
            )
            os.mkdir(Storage.get_default().path(logged_in_source.filesystem_id))

        if msg:
            logged_in_source_in_db.interaction_count += 1
            fnames.append(
                Storage.get_default().save_message_submission(
                    logged_in_source_in_db.filesystem_id,
                    logged_in_source_in_db.interaction_count,
                    logged_in_source_in_db.journalist_filename,
                    msg,
                )
            )
        if fh:
            logged_in_source_in_db.interaction_count += 1
            fnames.append(
                Storage.get_default().save_file_submission(
                    logged_in_source_in_db.filesystem_id,
                    logged_in_source_in_db.interaction_count,
                    logged_in_source_in_db.journalist_filename,
                    fh.filename,
                    fh.stream,
                )
            )

        if first_submission or msg or fh:
            if first_submission:
                html_contents = gettext(
                    "Thank you for sending this information to us. Please "
                    "check back later for replies."
                )
            elif msg and not fh:
                html_contents = gettext("Thanks! We received your message.")
            elif fh and not msg:
                html_contents = gettext("Thanks! We received your document.")
            else:
                html_contents = gettext("Thanks! We received your message and document.")

            flash_msg("success", gettext("Success!"), html_contents)

        new_submissions = []
        for fname in fnames:
            submission = Submission(logged_in_source_in_db, fname, Storage.get_default())
            db.session.add(submission)
            new_submissions.append(submission)

        logged_in_source_in_db.pending = False
        logged_in_source_in_db.last_updated = datetime.now(timezone.utc)
        db.session.commit()

        for sub in new_submissions:
            store.async_add_checksum_for_file(sub, Storage.get_default())

        normalize_timestamps(logged_in_source)

        return redirect(url_for("main.lookup"))