Skip to content

Commit

Permalink
Display a banner in the JI regarding the noble migration
Browse files Browse the repository at this point in the history
This is largely copied from the same functionality that was implemented
during the focal migration (ecfecea).

There are two banners that can be seen:

OS_PAST_EOL is in effect after April 2, 2025 if the system is still
running on focal. The Source Interface automatically disables itself and
the Journalist Interface will display a banner informing journalists to
contact their administrator.

OS_NEEDS_MIGRATION_FIXES will display a notice in the Journalist
Interface if the check script has run and found issues that need
resolution. It doesn't affect the Source Interface.

The banners point at <https://securedrop.org/focal-eol>, which will be
set up as a redirect to the relevant documentation.

Both checks are done during startup, which means if the state changes
(e.g. disk space is freed up or a systemd unit fails), the banner state
will only change after the nightly reboot.

Refs #7322

Co-authored-by: soleilera <[email protected]>
  • Loading branch information
legoktm and soleilera committed Nov 25, 2024
1 parent 037055d commit 5b50aad
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 2 deletions.
8 changes: 8 additions & 0 deletions securedrop/journalist_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any, Optional, Tuple, Union

import i18n
import server_os
import template_filters
import version
from db import db
Expand Down Expand Up @@ -54,6 +55,11 @@ def create_app(config: SecureDropConfig) -> Flask:

app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_DATABASE_URI"] = config.DATABASE_URI

# Check if the server OS is past EOL; if so, we'll display banners
app.config["OS_PAST_EOL"] = server_os.is_os_past_eol()
app.config["OS_NEEDS_MIGRATION_FIXES"] = server_os.needs_migration_fixes()

db.init_app(app)

class JSONEncoder(json.JSONEncoder):
Expand Down Expand Up @@ -109,6 +115,8 @@ def setup_g() -> Optional[Response]:
"""Store commonly used values in Flask's special g object"""

i18n.set_locale(config)
g.show_os_past_eol_warning = app.config["OS_PAST_EOL"]
g.show_os_needs_migration_fixes = app.config["OS_NEEDS_MIGRATION_FIXES"]

if InstanceConfig.get_default().organization_name:
g.organization_name = ( # pylint: disable=assigning-non-slot
Expand Down
9 changes: 9 additions & 0 deletions securedrop/journalist_templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
<body>

{% if session.logged_in() %}
{% if g.show_os_past_eol_warning %}
<div id="os-past-eol" class="alert-banner">
{{ gettext('<strong>Critical:</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="https://securedrop.org/focal-eol" rel="noreferrer">Learn More</a>') }}
</div>
{% elif g.show_os_needs_migration_fixes %}
<div id="os-near-eol" class="alert-banner">
{{ gettext('<strong>Important:</strong>&nbsp;&nbsp;Your SecureDrop server needs manual attention to resolve issues blocking automatic upgrade to the next operating system. Please contact your adminstrator. <a href="https://securedrop.org/focal-eol" rel="noreferrer">Learn More</a>') }}
</div>
{% endif %}
<nav aria-label="{{ gettext('Navigation') }}">
<a href="#main" class="visually-hidden until-focus">{{ gettext('Skip to main content') }}</a>
{{ gettext('Logged on as') }}
Expand Down
36 changes: 36 additions & 0 deletions securedrop/server_os.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import functools
import json
from datetime import date
from pathlib import Path

FOCAL_VERSION = "20.04"
NOBLE_VERSION = "24.04"

FOCAL_ENDOFLIFE = date(2025, 4, 2)


@functools.lru_cache
Expand All @@ -12,3 +18,33 @@ def get_os_release() -> str:
version_id = line.split("=")[1].strip().strip('"')
break
return version_id


def is_os_past_eol() -> bool:
"""
Check if it's focal and if today is past the official EOL date
"""
return get_os_release() == FOCAL_VERSION and date.today() >= FOCAL_ENDOFLIFE


def needs_migration_fixes() -> bool:
"""
See if the check script has flagged any issues
"""
if get_os_release() != FOCAL_VERSION:
return False
state_path = Path("/etc/securedrop-noble-migration.json")
if not state_path.exists():
# Script hasn't run yet
return False
try:
contents = json.loads(state_path.read_text())
except json.JSONDecodeError:
# Invalid output from the script is an error
return True
if "error" in contents:
# Something went wrong with the script itself,
# it needs manual fixes.
return True
# True if any of the checks failed
return not all(contents.values())
7 changes: 5 additions & 2 deletions securedrop/source_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Optional, Tuple

import i18n
import server_os
import template_filters
import version
import werkzeug
Expand Down Expand Up @@ -65,6 +66,8 @@ def setup_i18n() -> None:

# Check if the Submission Key is valid; if not, we'll disable the UI
app.config["SUBMISSION_KEY_VALID"] = validate_journalist_key()
# Check if the server OS is past EOL; if so, we'll disable the UI
app.config["OS_PAST_EOL"] = server_os.is_os_past_eol()

@app.errorhandler(CSRFError)
def handle_csrf_error(e: CSRFError) -> werkzeug.Response:
Expand Down Expand Up @@ -113,8 +116,8 @@ def check_tor2web() -> Optional[werkzeug.Response]:

@app.before_request
@ignore_static
def check_submission_key() -> Optional[werkzeug.Response]:
if not app.config["SUBMISSION_KEY_VALID"]:
def check_offline() -> Optional[werkzeug.Response]:
if not app.config["SUBMISSION_KEY_VALID"] or app.config["OS_PAST_EOL"]:
session.clear()
g.show_offline_message = True
return make_response(render_template("offline.html"), 503)
Expand Down
42 changes: 42 additions & 0 deletions securedrop/tests/test_journalist.py
Original file line number Diff line number Diff line change
Expand Up @@ -4026,3 +4026,45 @@ def test_journalist_deletion(journalist_app, app_storage):
assert len(SeenReply.query.filter_by(journalist_id=deleted.id).all()) == 2
# And there are no login attempts
assert JournalistLoginAttempt.query.all() == []


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_journalist(
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"' in text, text
assert 'id="os-near-eol"' not in text, text


def test_user_sees_os_warning_if_migration_fixes(config, journalist_app, test_journo):
journalist_app.config.update(OS_PAST_EOL=False, OS_NEEDS_MIGRATION_FIXES=True)
with journalist_app.test_client() as app:
login_journalist(
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"' 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_NEEDS_MIGRATION_FIXES=False)
with journalist_app.test_client() as app:
login_journalist(
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
6 changes: 6 additions & 0 deletions securedrop/translations/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,12 @@ msgstr ""
msgid "Can't scan the barcode? You can manually pair FreeOTP with this account by entering the following two-factor secret into the app:"
msgstr ""

msgid "<strong>Critical:</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=\"https://securedrop.org/focal-eol\" rel=\"noreferrer\">Learn More</a>"
msgstr ""

msgid "<strong>Important:</strong>&nbsp;&nbsp;Your SecureDrop server needs manual attention to resolve issues blocking automatic upgrade to the next operating system. Please contact your adminstrator. <a href=\"https://securedrop.org/focal-eol\" rel=\"noreferrer\">Learn More</a>"
msgstr ""

msgid "Navigation"
msgstr ""

Expand Down

0 comments on commit 5b50aad

Please sign in to comment.