Repository: freedomofpress/securedrop

Issue:
[xenial] Display advisory in Journalist Interface for instances running 14.04 (Trusty)
Part of #3204, and a narrower version of #4001.

As discussed in sprint planning today, at minimum, as part of the 0.12.0 release of SecureDrop, we'll want to display a warning (on the Journalist Interfaces) on instances running Trusty (14.04) that it's time to upgrade. This should link to the advisory on SecureDrop.org, which will include the most up-to-date information about the process.

This can potentially be accomplished by ensuring that this feature is enabled only for the Trusty packages. 

To allow for sufficient time for post-release on-site "canary" testing, I would recommend displaying the warning only after March 4, 2019.

# User Testing

As a SecureDrop journalist or administrator, I want to be aware of major administrative issues concerning the security of my instance, so that I can ensure appropriate steps are taken to perform necessary updates.


Discussion:
Noting that I'll be posting mox to this Issue, sometime tomorrow.
@ninavizz for first draft of upgrade message I'm thinking something like:
"Your instance is running on an unsupported OS and must be upgraded to continue to receive security updates. For more information, see <link here>"

(If you're thinking of different warning levels, this one would be critical.)

## Recommended banner/message/icon
**Skully icon, Admin:** https://drive.google.com/open?id=1pIp60_EBLRaosW8YRDtAmt_nixLh46B3
**Skully icon, Journo:** https://drive.google.com/open?id=1lLeHEduq5oh9a6kq2i7XBWcPIK6iN9DM
**Circle-bang icon, Journo:** https://drive.google.com/open?id=1VnlGMYrPwS86iwB1BmJd3nHwnBDR5V65

**Rationale:** 
* The user has not made an user-error, so first and foremost—it's important for them to understand that. Secondly, their instance has not in fact, been compromised—but is now more vulnerable to compromise, than it ordinarily would be. 
   * Is there a way to currently display to users that an instance has been compromised? This would be good to know. 

* I'm inclined towards the skully icon, because this is a software end-of-life issue. Home automation and the ubiquity of tech in our personal lives, has made end-of-life concepts more common to non-technical users... hence, my comfort with that mental model. 
   * My only hesitance, is that I don't want to confuse users with the idea their current instance may already be compromised. 
   * Should the latter be too high a concern for others, my alternate suggestion is the circle-bang. I'm not keen on how close the dot is to the line on the exclamation-point, as it interferes with small-size legibility. Why not the existing triangle-bang? Because this isn't a user-error, and it's important to keep that symbology separate. 

* Finally: I'm usually averse to using red in user messaging. The problem with red, is that it piques user anxiety—which can interfere with cognitive processes in resolving errors. Because the user has not made a task-related, immediately correctable error however, that is not a problem in this use case—and if anything, that anxiety is in this rare situation, seems appropriate. 

## Recommended message text
Critical Security: The operating system on your SecureDrop server has reached its end-of-life. A manual update is urgently required to remain safe. Learn More

**Sought points to communicate to the user** (a non-technical journalist, a nerdy journalist, or an admin) **seeing this, in order:**

1. You have not made an immediately correctable task-related mistake.
2. There is an urgent admin issue un-related to your tasks in this session.
3. You need to remember to act upon this once you've completed your current tasks.
4. The issue pertains to SD hardware outside the Workstation laptops & Tails sticks.
5. The issue pertains to the hardware most important to not be compromised. 
6. Your current instance has not been compromised.
7. Your current instance is now vulnerable to compromise.
8. If you don't know who to communicate with about this, please contact SD support.

Note: _At the **VERY TOP** of the article the above text links out to_, non-technical journalists should be spoken to in a single sentence/paragraph that encourages them to contact support if they don't know whom else to reach-out to for help, should help be needed. 

We learned from one customer in our recent user research, that their admin had left the org—and that the org had since been pokin' along without a SD admin, for several months. They'd "been meaning to" reach out to someone at FPF, but with the length of a journalist's to-do list, that was understandably low on the list. Their whole IT situation is in a state of transition, so the journos using SD also just didn't really know what to ask of FPF when contacting them. 

It's important to give folks in this situation a direct connection with a human to help them resolve this, as to not leave them dangling with such a vulnerability. My partner in the testing engaged with this user to prod them a little more about why they hadn't already yet reached-out to FPF, and the uncertainty w/ accompanying "oh, y'know" list of reasons was long. That has to be spoken to, simply, boldly, and discoverably. 

**Suggested icons:**
Skully (preferred): https://fontawesome.com/icons/skull-crossbones?style=solid
Circle-bang (alternate): https://fontawesome.com/icons/exclamation-circle?style=solid
:+1:  for skulls - one nit about the wording is that more than one server is affected. A SecureDrop instance has two, the application and monitor servers and both need updating. How about `The operating system used by your SecureDrop instance has reached its end-of-life. A manual update is urgently required to remain safe. Learn More`?
@eloquence if the March 4 thing is a requirement, it would probably require either some kind of cronjob to be set up via scripts in 0.12.0 (fun to test), a point release on March 4 to update the app, or logic in the app with hard-coded dates. None of these options sound great to me. Is there a downside to having it displayed in 0.12.0 from release?
Mh, the reason I think it might make sense to have it show up with a couple of weeks delay after the release is a) it'll give us some time to incorporate any findings with canary testers & early adopters into the advisory, b) it'll help spread out the support load a bit through Feb/Mar. The advisory URL is specific to Xenial so I'm not sure how problematic it is to also have a hardcoded date? But let's kick it around a bit on Monday, happy to drop that idea if y'all think it's not worth the extra complexity.

Resolving patch:
diff --git a/securedrop/journalist_app/__init__.py b/securedrop/journalist_app/__init__.py
--- a/securedrop/journalist_app/__init__.py
+++ b/securedrop/journalist_app/__init__.py
@@ -12,6 +12,7 @@
 import i18n
 import template_filters
 import version
+import platform
 
 from crypto_util import CryptoUtil
 from db import db
@@ -61,6 +62,12 @@ def create_app(config):
     app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
     db.init_app(app)
 
+    # Magic values for Xenial upgrade message
+    app.config.update(
+        XENIAL_WARNING_DATE=datetime.strptime('Mar 4 2019', '%b %d %Y'),
+        XENIAL_VER='16.04'
+    )
+
     app.storage = Storage(config.STORE_DIR,
                           config.TEMP_DIR,
                           config.JOURNALIST_KEY)
@@ -135,6 +142,10 @@ def setup_g():
         g.html_lang = i18n.locale_to_rfc_5646(g.locale)
         g.locales = i18n.get_locale2name()
 
+        if (platform.linux_distribution()[1] != app.config['XENIAL_VER'] and
+                datetime.now() >= app.config['XENIAL_WARNING_DATE']):
+            g.show_xenial_warning = True
+
         if request.path.split('/')[1] == 'api':
             pass  # We use the @token_required decorator for the API endpoints
         else:  # We are not using the API


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
@@ -5,6 +5,7 @@
 import random
 import zipfile
 import base64
+import datetime
 
 from base64 import b64decode
 from cStringIO import StringIO
@@ -1585,6 +1586,55 @@ def test_render_locales(config, journalist_app, test_journo, test_source):
     assert url_end + '?l=en_US' in text, text
 
 
+def test_render_xenial_positive(config, journalist_app, test_journo):
+    yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
+    journalist_app.config.update(
+        XENIAL_WARNING_DATE=yesterday,
+        XENIAL_VER='16.04'
+    )
+
+    with journalist_app.test_client() as app:
+        _login_user(app, test_journo['username'], test_journo['password'],
+                    test_journo['otp_secret'])
+
+        resp = app.get(url_for('main.index'))
+
+    text = resp.data.decode('utf-8')
+    assert "critical-skull" in text, text
+
+
+def test_render_xenial_negative_version(config, journalist_app, test_journo):
+    yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
+    journalist_app.config.update(
+        XENIAL_WARNING_DATE=yesterday,
+        XENIAL_VER='14.04'
+    )
+
+    with journalist_app.test_client() as app:
+        _login_user(app, test_journo['username'], test_journo['password'],
+                    test_journo['otp_secret'])
+        resp = app.get('/')
+
+    text = resp.data.decode('utf-8')
+    assert "critical-skull" not in text, text
+
+
+def test_render_xenial_negative_date(config, journalist_app, test_journo):
+    tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
+    journalist_app.config.update(
+        XENIAL_WARNING_DATE=tomorrow,
+        XENIAL_VER='16.04'
+    )
+
+    with journalist_app.test_client() as app:
+        _login_user(app, test_journo['username'], test_journo['password'],
+                    test_journo['otp_secret'])
+        resp = app.get('/')
+
+    text = resp.data.decode('utf-8')
+    assert "critical-skull" not in text, text
+
+
 def test_download_selected_submissions_from_source(journalist_app,
                                                    test_journo,
                                                    test_source):