Repository: freedomofpress/securedrop

Issue:
Returning "flagged for reply" sources generate an error
When a source is flagged for a reply.
And the source returns to the site after initial session is ended. 
It generates the error below.

```
[Mon Dec 02 21:49:44 2013] [error] ERROR:source:Exception on / [GET]
[Mon Dec 02 21:49:44 2013] [error] Traceback (most recent call last):
[Mon Dec 02 21:49:44 2013] [error]   File
"/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1817, in
wsgi_app
[Mon Dec 02 21:49:44 2013] [error]     response =
self.full_dispatch_request()
[Mon Dec 02 21:49:44 2013] [error]   File
"/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1477, in
full_dispatch_request
[Mon Dec 02 21:49:44 2013] [error]     rv = self.handle_user_exception(e)
[Mon Dec 02 21:49:44 2013] [error]   File
"/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1381, in
handle_user_exception
[Mon Dec 02 21:49:44 2013] [error]     reraise(exc_type, exc_value, tb)
[Mon Dec 02 21:49:44 2013] [error]   File
"/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1473, in
full_dispatch_request
[Mon Dec 02 21:49:44 2013] [error]     rv = self.preprocess_request()
[Mon Dec 02 21:49:44 2013] [error]   File
"/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1666, in
preprocess_request
[Mon Dec 02 21:49:44 2013] [error]     rv = func()
[Mon Dec 02 21:49:44 2013] [error]   File
"/var/www/securedrop/source.py", line 52, in decorated_function
[Mon Dec 02 21:49:44 2013] [error]     return f(*args, **kwargs)
[Mon Dec 02 21:49:44 2013] [error]   File
"/var/www/securedrop/source.py", line 64, in setup_g
[Mon Dec 02 21:49:44 2013] [error]     g.flagged = session['flagged']
[Mon Dec 02 21:49:44 2013] [error]   File
"/usr/local/lib/python2.7/dist-packages/werkzeug/local.py", line 368, in
<lambda>
[Mon Dec 02 21:49:44 2013] [error]     __getitem__ = lambda x, i:
x._get_current_object()[i]
[Mon Dec 02 21:49:44 2013] [error] KeyError: 'flagged'
```



Discussion:
I'll dig into this later tonight when I have time.


Resolving patch:
diff --git a/securedrop/source.py b/securedrop/source.py
--- a/securedrop/source.py
+++ b/securedrop/source.py
@@ -61,7 +61,9 @@ def setup_g():
     # and we don't need to waste time running if we're just serving a static
     # resource that won't need to access these common values.
     if logged_in():
-        g.flagged = session['flagged']
+        # We use session.get (which defaults to None if 'flagged' is not in the
+        # session) to avoid a KeyError on the redirect from login/ to lookup/
+        g.flagged = session.get('flagged')
         g.codename = session['codename']
         g.sid = crypto_util.shash(g.codename)
         g.loc = store.path(g.sid)
@@ -128,6 +130,8 @@ def lookup():
     msgs = []
     flagged = False
     for fn in os.listdir(g.loc):
+        # TODO: make 'flag' a db column, so we can replace this with a db
+        # lookup in the future
         if fn == '_FLAG':
             flagged = True
             continue


Test patch:
diff --git a/securedrop/test.py b/securedrop/test.py
--- a/securedrop/test.py
+++ b/securedrop/test.py
@@ -40,6 +40,15 @@ def _setup_test_docs(sid, files):
             fp.write(str(uuid.uuid4()))
     return filenames
 
+def _logout(app):
+    # See http://flask.pocoo.org/docs/testing/#accessing-and-modifying-sessions
+    # This is necessary because SecureDrop doesn't have a logout button, so a
+    # user is logged in until they close the browser, which clears the session.
+    # For testing, this function simulates closing the browser at places
+    # where a source is likely to do so (for instance, between submitting a
+    # document and checking for a journalist reply).
+    with app.session_transaction() as sess:
+        sess.clear()
 
 def shared_setup():
     """Set up the file system, GPG, and database"""
@@ -157,21 +166,27 @@ def test_lookup(self):
         rv = self.client.get('journalist-key')
         self.assertIn("BEGIN PGP PUBLIC KEY BLOCK", rv.data)
 
-    def test_login(self):
+    def test_login_and_logout(self):
         rv = self.client.get('/login')
         self.assert200(rv)
         self.assertIn("Already submitted something?", rv.data)
 
         codename = self._new_codename()
-        rv = self.client.post('/login', data=dict(codename=codename),
-                              follow_redirects=True)
-        self.assert200(rv)
-        self.assertIn("Submit a document, message, or both", rv.data)
+        with self.client as c:
+            rv = c.post('/login', data=dict(codename=codename),
+                                  follow_redirects=True)
+            self.assert200(rv)
+            self.assertIn("Submit a document, message, or both", rv.data)
+            self.assertTrue(session['logged_in'])
+            _logout(c)
+            self.assertEquals(len(session), 0)
 
-        rv = self.client.post('/login', data=dict(codename='invalid'),
-                              follow_redirects=True)
-        self.assert200(rv)
-        self.assertIn('Sorry, that is not a recognized codename.', rv.data)
+        with self.client as c:
+            rv = self.client.post('/login', data=dict(codename='invalid'),
+                                  follow_redirects=True)
+            self.assert200(rv)
+            self.assertIn('Sorry, that is not a recognized codename.', rv.data)
+            self.assertNotIn('logged_in', session)
 
     def test_submit_message(self):
         codename = self._new_codename()
@@ -262,12 +277,13 @@ def test_submit_message(self):
             rv = source_app.post('/create', follow_redirects=True)
             codename = session['codename']
             sid = g.sid
-        # redirected to submission form
-        rv = self.source_app.post('/submit', data=dict(
-            msg=test_msg,
-            fh=(StringIO(''), ''),
-        ), follow_redirects=True)
-        self.assertEqual(rv.status_code, 200)
+            # redirected to submission form
+            rv = self.source_app.post('/submit', data=dict(
+                msg=test_msg,
+                fh=(StringIO(''), ''),
+            ), follow_redirects=True)
+            self.assertEqual(rv.status_code, 200)
+            _logout(source_app)
 
         rv = self.journalist_app.get('/')
         self.assertEqual(rv.status_code, 200)
@@ -334,12 +350,13 @@ def test_submit_file(self):
             rv = source_app.post('/create', follow_redirects=True)
             codename = session['codename']
             sid = g.sid
-        # redirected to submission form
-        rv = self.source_app.post('/submit', data=dict(
-            msg="",
-            fh=(StringIO(test_file_contents), test_filename),
-        ), follow_redirects=True)
-        self.assertEqual(rv.status_code, 200)
+            # redirected to submission form
+            rv = self.source_app.post('/submit', data=dict(
+                msg="",
+                fh=(StringIO(test_file_contents), test_filename),
+            ), follow_redirects=True)
+            self.assertEqual(rv.status_code, 200)
+            _logout(source_app)
 
         rv = self.journalist_app.get('/')
         self.assertEqual(rv.status_code, 200)
@@ -412,13 +429,14 @@ def test_reply(self):
             codename = session['codename']
             flagged = session['flagged']
             sid = g.sid
-        # redirected to submission form
-        rv = self.source_app.post('/submit', data=dict(
-            msg=test_msg,
-            fh=(StringIO(''), ''),
-        ), follow_redirects=True)
-        self.assertEqual(rv.status_code, 200)
-        self.assertFalse(flagged)
+            # redirected to submission form
+            rv = source_app.post('/submit', data=dict(
+                msg=test_msg,
+                fh=(StringIO(''), ''),
+            ), follow_redirects=True)
+            self.assertEqual(rv.status_code, 200)
+            self.assertFalse(flagged)
+            _logout(source_app)
 
         rv = self.journalist_app.get('/')
         self.assertEqual(rv.status_code, 200)
@@ -433,11 +451,13 @@ def test_reply(self):
             rv = source_app.post('/login', data=dict(
                 codename=codename), follow_redirects=True)
             self.assertEqual(rv.status_code, 200)
-            self.assertFalse(session['flagged'])
+            _logout(source_app)
 
-        rv = self.journalist_app.post('/flag', data=dict(
-            sid=sid))
-        self.assertEqual(rv.status_code, 200)
+        with self.journalist_app as journalist_app:
+            rv = journalist_app.post('/flag', data=dict(
+                sid=sid))
+            self.assertEqual(rv.status_code, 200)
+            _logout(journalist_app)
 
         with self.source_app as source_app:
             rv = source_app.post('/login', data=dict(
@@ -446,6 +466,7 @@ def test_reply(self):
             self.assertTrue(session['flagged'])
             source_app.get('/lookup')
             self.assertTrue(g.flagged)
+            _logout(source_app)
 
         # Block until the reply keypair has been generated, so we can test
         # sending a reply
@@ -458,24 +479,30 @@ def test_reply(self):
         self.assertEqual(rv.status_code, 200)
         self.assertIn("Thanks! Your reply has been stored.", rv.data)
 
-        rv = self.journalist_app.get(col_url)
-        self.assertIn("reply-", rv.data)
+        with self.journalist_app as journalist_app:
+            rv = journalist_app.get(col_url)
+            self.assertIn("reply-", rv.data)
+            _logout(journalist_app)
 
         _block_on_reply_keypair_gen(codename)
-        rv = self.source_app.get('/lookup')
-        self.assertEqual(rv.status_code, 200)
-        self.assertIn(
-            "You have received a reply. For your security, please delete all replies when you're done with them.", rv.data)
-        self.assertIn(test_reply, rv.data)
-
-        soup = BeautifulSoup(rv.data)
-        msgid = soup.select('form.message > input[name="msgid"]')[0]['value']
-        rv = self.source_app.post('/delete', data=dict(
-            sid=sid,
-            msgid=msgid,
-        ), follow_redirects=True)
-        self.assertEqual(rv.status_code, 200)
-        self.assertIn("Reply deleted", rv.data)
+        with self.source_app as source_app:
+            rv = source_app.post('/login', data=dict(codename=codename), follow_redirects=True)
+            self.assertEqual(rv.status_code, 200)
+            rv = source_app.get('/lookup')
+            self.assertEqual(rv.status_code, 200)
+            self.assertIn(
+                "You have received a reply. For your security, please delete all replies when you're done with them.", rv.data)
+            self.assertIn(test_reply, rv.data)
+
+            soup = BeautifulSoup(rv.data)
+            msgid = soup.select('form.message > input[name="msgid"]')[0]['value']
+            rv = source_app.post('/delete', data=dict(
+                sid=sid,
+                msgid=msgid,
+            ), follow_redirects=True)
+            self.assertEqual(rv.status_code, 200)
+            self.assertIn("Reply deleted", rv.data)
+            _logout(source_app)
 
 
 class TestStore(unittest.TestCase):