Repository: freedomofpress/securedrop

Issue:
[reply refactor] Allow journalists to download replies from journalist interface
After #3673 is implemented, we should allow journalists to download replies from the journalist interface UI. Note that for long-running SecureDrop instances, there will be old replies encrypted only to the source key that should be unavailable for download.

Epic: #3097


Discussion:

Resolving patch:
diff --git a/securedrop/journalist_app/col.py b/securedrop/journalist_app/col.py
--- a/securedrop/journalist_app/col.py
+++ b/securedrop/journalist_app/col.py
@@ -67,18 +67,21 @@ def process():
         return method(cols_selected)
 
     @view.route('/<filesystem_id>/<fn>')
-    def download_single_submission(filesystem_id, fn):
-        """Sends a client the contents of a single submission."""
+    def download_single_file(filesystem_id, fn):
+        """Sends a client the contents of a single file, either a submission
+        or a journalist reply"""
         if '..' in fn or fn.startswith('/'):
             abort(404)
 
-        try:
-            Submission.query.filter(
-                Submission.filename == fn).one().downloaded = True
-            db.session.commit()
-        except NoResultFound as e:
-            current_app.logger.error(
-                "Could not mark " + fn + " as downloaded: %s" % (e,))
+        # only mark as read when it's a submission (and not a journalist reply)
+        if not fn.endswith('reply.gpg'):
+            try:
+                Submission.query.filter(
+                    Submission.filename == fn).one().downloaded = True
+                db.session.commit()
+            except NoResultFound as e:
+                current_app.logger.error(
+                    "Could not mark " + fn + " as downloaded: %s" % (e,))
 
         return send_file(current_app.storage.path(filesystem_id, fn),
                          mimetype="application/pgp-encrypted")


Test patch:
diff --git a/securedrop/tests/test_journalist.py b/securedrop/tests/test_journalist.py
--- a/securedrop/tests/test_journalist.py
+++ b/securedrop/tests/test_journalist.py
@@ -1259,7 +1259,7 @@ def test_admin_page_restriction_http_posts(journalist_app, test_journo):
 
 def test_user_authorization_for_gets(journalist_app):
     urls = [url_for('main.index'), url_for('col.col', filesystem_id='1'),
-            url_for('col.download_single_submission',
+            url_for('col.download_single_file',
                     filesystem_id='1', fn='1'),
             url_for('account.edit')]
 
@@ -1344,6 +1344,26 @@ def test_passphrase_migration_on_reset(journalist_app):
     assert journalist.valid_password(VALID_PASSWORD)
 
 
+def test_journalist_reply_view(journalist_app, test_source, test_journo):
+    source, _ = utils.db_helper.init_source()
+    journalist, _ = utils.db_helper.init_journalist()
+    submissions = utils.db_helper.submit(source, 1)
+    replies = utils.db_helper.reply(journalist, source, 1)
+
+    subm_url = url_for('col.download_single_file',
+                       filesystem_id=submissions[0].source.filesystem_id,
+                       fn=submissions[0].filename)
+    reply_url = url_for('col.download_single_file',
+                        filesystem_id=replies[0].source.filesystem_id,
+                        fn=replies[0].filename)
+
+    with journalist_app.test_client() as app:
+        resp = app.get(subm_url)
+        assert resp.status_code == 302
+        resp = app.get(reply_url)
+        assert resp.status_code == 302
+
+
 class TestJournalistApp(TestCase):
 
     # A method required by flask_testing.TestCase