Example #1
0
 def update_submission_preferences():
     form = SubmissionPreferencesForm()
     if form.validate_on_submit():
         # The UI prompt ("prevent") is the opposite of the setting ("allow"):
         value = not bool(request.form.get('prevent_document_uploads'))
         InstanceConfig.set('allow_document_uploads', value)
         return redirect(url_for('admin.manage_config'))
Example #2
0
def test_submit_initial_short_message(source_app):
    """
    Test the message size limit.
    """
    with source_app.test_client() as app:
        InstanceConfig.get_default().update_submission_prefs(
            allow_uploads=True, min_length=10, reject_codenames=False)
        new_codename(app, session)
        resp = app.post(
            url_for("main.submit"),
            data=dict(msg="A" * 5, fh=(StringIO(""), "")),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Your first message must be at least 10 characters long." in text
        # Now retry with a longer message
        resp = app.post(
            url_for("main.submit"),
            data=dict(msg="A" * 25, fh=(StringIO(""), "")),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Thank you for sending this information to us." in text
        # Now send another short message, that should still be accepted since
        # it's no longer the initial one
        resp = app.post(url_for("main.submit"),
                        data=dict(msg="A", fh=(StringIO(""), "")),
                        follow_redirects=True)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Thanks! We received your message." in text
Example #3
0
    def update_submission_preferences() -> Optional[werkzeug.Response]:
        form = SubmissionPreferencesForm()
        if form.validate_on_submit():
            # The UI prompt ("prevent") is the opposite of the setting ("allow"):
            allow_uploads = not form.prevent_document_uploads.data

            if form.prevent_short_messages.data:
                msg_length = form.min_message_length.data
            else:
                msg_length = 0

            reject_codenames = form.reject_codename_messages.data

            InstanceConfig.update_submission_prefs(allow_uploads, msg_length,
                                                   reject_codenames)
            flash(gettext("Preferences saved."),
                  "submission-preferences-success")
            return redirect(
                url_for("admin.manage_config") + "#config-preventuploads")
        else:
            for field, errors in list(form.errors.items()):
                for error in errors:
                    flash(
                        gettext("Preferences not updated.") + " " + error,
                        "submission-preferences-error",
                    )
        return redirect(
            url_for("admin.manage_config") + "#config-preventuploads")
Example #4
0
def test_submit_codename(source_app):
    """
    Test preventions against people submitting their codename.
    """
    with source_app.test_client() as app:
        InstanceConfig.get_default().update_submission_prefs(
            allow_uploads=True, min_length=0, reject_codenames=True)
        codename = new_codename(app, session)
        resp = app.post(
            url_for("main.submit"),
            data=dict(msg=codename, fh=(StringIO(""), "")),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Please do not submit your codename!" in text
        # Do a dummy submission
        _dummy_submission(app)
        # Now resubmit the codename, should be accepted.
        resp = app.post(
            url_for("main.submit"),
            data=dict(msg=codename, fh=(StringIO(""), "")),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Thanks! We received your message" in text
Example #5
0
def test_only_one_active_instance_config_can_exist(config, source_app):
    """
    Checks that attempts to add multiple active InstanceConfig records fail.

    InstanceConfig is supposed to be invalidated by setting
    valid_until to the time the config was no longer in effect. Until
    we added the partial index preventing multiple rows with a null
    valid_until, it was possible for the system to create multiple
    active records, which would cause MultipleResultsFound exceptions
    in InstanceConfig.get_current.
    """
    # create a separate session
    engine = create_engine(source_app.config["SQLALCHEMY_DATABASE_URI"])
    session = sessionmaker(bind=engine)()

    # in the separate session, create an InstanceConfig with default
    # values, but don't commit it
    conflicting_config = InstanceConfig()
    session.add(conflicting_config)

    with source_app.app_context():
        # get_current will create another InstanceConfig with default values
        InstanceConfig.get_current()

    # now the commit of the first instance should fail
    with pytest.raises(IntegrityError):
        session.commit()
Example #6
0
 def update_submission_preferences() -> Optional[werkzeug.Response]:
     form = SubmissionPreferencesForm()
     if form.validate_on_submit():
         # The UI prompt ("prevent") is the opposite of the setting ("allow"):
         flash(gettext("Preferences saved."),
               "submission-preferences-success")
         value = not bool(request.form.get('prevent_document_uploads'))
         InstanceConfig.set_allow_document_uploads(value)
         return redirect(url_for('admin.manage_config'))
     else:
         return None
Example #7
0
    def lookup(logged_in_source: SourceUser) -> str:
        replies = []
        logged_in_source_in_db = logged_in_source.get_db_record()
        source_inbox = Reply.query.filter_by(
            source_id=logged_in_source_in_db.id, deleted_by_source=False
        ).all()

        first_submission = logged_in_source_in_db.interaction_count == 0

        if first_submission:
            min_message_length = InstanceConfig.get_default().initial_message_min_len
        else:
            min_message_length = 0

        for reply in source_inbox:
            reply_path = Storage.get_default().path(
                logged_in_source.filesystem_id,
                reply.filename,
            )
            try:
                with io.open(reply_path, "rb") as f:
                    contents = f.read()
                decrypted_reply = EncryptionManager.get_default().decrypt_journalist_reply(
                    for_source_user=logged_in_source, ciphertext_in=contents
                )
                reply.decrypted = decrypted_reply
            except UnicodeDecodeError:
                current_app.logger.error("Could not decode reply %s" % reply.filename)
            except FileNotFoundError:
                current_app.logger.error("Reply file missing: %s" % reply.filename)
            else:
                reply.date = datetime.utcfromtimestamp(os.stat(reply_path).st_mtime)
                replies.append(reply)

        # Sort the replies by date
        replies.sort(key=operator.attrgetter("date"), reverse=True)

        # If not done yet, generate a keypair to encrypt replies from the journalist
        encryption_mgr = EncryptionManager.get_default()
        try:
            encryption_mgr.get_source_public_key(logged_in_source.filesystem_id)
        except GpgKeyNotFoundError:
            encryption_mgr.generate_source_key_pair(logged_in_source)

        return render_template(
            "lookup.html",
            is_user_logged_in=True,
            allow_document_uploads=InstanceConfig.get_default().allow_document_uploads,
            replies=replies,
            min_len=min_message_length,
            new_user_codename=session.get("new_user_codename", None),
            form=SubmissionForm(),
        )
Example #8
0
    def manage_config() -> Union[str, werkzeug.Response]:
        if InstanceConfig.get_default().initial_message_min_len > 0:
            prevent_short_messages = True
        else:
            prevent_short_messages = False

        # The UI document upload prompt ("prevent") is the opposite of the setting ("allow")
        submission_preferences_form = SubmissionPreferencesForm(
            prevent_document_uploads=not InstanceConfig.get_default(
            ).allow_document_uploads,
            prevent_short_messages=prevent_short_messages,
            min_message_length=InstanceConfig.get_default(
            ).initial_message_min_len,
            reject_codename_messages=InstanceConfig.get_default().
            reject_message_with_codename,
        )
        organization_name_form = OrgNameForm(
            organization_name=InstanceConfig.get_default().organization_name)
        logo_form = LogoForm()
        if logo_form.validate_on_submit():
            f = logo_form.logo.data

            if current_app.static_folder is None:
                abort(500)
            custom_logo_filepath = os.path.join(current_app.static_folder, "i",
                                                "custom_logo.png")
            try:
                f.save(custom_logo_filepath)
                flash(gettext("Image updated."), "logo-success")
            except Exception:
                flash(
                    # Translators: This error is shown when an uploaded image cannot be used.
                    gettext(
                        "Unable to process the image file. Please try another one."
                    ),
                    "logo-error",
                )
            finally:
                return redirect(
                    url_for("admin.manage_config") + "#config-logoimage")
        else:
            for field, errors in list(logo_form.errors.items()):
                for error in errors:
                    flash(error, "logo-error")
            return render_template(
                "config.html",
                submission_preferences_form=submission_preferences_form,
                organization_name_form=organization_name_form,
                max_len=Submission.MAX_MESSAGE_LEN,
                logo_form=logo_form,
            )
Example #9
0
 def metadata() -> flask.Response:
     meta = {
         "organization_name":
         InstanceConfig.get_default().organization_name,
         "allow_document_uploads":
         InstanceConfig.get_default().allow_document_uploads,
         "gpg_fpr": config.JOURNALIST_KEY,
         "sd_version": version.__version__,
         "server_os": server_os.get_os_release(),
         "supported_languages": config.SUPPORTED_LOCALES,
         "v3_source_url": get_sourcev3_url(),
     }
     resp = make_response(json.dumps(meta))
     resp.headers["Content-Type"] = "application/json"
     return resp
Example #10
0
    def setup_g() -> "Optional[Response]":
        """Store commonly used values in Flask's special g object"""
        if "expires" in session and datetime.now(timezone.utc) >= session["expires"]:
            session.clear()
            flash(gettext("You have been logged out due to inactivity."), "error")

        uid = session.get("uid", None)
        if uid:
            user = Journalist.query.get(uid)
            if user and "nonce" in session and session["nonce"] != user.session_nonce:
                session.clear()
                flash(gettext("You have been logged out due to password change"), "error")

        session["expires"] = datetime.now(timezone.utc) + timedelta(
            minutes=getattr(config, "SESSION_EXPIRATION_MINUTES", 120)
        )

        uid = session.get("uid", None)
        if uid:
            g.user = Journalist.query.get(uid)  # pylint: disable=assigning-non-slot

        i18n.set_locale(config)

        if InstanceConfig.get_default().organization_name:
            g.organization_name = (  # pylint: disable=assigning-non-slot
                InstanceConfig.get_default().organization_name
            )
        else:
            g.organization_name = gettext("SecureDrop")  # pylint: disable=assigning-non-slot

        try:
            g.logo = get_logo_url(app)  # pylint: disable=assigning-non-slot
        except FileNotFoundError:
            app.logger.error("Site logo not found.")

        if request.path.split("/")[1] == "api":
            pass  # We use the @token_required decorator for the API endpoints
        else:  # We are not using the API
            if request.endpoint not in _insecure_views and not logged_in():
                return redirect(url_for("main.login"))

        if request.method == "POST":
            filesystem_id = request.form.get("filesystem_id")
            if filesystem_id:
                g.filesystem_id = filesystem_id  # pylint: disable=assigning-non-slot
                g.source = get_source(filesystem_id)  # pylint: disable=assigning-non-slot

        return None
Example #11
0
 def update_org_name() -> Union[str, werkzeug.Response]:
     form = OrgNameForm()
     if form.validate_on_submit():
         try:
             value = request.form['organization_name']
             InstanceConfig.set_organization_name(escape(value, quote=True))
             flash(gettext("Preferences saved."), "org-name-success")
         except Exception:
             flash(gettext('Failed to update organization name.'),
                   'org-name-error')
         return redirect(url_for('admin.manage_config') + "#config-orgname")
     else:
         for field, errors in list(form.errors.items()):
             for error in errors:
                 flash(error, "org-name-error")
     return redirect(url_for('admin.manage_config') + "#config-orgname")
Example #12
0
 def validate_msg(self, field: wtforms.Field) -> None:
     if len(field.data) > Submission.MAX_MESSAGE_LEN:
         message = gettext("Message text too long.")
         if InstanceConfig.get_default().allow_document_uploads:
             message = "{} {}".format(
                 message,
                 gettext(
                     "Large blocks of text must be uploaded as a file, not copied and pasted."
                 ),
             )
         raise ValidationError(message)
Example #13
0
def test_metadata_route(config, source_app):
    with patch("server_os.get_os_release", return_value="20.04"):
        with source_app.test_client() as app:
            resp = app.get(url_for("api.metadata"))
            assert resp.status_code == 200
            assert resp.headers.get("Content-Type") == "application/json"
            assert (resp.json.get("allow_document_uploads") ==
                    InstanceConfig.get_current().allow_document_uploads)
            assert resp.json.get("sd_version") == version.__version__
            assert resp.json.get("server_os") == "20.04"
            assert resp.json.get(
                "supported_languages") == config.SUPPORTED_LOCALES
            assert resp.json.get("v3_source_url") is None
Example #14
0
def test_metadata_route(config, source_app):
    with patch.object(source_app_api.platform, "linux_distribution") as mocked_platform:
        mocked_platform.return_value = ("Ubuntu", "16.04", "xenial")
        with source_app.test_client() as app:
            resp = app.get(url_for('api.metadata'))
            assert resp.status_code == 200
            assert resp.headers.get('Content-Type') == 'application/json'
            assert resp.json.get('allow_document_uploads') ==\
                InstanceConfig.get_current().allow_document_uploads
            assert resp.json.get('sd_version') == version.__version__
            assert resp.json.get('server_os') == '16.04'
            assert resp.json.get('supported_languages') ==\
                config.SUPPORTED_LOCALES
Example #15
0
def test_metadata_route(config, source_app):
    with patch.object(source_app_api, "server_os", new="16.04"):
        with source_app.test_client() as app:
            resp = app.get(url_for('api.metadata'))
            assert resp.status_code == 200
            assert resp.headers.get('Content-Type') == 'application/json'
            assert resp.json.get('allow_document_uploads') ==\
                InstanceConfig.get_current().allow_document_uploads
            assert resp.json.get('sd_version') == version.__version__
            assert resp.json.get('server_os') == '16.04'
            assert resp.json.get('supported_languages') ==\
                config.SUPPORTED_LOCALES
            assert resp.json.get('v2_source_url') is None
            assert resp.json.get('v3_source_url') is None
Example #16
0
def test_submit_codename_second_login(source_app):
    """
    Test codename submissions *not* prevented on second session
    """
    with source_app.test_client() as app:
        InstanceConfig.get_default().update_submission_prefs(
            allow_uploads=True, min_length=0, reject_codenames=True)
        codename = new_codename(app, session)
        resp = app.post(
            url_for("main.submit"),
            data=dict(msg=codename, fh=(StringIO(""), "")),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Please do not submit your codename!" in text

        resp = app.get(url_for("main.logout"), follow_redirects=True)
        assert not SessionManager.is_user_logged_in(db_session=db.session)
        text = resp.data.decode("utf-8")
        assert "This will clear your Tor Browser activity data" in text

        resp = app.post(url_for("main.login"),
                        data=dict(codename=codename),
                        follow_redirects=True)
        assert resp.status_code == 200
        assert SessionManager.is_user_logged_in(db_session=db.session)

        resp = app.post(
            url_for("main.submit"),
            data=dict(msg=codename, fh=(StringIO(""), "")),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Thank you for sending this information" in text
Example #17
0
 def load_instance_config():
     app.instance_config = InstanceConfig.get_current()
Example #18
0
 def update_instance_config() -> None:
     InstanceConfig.get_default(refresh=True)
Example #19
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"))