Skip to content

Commit 5b50aad

Browse files
legoktmsoleilera
andcommitted
Display a banner in the JI regarding the noble migration
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]>
1 parent 037055d commit 5b50aad

File tree

6 files changed

+106
-2
lines changed

6 files changed

+106
-2
lines changed

securedrop/journalist_app/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any, Optional, Tuple, Union
44

55
import i18n
6+
import server_os
67
import template_filters
78
import version
89
from db import db
@@ -54,6 +55,11 @@ def create_app(config: SecureDropConfig) -> Flask:
5455

5556
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
5657
app.config["SQLALCHEMY_DATABASE_URI"] = config.DATABASE_URI
58+
59+
# Check if the server OS is past EOL; if so, we'll display banners
60+
app.config["OS_PAST_EOL"] = server_os.is_os_past_eol()
61+
app.config["OS_NEEDS_MIGRATION_FIXES"] = server_os.needs_migration_fixes()
62+
5763
db.init_app(app)
5864

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

111117
i18n.set_locale(config)
118+
g.show_os_past_eol_warning = app.config["OS_PAST_EOL"]
119+
g.show_os_needs_migration_fixes = app.config["OS_NEEDS_MIGRATION_FIXES"]
112120

113121
if InstanceConfig.get_default().organization_name:
114122
g.organization_name = ( # pylint: disable=assigning-non-slot

securedrop/journalist_templates/base.html

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818
<body>
1919

2020
{% if session.logged_in() %}
21+
{% if g.show_os_past_eol_warning %}
22+
<div id="os-past-eol" class="alert-banner">
23+
{{ 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>') }}
24+
</div>
25+
{% elif g.show_os_needs_migration_fixes %}
26+
<div id="os-near-eol" class="alert-banner">
27+
{{ 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>') }}
28+
</div>
29+
{% endif %}
2130
<nav aria-label="{{ gettext('Navigation') }}">
2231
<a href="#main" class="visually-hidden until-focus">{{ gettext('Skip to main content') }}</a>
2332
{{ gettext('Logged on as') }}

securedrop/server_os.py

+36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import functools
2+
import json
3+
from datetime import date
4+
from pathlib import Path
25

36
FOCAL_VERSION = "20.04"
7+
NOBLE_VERSION = "24.04"
8+
9+
FOCAL_ENDOFLIFE = date(2025, 4, 2)
410

511

612
@functools.lru_cache
@@ -12,3 +18,33 @@ def get_os_release() -> str:
1218
version_id = line.split("=")[1].strip().strip('"')
1319
break
1420
return version_id
21+
22+
23+
def is_os_past_eol() -> bool:
24+
"""
25+
Check if it's focal and if today is past the official EOL date
26+
"""
27+
return get_os_release() == FOCAL_VERSION and date.today() >= FOCAL_ENDOFLIFE
28+
29+
30+
def needs_migration_fixes() -> bool:
31+
"""
32+
See if the check script has flagged any issues
33+
"""
34+
if get_os_release() != FOCAL_VERSION:
35+
return False
36+
state_path = Path("/etc/securedrop-noble-migration.json")
37+
if not state_path.exists():
38+
# Script hasn't run yet
39+
return False
40+
try:
41+
contents = json.loads(state_path.read_text())
42+
except json.JSONDecodeError:
43+
# Invalid output from the script is an error
44+
return True
45+
if "error" in contents:
46+
# Something went wrong with the script itself,
47+
# it needs manual fixes.
48+
return True
49+
# True if any of the checks failed
50+
return not all(contents.values())

securedrop/source_app/__init__.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Optional, Tuple
55

66
import i18n
7+
import server_os
78
import template_filters
89
import version
910
import werkzeug
@@ -65,6 +66,8 @@ def setup_i18n() -> None:
6566

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

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

114117
@app.before_request
115118
@ignore_static
116-
def check_submission_key() -> Optional[werkzeug.Response]:
117-
if not app.config["SUBMISSION_KEY_VALID"]:
119+
def check_offline() -> Optional[werkzeug.Response]:
120+
if not app.config["SUBMISSION_KEY_VALID"] or app.config["OS_PAST_EOL"]:
118121
session.clear()
119122
g.show_offline_message = True
120123
return make_response(render_template("offline.html"), 503)

securedrop/tests/test_journalist.py

+42
Original file line numberDiff line numberDiff line change
@@ -4026,3 +4026,45 @@ def test_journalist_deletion(journalist_app, app_storage):
40264026
assert len(SeenReply.query.filter_by(journalist_id=deleted.id).all()) == 2
40274027
# And there are no login attempts
40284028
assert JournalistLoginAttempt.query.all() == []
4029+
4030+
4031+
def test_user_sees_os_warning_if_server_past_eol(config, journalist_app, test_journo):
4032+
journalist_app.config.update(OS_PAST_EOL=True, OS_NEAR_EOL=False)
4033+
with journalist_app.test_client() as app:
4034+
login_journalist(
4035+
app, test_journo["username"], test_journo["password"], test_journo["otp_secret"]
4036+
)
4037+
4038+
resp = app.get(url_for("main.index"))
4039+
4040+
text = resp.data.decode("utf-8")
4041+
assert 'id="os-past-eol"' in text, text
4042+
assert 'id="os-near-eol"' not in text, text
4043+
4044+
4045+
def test_user_sees_os_warning_if_migration_fixes(config, journalist_app, test_journo):
4046+
journalist_app.config.update(OS_PAST_EOL=False, OS_NEEDS_MIGRATION_FIXES=True)
4047+
with journalist_app.test_client() as app:
4048+
login_journalist(
4049+
app, test_journo["username"], test_journo["password"], test_journo["otp_secret"]
4050+
)
4051+
4052+
resp = app.get(url_for("main.index"))
4053+
4054+
text = resp.data.decode("utf-8")
4055+
assert 'id="os-past-eol"' not in text, text
4056+
assert 'id="os-near-eol"' in text, text
4057+
4058+
4059+
def test_user_does_not_see_os_warning_if_server_is_current(config, journalist_app, test_journo):
4060+
journalist_app.config.update(OS_PAST_EOL=False, OS_NEEDS_MIGRATION_FIXES=False)
4061+
with journalist_app.test_client() as app:
4062+
login_journalist(
4063+
app, test_journo["username"], test_journo["password"], test_journo["otp_secret"]
4064+
)
4065+
4066+
resp = app.get(url_for("main.index"))
4067+
4068+
text = resp.data.decode("utf-8")
4069+
assert 'id="os-past-eol"' not in text, text
4070+
assert 'id="os-near-eol"' not in text, text

securedrop/translations/messages.pot

+6
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,12 @@ msgstr ""
419419
msgid "Can't scan the barcode? You can manually pair FreeOTP with this account by entering the following two-factor secret into the app:"
420420
msgstr ""
421421

422+
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>"
423+
msgstr ""
424+
425+
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>"
426+
msgstr ""
427+
422428
msgid "Navigation"
423429
msgstr ""
424430

0 commit comments

Comments
 (0)