Repository: freedomofpress/securedrop

Issue:
Graphical updater re-appears, fails after initial run without admin password
## Description

The graphical updater fails to successfully complete an update, if it is re-run after an update that failed due to a Tails admin password not being set.

## Steps to Reproduce

1. Boot up the Admin Workstation with an update pending;
2. Do _not_ set a Tails admin password;
3. Proceed with the update when prompted by the graphical updater.
4. Cancel the update and reboot the workstation at the step that requires a Tails admin password.
5. Proceed with the update when prompted by the graphical updater.

## Expected Behavior

I can successfully complete the update.

## Actual Behavior

The update fails ("Update failed").


Discussion:
Hey folks - any proposed workaround for this issue? Have a user reporting it in our Support Portal.
Yup, this is caused by a flag file that does not get removed when the updater fails in this scenario.

The most straightforward workaround is to:
- delete the file `~amnesia/Persistent/.securedrop/securedrop_update.flag` on the Admin Workstation
- reboot Tails and start with the PV unlocked and an admin password set.

If the `git checkout` portion of the update completed successfully, the GUI updater will no longer appear. If you want to force it to run again (not necessary for releases that don't require a `setup` or `tailsconfig` update), open a terminal and run:
```
cd ~/Persistent/securedrop
git tag -v <previous release here> # verify that tag is signed
git checkout <previous release here>
```
Then, bounce the network connection. The GUI updater will appear and should work correctly, prompting you for the Tails admin pw about halfway through.

Resolving patch:
diff --git a/journalist_gui/journalist_gui/SecureDropUpdater.py b/journalist_gui/journalist_gui/SecureDropUpdater.py
--- a/journalist_gui/journalist_gui/SecureDropUpdater.py
+++ b/journalist_gui/journalist_gui/SecureDropUpdater.py
@@ -15,6 +15,15 @@
 ESCAPE_POD = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
 
 
+def password_is_set():
+
+    pwd_flag = subprocess.check_output(['passwd', '--status']).decode('utf-8').split()[1]
+
+    if pwd_flag == 'NP':
+        return False
+    return True
+
+
 def prevent_second_instance(app: QtWidgets.QApplication, name: str) -> None:  # noqa
 
     # Null byte triggers abstract namespace
@@ -27,7 +36,7 @@ def prevent_second_instance(app: QtWidgets.QApplication, name: str) -> None:  #
     except OSError as e:
         if e.errno == ALREADY_BOUND_ERRNO:
             err_dialog = QtWidgets.QMessageBox()
-            err_dialog.setText(name + ' is already running.')
+            err_dialog.setText(name + strings.app_is_already_running)
             err_dialog.exec()
             sys.exit()
         else:
@@ -280,11 +289,17 @@ def on_failure(self):
         self.progressBar.setProperty("value", 0)
 
     def update_securedrop(self):
-        self.pushButton_2.setEnabled(False)
-        self.pushButton.setEnabled(False)
-        self.progressBar.setProperty("value", 10)
-        self.update_status_bar_and_output(strings.fetching_update)
-        self.update_thread.start()
+        if password_is_set():
+            self.pushButton_2.setEnabled(False)
+            self.pushButton.setEnabled(False)
+            self.progressBar.setProperty("value", 10)
+            self.update_status_bar_and_output(strings.fetching_update)
+            self.update_thread.start()
+        else:
+            self.pushButton_2.setEnabled(False)
+            pwd_err_dialog = QtWidgets.QMessageBox()
+            pwd_err_dialog.setText(strings.no_password_set_message)
+            pwd_err_dialog.exec()
 
     def alert_success(self):
         self.success_dialog = QtWidgets.QMessageBox()
diff --git a/journalist_gui/journalist_gui/strings.py b/journalist_gui/journalist_gui/strings.py
--- a/journalist_gui/journalist_gui/strings.py
+++ b/journalist_gui/journalist_gui/strings.py
@@ -42,3 +42,7 @@
 initial_text_box = ("When the update begins, this area will populate with "
                     "output.\n")
 doing_setup = "Checking dependencies are up to date... (2 mins remaining)"
+no_password_set_message = ("The Tails Administration Password was not set.\n\n"
+                           "Please reboot and set a password before updating "
+                           "SecureDrop.")
+app_is_already_running = " is already running."


Test patch:
diff --git a/journalist_gui/test_gui.py b/journalist_gui/test_gui.py
--- a/journalist_gui/test_gui.py
+++ b/journalist_gui/test_gui.py
@@ -250,6 +250,14 @@ def test_tails_status_failure(self):
         mock_remove.assert_not_called()
         self.assertEqual(self.window.progressBar.value(), 0)
 
+    @mock.patch('journalist_gui.SecureDropUpdater.QtWidgets.QMessageBox')
+    def test_no_update_without_password(self, mock_msgbox):
+        with mock.patch('journalist_gui.SecureDropUpdater.password_is_set',
+                        return_value=False):
+            self.window.update_securedrop()
+        self.assertEqual(self.window.pushButton.isEnabled(), True)
+        self.assertEqual(self.window.pushButton_2.isEnabled(), False)
+
 
 if __name__ == '__main__':
     unittest.main()