diff --git a/application/Espo/Controllers/User.php b/application/Espo/Controllers/User.php index e3d97ae31a..5ab63e5394 100644 --- a/application/Espo/Controllers/User.php +++ b/application/Espo/Controllers/User.php @@ -69,24 +69,29 @@ class User extends \Espo\Core\Controllers\Record throw new BadRequest(); } - $p = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where(array( - 'requestId' => $data->requestId - ))->findOne(); + if ($this->getConfig()->get('passwordRecoveryDisabled')) { + throw new Forbidden("Password recovery disabled"); + } - if (!$p) { + $request = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where([ + 'requestId' => $data->requestId + ])->findOne(); + + if (!$request) { throw new Forbidden(); } - $userId = $p->get('userId'); + + $userId = $request->get('userId'); if (!$userId) { throw new Error(); } - $this->getEntityManager()->removeEntity($p); + $this->getEntityManager()->removeEntity($request); if ($this->getService('User')->changePassword($userId, $data->password)) { - return array( - 'url' => $p->get('url') - ); + return [ + 'url' => $request->get('url') + ]; } } diff --git a/application/Espo/Core/defaults/systemConfig.php b/application/Espo/Core/defaults/systemConfig.php index 191fef3862..406995ec9c 100644 --- a/application/Espo/Core/defaults/systemConfig.php +++ b/application/Espo/Core/defaults/systemConfig.php @@ -198,6 +198,8 @@ return [ 'cleanupDeletedRecords', 'authTokenPreventConcurrent', 'emailParser', + 'passwordRecoveryDisabled', + 'passwordRecoveryForAdminDisabled', 'latestVersion', ], 'superAdminItems' => [ diff --git a/application/Espo/EntryPoints/ChangePassword.php b/application/Espo/EntryPoints/ChangePassword.php index 902466fed6..1ae2cfcb62 100644 --- a/application/Espo/EntryPoints/ChangePassword.php +++ b/application/Espo/EntryPoints/ChangePassword.php @@ -39,21 +39,18 @@ class ChangePassword extends \Espo\Core\EntryPoints\Base public function run() { - $requestId = $_GET['id']; - if (empty($requestId)) { - throw new BadRequest(); - } + $requestId = $_GET['id'] ?? null; + + if (!$requestId) throw new BadRequest(); $config = $this->getConfig(); $themeManager = $this->getThemeManager(); - $p = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where(array( + $request = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where([ 'requestId' => $requestId - ))->findOne(); + ])->findOne(); - if (!$p) { - throw new NotFound(); - } + if (!$request) throw new NotFound(); $runScript = " app.getController('PasswordChangeRequest', function (controller) { @@ -69,4 +66,3 @@ class ChangePassword extends \Espo\Core\EntryPoints\Base return $this->getContainer()->get('themeManager'); } } - diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index c3753d849d..c8e9bde1fd 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -119,6 +119,8 @@ "cronDisabled": "Disable Cron", "maintenanceMode": "Maintenance Mode", "useWebSocket": "Use WebSocket", + "passwordRecoveryDisabled": "Disable password recovery", + "passwordRecoveryForAdminDisabled": "Disable password recover for admin users", "auth2FA": "Enable 2-Factor Authentication", "auth2FAMethodList": "Available 2FA methods" }, @@ -207,6 +209,7 @@ "Connecting": "Connecting...", "Activities": "Activities", "Admin Notifications": "Admin Notifications", + "Passwords": "Passwords", "2-Factor Authentication": "2-Factor Authentication" }, "messages": { diff --git a/application/Espo/Resources/layouts/Settings/authentication.json b/application/Espo/Resources/layouts/Settings/authentication.json index fe52174f97..b6349db6e1 100644 --- a/application/Espo/Resources/layouts/Settings/authentication.json +++ b/application/Espo/Resources/layouts/Settings/authentication.json @@ -11,5 +11,11 @@ "rows": [ [{"name": "auth2FA"}, {"name": "auth2FAMethodList"}] ] + }, + { + "label": "Passwords", + "rows": [ + [{"name": "passwordRecoveryDisabled"}, {"name": "passwordRecoveryForAdminDisabled"}] + ] } ] diff --git a/application/Espo/Resources/metadata/entityDefs/PasswordChangeRequest.json b/application/Espo/Resources/metadata/entityDefs/PasswordChangeRequest.json index a71b3097c6..4b8fd244c7 100644 --- a/application/Espo/Resources/metadata/entityDefs/PasswordChangeRequest.json +++ b/application/Espo/Resources/metadata/entityDefs/PasswordChangeRequest.json @@ -2,7 +2,7 @@ "fields": { "requestId": { "type": "varchar", - "maxLength": 24, + "maxLength": 64, "index": true }, "user": { diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index 19cb2e724d..9ae8647b6d 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -160,6 +160,12 @@ "type": "multiEnum", "view": "views/settings/fields/auth-two-fa-method-list" }, + "passwordRecoveryDisabled": { + "type": "bool" + }, + "passwordRecoveryForAdminDisabled": { + "type": "bool" + }, "ldapHost": { "type": "varchar", "required": true diff --git a/application/Espo/Services/Settings.php b/application/Espo/Services/Settings.php index 27af265a50..d35064cc34 100644 --- a/application/Espo/Services/Settings.php +++ b/application/Espo/Services/Settings.php @@ -111,7 +111,7 @@ class Settings extends \Espo\Core\Services\Base } } - if ($this->getConfig()->get('smtpServer')) { + if ($this->getConfig()->get('smtpServer') && !$this->getConfig()->geT('passwordRecoveryDisabled')) { $data->passwordRecoveryEnabled = true; } diff --git a/application/Espo/Services/User.php b/application/Espo/Services/User.php index 5cdc690e5a..e990b8d4f1 100644 --- a/application/Espo/Services/User.php +++ b/application/Espo/Services/User.php @@ -152,6 +152,10 @@ class User extends Record public function passwordChangeRequest($userName, $emailAddress, $url = null) { + if ($this->getConfig()->get('passwordRecoveryDisabled')) { + throw new Forbidden("Password recovery disabled"); + } + $user = $this->getEntityManager()->getRepository('User')->where([ 'userName' => $userName, 'emailAddress' => $emailAddress @@ -165,6 +169,16 @@ class User extends Record throw new NotFound(); } + if ($user->isApi()) { + throw new NotFound(); + } + + if ($this->getConfig()->get('passwordRecoveryForAdminDisabled')) { + if ($user->isAdmin()) { + throw new NotFound(); + } + } + $userId = $user->id; $passwordChangeRequest = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where([ @@ -174,7 +188,7 @@ class User extends Record throw new Forbidden(json_encode(['reason' => 'Already-Sent'])); } - $requestId = Util::generateId(); + $requestId = Util::generateId() . Util::generateKey(); $passwordChangeRequest = $this->getEntityManager()->getEntity('PasswordChangeRequest'); $passwordChangeRequest->set([