Skip to content

Commit

Permalink
Merge pull request #5789 from freedomofpress/v2-and-xenial-eol
Browse files Browse the repository at this point in the history
Provide end-of life messaging and disable on source interface after Xenial EOL
  • Loading branch information
emkll authored Feb 19, 2021
2 parents 55fb4f7 + ecfecea commit 33f94c9
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# Last Modified: Wed Oct 29 08:16:32 2014
#include <tunables/global>

/usr/sbin/apache2 {
Expand Down Expand Up @@ -336,6 +335,7 @@
/var/www/securedrop/template_filters.py r,
/var/www/securedrop/translations/ r,
/var/www/securedrop/translations/** r,
/var/www/securedrop/server_os.py r,
/var/www/securedrop/version.py r,
/var/www/securedrop/wordlists/ r,
/var/www/securedrop/wordlists/** r,
Expand Down
19 changes: 6 additions & 13 deletions securedrop/journalist_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
JournalistInterfaceSessionInterface,
cleanup_expired_revoked_tokens)
from models import InstanceConfig, Journalist
from server_os import is_os_near_eol, is_os_past_eol
from store import Storage

import typing
Expand Down Expand Up @@ -53,14 +54,7 @@ def create_app(config: 'SDConfig') -> Flask:
app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI
db.init_app(app)

def _url_exists(u: str) -> bool:
return path.exists(path.join(config.SECUREDROP_DATA_ROOT, u))

v2_enabled = _url_exists('source_v2_url') or ((not _url_exists('source_v2_url'))
and (not _url_exists('source_v3_url')))
v3_enabled = _url_exists('source_v3_url')

app.config.update(V2_ONION_ENABLED=v2_enabled, V3_ONION_ENABLED=v3_enabled)
app.config.update(OS_PAST_EOL=is_os_past_eol(), OS_NEAR_EOL=is_os_near_eol())

# TODO: Attaching a Storage dynamically like this disables all type checking (and
# breaks code analysis tools) for code that uses current_app.storage; it should be refactored
Expand Down Expand Up @@ -163,11 +157,10 @@ def setup_g() -> 'Optional[Response]':
else:
g.organization_name = gettext('SecureDrop')

if app.config['V2_ONION_ENABLED'] and not app.config['V3_ONION_ENABLED']:
g.show_v2_onion_eol_warning = True

if app.config['V2_ONION_ENABLED'] and app.config['V3_ONION_ENABLED']:
g.show_v2_onion_migration_warning = True
if app.config["OS_PAST_EOL"]:
g.show_os_past_eol_warning = True
elif app.config["OS_NEAR_EOL"]:
g.show_os_near_eol_warning = True

if request.path.split('/')[1] == 'api':
pass # We use the @token_required decorator for the API endpoints
Expand Down
20 changes: 9 additions & 11 deletions securedrop/journalist_templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,15 @@
<body>

{% if g.user %}
{% if g.show_v2_onion_eol_warning %}
<div id="v2-onion-eol" class="alert-banner">
<img src="{{ url_for('static', filename='i/bang-circle.png') }}" width="20" height="20"> {{ gettext('<strong>Update Required</strong>&nbsp;&nbsp;Set up v3 Onion Services before April 30 to keep your SecureDrop servers online. Please contact your administrator. <a href="//securedrop.org/v2-onion-eol" rel="noreferrer">Learn More</a>') }}
</div>
{% endif %}

{% if g.show_v2_onion_migration_warning %}
<div id="v2-complete-migration" class="alert-banner">
<img src="{{ url_for('static', filename='i/bang-circle.png') }}" width="20" height="20"> {{ gettext('<strong>Update Required</strong>&nbsp;&nbsp;Complete the v3 Onion Services setup before April 30. Please contact your administrator. <a href="//securedrop.org/v2-onion-eol" rel="noreferrer">Learn More</a>') }}
</div>
{% endif %}
{% if g.show_os_past_eol_warning %}
<div id="os-past-eol" class="alert-banner">
<img src="{{ url_for('static', filename='i/bang-circle.png') }}" width="20" height="20"> {{ gettext ('<strong>Critical Security:</strong>&nbsp;&nbsp;The operating system used by your SecureDrop servers has reached its end-of-life. A manual update is required to re-enable the Source Interface and remain safe. Please contact your administrator. <a href="//securedrop.org/xenial-eol" rel="noreferrer">Learn More</a>') }}
</div>
{% elif g.show_os_near_eol_warning %}
<div id="os-near-eol" class="alert-banner">
<img src="{{ url_for('static', filename='i/bang-circle.png') }}" width="20" height="20"> {{ gettext ('<strong>Critical Security:</strong>&nbsp;&nbsp;The operating system used by your SecureDrop servers will reach its end-of-life on April 30, 2021. A manual update is urgently required to remain safe. Please contact your adminstrator. <a href="//securedrop.org/xenial-eol" rel="noreferrer">Learn More</a>') }}
</div>
{% endif %}

<div id="logout">
{{ gettext('Logged on as') }} <a href="{{ url_for('account.edit') }}" id="link-edit-account">{{ g.user.username }}</a> |
Expand Down
25 changes: 25 additions & 0 deletions securedrop/server_os.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from datetime import date

FOCAL_VERSION = "20.04"
XENIAL_EOL_DATE = date(2021, 4, 30)

with open("/etc/lsb-release", "r") as f:
installed_version = f.readlines()[1].split("=")[1].strip("\n")


def is_os_past_eol() -> bool:
"""
Assumption: Any OS that is not Focal is an earlier version of the OS.
"""
if installed_version != FOCAL_VERSION and date.today() > XENIAL_EOL_DATE:
return True
return False


def is_os_near_eol() -> bool:
"""
Assumption: Any OS that is not Focal is an earlier version of the OS.
"""
if installed_version != FOCAL_VERSION and date.today() <= XENIAL_EOL_DATE:
return True
return False
12 changes: 12 additions & 0 deletions securedrop/source_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from source_app.decorators import ignore_static
from source_app.utils import logged_in, was_in_generate_flow
from store import Storage
from server_os import is_os_past_eol


def create_app(config: SDConfig) -> Flask:
Expand All @@ -42,6 +43,17 @@ def setup_i18n() -> None:
"""Store i18n-related values in Flask's special g object"""
i18n.set_locale(config)

app.config.update(OS_PAST_EOL=is_os_past_eol())

@app.before_request
@ignore_static
def disable_ui() -> Optional[str]:
if app.config["OS_PAST_EOL"]:
session.clear()
g.show_offline_message = True
return render_template("base.html")
return None

# The default CSRF token expiration is 1 hour. Since large uploads can
# take longer than an hour over Tor, we increase the valid window to 24h.
app.config['WTF_CSRF_TIME_LIMIT'] = 60 * 60 * 24
Expand Down
2 changes: 1 addition & 1 deletion securedrop/source_app/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def ignore_static(f: Callable) -> Callable:
a static resource."""
@wraps(f)
def decorated_function(*args: Any, **kwargs: Any) -> Any:
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
8 changes: 7 additions & 1 deletion securedrop/source_templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
{% endblock %}

<div class="panel selected">
{% if g.show_offline_message %}
<h1>{{ gettext("We're sorry, our SecureDrop is currently offline.") }}</h1>
<p>{{ gettext("Please try again later. Check our website for more information.") }}</p>
{% else %}

{% if 'logged_in' in session %}
<a href="{{ url_for('main.logout') }}" class="btn pull-right" id="logout">{{ gettext('LOG OUT') }}</a>
{% endif %}
Expand All @@ -36,8 +41,9 @@
<hr class="no-line">
{% endif %}

{% block body %}{% endblock %}
{% endif %}

{% block body %}{% endblock %}
</div>
</div>

Expand Down
71 changes: 41 additions & 30 deletions securedrop/tests/test_journalist.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,52 +64,63 @@ def _login_user(app, username, password, otp_secret):
assert hasattr(g, 'user') # ensure logged in


def test_user_sees_v2_eol_warning_if_only_v2_is_enabled(config, journalist_app, test_journo):
journalist_app.config.update(V2_ONION_ENABLED=True, V3_ONION_ENABLED=False)
def test_user_sees_os_warning_if_server_past_eol(config, journalist_app, test_journo):
journalist_app.config.update(OS_PAST_EOL=True, OS_NEAR_EOL=False)
with journalist_app.test_client() as app:
_login_user(
app,
test_journo['username'],
test_journo['password'],
test_journo['otp_secret'])
app, test_journo["username"], test_journo["password"], test_journo["otp_secret"]
)

resp = app.get(url_for('main.index'))
resp = app.get(url_for("main.index"))

text = resp.data.decode('utf-8')
assert 'id="v2-onion-eol"' in text, text
assert 'id="v2-complete-migration"' not in text, text
text = resp.data.decode("utf-8")
assert 'id="os-past-eol"' in text, text
assert 'id="os-near-eol"' not in text, text


def test_user_sees_v2_eol_warning_if_both_v2_and_v3_enabled(config, journalist_app, test_journo):
journalist_app.config.update(V2_ONION_ENABLED=True, V3_ONION_ENABLED=True)
def test_user_sees_os_warning_if_server_past_eol_sanity_check(config, journalist_app, test_journo):
"""
Sanity check (both conditions cannot be True but test guard against developer error)
"""
journalist_app.config.update(OS_PAST_EOL=True, OS_NEAR_EOL=True)
with journalist_app.test_client() as app:
_login_user(
app,
test_journo['username'],
test_journo['password'],
test_journo['otp_secret'])
app, test_journo["username"], test_journo["password"], test_journo["otp_secret"]
)

resp = app.get(url_for('main.index'))
resp = app.get(url_for("main.index"))

text = resp.data.decode('utf-8')
assert 'id="v2-onion-eol"' not in text, text
assert 'id="v2-complete-migration"' in text, text
text = resp.data.decode("utf-8")
assert 'id="os-past-eol"' in text, text
assert 'id="os-near-eol"' not in text, text


def test_user_does_not_see_v2_eol_warning_if_only_v3_enabled(config, journalist_app, test_journo):
journalist_app.config.update(V2_ONION_ENABLED=False, V3_ONION_ENABLED=True)
def test_user_sees_os_warning_if_server_close_to_eol(config, journalist_app, test_journo):
journalist_app.config.update(OS_PAST_EOL=False, OS_NEAR_EOL=True)
with journalist_app.test_client() as app:
_login_user(
app,
test_journo['username'],
test_journo['password'],
test_journo['otp_secret'])
app, test_journo["username"], test_journo["password"], test_journo["otp_secret"]
)

resp = app.get(url_for('main.index'))
resp = app.get(url_for("main.index"))

text = resp.data.decode('utf-8')
assert 'id="v2-onion-eol"' not in text, text
assert 'id="v2-complete-migration"' not in text, text
text = resp.data.decode("utf-8")
assert 'id="os-past-eol"' not in text, text
assert 'id="os-near-eol"' in text, text


def test_user_does_not_see_os_warning_if_server_is_current(config, journalist_app, test_journo):
journalist_app.config.update(OS_PAST_EOL=False, OS_NEAR_EOL=False)
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 'id="os-past-eol"' not in text, text
assert 'id="os-near-eol"' not in text, text


def test_user_with_whitespace_in_username_can_login(journalist_app):
Expand Down
75 changes: 74 additions & 1 deletion securedrop/tests/test_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import time
import os
import shutil

from datetime import date
from io import BytesIO, StringIO
from pathlib import Path

Expand All @@ -18,6 +18,7 @@
from . import utils
import version

import server_os
from db import db
from journalist_app.utils import delete_collection
from models import InstanceConfig, Source, Reply
Expand All @@ -30,6 +31,78 @@
overly_long_codename = 'a' * (PassphraseGenerator.MAX_PASSPHRASE_LENGTH + 1)


def test_source_interface_is_disabled_when_xenial_is_eol(config, source_app):
disabled_endpoints = [
"main.index",
"main.generate",
"main.login",
"info.download_public_key",
"info.tor2web_warning",
"info.recommend_tor_browser",
"info.why_download_public_key",
]
static_assets = [
"css/source.css",
"i/custom_logo.png",
"i/font-awesome/fa-globe-black.png",
"i/favicon.png",
]
with source_app.test_client() as app:
server_os.installed_version = "16.04"
server_os.XENIAL_EOL_DATE = date(2020, 1, 1)
for endpoint in 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_interface_is_not_disabled_before_xenial_eol(config, source_app):
disabled_endpoints = [
"main.index",
"main.generate",
"main.login",
"info.download_public_key",
"info.tor2web_warning",
"info.recommend_tor_browser",
"info.why_download_public_key",
]
with source_app.test_client() as app:
server_os.installed_version = "16.04"
server_os.XENIAL_EOL_DATE = date(2200, 1, 1)
for endpoint in 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_interface_is_not_disabled_for_focal(config, source_app):
disabled_endpoints = [
"main.index",
"main.generate",
"main.login",
"info.download_public_key",
"info.tor2web_warning",
"info.recommend_tor_browser",
"info.why_download_public_key",
]
with source_app.test_client() as app:
server_os.installed_version = "20.04"
server_os.XENIAL_EOL_DATE = date(2020, 1, 1)
for endpoint in 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." not in text


def test_logo_default_available(source_app):
# if the custom image is available, this test will fail
custom_image_location = os.path.join(config.SECUREDROP_ROOT, "static/i/custom_logo.png")
Expand Down

0 comments on commit 33f94c9

Please sign in to comment.