Repository: freedomofpress/securedrop

Issue:
Resolve inconsistently named and confusing source IDs
## Description

When working with the SecureDrop application code, there are multiple IDs referred to as the `source_id`:

* the filesystem ID (the string used to refer to sources on disk)
  * e.g. referred to as `source_id` in `delete_collection()` in `journalist.py`
* the source ID (primary key of the source in the SecureDrop database)
  * e.g. referred to as `source_id` in `index()` in `journalist.py`
* the `sid` - it is unclear what this is on first inspection (it's the filesystem ID but these instances of `sid` should be replaced with which column this is referring to)

These are inconsistently named across the application and this is very confusing for new developers. 

## Fix

We should resolve this confusion by using these variables in a consistent manner.

There should be two IDs corresponding to the two columns in the `sources` table:

* `filesystem_id`: replace instances of `sid` and replace any `source_id`s that actually refer to the `filesystem_id`
* `source_id`: this should _always_ refer to the primary key of the source in the `sources` table



Discussion:
:+1: it got me confused when reading the code :-)

Resolving patch:
diff --git a/securedrop/journalist.py b/securedrop/journalist.py
--- a/securedrop/journalist.py
+++ b/securedrop/journalist.py
@@ -46,11 +46,11 @@ def shutdown_session(exception=None):
     db_session.remove()
 
 
-def get_source(sid):
+def get_source(filesystem_id):
     """Return a Source object, representing the database row, for the source
-    with id `sid`"""
+    with the `filesystem_id`"""
     source = None
-    query = Source.query.filter(Source.filesystem_id == sid)
+    query = Source.query.filter(Source.filesystem_id == filesystem_id)
     source = get_one_or_else(query, app.logger, abort)
 
     return source
@@ -64,10 +64,10 @@ def setup_g():
         g.user = Journalist.query.get(uid)
 
     if request.method == 'POST':
-        sid = request.form.get('sid')
-        if sid:
-            g.sid = sid
-            g.source = get_source(sid)
+        filesystem_id = request.form.get('filesystem_id')
+        if filesystem_id:
+            g.filesystem_id = filesystem_id
+            g.source = get_source(filesystem_id)
 
 
 def logged_in():
@@ -446,8 +446,8 @@ def account_reset_two_factor_hotp():
         return render_template('account_edit_hotp_secret.html')
 
 
-def make_star_true(sid):
-    source = get_source(sid)
+def make_star_true(filesystem_id):
+    source = get_source(filesystem_id)
     if source.star:
         source.star.starred = True
     else:
@@ -455,8 +455,8 @@ def make_star_true(sid):
         db_session.add(source_star)
 
 
-def make_star_false(sid):
-    source = get_source(sid)
+def make_star_false(filesystem_id):
+    source = get_source(filesystem_id)
     if not source.star:
         source_star = SourceStar(source)
         db_session.add(source_star)
@@ -464,18 +464,18 @@ def make_star_false(sid):
     source.star.starred = False
 
 
-@app.route('/col/add_star/<sid>', methods=('POST',))
+@app.route('/col/add_star/<filesystem_id>', methods=('POST',))
 @login_required
-def add_star(sid):
-    make_star_true(sid)
+def add_star(filesystem_id):
+    make_star_true(filesystem_id)
     db_session.commit()
     return redirect(url_for('index'))
 
 
-@app.route("/col/remove_star/<sid>", methods=('POST',))
+@app.route("/col/remove_star/<filesystem_id>", methods=('POST',))
 @login_required
-def remove_star(sid):
-    make_star_false(sid)
+def remove_star(filesystem_id):
+    make_star_false(filesystem_id)
     db_session.commit()
     return redirect(url_for('index'))
 
@@ -505,23 +505,24 @@ def index():
     return render_template('index.html', unstarred=unstarred, starred=starred)
 
 
-@app.route('/col/<sid>')
+@app.route('/col/<filesystem_id>')
 @login_required
-def col(sid):
-    source = get_source(sid)
-    source.has_key = crypto_util.getkey(sid)
-    return render_template("col.html", sid=sid, source=source)
+def col(filesystem_id):
+    source = get_source(filesystem_id)
+    source.has_key = crypto_util.getkey(filesystem_id)
+    return render_template("col.html", filesystem_id=filesystem_id,
+                           source=source)
 
 
-def delete_collection(source_id):
+def delete_collection(filesystem_id):
     # Delete the source's collection of submissions
-    job = worker.enqueue(store.delete_source_directory, source_id)
+    job = worker.enqueue(store.delete_source_directory, filesystem_id)
 
     # Delete the source's reply keypair
-    crypto_util.delete_reply_keypair(source_id)
+    crypto_util.delete_reply_keypair(filesystem_id)
 
     # Delete their entry in the db
-    source = get_source(source_id)
+    source = get_source(filesystem_id)
     db_session.delete(source)
     db_session.commit()
     return job
@@ -551,8 +552,9 @@ def col_process():
 def col_download_unread(cols_selected):
     """Download all unread submissions from all selected sources."""
     submissions = []
-    for sid in cols_selected:
-        id = Source.query.filter(Source.filesystem_id == sid).one().id
+    for filesystem_id in cols_selected:
+        id = Source.query.filter(Source.filesystem_id == filesystem_id) \
+                   .one().id
         submissions += Submission.query.filter(
             Submission.downloaded == false(),
             Submission.source_id == id).all()
@@ -565,35 +567,36 @@ def col_download_unread(cols_selected):
 def col_download_all(cols_selected):
     """Download all submissions from all selected sources."""
     submissions = []
-    for sid in cols_selected:
-        id = Source.query.filter(Source.filesystem_id == sid).one().id
+    for filesystem_id in cols_selected:
+        id = Source.query.filter(Source.filesystem_id == filesystem_id) \
+                   .one().id
         submissions += Submission.query.filter(
             Submission.source_id == id).all()
     return download("all", submissions)
 
 
 def col_star(cols_selected):
-    for sid in cols_selected:
-        make_star_true(sid)
+    for filesystem_id in cols_selected:
+        make_star_true(filesystem_id)
 
     db_session.commit()
     return redirect(url_for('index'))
 
 
 def col_un_star(cols_selected):
-    for source_id in cols_selected:
-        make_star_false(source_id)
+    for filesystem_id in cols_selected:
+        make_star_false(filesystem_id)
 
     db_session.commit()
     return redirect(url_for('index'))
 
 
-@app.route('/col/delete/<sid>', methods=('POST',))
+@app.route('/col/delete/<filesystem_id>', methods=('POST',))
 @login_required
-def col_delete_single(sid):
+def col_delete_single(filesystem_id):
     """deleting a single collection from its /col page"""
-    source = get_source(sid)
-    delete_collection(sid)
+    source = get_source(filesystem_id)
+    delete_collection(filesystem_id)
     flash(
         "%s's collection deleted" %
         (source.journalist_designation,), "notification")
@@ -605,8 +608,8 @@ def col_delete(cols_selected):
     if len(cols_selected) < 1:
         flash("No collections selected to delete!", "error")
     else:
-        for source_id in cols_selected:
-            delete_collection(source_id)
+        for filesystem_id in cols_selected:
+            delete_collection(filesystem_id)
         flash("%s %s deleted" % (
             len(cols_selected),
             "collection" if len(cols_selected) == 1 else "collections"
@@ -615,9 +618,9 @@ def col_delete(cols_selected):
     return redirect(url_for('index'))
 
 
-@app.route('/col/<sid>/<fn>')
+@app.route('/col/<filesystem_id>/<fn>')
 @login_required
-def download_single_submission(sid, fn):
+def download_single_submission(filesystem_id, fn):
     """Sends a client the contents of a single submission."""
     if '..' in fn or fn.startswith('/'):
         abort(404)
@@ -629,7 +632,8 @@ def download_single_submission(sid, fn):
     except NoResultFound as e:
         app.logger.error("Could not mark " + fn + " as downloaded: %s" % (e,))
 
-    return send_file(store.path(sid, fn), mimetype="application/pgp-encrypted")
+    return send_file(store.path(filesystem_id, fn),
+                     mimetype="application/pgp-encrypted")
 
 
 @app.route('/reply', methods=('POST',))
@@ -652,14 +656,15 @@ def reply():
     # Reject empty replies
     if not msg:
         flash("You cannot send an empty reply!", "error")
-        return redirect(url_for('col', sid=g.sid))
+        return redirect(url_for('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)
     crypto_util.encrypt(msg,
-                        [crypto_util.getkey(g.sid), config.JOURNALIST_KEY],
-                        output=store.path(g.sid, filename))
+                        [crypto_util.getkey(g.filesystem_id),
+                         config.JOURNALIST_KEY],
+                        output=store.path(g.filesystem_id, filename))
     reply = Reply(g.user, g.source, filename)
 
     try:
@@ -678,7 +683,7 @@ def reply():
     else:
         flash("Thanks! Your reply has been stored.", "notification")
     finally:
-        return redirect(url_for('col', sid=g.sid))
+        return redirect(url_for('col', filesystem_id=g.filesystem_id))
 
 
 @app.route('/regenerate-code', methods=('POST',))
@@ -689,7 +694,7 @@ def generate_code():
 
     for item in g.source.collection:
         item.filename = store.rename_submission(
-            g.sid,
+            g.filesystem_id,
             item.filename,
             g.source.journalist_filename)
     db_session.commit()
@@ -699,20 +704,20 @@ def generate_code():
         (original_journalist_designation,
          g.source.journalist_designation),
         "notification")
-    return redirect('/col/' + g.sid)
+    return redirect('/col/' + g.filesystem_id)
 
 
-@app.route('/download_unread/<sid>')
+@app.route('/download_unread/<filesystem_id>')
 @login_required
-def download_unread_sid(sid):
-    id = Source.query.filter(Source.filesystem_id == sid).one().id
+def download_unread_filesystem_id(filesystem_id):
+    id = Source.query.filter(Source.filesystem_id == filesystem_id).one().id
     submissions = Submission.query.filter(
         Submission.source_id == id,
         Submission.downloaded == false()).all()
     if submissions == []:
         flash("No unread submissions for this source!")
-        return redirect(url_for('col', sid=sid))
-    source = get_source(sid)
+        return redirect(url_for('col', filesystem_id=filesystem_id))
+    source = get_source(filesystem_id)
     return download(source.journalist_filename, submissions)
 
 
@@ -729,29 +734,29 @@ def bulk():
             flash("No collections selected to download!", "error")
         elif action in ('delete', 'confirm_delete'):
             flash("No collections selected to delete!", "error")
-        return redirect(url_for('col', sid=g.sid))
+        return redirect(url_for('col', filesystem_id=g.filesystem_id))
 
     if action == 'download':
-        source = get_source(g.sid)
+        source = get_source(g.filesystem_id)
         return download(source.journalist_filename, selected_docs)
     elif action == 'delete':
-        return bulk_delete(g.sid, selected_docs)
+        return bulk_delete(g.filesystem_id, selected_docs)
     elif action == 'confirm_delete':
-        return confirm_bulk_delete(g.sid, selected_docs)
+        return confirm_bulk_delete(g.filesystem_id, selected_docs)
     else:
         abort(400)
 
 
-def confirm_bulk_delete(sid, items_selected):
+def confirm_bulk_delete(filesystem_id, items_selected):
     return render_template('delete.html',
-                           sid=sid,
+                           filesystem_id=filesystem_id,
                            source=g.source,
                            items_selected=items_selected)
 
 
-def bulk_delete(sid, items_selected):
+def bulk_delete(filesystem_id, items_selected):
     for item in items_selected:
-        item_path = store.path(sid, item.filename)
+        item_path = store.path(filesystem_id, item.filename)
         worker.enqueue(store.secure_unlink, item_path)
         db_session.delete(item)
     db_session.commit()
@@ -760,7 +765,7 @@ def bulk_delete(sid, items_selected):
         "Submission{} deleted.".format(
             "s" if len(items_selected) > 1 else ""),
         "notification")
-    return redirect(url_for('col', sid=sid))
+    return redirect(url_for('col', filesystem_id=filesystem_id))
 
 
 def download(zip_basename, submissions):
@@ -794,7 +799,7 @@ def download(zip_basename, submissions):
 def flag():
     g.source.flagged = True
     db_session.commit()
-    return render_template('flag.html', sid=g.sid,
+    return render_template('flag.html', filesystem_id=g.filesystem_id,
                            codename=g.source.journalist_designation)
 
 
diff --git a/securedrop/source.py b/securedrop/source.py
--- a/securedrop/source.py
+++ b/securedrop/source.py
@@ -92,9 +92,11 @@ def setup_g():
     # serving a static resource that won't need to access these common values.
     if logged_in():
         g.codename = session['codename']
-        g.sid = crypto_util.hash_codename(g.codename)
+        g.filesystem_id = crypto_util.hash_codename(g.codename)
         try:
-            g.source = Source.query.filter(Source.filesystem_id == g.sid).one()
+            g.source = Source.query \
+                             .filter(Source.filesystem_id == g.filesystem_id) \
+                             .one()
         except MultipleResultsFound as e:
             app.logger.error(
                 "Found multiple Sources when one was expected: %s" %
@@ -107,7 +109,7 @@ def setup_g():
             del session['logged_in']
             del session['codename']
             return redirect(url_for('index'))
-        g.loc = store.path(g.sid)
+        g.loc = store.path(g.filesystem_id)
 
 
 @app.before_request
@@ -145,9 +147,9 @@ def generate_unique_codename():
                     "(Codename='{}')".format(codename))
             continue
 
-        sid = crypto_util.hash_codename(codename)  # scrypt (slow)
+        filesystem_id = crypto_util.hash_codename(codename)  # scrypt (slow)
         matching_sources = Source.query.filter(
-            Source.filesystem_id == sid).all()
+            Source.filesystem_id == filesystem_id).all()
         if len(matching_sources) == 0:
             return codename
 
@@ -167,9 +169,9 @@ def generate():
 
 @app.route('/create', methods=['POST'])
 def create():
-    sid = crypto_util.hash_codename(session['codename'])
+    filesystem_id = crypto_util.hash_codename(session['codename'])
 
-    source = Source(sid, crypto_util.display_id())
+    source = Source(filesystem_id, crypto_util.display_id())
     db_session.add(source)
     try:
         db_session.commit()
@@ -178,7 +180,7 @@ def create():
             "Attempt to create a source with duplicate codename: %s" %
             (e,))
     else:
-        os.mkdir(store.path(sid))
+        os.mkdir(store.path(filesystem_id))
 
     session['logged_in'] = True
     return redirect(url_for('lookup'))
@@ -192,18 +194,20 @@ def wrapper(*args, **kwargs):
 
 
 @async
-def async_genkey(sid, codename):
-    crypto_util.genkeypair(sid, codename)
+def async_genkey(filesystem_id, codename):
+    crypto_util.genkeypair(filesystem_id, codename)
 
     # Register key generation as update to the source, so sources will
     # filter to the top of the list in the journalist interface if a
     # flagged source logs in and has a key generated for them. #789
     try:
-        source = Source.query.filter(Source.filesystem_id == sid).one()
+        source = Source.query.filter(Source.filesystem_id == filesystem_id) \
+                       .one()
         source.last_updated = datetime.utcnow()
         db_session.commit()
     except Exception as e:
-        app.logger.error("async_genkey for source (sid={}): {}".format(sid, e))
+        app.logger.error("async_genkey for source "
+                         "(filesystem_id={}): {}".format(filesystem_id, e))
 
 
 @app.route('/lookup', methods=('GET',))
@@ -211,7 +215,7 @@ def async_genkey(sid, codename):
 def lookup():
     replies = []
     for reply in g.source.replies:
-        reply_path = store.path(g.sid, reply.filename)
+        reply_path = store.path(g.filesystem_id, reply.filename)
         try:
             reply.decrypted = crypto_util.decrypt(
                 g.codename,
@@ -229,8 +233,8 @@ def lookup():
     # Generate a keypair to encrypt replies from the journalist
     # Only do this if the journalist has flagged the source as one
     # that they would like to reply to. (Issue #140.)
-    if not crypto_util.getkey(g.sid) and g.source.flagged:
-        async_genkey(g.sid, g.codename)
+    if not crypto_util.getkey(g.filesystem_id) and g.source.flagged:
+        async_genkey(g.filesystem_id, g.codename)
 
     return render_template(
         'lookup.html',
@@ -238,16 +242,16 @@ def lookup():
         replies=replies,
         flagged=g.source.flagged,
         haskey=crypto_util.getkey(
-            g.sid))
+            g.filesystem_id))
 
 
-def normalize_timestamps(sid):
+def normalize_timestamps(filesystem_id):
     """
     Update the timestamps on all of the source's submissions to match that of
     the latest submission. This minimizes metadata that could be useful to
     investigators. See #301.
     """
-    sub_paths = [store.path(sid, submission.filename)
+    sub_paths = [store.path(filesystem_id, submission.filename)
                  for submission in g.source.submissions]
     if len(sub_paths) > 1:
         args = ["touch"]
@@ -279,7 +283,7 @@ def submit():
         g.source.interaction_count += 1
         fnames.append(
             store.save_message_submission(
-                g.sid,
+                g.filesystem_id,
                 g.source.interaction_count,
                 journalist_filename,
                 msg))
@@ -287,7 +291,7 @@ def submit():
         g.source.interaction_count += 1
         fnames.append(
             store.save_file_submission(
-                g.sid,
+                g.filesystem_id,
                 g.source.interaction_count,
                 journalist_filename,
                 fh.filename,
@@ -320,11 +324,11 @@ def submit():
         entropy_avail = int(
             open('/proc/sys/kernel/random/entropy_avail').read())
         if entropy_avail >= 2400:
-            async_genkey(g.sid, g.codename)
+            async_genkey(g.filesystem_id, g.codename)
 
     g.source.last_updated = datetime.utcnow()
     db_session.commit()
-    normalize_timestamps(g.sid)
+    normalize_timestamps(g.filesystem_id)
 
     return redirect(url_for('lookup'))
 
@@ -335,7 +339,7 @@ def delete():
     query = Reply.query.filter(
         Reply.filename == request.form['reply_filename'])
     reply = get_one_or_else(query, app.logger, abort)
-    store.secure_unlink(store.path(g.sid, reply.filename))
+    store.secure_unlink(store.path(g.filesystem_id, reply.filename))
     db_session.delete(reply)
     db_session.commit()
 
@@ -351,7 +355,7 @@ def batch_delete():
         app.logger.error("Found no replies when at least one was expected")
         return redirect(url_for('lookup'))
     for reply in replies:
-        store.secure_unlink(store.path(g.sid, reply.filename))
+        store.secure_unlink(store.path(g.filesystem_id, reply.filename))
         db_session.delete(reply)
     db_session.commit()
 
diff --git a/securedrop/store.py b/securedrop/store.py
--- a/securedrop/store.py
+++ b/securedrop/store.py
@@ -95,7 +95,8 @@ def get_bulk_archive(selected_submissions, zip_directory=''):
     return zip_file
 
 
-def save_file_submission(sid, count, journalist_filename, filename, stream):
+def save_file_submission(filesystem_id, count, journalist_filename, filename,
+                         stream):
     sanitized_filename = secure_filename(filename)
 
     # We store file submissions in a .gz file for two reasons:
@@ -114,7 +115,7 @@ def save_file_submission(sid, count, journalist_filename, filename, stream):
     encrypted_file_name = "{0}-{1}-doc.gz.gpg".format(
         count,
         journalist_filename)
-    encrypted_file_path = path(sid, encrypted_file_name)
+    encrypted_file_path = path(filesystem_id, encrypted_file_name)
     with SecureTemporaryFile("/tmp") as stf:
         with gzip.GzipFile(filename=sanitized_filename,
                            mode='wb', fileobj=stf) as gzf:
@@ -131,14 +132,15 @@ def save_file_submission(sid, count, journalist_filename, filename, stream):
     return encrypted_file_name
 
 
-def save_message_submission(sid, count, journalist_filename, message):
+def save_message_submission(filesystem_id, count, journalist_filename,
+                            message):
     filename = "{0}-{1}-msg.gpg".format(count, journalist_filename)
-    msg_loc = path(sid, filename)
+    msg_loc = path(filesystem_id, filename)
     crypto_util.encrypt(message, config.JOURNALIST_KEY, msg_loc)
     return filename
 
 
-def rename_submission(sid, orig_filename, journalist_filename):
+def rename_submission(filesystem_id, orig_filename, journalist_filename):
     check_submission_name = VALIDATE_FILENAME(orig_filename)
     if check_submission_name:
         parsed_filename = check_submission_name.groupdict()
@@ -147,7 +149,8 @@ def rename_submission(sid, orig_filename, journalist_filename):
                 parsed_filename['index'], journalist_filename,
                 parsed_filename['file_type'])
             try:
-                os.rename(path(sid, orig_filename), path(sid, new_filename))
+                os.rename(path(filesystem_id, orig_filename),
+                          path(filesystem_id, new_filename))
             except OSError:
                 pass
             else:
@@ -165,6 +168,6 @@ def secure_unlink(fn, recursive=False):
     return "success"
 
 
-def delete_source_directory(source_id):
-    secure_unlink(path(source_id), recursive=True)
+def delete_source_directory(filesystem_id):
+    secure_unlink(path(filesystem_id), recursive=True)
     return "success"


Test patch:
diff --git a/securedrop/tests/test_integration.py b/securedrop/tests/test_integration.py
--- a/securedrop/tests/test_integration.py
+++ b/securedrop/tests/test_integration.py
@@ -66,7 +66,7 @@ def test_submit_message(self):
         with self.source_app as source_app:
             resp = source_app.get('/generate')
             resp = source_app.post('/create', follow_redirects=True)
-            sid = g.sid
+            filesystem_id = g.filesystem_id
             # redirected to submission form
             resp = self.source_app.post('/submit', data=dict(
                 msg=test_msg,
@@ -109,7 +109,7 @@ def test_submit_message(self):
             'ul > li > input[name="doc_names_selected"]')[0]['value']
         resp = self.journalist_app.post('/bulk', data=dict(
             action='confirm_delete',
-            sid=sid,
+            filesystem_id=filesystem_id,
             doc_names_selected=doc_name
         ))
 
@@ -123,7 +123,7 @@ def test_submit_message(self):
             'ul > li > input[name="doc_names_selected"]')[0]['value']
         resp = self.journalist_app.post('/bulk', data=dict(
             action='delete',
-            sid=sid,
+            filesystem_id=filesystem_id,
             doc_names_selected=doc_name,
         ), follow_redirects=True)
         self.assertEqual(resp.status_code, 200)
@@ -139,7 +139,9 @@ def test_submit_message(self):
         # since file deletion is handled by a polling worker, this test needs
         # to wait for the worker to get the job and execute it
         utils.async.wait_for_assertion(
-            lambda: self.assertFalse(os.path.exists(store.path(sid, doc_name)))
+            lambda: self.assertFalse(
+                os.path.exists(store.path(filesystem_id, doc_name))
+            )
         )
 
     def test_submit_file(self):
@@ -151,7 +153,7 @@ def test_submit_file(self):
         with self.source_app as source_app:
             resp = source_app.get('/generate')
             resp = source_app.post('/create', follow_redirects=True)
-            sid = g.sid
+            filesystem_id = g.filesystem_id
             # redirected to submission form
             resp = self.source_app.post('/submit', data=dict(
                 msg="",
@@ -197,7 +199,7 @@ def test_submit_file(self):
             'ul > li > input[name="doc_names_selected"]')[0]['value']
         resp = self.journalist_app.post('/bulk', data=dict(
             action='confirm_delete',
-            sid=sid,
+            filesystem_id=filesystem_id,
             doc_names_selected=doc_name
         ))
 
@@ -211,7 +213,7 @@ def test_submit_file(self):
             'ul > li > input[name="doc_names_selected"]')[0]['value']
         resp = self.journalist_app.post('/bulk', data=dict(
             action='delete',
-            sid=sid,
+            filesystem_id=filesystem_id,
             doc_names_selected=doc_name,
         ), follow_redirects=True)
         self.assertEqual(resp.status_code, 200)
@@ -227,7 +229,9 @@ def test_submit_file(self):
         # since file deletion is handled by a polling worker, this test needs
         # to wait for the worker to get the job and execute it
         utils.async.wait_for_assertion(
-            lambda: self.assertFalse(os.path.exists(store.path(sid, doc_name)))
+            lambda: self.assertFalse(
+                os.path.exists(store.path(filesystem_id, doc_name))
+            )
         )
 
     def test_reply_normal(self):
@@ -290,7 +294,7 @@ def helper_test_reply(self, test_reply, expected_success=True):
             resp = source_app.get('/generate')
             resp = source_app.post('/create', follow_redirects=True)
             codename = session['codename']
-            sid = g.sid
+            filesystem_id = g.filesystem_id
             # redirected to submission form
             resp = source_app.post('/submit', data=dict(
                 msg=test_msg,
@@ -318,7 +322,7 @@ def helper_test_reply(self, test_reply, expected_success=True):
 
         with self.journalist_app as journalist_app:
             resp = journalist_app.post('/flag', data=dict(
-                sid=sid))
+                filesystem_id=filesystem_id))
             self.assertEqual(resp.status_code, 200)
 
         with self.source_app as source_app:
@@ -332,12 +336,14 @@ def helper_test_reply(self, test_reply, expected_success=True):
 
         # Block up to 15s for the reply keypair, so we can test sending a reply
         utils.async.wait_for_assertion(
-            lambda: self.assertNotEqual(crypto_util.getkey(sid), None), 15)
+            lambda: self.assertNotEqual(crypto_util.getkey(filesystem_id),
+                                        None),
+            15)
 
         # Create 2 replies to test deleting on journalist and source interface
         for i in range(2):
             resp = self.journalist_app.post('/reply', data=dict(
-                sid=sid,
+                filesystem_id=filesystem_id,
                 msg=test_reply
             ), follow_redirects=True)
             self.assertEqual(resp.status_code, 200)
@@ -355,11 +361,11 @@ def helper_test_reply(self, test_reply, expected_success=True):
 
         # Download the reply and verify that it can be decrypted with the
         # journalist's key as well as the source's reply key
-        sid = soup.select('input[name="sid"]')[0]['value']
+        filesystem_id = soup.select('input[name="filesystem_id"]')[0]['value']
         checkbox_values = [
             soup.select('input[name="doc_names_selected"]')[1]['value']]
         resp = self.journalist_app.post('/bulk', data=dict(
-            sid=sid,
+            filesystem_id=filesystem_id,
             action='download',
             doc_names_selected=checkbox_values
         ), follow_redirects=True)
@@ -368,7 +374,8 @@ def helper_test_reply(self, test_reply, expected_success=True):
         zf = zipfile.ZipFile(StringIO(resp.data), 'r')
         data = zf.read(zf.namelist()[0])
         self._can_decrypt_with_key(data, config.JOURNALIST_KEY)
-        self._can_decrypt_with_key(data, crypto_util.getkey(sid), codename)
+        self._can_decrypt_with_key(data, crypto_util.getkey(filesystem_id),
+                                   codename)
 
         # Test deleting reply on the journalist interface
         last_reply_number = len(
@@ -394,7 +401,7 @@ def helper_test_reply(self, test_reply, expected_success=True):
                 msgid = soup.select(
                     'form.message > input[name="reply_filename"]')[0]['value']
                 resp = source_app.post('/delete', data=dict(
-                    sid=sid,
+                    filesystem_id=filesystem_id,
                     reply_filename=msgid
                 ), follow_redirects=True)
                 self.assertEqual(resp.status_code, 200)
@@ -403,7 +410,7 @@ def helper_test_reply(self, test_reply, expected_success=True):
                 # Make sure the reply is deleted from the filesystem
                 utils.async.wait_for_assertion(
                     lambda: self.assertFalse(os.path.exists(
-                        store.path(sid, msgid))))
+                        store.path(filesystem_id, msgid))))
 
             source_app.get('/logout')
 
@@ -427,9 +434,9 @@ def test_delete_collection(self):
         # find the delete form and extract the post parameters
         soup = BeautifulSoup(resp.data, 'html.parser')
         delete_form_inputs = soup.select('form#delete-collection')[0]('input')
-        sid = delete_form_inputs[1]['value']
+        filesystem_id = delete_form_inputs[1]['value']
         col_name = delete_form_inputs[2]['value']
-        resp = self.journalist_app.post('/col/delete/' + sid,
+        resp = self.journalist_app.post('/col/delete/' + filesystem_id,
                                         follow_redirects=True)
         self.assertEquals(resp.status_code, 200)
 
@@ -439,7 +446,7 @@ def test_delete_collection(self):
 
         # Make sure the collection is deleted from the filesystem
         utils.async.wait_for_assertion(
-            lambda: self.assertFalse(os.path.exists(store.path(sid)))
+            lambda: self.assertFalse(os.path.exists(store.path(filesystem_id)))
         )
 
     def test_delete_collections(self):
@@ -470,7 +477,8 @@ def test_delete_collections(self):
 
         # Make sure the collections are deleted from the filesystem
         utils.async.wait_for_assertion(lambda: self.assertFalse(
-            any([os.path.exists(store.path(sid)) for sid in checkbox_values])))
+            any([os.path.exists(store.path(filesystem_id))
+                for filesystem_id in checkbox_values])))
 
     def test_filenames(self):
         """Test pretty, sequential filenames when source uploads messages
@@ -586,13 +594,13 @@ def helper_filenames_submit(self):
         ), follow_redirects=True)
 
     def helper_filenames_delete(self, soup, i):
-        sid = soup.select('input[name="sid"]')[0]['value']
+        filesystem_id = soup.select('input[name="filesystem_id"]')[0]['value']
         checkbox_values = [
             soup.select('input[name="doc_names_selected"]')[i]['value']]
 
         # delete
         resp = self.journalist_app.post('/bulk', data=dict(
-            sid=sid,
+            filesystem_id=filesystem_id,
             action='confirm_delete',
             doc_names_selected=checkbox_values
         ), follow_redirects=True)
@@ -604,7 +612,7 @@ def helper_filenames_delete(self, soup, i):
 
         # confirm delete
         resp = self.journalist_app.post('/bulk', data=dict(
-            sid=sid,
+            filesystem_id=filesystem_id,
             action='delete',
             doc_names_selected=checkbox_values
         ), follow_redirects=True)
@@ -613,5 +621,5 @@ def helper_filenames_delete(self, soup, i):
 
         # Make sure the files were deleted from the filesystem
         utils.async.wait_for_assertion(lambda: self.assertFalse(
-            any([os.path.exists(store.path(sid, doc_name))
+            any([os.path.exists(store.path(filesystem_id, doc_name))
                  for doc_name in checkbox_values])))
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
@@ -54,7 +54,7 @@ def test_make_password(self, mocked_pw_gen):
     @patch('journalist.app.logger.error')
     def test_reply_error_logging(self, mocked_error_logger):
         source, _ = utils.db_helper.init_source()
-        sid = source.filesystem_id
+        filesystem_id = source.filesystem_id
         self._login_user()
 
         exception_class = StaleDataError
@@ -62,7 +62,8 @@ def test_reply_error_logging(self, mocked_error_logger):
 
         with patch('db.db_session.commit',
                    side_effect=exception_class(exception_msg)):
-            self.client.post(url_for('reply'), data={'sid': sid, 'msg': '_'})
+            self.client.post(url_for('reply'),
+                             data={'filesystem_id': filesystem_id, 'msg': '_'})
 
         # Notice the "potentially sensitive" exception_msg is not present in
         # the log event.
@@ -73,13 +74,14 @@ def test_reply_error_logging(self, mocked_error_logger):
 
     def test_reply_error_flashed_message(self):
         source, _ = utils.db_helper.init_source()
-        sid = source.filesystem_id
+        filesystem_id = source.filesystem_id
         self._login_user()
 
         exception_class = StaleDataError
 
         with patch('db.db_session.commit', side_effect=exception_class()):
-            self.client.post(url_for('reply'), data={'sid': sid, 'msg': '_'})
+            self.client.post(url_for('reply'),
+                             data={'filesystem_id': filesystem_id, 'msg': '_'})
 
         self.assertMessageFlashed(
             'An unexpected error occurred! Please check '
@@ -87,22 +89,24 @@ def test_reply_error_flashed_message(self):
 
     def test_empty_replies_are_rejected(self):
         source, _ = utils.db_helper.init_source()
-        sid = source.filesystem_id
+        filesystem_id = source.filesystem_id
         self._login_user()
 
         resp = self.client.post(url_for('reply'),
-                                data={'sid': sid, 'msg': ''},
+                                data={'filesystem_id': filesystem_id,
+                                      'msg': ''},
                                 follow_redirects=True)
 
         self.assertIn("You cannot send an empty reply!", resp.data)
 
     def test_nonempty_replies_are_accepted(self):
         source, _ = utils.db_helper.init_source()
-        sid = source.filesystem_id
+        filesystem_id = source.filesystem_id
         self._login_user()
 
         resp = self.client.post(url_for('reply'),
-                                data={'sid': sid, 'msg': '_'},
+                                data={'filesystem_id': filesystem_id,
+                                      'msg': '_'},
                                 follow_redirects=True)
 
         self.assertNotIn("You cannot send an empty reply!", resp.data)
@@ -588,8 +592,9 @@ def test_admin_page_restriction_http_posts(self):
             self.assertStatus(resp, 302)
 
     def test_user_authorization_for_gets(self):
-        urls = [url_for('index'), url_for('col', sid='1'),
-                url_for('download_single_submission', sid='1', fn='1'),
+        urls = [url_for('index'), url_for('col', filesystem_id='1'),
+                url_for('download_single_submission',
+                        filesystem_id='1', fn='1'),
                 url_for('edit_account')]
 
         for url in urls:
@@ -597,8 +602,10 @@ def test_user_authorization_for_gets(self):
             self.assertStatus(resp, 302)
 
     def test_user_authorization_for_posts(self):
-        urls = [url_for('add_star', sid='1'), url_for('remove_star', sid='1'),
-                url_for('col_process'), url_for('col_delete_single', sid='1'),
+        urls = [url_for('add_star', filesystem_id='1'),
+                url_for('remove_star', filesystem_id='1'),
+                url_for('col_process'),
+                url_for('col_delete_single', filesystem_id='1'),
                 url_for('reply'), url_for('generate_code'), url_for('bulk'),
                 url_for('account_new_two_factor'),
                 url_for('account_reset_two_factor_totp'),
@@ -740,7 +747,7 @@ def test_download_selected_submissions_from_source(self):
         self._login_user()
         resp = self.client.post(
             '/bulk', data=dict(action='download',
-                               sid=source.filesystem_id,
+                               filesystem_id=source.filesystem_id,
                                doc_names_selected=selected_fnames))
 
         # The download request was succesful, and the app returned a zipfile
@@ -908,7 +915,8 @@ def test_download_all_selected_sources(self):
     def test_add_star_redirects_to_index(self):
         source, _ = utils.db_helper.init_source()
         self._login_user()
-        resp = self.client.post(url_for('add_star', sid=source.filesystem_id))
+        resp = self.client.post(url_for('add_star',
+                                        filesystem_id=source.filesystem_id))
         self.assertRedirects(resp, url_for('index'))
 
 
@@ -968,9 +976,9 @@ def test_col_process_returns_404_with_bad_action(self, abort):
     @patch("journalist.make_star_true")
     @patch("journalist.db_session")
     def test_col_star_call_db_(self, db_session, make_star_true):
-        journalist.col_star(['sid'])
+        journalist.col_star(['filesystem_id'])
 
-        make_star_true.assert_called_with('sid')
+        make_star_true.assert_called_with('filesystem_id')
 
     @patch("journalist.db_session")
     def test_col_un_star_call_db(self, db_session):
@@ -1043,44 +1051,44 @@ def setUp(self):
     @patch('journalist.url_for')
     @patch('journalist.redirect')
     def test_add_star_renders_template(self, redirect, url_for):
-        redirect_template = journalist.add_star('sid')
+        redirect_template = journalist.add_star('filesystem_id')
 
         self.assertEqual(redirect_template, redirect(url_for('index')))
 
     @patch('journalist.db_session')
     def test_add_star_makes_commits(self, db_session):
-        journalist.add_star('sid')
+        journalist.add_star('filesystem_id')
 
         db_session.commit.assert_called_with()
 
     @patch('journalist.make_star_true')
     def test_single_delegates_to_make_star_true(self, make_star_true):
-        sid = 'sid'
+        filesystem_id = 'filesystem_id'
 
-        journalist.add_star(sid)
+        journalist.add_star(filesystem_id)
 
-        make_star_true.assert_called_with(sid)
+        make_star_true.assert_called_with(filesystem_id)
 
     @patch('journalist.url_for')
     @patch('journalist.redirect')
     def test_remove_star_renders_template(self, redirect, url_for):
-        redirect_template = journalist.remove_star('sid')
+        redirect_template = journalist.remove_star('filesystem_id')
 
         self.assertEqual(redirect_template, redirect(url_for('index')))
 
     @patch('journalist.db_session')
     def test_remove_star_makes_commits(self, db_session):
-        journalist.remove_star('sid')
+        journalist.remove_star('filesystem_id')
 
         db_session.commit.assert_called_with()
 
     @patch('journalist.make_star_false')
     def test_remove_star_delegates_to_make_star_false(self, make_star_false):
-        sid = 'sid'
+        filesystem_id = 'filesystem_id'
 
-        journalist.remove_star(sid)
+        journalist.remove_star(filesystem_id)
 
-        make_star_false.assert_called_with(sid)
+        make_star_false.assert_called_with(filesystem_id)
 
     @classmethod
     def tearDownClass(cls):