Repository: freedomofpress/securedrop

Issue:
[v3 onion migration] securedrop-admin sdconfig should set v2_onion_services and v3_onion_services variables
## Description

We should enable the setting of two new boolean configuration variables in `site-specific` by `securedrop-admin sdconfig`: `v2_onion_services` and `v3_onion_services`.

The expected behavior here is:

* The default value of the `v2_onion_services` variable is set to:
  - `True` if there is a ths file (`app-ths-source`) files with a 16 char onion address in `install_files/ansible-base/` - this is to prevent admins from accidentally disabling v2 onion services
  - `False` otherwise - we don't want new installs to be using v2 services beginning in SecureDrop 1.0.0
* The default value of the `v3_onion_services` variable is set to `True`:
  - we want new SecureDrop instances to _only_ use v3 services beginning in SecureDrop 1.0.0 and we want _existing_ installs to enable v3 alongside v2 so they can begin transitioning users to v3. 

Note that we may need to do another iteration to improve the messaging to admins / point to some overview docs (to be written) in `docs.securedrop.org` explaining the migration process. This ticket is primarily just for the functionality of setting the proper defaults (the docs and messaging in sdconfig should also communicate that users using HTTPS on the source interface will need to get a new certificate including the v3 interface before advertising source users to use the v3 onion service).

Subticket of #2951 




Discussion:

Resolving patch:
diff --git a/admin/securedrop_admin/__init__.py b/admin/securedrop_admin/__init__.py
--- a/admin/securedrop_admin/__init__.py
+++ b/admin/securedrop_admin/__init__.py
@@ -409,11 +409,33 @@ def load_and_update_config(self):
 
     def update_config(self):
         self.config.update(self.user_prompt_config())
+        self.update_onion_version_config()
         self.save()
         self.validate_gpg_keys()
         self.validate_journalist_alert_email()
         return True
 
+    def update_onion_version_config(self):
+        """
+        This method updates onion service related configurations.
+        """
+        v2 = False
+        v3 = True
+        source_ths = os.path.join(self.args.ansible_path, "app-source-ths")
+        if os.path.exists(source_ths):  # Means old installation
+            data = ""
+            with open(source_ths) as fobj:
+                data = fobj.read()
+
+            data = data.strip()
+            if len(data) < 56:  # Old v2 onion address
+                v2 = True
+
+        # Now update the configuration
+        config = {"v2_onion_services": v2,
+                  "v3_onion_services": v3}
+        self.config.update(config)
+
     def user_prompt_config(self):
         config = {}
         for desc in self.desc:


Test patch:
diff --git a/admin/tests/test_integration.py b/admin/tests/test_integration.py
--- a/admin/tests/test_integration.py
+++ b/admin/tests/test_integration.py
@@ -44,6 +44,8 @@
 smtp_relay: smtp.gmail.com
 smtp_relay_port: 587
 ssh_users: sd
+v2_onion_services: false
+v3_onion_services: true
 '''
 
 JOURNALIST_ALERT_OUTPUT = '''app_hostname: app
@@ -74,6 +76,8 @@
 smtp_relay: smtp.gmail.com
 smtp_relay_port: 587
 ssh_users: sd
+v2_onion_services: false
+v3_onion_services: true
 '''
 
 HTTPS_OUTPUT = '''app_hostname: app
@@ -104,6 +108,8 @@
 smtp_relay: smtp.gmail.com
 smtp_relay_port: 587
 ssh_users: sd
+v2_onion_services: false
+v3_onion_services: true
 '''
 
 
diff --git a/admin/tests/test_securedrop-admin.py b/admin/tests/test_securedrop-admin.py
--- a/admin/tests/test_securedrop-admin.py
+++ b/admin/tests/test_securedrop-admin.py
@@ -18,6 +18,7 @@
 #
 
 import io
+import os
 import argparse
 from flaky import flaky
 from os.path import dirname, join, basename, exists
@@ -617,6 +618,66 @@ def test_save(self, tmpdir):
         """)
         assert expected == io.open(site_config_path).read()
 
+    def test_old_v2_onion_services(self, tmpdir):
+        "Checks for exitsing v2 source address"
+        site_config_path = join(str(tmpdir), 'site_config')
+        args = argparse.Namespace(site_config=site_config_path,
+                                  ansible_path='.',
+                                  app_path=dirname(__file__))
+        site_config = securedrop_admin.SiteConfig(args)
+        with open("app-source-ths", "w") as fobj:
+            fobj.write("aaaaaaaaaaaaaaaa.onion\n")
+        site_config.update_onion_version_config()
+        site_config.save()
+        data = ""
+        with open(site_config_path) as fobj:
+            data = fobj.read()
+        expected = textwrap.dedent("""\
+        v2_onion_services: true
+        v3_onion_services: true
+        """)
+        os.remove("app-source-ths")
+        assert expected == data
+
+    def test_no_v2_onion_services(self, tmpdir):
+        "Checks for new installation for only v3"
+        site_config_path = join(str(tmpdir), 'site_config')
+        args = argparse.Namespace(site_config=site_config_path,
+                                  ansible_path='.',
+                                  app_path=dirname(__file__))
+        site_config = securedrop_admin.SiteConfig(args)
+        site_config.update_onion_version_config()
+        site_config.save()
+        data = ""
+        with open(site_config_path) as fobj:
+            data = fobj.read()
+        expected = textwrap.dedent("""\
+        v2_onion_services: false
+        v3_onion_services: true
+        """)
+        assert expected == data
+
+    def test_only_v3_onion_services(self, tmpdir):
+        "Checks for new installation for only v3 ths file"
+        site_config_path = join(str(tmpdir), 'site_config')
+        args = argparse.Namespace(site_config=site_config_path,
+                                  ansible_path='.',
+                                  app_path=dirname(__file__))
+        site_config = securedrop_admin.SiteConfig(args)
+        with open("app-source-ths", "w") as fobj:
+            fobj.write("a" * 56 + ".onion\n")
+        site_config.update_onion_version_config()
+        site_config.save()
+        data = ""
+        with open(site_config_path) as fobj:
+            data = fobj.read()
+        expected = textwrap.dedent("""\
+        v2_onion_services: false
+        v3_onion_services: true
+        """)
+        os.remove("app-source-ths")
+        assert expected == data
+
     def test_validate_gpg_key(self, caplog):
         args = argparse.Namespace(site_config='INVALID',
                                   ansible_path='tests/files',