예제 #1
0
def add_reply(
    source: Source, journalist: Journalist, journalist_who_saw: Optional[Journalist]
) -> None:
    """
    Adds a single reply to a source.
    """
    record_source_interaction(source)
    fname = "{}-{}-reply.gpg".format(source.interaction_count, source.journalist_filename)
    current_app.crypto_util.encrypt(
        next(replies),
        [
            current_app.crypto_util.get_fingerprint(source.filesystem_id),
            config.JOURNALIST_KEY,
        ],
        current_app.storage.path(source.filesystem_id, fname),
    )

    reply = Reply(journalist, source, fname)
    db.session.add(reply)
    db.session.flush()

    # Journalist who replied has seen the reply
    author_seen_reply = SeenReply(reply_id=reply.id, journalist_id=journalist.id)
    db.session.add(author_seen_reply)

    if journalist_who_saw:
        other_seen_reply = SeenReply(reply_id=reply.id, journalist_id=journalist_who_saw.id)
        db.session.add(other_seen_reply)

    db.session.commit()
예제 #2
0
def add_reply(source: Source, journalist: Journalist,
              journalist_who_saw: Optional[Journalist]) -> None:
    """
    Adds a single reply to a source.
    """
    record_source_interaction(source)
    fname = "{}-{}-reply.gpg".format(source.interaction_count,
                                     source.journalist_filename)
    EncryptionManager.get_default().encrypt_journalist_reply(
        for_source_with_filesystem_id=source.filesystem_id,
        reply_in=next(replies),
        encrypted_reply_path_out=Path(Storage.get_default().path(
            source.filesystem_id, fname)),
    )
    reply = Reply(journalist, source, fname, Storage.get_default())
    db.session.add(reply)

    # Journalist who replied has seen the reply
    author_seen_reply = SeenReply(reply=reply, journalist=journalist)
    db.session.add(author_seen_reply)

    if journalist_who_saw:
        other_seen_reply = SeenReply(reply=reply,
                                     journalist=journalist_who_saw)
        db.session.add(other_seen_reply)

    db.session.commit()
예제 #3
0
def mark_seen(targets: List[Union[Submission, Reply]],
              user: Journalist) -> None:
    """
    Marks a list of submissions or replies seen by the given journalist.
    """
    for t in targets:
        try:
            if isinstance(t, Submission):
                t.downloaded = True
                if t.is_file:
                    sf = SeenFile(file_id=t.id, journalist_id=user.id)
                    db.session.add(sf)
                elif t.is_message:
                    sm = SeenMessage(message_id=t.id, journalist_id=user.id)
                    db.session.add(sm)
                db.session.commit()
            elif isinstance(t, Reply):
                sr = SeenReply(reply_id=t.id, journalist_id=user.id)
                db.session.add(sr)
                db.session.commit()
        except IntegrityError as e:
            db.session.rollback()
            if 'UNIQUE constraint failed' in str(e):
                continue
            raise
예제 #4
0
def reply(storage, journalist, source, num_replies):
    """Generates and submits *num_replies* replies to *source*
    from *journalist*. Returns reply objects as a list.

    :param Journalist journalist: The journalist to write the
                                     reply from.

    :param Source source: The source to send the reply to.

    :param int num_replies: Number of random-data replies to make.

    :returns: A list of the :class:`Reply`s submitted.
    """
    assert num_replies >= 1
    replies = []
    for _ in range(num_replies):
        source.interaction_count += 1
        fname = "{}-{}-reply.gpg".format(source.interaction_count,
                                         source.journalist_filename)

        EncryptionManager.get_default().encrypt_journalist_reply(
            for_source_with_filesystem_id=source.filesystem_id,
            reply_in=str(os.urandom(1)),
            encrypted_reply_path_out=storage.path(source.filesystem_id, fname),
        )

        reply = Reply(journalist, source, fname, storage)
        replies.append(reply)
        db.session.add(reply)
        seen_reply = SeenReply(reply=reply, journalist=journalist)
        db.session.add(seen_reply)

    db.session.commit()
    return replies
예제 #5
0
파일: main.py 프로젝트: jingru97/securedrop
    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))
예제 #6
0
def reply(journalist, source, num_replies):
    """Generates and submits *num_replies* replies to *source*
    from *journalist*. Returns reply objects as a list.

    :param Journalist journalist: The journalist to write the
                                     reply from.

    :param Source source: The source to send the reply to.

    :param int num_replies: Number of random-data replies to make.

    :returns: A list of the :class:`Reply`s submitted.
    """
    assert num_replies >= 1
    replies = []
    for _ in range(num_replies):
        source.interaction_count += 1
        fname = "{}-{}-reply.gpg".format(source.interaction_count,
                                         source.journalist_filename)
        current_app.crypto_util.encrypt(
            str(os.urandom(1)), [
                current_app.crypto_util.get_fingerprint(source.filesystem_id),
                config.JOURNALIST_KEY
            ], current_app.storage.path(source.filesystem_id, fname))

        reply = Reply(journalist, source, fname)
        replies.append(reply)
        db.session.add(reply)
        db.session.flush()
        seen_reply = SeenReply(reply_id=reply.id, journalist_id=journalist.id)
        db.session.add(seen_reply)

    db.session.commit()
    return replies
예제 #7
0
def create_source_and_submissions(
        source_index,
        source_count,
        num_submissions=2,
        num_replies=2,
        journalist_who_replied=None  # noqa: W605, E501
):
    # Store source in database
    codename = current_app.crypto_util.genrandomid()
    filesystem_id = current_app.crypto_util.hash_codename(codename)
    journalist_designation = current_app.crypto_util.display_id()
    source = Source(filesystem_id, journalist_designation)
    source.pending = False
    db.session.add(source)
    db.session.commit()

    # Generate submissions directory and generate source key
    os.mkdir(current_app.storage.path(source.filesystem_id))
    current_app.crypto_util.genkeypair(source.filesystem_id, codename)

    # Generate some test submissions
    for _ in range(num_submissions):
        source.interaction_count += 1
        submission_text = next(submissions)
        fpath = current_app.storage.save_message_submission(
            source.filesystem_id, source.interaction_count,
            source.journalist_filename, submission_text)
        source.last_updated = datetime.datetime.utcnow()
        submission = Submission(source, fpath)
        db.session.add(submission)

    # Generate some test replies
    for _ in range(num_replies):
        source.interaction_count += 1
        fname = "{}-{}-reply.gpg".format(source.interaction_count,
                                         source.journalist_filename)
        current_app.crypto_util.encrypt(
            next(replies), [
                current_app.crypto_util.get_fingerprint(source.filesystem_id),
                config.JOURNALIST_KEY
            ], current_app.storage.path(source.filesystem_id, fname))

        if not journalist_who_replied:
            journalist = Journalist.query.first()
        else:
            journalist = journalist_who_replied
        reply = Reply(journalist, source, fname)
        db.session.add(reply)
        db.session.flush()
        seen_reply = SeenReply(reply_id=reply.id, journalist_id=journalist.id)
        db.session.add(seen_reply)

    db.session.commit()

    print("Test source {}/{} (codename: '{}', journalist designation '{}') "
          "added with {} submissions and {} replies".format(
              source_index, source_count, codename, journalist_designation,
              num_submissions, num_replies))
예제 #8
0
파일: api.py 프로젝트: itsibitzi/securedrop
    def all_source_replies(source_uuid: str) -> Tuple[flask.Response, int]:
        if request.method == 'GET':
            source = get_or_404(Source, source_uuid, column=Source.uuid)
            return jsonify(
                {'replies':
                 [reply.to_json() for reply in source.replies]}), 200
        elif request.method == 'POST':
            source = get_or_404(Source, source_uuid, column=Source.uuid)
            if request.json is None:
                abort(400, 'please send requests in valid JSON')

            if 'reply' not in request.json:
                abort(400, 'reply not found in request body')

            user = _authenticate_user_from_auth_header(request)

            data = request.json
            if not data['reply']:
                abort(400, 'reply should not be empty')

            source.interaction_count += 1
            try:
                filename = current_app.storage.save_pre_encrypted_reply(
                    source.filesystem_id, source.interaction_count,
                    source.journalist_filename, data['reply'])
            except NotEncrypted:
                return jsonify(
                    {'message': 'You must encrypt replies client side'}), 400

            # issue #3918
            filename = path.basename(filename)

            reply = Reply(user, source, filename)

            reply_uuid = data.get('uuid', None)
            if reply_uuid is not None:
                # check that is is parseable
                try:
                    UUID(reply_uuid)
                except ValueError:
                    abort(400, "'uuid' was not a valid UUID")
                reply.uuid = reply_uuid

            try:
                db.session.add(reply)
                db.session.flush()
                seen_reply = SeenReply(reply_id=reply.id,
                                       journalist_id=user.id)
                db.session.add(seen_reply)
                db.session.add(source)
                db.session.commit()
            except IntegrityError as e:
                db.session.rollback()
                if 'UNIQUE constraint failed: replies.uuid' in str(e):
                    abort(409, 'That UUID is already in use.')
                else:
                    raise e

            return jsonify({
                'message': 'Your reply has been stored',
                'uuid': reply.uuid,
                'filename': reply.filename
            }), 201
        else:
            abort(405)
예제 #9
0
    def all_source_replies(source_uuid: str) -> Tuple[flask.Response, int]:
        if request.method == "GET":
            source = get_or_404(Source, source_uuid, column=Source.uuid)
            return jsonify(
                {"replies":
                 [reply.to_json() for reply in source.replies]}), 200
        elif request.method == "POST":
            source = get_or_404(Source, source_uuid, column=Source.uuid)
            if request.json is None:
                abort(400, "please send requests in valid JSON")

            if "reply" not in request.json:
                abort(400, "reply not found in request body")

            user = _authenticate_user_from_auth_header(request)

            data = request.json
            if not data["reply"]:
                abort(400, "reply should not be empty")

            source.interaction_count += 1
            try:
                filename = Storage.get_default().save_pre_encrypted_reply(
                    source.filesystem_id,
                    source.interaction_count,
                    source.journalist_filename,
                    data["reply"],
                )
            except NotEncrypted:
                return jsonify(
                    {"message": "You must encrypt replies client side"}), 400

            # issue #3918
            filename = path.basename(filename)

            reply = Reply(user, source, filename, Storage.get_default())

            reply_uuid = data.get("uuid", None)
            if reply_uuid is not None:
                # check that is is parseable
                try:
                    UUID(reply_uuid)
                except ValueError:
                    abort(400, "'uuid' was not a valid UUID")
                reply.uuid = reply_uuid

            try:
                db.session.add(reply)
                seen_reply = SeenReply(reply=reply, journalist=user)
                db.session.add(seen_reply)
                db.session.add(source)
                db.session.commit()
            except IntegrityError as e:
                db.session.rollback()
                if "UNIQUE constraint failed: replies.uuid" in str(e):
                    abort(409, "That UUID is already in use.")
                else:
                    raise e

            return (
                jsonify({
                    "message": "Your reply has been stored",
                    "uuid": reply.uuid,
                    "filename": reply.filename,
                }),
                201,
            )
        else:
            abort(405)
예제 #10
0
def create_source_data(
    source_index: int,
    source_count: int,
    journalist_who_replied: Journalist,
    journalist_who_saw: Journalist,
    num_files: int = 2,
    num_messages: int = 2,
    num_replies: int = 2,
) -> None:
    # Store source in database
    codename = current_app.crypto_util.genrandomid()
    filesystem_id = current_app.crypto_util.hash_codename(codename)
    journalist_designation = current_app.crypto_util.display_id()
    source = Source(filesystem_id, journalist_designation)
    source.pending = False
    db.session.add(source)
    db.session.commit()

    # Generate submissions directory and generate source key
    os.mkdir(current_app.storage.path(source.filesystem_id))
    current_app.crypto_util.genkeypair(source.filesystem_id, codename)

    # Mark a third of sources as seen, a third as partially-seen, and a third as unseen
    seen_files = 0 if source_index % 3 == 0 else math.floor(num_files /
                                                            (source_index % 3))
    seen_messages = 0 if source_index % 3 == 0 else math.floor(
        num_messages / (source_index % 3))
    seen_replies = 0 if source_index % 3 == 0 else math.floor(
        num_replies / (source_index % 3))

    # Generate some test messages
    seen_messages_count = 0
    for _ in range(num_messages):
        source.interaction_count += 1
        submission_text = next(messages)
        fpath = current_app.storage.save_message_submission(
            source.filesystem_id, source.interaction_count,
            source.journalist_filename, submission_text)
        source.last_updated = datetime.datetime.utcnow()
        submission = Submission(source, fpath)
        db.session.add(submission)
        if seen_messages_count < seen_messages:
            seen_messages_count = seen_messages_count + 1
            db.session.flush()
            seen_message = SeenMessage(message_id=submission.id,
                                       journalist_id=journalist_who_saw.id)
            db.session.add(seen_message)

    # Generate some test files
    seen_files_count = 0
    for _ in range(num_files):
        source.interaction_count += 1
        fpath = current_app.storage.save_file_submission(
            source.filesystem_id, source.interaction_count,
            source.journalist_filename, "memo.txt",
            io.BytesIO(b"This is an example of a plain text file upload."))
        source.last_updated = datetime.datetime.utcnow()
        submission = Submission(source, fpath)
        db.session.add(submission)
        if seen_files_count < seen_files:
            seen_files_count = seen_files_count + 1
            db.session.flush()
            seen_file = SeenFile(file_id=submission.id,
                                 journalist_id=journalist_who_saw.id)
            db.session.add(seen_file)

    # Generate some test replies
    seen_replies_count = 0
    for _ in range(num_replies):
        source.interaction_count += 1
        fname = "{}-{}-reply.gpg".format(source.interaction_count,
                                         source.journalist_filename)
        current_app.crypto_util.encrypt(
            next(replies), [
                current_app.crypto_util.get_fingerprint(source.filesystem_id),
                config.JOURNALIST_KEY
            ], current_app.storage.path(source.filesystem_id, fname))

        reply = Reply(journalist_who_replied, source, fname)
        db.session.add(reply)
        db.session.flush()
        # Journalist who replied has seen the reply
        seen_reply = SeenReply(reply_id=reply.id,
                               journalist_id=journalist_who_replied.id)
        db.session.add(seen_reply)
        if seen_replies_count < seen_replies:
            seen_replies_count = seen_replies_count + 1
            seen_reply = SeenReply(reply_id=reply.id,
                                   journalist_id=journalist_who_saw.id)
            db.session.add(seen_reply)

    db.session.commit()

    print("Test source {}/{} (codename: '{}', journalist designation '{}') "
          "added with {} files, {} messages, and {} replies".format(
              source_index + 1, source_count, codename, journalist_designation,
              num_files, num_messages, num_replies))