Repository: freedomofpress/securedrop

Issue:
Add clearer action reminder in Journalist Interface to enable v3
As part of the 1.7.0 release, we've agreed that we want to improve our reminder in the Journalist Interface to enable v3 onion services. The current banner looks as follows:

![Screen Shot 2020-12-15 at 15 18 06-fullpage](https://user-images.githubusercontent.com/213636/102284729-913e1880-3ee9-11eb-9518-8b685d2b0e5a.png)

You can also see it on https://demo-journalist.securedrop.org/ after logging in using the dev credentials.

The plan of record is as follows:
- SecureDrop 1.7.0 (released in January) will still fully support v2 onion services
- SecureDrop 1.8.0 (released in February) will support Ubuntu 20.04 (#4768), and _exclusively_ support v3 onion services for Focal installs, without changing the behavior on Xenial.
- After April 30 (Ubuntu 16.04 EOL), instances not upgraded to Ubuntu 20.04 will self-disable.

In other words, the v3 switch is unavoidable due to the Xenial end-of-life. Our goal with the 1.7.0 release is to get more admins to make the switch prior to a reinstall on Focal, to make the process a bit easier for them. The specific action we need to motivate:

1) *If v3 is already enabled, but v2 services are still available:* Disable v2 services, ensure that all journalists/admins have v3 creds, and ensure that landing page points to v3 onion.

2) *If v3 is not enabled yet:* Enable v3 services (can still run them alongside v2 for a bit, per docs, then go v3-only).

We've discussed that this could potentially be done via different banners for 1) and 2) in the Journalist Interface. Let's kick around language/UX a bit in the comments, and I'll add the final agreed upon spec to the top-level issue.

## User Story

As an administrator, I want to be reminded of critical actions I must take to keep my instance running, so that I'm not caught by surprise.


Discussion:
Some initial suggestions re: UX & language:

- I think a more urgent banner color is appropriate in either case.
- For case 1: **Action Required:** Your SecureDrop servers are still reachable via v2 and v3 onion services. Make sure admins, journalists and sources are using v3 onion services, then disable v2 onion services. [Learn more]
- For case 2: **Action Required:** You must switch your SecureDrop to v3 onion services to ensure that it is reachable after April 30, 2021. [Learn more]

The omission of "April 30" in case 1 is intentional; IMO here it is most important to emphasize internal action vis-a-vis journalists & sources. Bear in mind that these banners would be swapped out again with the 1.8.0 release a few weeks later, which would emphasize the Ubuntu 20.04 transition and the April 30 deadline.

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
@@ -53,8 +53,13 @@ def create_app(config: 'SDConfig') -> Flask:
     app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI
     db.init_app(app)
 
-    v2_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v2_url'))
-    v3_enabled = path.exists(path.join(config.SECUREDROP_DATA_ROOT, 'source_v3_url'))
+    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)
 
     # TODO: Attaching a Storage dynamically like this disables all type checking (and
@@ -161,9 +166,12 @@ def setup_g() -> 'Optional[Response]':
         else:
             g.organization_name = gettext('SecureDrop')
 
-        if not app.config['V3_ONION_ENABLED'] or app.config['V2_ONION_ENABLED']:
+        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 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
@@ -76,7 +76,8 @@ def test_user_sees_v2_eol_warning_if_only_v2_is_enabled(config, journalist_app,
         resp = app.get(url_for('main.index'))
 
     text = resp.data.decode('utf-8')
-    assert "v2-onion-eol" in text, text
+    assert 'id="v2-onion-eol"' in text, text
+    assert 'id="v2-complete-migration"' not in text, text
 
 
 def test_user_sees_v2_eol_warning_if_both_v2_and_v3_enabled(config, journalist_app, test_journo):
@@ -91,7 +92,8 @@ def test_user_sees_v2_eol_warning_if_both_v2_and_v3_enabled(config, journalist_a
         resp = app.get(url_for('main.index'))
 
     text = resp.data.decode('utf-8')
-    assert "v2-onion-eol" in text, text
+    assert 'id="v2-onion-eol"' not in text, text
+    assert 'id="v2-complete-migration"' in text, text
 
 
 def test_user_does_not_see_v2_eol_warning_if_only_v3_enabled(config, journalist_app, test_journo):
@@ -106,22 +108,8 @@ def test_user_does_not_see_v2_eol_warning_if_only_v3_enabled(config, journalist_
         resp = app.get(url_for('main.index'))
 
     text = resp.data.decode('utf-8')
-    assert "v2-onion-eol" not in text, text
-
-
-def test_user_sees_v2_eol_warning_if_both_urls_do_not_exist(config, journalist_app, test_journo):
-    journalist_app.config.update(V2_ONION_ENABLED=False, V3_ONION_ENABLED=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 "v2-onion-eol" in text, text
+    assert 'id="v2-onion-eol"' not in text, text
+    assert 'id="v2-complete-migration"' not in text, text
 
 
 def test_user_with_whitespace_in_username_can_login(journalist_app):