[TASK] Add informational upgrade wizard for argon2i 11/58411/7
authorChristian Kuhn <lolli@schwarzbu.ch>
Thu, 27 Sep 2018 13:35:39 +0000 (15:35 +0200)
committerFrank Naegler <frank.naegler@typo3.org>
Sat, 29 Sep 2018 11:28:48 +0000 (13:28 +0200)
This adds a dummy wizard to remind admins during upgrade
to check the live system for argon2i support if the local
instance uses it, or to select a different hash algorithm.
Having this wizard gives this information to admins early
in the upgrade phase, so they have time to check the live
system or to select a different mechanism before too many
passwords have been upgraded.

Resolves: #86402
Releases: master
Change-Id: I2b1f75ecf079dc2e29d2675dda558c79b67f77e0
Reviewed-on: https://review.typo3.org/58411
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php
typo3/sysext/install/Classes/Service/UpgradeWizardsService.php
typo3/sysext/install/Classes/Updates/Argon2iPasswordHashes.php [new file with mode: 0644]
typo3/sysext/install/Classes/Updates/Confirmation.php
typo3/sysext/install/ext_localconf.php

index 3e79e5f..27ad21e 100644 (file)
@@ -216,18 +216,24 @@ class UpgradeWizardRunCommand extends Command
         $this->output->title('Running Wizard ' . $instance->getTitle());
         if ($instance instanceof ConfirmableInterface) {
             $confirmation = $instance->getConfirmation();
-            $defaultString = $confirmation->getDefaultValue() ? '(Y/n)' : '(y/N)';
+            $defaultString = $confirmation->getDefaultValue() ? 'Y/n' : 'y/N';
             $question = new ConfirmationQuestion(
                 sprintf(
-                    '<info>%s</info>' . LF . '%s %s',
+                    '<info>%s</info>' . LF . '%s' . LF . '%s %s (%s)',
                     $confirmation->getTitle(),
                     $confirmation->getMessage(),
+                    $confirmation->getConfirm(),
+                    $confirmation->getDeny(),
                     $defaultString
                 ),
                 $confirmation->getDefaultValue()
             );
             $helper = $this->getHelper('question');
             if (!$helper->ask($this->input, $this->output, $question)) {
+                if ($confirmation->isRequired()) {
+                    $this->output->error('You have to acknowledge this wizard to continue');
+                    return 1;
+                }
                 if ($instance instanceof RepeatableInterface) {
                     $this->output->note('No changes applied.');
                 } else {
index d7e20df..fc8fd01 100644 (file)
@@ -303,28 +303,28 @@ class UpgradeWizardsService
                 E_USER_DEPRECATED
             );
         } elseif ($updateObject instanceof UpgradeWizardInterface && $updateObject instanceof ConfirmableInterface) {
-            $wizardHtml = '
-            <div class="panel panel-danger">
-                <div class="panel-heading">
-                ' . htmlspecialchars($updateObject->getConfirmation()->getTitle()) . '
-                </div>
-                <div class="panel-body">
-                    <p>' . nl2br(htmlspecialchars($updateObject->getConfirmation()->getMessage())) . '</p>
-                    <div class="btn-group" data-toggle="buttons">
-                        <label class="btn btn-default active">
-                            <input type="radio" name="install[values][' .
-                htmlspecialchars($updateObject->getIdentifier()) .
-                '][install]" value="0" checked="checked" /> No, skip wizard
-                        </label>
-                        <label class="btn btn-default">
-                            <input type="radio" name="install[values][' .
-                htmlspecialchars($updateObject->getIdentifier()) .
-                '][install]" value="1" /> Yes, execute wizard
-                        </label>
-                    </div>
-                </div>
-            </div>
-        ';
+            $markup = [];
+            $radioAttributes = [
+                'type' => 'radio',
+                'name' => 'install[values][' . $updateObject->getIdentifier() . '][install]',
+                'value' => 0
+            ];
+            $markup[] = '<div class="panel panel-danger">';
+            $markup[] = '   <div class="panel-heading">';
+            $markup[] = htmlspecialchars($updateObject->getConfirmation()->getTitle());
+            $markup[] = '    </div>';
+            $markup[] = '    <div class="panel-body">';
+            $markup[] = '        <p>' . nl2br(htmlspecialchars($updateObject->getConfirmation()->getMessage())) . '</p>';
+            $markup[] = '        <div class="btn-group" data-toggle="buttons">';
+            if (!$updateObject->getConfirmation()->isRequired()) {
+                $markup[] = '        <label class="btn btn-default active"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' checked="checked" />' . $updateObject->getConfirmation()->getDeny() . '</label>';
+            }
+            $radioAttributes['value'] = 1;
+            $markup[] = '            <label class="btn btn-default"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' />' . $updateObject->getConfirmation()->getConfirm() . '</label>';
+            $markup[] = '        </div>';
+            $markup[] = '    </div>';
+            $markup[] = '</div>';
+            $wizardHtml = implode('', $markup);
         }
 
         $result = [
@@ -372,15 +372,25 @@ class UpgradeWizardsService
         } else {
             if ($updateObject instanceof UpgradeWizardInterface) {
                 $requestParams = GeneralUtility::_GP('install');
-                if ($updateObject instanceof ConfirmableInterface
-                    && (
-                        isset($requestParams['values'][$updateObject->getIdentifier()]['install'])
-                        && empty($requestParams['values'][$updateObject->getIdentifier()]['install'])
-                    )
-                ) {
-                    $this->output->writeln('No changes applied, marking wizard as done.');
-                    // confirmation was set to "no"
-                    $performResult = true;
+                if ($updateObject instanceof ConfirmableInterface) {
+                    // value is set in request but is empty
+                    $isSetButEmpty = isset($requestParams['values'][$updateObject->getIdentifier()]['install'])
+                        && empty($requestParams['values'][$updateObject->getIdentifier()]['install']);
+
+                    $checkValue = (int)$requestParams['values'][$updateObject->getIdentifier()]['install'];
+
+                    if ($checkValue === 1) {
+                        // confirmation = yes, we do the update
+                        $performResult = $updateObject->executeUpdate();
+                    } elseif ($updateObject->getConfirmation()->isRequired()) {
+                        // confirmation = no, but is required, we do *not* the update and fail
+                        $performResult = false;
+                    } elseif ($isSetButEmpty) {
+                        // confirmation = no, but it is *not* required, we do *not* the update, but mark the wizard as done
+                        $this->output->writeln('No changes applied, marking wizard as done.');
+                        // confirmation was set to "no"
+                        $performResult = true;
+                    }
                 } else {
                     // confirmation yes or non-confirmable
                     $performResult = $updateObject->executeUpdate();
diff --git a/typo3/sysext/install/Classes/Updates/Argon2iPasswordHashes.php b/typo3/sysext/install/Classes/Updates/Argon2iPasswordHashes.php
new file mode 100644 (file)
index 0000000..1dce8f3
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Install\Updates;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Informational upgrade wizard to remind upgrading instances
+ * may have to verify argon2i is available on the live servers
+ */
+class Argon2iPasswordHashes implements UpgradeWizardInterface, ConfirmableInterface
+{
+    protected $confirmation;
+
+    public function __construct()
+    {
+        $this->confirmation = new Confirmation(
+            'Please make sure to read the following carefully:',
+            $this->getDescription(),
+            false,
+            'Yes, I understand!',
+            '',
+            true
+        );
+    }
+
+    /**
+     * @return string Unique identifier of this updater
+     */
+    public function getIdentifier(): string
+    {
+        return 'argon2iPasswordHashes';
+    }
+
+    /**
+     * @return string Title of this updater
+     */
+    public function getTitle(): string
+    {
+        return 'Reminder to verify live system supports argon2i';
+    }
+
+    /**
+     * @return string Longer description of this updater
+     */
+    public function getDescription(): string
+    {
+        return 'TYPO3 uses the modern hash mechanism "argon2i" on this system. Existing passwords'
+               . ' will be automatically upgraded to this mechanism upon user login. If this instance'
+               . ' is later deployed to a different system, make sure the system does support argon2i'
+               . ' too, otherwise logins will fail. If that is not possible, select a different hash'
+               . ' algorithm in Setting > Presets > Password hashing settings and make sure no user'
+               . ' has been upgraded yet. This upgrade wizard exists only to inform you, it does not'
+               . ' change the system';
+    }
+
+    /**
+     * Checks whether updates are required.
+     *
+     * @return bool Whether an update is required (TRUE) or not (FALSE)
+     */
+    public function updateNecessary(): bool
+    {
+        $passwordHashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
+        $feHash = $passwordHashFactory->getDefaultHashInstance('BE');
+        $beHash = $passwordHashFactory->getDefaultHashInstance('FE');
+        return $feHash instanceof Argon2iPasswordHash || $beHash instanceof Argon2iPasswordHash;
+    }
+
+    /**
+     * @return string[] All new fields and tables must exist
+     */
+    public function getPrerequisites(): array
+    {
+        return [
+            DatabaseUpdatedPrerequisite::class,
+        ];
+    }
+
+    /**
+     * This upgrade wizard has informational character only, it does not perform actions.
+     *
+     * @return bool Whether everything went smoothly or not
+     */
+    public function executeUpdate(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Return a confirmation message instance
+     *
+     * @return \TYPO3\CMS\Install\Updates\Confirmation
+     */
+    public function getConfirmation(): Confirmation
+    {
+        return $this->confirmation;
+    }
+}
index b430869..c0950d3 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 declare(strict_types = 1);
+
 namespace TYPO3\CMS\Install\Updates;
 
 /*
@@ -33,15 +34,66 @@ class Confirmation
     protected $message = '';
 
     /**
+     * @var string
+     */
+    protected $confirm;
+
+    /**
+     * @var string
+     */
+    protected $deny;
+
+    /**
+     * @var bool
+     */
+    protected $required;
+
+    /**
      * @param string $title
      * @param string $message
      * @param bool $defaultValue
+     * @param string $confirm
+     * @param string $deny
+     * @param bool $required
      */
-    public function __construct(string $title, string $message, bool $defaultValue = false)
-    {
+    public function __construct(
+        string $title,
+        string $message,
+        bool $defaultValue = false,
+        string $confirm = 'Yes, execute',
+        string $deny = 'No, do not execute',
+        bool $required = false
+    ) {
         $this->title = $title;
         $this->message = $message;
         $this->defaultValue = $defaultValue;
+        $this->confirm = $confirm;
+        $this->deny = $deny;
+        $this->required = $required;
+    }
+
+    /**
+     * @return string
+     */
+    public function getConfirm(): string
+    {
+        return $this->confirm;
+    }
+
+    /**
+     * @return string
+     */
+    public function getDeny(): string
+    {
+        return $this->deny;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isRequired(): bool
+    {
+        return $this->required;
     }
 
     /**
index a113b15..90162ed 100644 (file)
@@ -66,6 +66,8 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['adminpanelEx
     = \TYPO3\CMS\Install\Updates\AdminPanelInstall::class;
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['pagesSlugs']
     = \TYPO3\CMS\Install\Updates\PopulatePageSlugs::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['argon2iPasswordHashes']
+    = \TYPO3\CMS\Install\Updates\Argon2iPasswordHashes::class;
 
 $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
 $icons = [