Repository: freedomofpress/securedrop

Issue:
Disable Source Interface on instances running Trusty after April 30
> **NOTE: If you are an administrator and would like to receive help with the upgrade from Trusty to Xenial, please contact us via securedrop@freedom.press ([GPG encrypted](https://securedrop.org/sites/default/files/fpf-email.asc)), or open an issue in our support portal if you are a member.**

As [previously communicated](https://securedrop.org/news/advisory-why-you-must-manually-upgrade-your-securedrop-servers-april-30/), news organizations must upgrade to Ubuntu 16.04 by April 30, as Ubuntu 14.04 reaches EOL on that date. The Journalist Interface of impacted instances has [displayed a warning](https://github.com/freedomofpress/securedrop/issues/4027) since March 4, and extensive communications continue through all available channels. 

In our previous advisory, we have indicated that we will remove Trusty instances from the [SecureDrop directory](https://securedrop.org/directory/) after April 30, and that “we cannot guarantee that your SecureDrop instance will continue to function after that date."

Running an unsupported operating system on the servers presents an unacceptable risk to sources and newsrooms alike. For this reason, after April 30, news organizations running SecureDrop on Trusty should no longer be able to communicate with sources until they upgrade. The Source Interface should show a neutral message similar to the following (final language TBD):

> This SecureDrop instance is currently unavailable, pending a server upgrade. Please check back again later.

All source interface functionality should be disabled. The journalist interface should continue to work as before. We should also plan additional comms through all available channels in early April (tracked internally).

## User Stories

As a source, I want to have confidence that all my interactions via SecureDrop meet the highest standard of information security, so I face minimal risks of deanonymization by an adversary.

As a news organization, I want my SecureDrop instance to be protected against external compromise at all times, so that existing submissions and correspondence are not exposed to adversaries.

## Acceptance Criteria

**Given** that my SecureDrop servers are running Trusty after April 30
**When** I connect to the server’s Source Interface
**Then** I should see a maintenance notice
**And** all other Source Interface functionality should be disabled

**Given** that my SecureDrop servers were running Trusty after April 30, but have since been upgraded to Xenial following [instructions](https://securedrop.org/xenial-upgrade)
**When** I connect to the server’s Source Interface
**Then** the Source Interface should operate normally
**And** no warning message should be displayed



Discussion:
Pinging self to get on my radar... likely just messaging (or, time for a "sailed-ship" icon, heh)?
Follow up from the discussion during sprint planning, this is a minimal implementation that would achieve this functionality.

```python
# file `disable.py`
DISABLE_DATE = date(2019, 4, 30)

def disable_app(app):
    @app.before_request
    def disable_ui():
        if date.now() > DISABLE_DATE:
            return send_file('path/to/static/file')
```

Then we we add the following lines to `{source,journalist}_app/__init__.py`

```python
import disable

def create_app(config):
    # snip

    # this must occur in before any other `before_request` decoration or extensions
    disable.disable_app(app)

    # snip
```

This works because if a `before_request` callback returns `None`, the request falls through to the next `before_request` callback before eventually hitting the endpoints.
Thanks @heartsucker that solution looks good for the near term. I agree that the flask method is the most prudent in the near-term, but we must also be prepared to disable the source interface at either Apache or THS level if there's a remotely exploitable Tor, Apache, Python or Flask vulnerability in the future. Perhaps worth tracking in a follow-up ticket once a PR is opened.
WIP branch here: https://github.com/freedomofpress/securedrop/tree/4305-disable-submissions-trusty-eol

The current UX is very simple, and is identical to the current Source Interface generic error page:
![SecureDrop | Protecting Journalists and Sources - Mozilla Firefox_002](https://user-images.githubusercontent.com/15223328/55743491-702ce100-5a00-11e9-912c-426c96adc960.png)

I'd assumed we were seeking to block both from submitting _and_ Messaging. 

I'd prefer to be clearer with users, and had composed the below—based off the current Index page (tho I moved the languages widget to below the logo, because I'd prefer that gets moved on the Index pages anyway):

![image](https://user-images.githubusercontent.com/8262612/55745600-7913b780-59ec-11e9-946b-47fc8cecadfd.png)

```<?xml version="1.0" encoding="UTF-8"?>
<svg width="49px" height="19px" viewBox="0 0 49 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 52.3 (67297) - http://www.bohemiancoding.com/sketch -->
    <title>Group 15</title>
    <desc>Created with Sketch.</desc>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="A" transform="translate(-544.000000, -370.000000)">
            <g id="Group-15" transform="translate(544.000000, 370.000000)">
                <path d="M10.2467,7.6739 L7.3597,7.6739 L7.3597,6.2299 C7.3597,5.4609 8.0337,4.9789 8.8037,4.9789 C9.5727,4.9789 10.2467,5.4609 10.2467,6.2299 L10.2467,7.6739 Z M11.4977,7.6739 L11.4977,6.2299 C11.4977,4.8829 10.1507,3.8249 8.8037,3.8249 C7.4557,3.8249 6.1087,4.8829 6.1087,6.2299 L6.1087,7.6739 C5.5317,7.6739 4.9547,8.2509 4.9547,8.8279 L4.9547,12.1959 C4.9547,12.8699 5.5317,13.4469 6.1087,13.4469 L11.4017,13.4469 C12.0747,13.4469 12.6527,12.8699 12.6527,12.2929 L12.6527,8.9249 C12.6527,8.2509 12.0747,7.6739 11.4977,7.6739 Z" id="Fill-1" fill="#83C341"></path>
                <path d="M44.1407,17.6929 L4.0207,17.6929 C2.0407,17.6929 0.4207,16.0729 0.4207,14.0929 L0.4207,4.0209 C0.4207,2.0409 2.0407,0.4209 4.0207,0.4209 L44.1407,0.4209 C46.1197,0.4209 47.7407,2.0409 47.7407,4.0209 L47.7407,14.0929 C47.7407,16.0729 46.1197,17.6929 44.1407,17.6929 Z" id="Stroke-3" stroke="#EDEDED" stroke-width="0.841"></path>
                <path d="M36.3693,11.3911 L37.8933,11.1501 C38.0283,11.4721 38.1973,11.6991 38.3983,11.8321 C38.5983,11.9661 38.8733,12.0331 39.2213,12.0331 C39.5793,12.0331 39.8653,11.9521 40.0823,11.7911 C40.2313,11.6821 40.3073,11.5481 40.3073,11.3911 C40.3073,11.2851 40.2673,11.1901 40.1913,11.1061 C40.1113,11.0251 39.8923,10.9271 39.5393,10.8101 C38.5883,10.4951 38.0003,10.2461 37.7733,10.0641 C37.4193,9.7791 37.2413,9.4061 37.2413,8.9451 C37.2413,8.4851 37.4133,8.0881 37.7573,7.7551 C38.2353,7.2911 38.9463,7.0591 39.8903,7.0591 C40.6393,7.0591 41.2063,7.1961 41.5903,7.4701 C41.9733,7.7451 42.2173,8.1161 42.3183,8.5831 L40.8653,8.8361 C40.7893,8.6231 40.6653,8.4641 40.4933,8.3591 C40.2593,8.2161 39.9773,8.1451 39.6483,8.1451 C39.3193,8.1451 39.0823,8.2001 38.9383,8.3091 C38.7943,8.4191 38.7223,8.5451 38.7223,8.6871 C38.7223,8.8341 38.7953,8.9541 38.9413,9.0501 C39.0323,9.1081 39.3273,9.2101 39.8243,9.3571 C40.5923,9.5801 41.1043,9.7991 41.3643,10.0151 C41.7293,10.3181 41.9133,10.6831 41.9133,11.1111 C41.9133,11.6631 41.6813,12.1421 41.2173,12.5481 C40.7533,12.9541 40.0983,13.1571 39.2543,13.1571 C38.4123,13.1571 37.7633,13.0021 37.3053,12.6931 C36.8453,12.3851 36.5343,11.9501 36.3693,11.3911" id="Fill-5" fill="#231F20"></path>
                <path d="M15.7906,12.981 L15.7906,5.107 L16.7576,5.107 L16.7576,7.932 C17.2086,7.409 17.7776,7.148 18.4656,7.148 C18.8876,7.148 19.2546,7.231 19.5666,7.398 C19.8776,7.564 20.1006,7.794 20.2356,8.088 C20.3696,8.381 20.4366,8.808 20.4366,9.366 L20.4366,12.981 L19.4696,12.981 L19.4696,9.366 C19.4696,8.883 19.3656,8.531 19.1556,8.311 C18.9466,8.09 18.6496,7.981 18.2666,7.981 C17.9796,7.981 17.7106,8.055 17.4576,8.204 C17.2066,8.352 17.0256,8.553 16.9186,8.808 C16.8116,9.062 16.7576,9.413 16.7576,9.86 L16.7576,12.981 L15.7906,12.981 Z" id="Fill-7" fill="#939597"></path>
                <path d="M24.2394,12.1162 L24.3794,12.9702 C24.1064,13.0272 23.8634,13.0562 23.6484,13.0562 C23.2984,13.0562 23.0254,13.0002 22.8324,12.8892 C22.6384,12.7792 22.5034,12.6332 22.4244,12.4512 C22.3444,12.2712 22.3054,11.8902 22.3054,11.3102 L22.3054,8.0292 L21.5964,8.0292 L21.5964,7.2772 L22.3054,7.2772 L22.3054,5.8642 L23.2664,5.2842 L23.2664,7.2772 L24.2394,7.2772 L24.2394,8.0292 L23.2664,8.0292 L23.2664,11.3642 C23.2664,11.6402 23.2844,11.8172 23.3184,11.8962 C23.3524,11.9742 23.4074,12.0372 23.4844,12.0842 C23.5614,12.1302 23.6724,12.1542 23.8154,12.1542 C23.9234,12.1542 24.0634,12.1412 24.2394,12.1162" id="Fill-9" fill="#939597"></path>
                <path d="M27.9552,12.1162 L28.0952,12.9702 C27.8232,13.0272 27.5792,13.0562 27.3642,13.0562 C27.0132,13.0562 26.7412,13.0002 26.5482,12.8892 C26.3542,12.7792 26.2182,12.6332 26.1402,12.4512 C26.0612,12.2712 26.0212,11.8902 26.0212,11.3102 L26.0212,8.0292 L25.3122,8.0292 L25.3122,7.2772 L26.0212,7.2772 L26.0212,5.8642 L26.9832,5.2842 L26.9832,7.2772 L27.9552,7.2772 L27.9552,8.0292 L26.9832,8.0292 L26.9832,11.3642 C26.9832,11.6402 27.0002,11.8172 27.0342,11.8962 C27.0672,11.9742 27.1242,12.0372 27.2002,12.0842 C27.2772,12.1302 27.3882,12.1542 27.5302,12.1542 C27.6382,12.1542 27.7802,12.1412 27.9552,12.1162" id="Fill-11" fill="#939597"></path>
                <path d="M30.4366,10.1612 C30.4366,10.8952 30.5846,11.4372 30.8816,11.7882 C31.1786,12.1402 31.5386,12.3152 31.9616,12.3152 C32.3916,12.3152 32.7586,12.1332 33.0656,11.7702 C33.3726,11.4062 33.5246,10.8432 33.5246,10.0802 C33.5246,9.3532 33.3746,8.8092 33.0756,8.4482 C32.7766,8.0862 32.4186,7.9052 32.0046,7.9052 C31.5926,7.9052 31.2276,8.0982 30.9116,8.4832 C30.5946,8.8682 30.4366,9.4272 30.4366,10.1612 Z M29.5606,15.1662 L29.5606,7.2772 L30.4406,7.2772 L30.4406,8.0182 C30.6496,7.7282 30.8836,7.5102 31.1456,7.3662 C31.4076,7.2202 31.7236,7.1482 32.0946,7.1482 C32.5826,7.1482 33.0126,7.2732 33.3836,7.5242 C33.7566,7.7742 34.0386,8.1282 34.2276,8.5842 C34.4166,9.0412 34.5126,9.5412 34.5126,10.0862 C34.5126,10.6692 34.4076,11.1952 34.1986,11.6622 C33.9896,12.1292 33.6846,12.4882 33.2866,12.7372 C32.8856,12.9852 32.4656,13.1102 32.0266,13.1102 C31.7046,13.1102 31.4156,13.0422 31.1596,12.9062 C30.9036,12.7692 30.6926,12.5982 30.5266,12.3902 L30.5266,15.1662 L29.5606,15.1662 Z" id="Fill-13" fill="#939597"></path>
            </g>
        </g>
    </g>
</svg>```
Related general nit: Preference to avoid use of nrrdspeak like "instance" and "mode" in user-facing content, on the Source UI. Yes, most folks can figure those things out, but they impose an emotional burden onto users I'd like to be sensitive to avoid. Sources are already taking quite a leap by engaging with a SecureDrop at all. <3
Thanks @ninavizz , great points. Yes, submissions *and* replies will be disabled. I've updated the wording based on your feedback, what do you think?:
![sorry](https://user-images.githubusercontent.com/15223328/55747194-b76b9f80-5a09-11e9-8713-19d2d3ecf659.png)


The rationale is that I would prefer if we kept the messaging generic as possible: since sources are likely to originate from the org's landing page, they can elect themselves to submit documents through other means (if they choose and if available). Does this make sense?
@emkll It makes sense, but I still am not comfortable with the terseness of that. It’s very important to point users towards a path for probable success, when effectively slamming a door in their face (which this kinda is).
@ninavizz, thanks for your helpful user-centric input here.

We have to be careful communicating on another organization's behalf, even if that does make the message a bit more technical or opaque. It is ultimately the news organization's job to clarify the status of their SecureDrop on their landing page, and we cannot make judgments about this on their behalf or offer too much in the way of resolution.

Even a statement like "We don't know when _our_ SecureDrop will be back online" IMO presupposes too much about the other organization. We should not speak on their behalf in this manner. 

Similarly, we cannot say with certainty that other contact methods are available on the landing page, or that the organization is currently prepared to process submissions by other means.

I would therefore suggest sticking with simple language, e.g.:

> **We're sorry, this SecureDrop is currently offline.**

> Please try again later. Additional information may be available on the organization's website.

(Note the avoidance of "our".)
@eloquence I hear yous and @emkll's concerns. 

> Please try again later

Feeds into a common mental model other online services have established in most users, that later the same day, or in a few days, might work. We however, know that is unlikely to work. 

> ...on the organization's website...

Again, working with user mental models here; that makes this look like a phishing page, or like they're on the wrong page. Especially for orgs that don't use a custom logo and instead use the SecureDrop logo. Why would any org not speak to its users in the first-person? It's a standard expectation; and with commercial integrations, 3rd parties do it all the time in their generic/default copy.

It's not possible with any software solution, to 100% appease the needs of the customer, the end user, and larger behavioral interests. So tradeoffs have to be made. Are we out to protect the users first, or to represent all organizations as neutrally as absolutely possible, first? 

> We don't know when...

IMHO is appropriately vague for messaging users. 

Bottom line: we've given orgs over a month to update their systems. If they're neither communicating with FPF nor with their own users, why are we sticking our necks out for them at the expense of user expectations management, in this messaging? I feel it's entirely fair if an org gets up in a huff, to say just that: we gave y'all a chance, we went out of our way, and we needed to shift our prioritization to the users.

```
We’re sorry. 
Our SecureDrop is offline right now.

We don’t know when our SecureDrop will be live again.
We encourage you to consider other secure means of communication with us, while our SecureDrop is unavailable. 
```
If an org has in fact not yet upgraded to Xenial by now, what is the likelihood that they ever will—if they've also ignored all our outreach? That is also a very real thing to consider, that I want to respect Source expectations around.
Regardless of news orgs ignoring the need to upgrade, I don't think it's appropriate for FPF to "step in" and communicate on their behalf as "We" on the source interface index page, even in vague terms like "We don't know when our SecureDrop will be live again", or in terms of giving specific recommendations to sources. But I also need to reiterate that this is quite time-sensitive, so one way or another we should finalize the wording ASAP.

@redshiftzero could you act as a tie-breaker on this wording question?
I would be ok with killing the "We" to get more neutral with `It is not known when our SecureDrop will be... `but I feel it's imperative for news orgs to always reference their instance in the **_our._** In fact, I feel that should be governed as a requirement (at least to get listed on the Directory) in the docs, too. Eventually, with using a logo to TBD specifications that presents the instance as _theirs_. 

Who owns the servers, and SecureDrop not being this global monolith thing, is a very important concept to communicate to _Sources_. This is the Customer's SecureDrop. We're irresponsible to not speak directly to that. 
I'd suggest the following compromise language:

> **We're sorry, our SecureDrop is currently offline.**
> Please try again later, and check our website for more information. 

I still think "try again later" is fine; it could indeed be days (this change may motivate organizations to upgrade). Beyond that I don't think we should make statements on the org's behalf.

I'm OK with "our" for statements that are unproblematic and unlikely to contradict the news org's other public comms. But consider:
- Some orgs ask to exclusively be contacted via SecureDrop;
- Some orgs may update their landing page with maintenance dates etc.

In those cases we should not contradict what the org is saying by making presumptions on their behalf like "We don't know when" or "consider X". In future, if the source interface becomes more customizable, we may be able to offer richer templates, but given that there's no org preview or sign-off involved in rolling out this change, nor any customizability, I would argue for conservatism whenever we speak _as_ the news organization to the user.

That said, I'm fine with whatever final language Jen signs off on.
Ok—fair point on the "other means to communicate with us..." bit.

Only remaining nit then, is structural—and a general UI copywriting best-practices nit I have across SD documentation, Source UI, Journalist UI... everywhere. 

Sentence conjunctions are good for prose, and they make articles interesting to read. They make instructional text more difficult to parse, though. Guiding behavior, not ideas, is the purpose of instructional text. I personally cannot compose sentences without conjunctions, but for messaging users it's important to break-apart conjunctions into stand-alone thoughts/sentences. No commas. Small, simple sentences. Simple words. KISS: Keep It Simple, Stupid. 

> 
> We're sorry, our SecureDrop is currently offline.
> Please check our website for more information. 

...and then my jury is out, on how to fit in the "try again later" part. As a total aside from my hesitance to include it at all. I know, I used a comma after "We're sorry," above. A rare exception to keep the tone friendly.

Thank you for working this through with me, Erik. And Jen, I trust this in your no-pressure-at-all hands. 😸 
OK how about we go with:

> We're sorry, our SecureDrop is currently offline.
Please try again later. Check our website for more information.

Despite the fact there's no guarantee the organization has written anything about their SecureDrop downtime on their website, it at least provides a pointer for finding other contact methods. There's only so much we can do here, I agree we should only be making very uncontroversial statements since we are speaking from the perspective of the news organization. 

Resolving patch:
diff --git a/securedrop/source_app/__init__.py b/securedrop/source_app/__init__.py
--- a/securedrop/source_app/__init__.py
+++ b/securedrop/source_app/__init__.py
@@ -16,7 +16,7 @@
 from db import db
 from models import Source
 from request_that_secures_file_uploads import RequestThatSecuresFileUploads
-from source_app import main, info, api
+from source_app import main, info, api, disable
 from source_app.decorators import ignore_static
 from source_app.utils import logged_in
 from store import Storage
@@ -106,6 +106,18 @@ def handle_csrf_error(e):
     for module in [main, info, api]:
         app.register_blueprint(module.make_blueprint(config))  # type: ignore
 
+    @app.before_request
+    @ignore_static
+    def setup_i18n():
+        """Store i18n-related values in Flask's special g object"""
+        g.locale = i18n.get_locale(config)
+        g.text_direction = i18n.get_text_direction(g.locale)
+        g.html_lang = i18n.locale_to_rfc_5646(g.locale)
+        g.locales = i18n.get_locale2name()
+
+    # Disables the app if the server is running Trusty past its EOL date.
+    disable.disable_app(app)
+
     @app.before_request
     @ignore_static
     def check_tor2web():
@@ -125,10 +137,6 @@ def check_tor2web():
     @ignore_static
     def setup_g():
         """Store commonly used values in Flask's special g object"""
-        g.locale = i18n.get_locale(config)
-        g.text_direction = i18n.get_text_direction(g.locale)
-        g.html_lang = i18n.locale_to_rfc_5646(g.locale)
-        g.locales = i18n.get_locale2name()
 
         if 'expires' in session and datetime.utcnow() >= session['expires']:
             msg = render_template('session_timeout.html')
diff --git a/securedrop/source_app/decorators.py b/securedrop/source_app/decorators.py
--- a/securedrop/source_app/decorators.py
+++ b/securedrop/source_app/decorators.py
@@ -18,7 +18,7 @@ def ignore_static(f):
     a static resource."""
     @wraps(f)
     def decorated_function(*args, **kwargs):
-        if request.path.startswith('/static'):
+        if request.path.startswith('/static') or request.path == '/org-logo':
             return  # don't execute the decorated function
         return f(*args, **kwargs)
     return decorated_function
diff --git a/securedrop/source_app/disable.py b/securedrop/source_app/disable.py
new file mode 100644
--- /dev/null
+++ b/securedrop/source_app/disable.py
@@ -0,0 +1,17 @@
+from datetime import date
+from flask import render_template
+from source_app.decorators import ignore_static
+import platform
+
+XENIAL_VER = "16.04"
+TRUSTY_DISABLE_DATE = date(2019, 4, 30)
+
+
+def disable_app(app):
+
+    @app.before_request
+    @ignore_static
+    def disable_ui():
+        if(platform.linux_distribution()[1] != XENIAL_VER and
+                date.today() > TRUSTY_DISABLE_DATE):
+            return render_template('disabled.html')


Test patch:
diff --git a/securedrop/tests/test_source.py b/securedrop/tests/test_source.py
--- a/securedrop/tests/test_source.py
+++ b/securedrop/tests/test_source.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 import gzip
 import json
+import platform
 import re
 import subprocess
 
@@ -13,14 +14,21 @@
 import utils
 import version
 
+from datetime import date
 from db import db
 from models import Source, Reply
 from source_app import main as source_app_main
 from source_app import api as source_app_api
+from source_app import disable as source_app_disable
 from utils.db_helper import new_codename
 from utils.instrument import InstrumentedApp
 
 overly_long_codename = 'a' * (Source.MAX_CODENAME_LEN + 1)
+TRUSTY_DISABLED_ENDPOINTS = ['main.index', 'main.lookup', 'main.generate', 'main.login',
+                             'info.download_journalist_pubkey', 'info.tor2web_warning',
+                             'info.recommend_tor_browser', 'info.why_download_journalist_pubkey']
+STATIC_ASSETS = ['css/source.css', 'i/custom_logo.png', 'i/font-awesome/fa-globe-black.png',
+                 'i/favicon.png']
 
 
 def test_page_not_found(source_app):
@@ -696,3 +704,53 @@ def test_source_can_only_delete_own_replies(source_app):
 
     reply = Reply.query.filter_by(filename=filename).one()
     assert reply.deleted_by_source
+
+
+def test_source_disabled_when_trusty_is_eol(config, source_app):
+    with patch.object(source_app_api.platform, "linux_distribution") as mocked_platform:
+        mocked_platform.return_value = ("Ubuntu", "14.04", "Trusty")
+
+        with source_app.test_client() as app:
+            source_app_disable.TRUSTY_DISABLE_DATE = date(2001, 1, 1)
+
+            assert platform.linux_distribution()[1] == "14.04"
+            for endpoint in TRUSTY_DISABLED_ENDPOINTS:
+                resp = app.get(url_for(endpoint))
+                assert resp.status_code == 200
+                text = resp.data.decode('utf-8')
+                assert "We're sorry, our SecureDrop is currently offline." in text
+            # Ensure static assets are properly served
+            for asset in STATIC_ASSETS:
+                resp = app.get(url_for('static', filename=asset))
+                assert resp.status_code == 200
+                text = resp.data.decode('utf-8')
+                assert "We're sorry, our SecureDrop is currently offline." not in text
+
+
+def test_source_not_disabled_before_trusty_eol(config, source_app):
+    with patch.object(source_app_api.platform, "linux_distribution") as mocked_platform:
+        mocked_platform.return_value = ("Ubuntu", "14.04", "Trusty")
+
+        with source_app.test_client() as app:
+            source_app_disable.TRUSTY_DISABLE_DATE = date(2097, 1, 1)
+            assert platform.linux_distribution()[1] == "14.04"
+            for endpoint in TRUSTY_DISABLED_ENDPOINTS:
+                resp = app.get(url_for(endpoint), follow_redirects=True)
+                assert resp.status_code == 200
+                text = resp.data.decode('utf-8')
+                assert "We're sorry, our SecureDrop is currently offline." not in text
+
+
+def test_source_not_disabled_xenial(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:
+            source_app_disable.TRUSTY_DISABLE_DATE = date(2001, 1, 1)
+
+            assert platform.linux_distribution()[1] == "16.04"
+            for endpoint in TRUSTY_DISABLED_ENDPOINTS:
+                resp = app.get(url_for(endpoint), follow_redirects=True)
+                assert resp.status_code == 200
+                text = resp.data.decode('utf-8')
+                assert "We're sorry, our SecureDrop is currently offline." not in text