diff --git a/application/Espo/Controllers/TwoFactorEmail.php b/application/Espo/Controllers/TwoFactorEmail.php index 2cb6efe742..82e8ff1666 100644 --- a/application/Espo/Controllers/TwoFactorEmail.php +++ b/application/Espo/Controllers/TwoFactorEmail.php @@ -55,7 +55,8 @@ class TwoFactorEmail if ( !$this->user->isAdmin() && - !$this->user->isRegular() + !$this->user->isRegular() && + !$this->user->isPortal() ) { throw new Forbidden(); } diff --git a/application/Espo/Controllers/TwoFactorSms.php b/application/Espo/Controllers/TwoFactorSms.php index 257d89ac71..9293003637 100644 --- a/application/Espo/Controllers/TwoFactorSms.php +++ b/application/Espo/Controllers/TwoFactorSms.php @@ -55,7 +55,8 @@ class TwoFactorSms if ( !$this->user->isAdmin() && - !$this->user->isRegular() + !$this->user->isRegular() && + !$this->user->isPortal() ) { throw new Forbidden(); } diff --git a/application/Espo/Controllers/UserSecurity.php b/application/Espo/Controllers/UserSecurity.php index 5f4949019f..571acb9ce9 100644 --- a/application/Espo/Controllers/UserSecurity.php +++ b/application/Espo/Controllers/UserSecurity.php @@ -57,7 +57,8 @@ class UserSecurity if ( !$this->user->isAdmin() && - !$this->user->isRegular() + !$this->user->isRegular() && + !$this->user->isPortal() ) { throw new Forbidden(); } diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php index 37686c7ce1..923d04576f 100644 --- a/application/Espo/Resources/defaults/config.php +++ b/application/Espo/Resources/defaults/config.php @@ -204,6 +204,7 @@ return [ 'useWebSocket' => false, 'webSocketMessager' => 'ZeroMQ', 'auth2FAMethodList' => ['Totp'], + 'auth2FAInPortal' => false, 'personNameFormat' => 'firstLast', 'newNotificationCountInTitle' => false, 'pdfEngine' => 'Dompdf', diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index 82ad47c16e..a77634b2d8 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -141,6 +141,7 @@ "auth2FA": "Enable 2-Factor Authentication", "auth2FAForced": "Force regular users to set up 2FA", "auth2FAMethodList": "Available 2FA methods", + "auth2FAInPortal": "Allow 2FA in portals", "workingTimeCalendar": "Working Time Calendar", "oidcClientId": "OIDC Client ID", "oidcClientSecret": "OIDC Client Secret", diff --git a/application/Espo/Resources/layouts/Settings/authentication.json b/application/Espo/Resources/layouts/Settings/authentication.json index 3e45e569bc..08e8854ce2 100644 --- a/application/Espo/Resources/layouts/Settings/authentication.json +++ b/application/Espo/Resources/layouts/Settings/authentication.json @@ -10,7 +10,7 @@ "label": "2-Factor Authentication", "rows": [ [{"name": "auth2FA"}, {"name": "auth2FAMethodList"}], - [{"name": "auth2FAForced"}, false] + [{"name": "auth2FAForced"}, {"name": "auth2FAInPortal"}] ] }, { diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index f93f240ade..e7e7e7d1db 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -289,6 +289,9 @@ "auth2FAForced": { "type": "bool" }, + "auth2FAInPortal": { + "type": "bool" + }, "passwordRecoveryDisabled": { "type": "bool" }, diff --git a/application/Espo/Tools/UserSecurity/Service.php b/application/Espo/Tools/UserSecurity/Service.php index 814f143f14..800aad502a 100644 --- a/application/Espo/Tools/UserSecurity/Service.php +++ b/application/Espo/Tools/UserSecurity/Service.php @@ -76,7 +76,12 @@ class Service throw new NotFound(); } - if (!$user->isAdmin() && !$user->isRegular()) { + $allow = + $user->isAdmin() || + $user->isRegular() || + $user->isPortal() && $this->config->get('auth2FAInPortal'); + + if (!$allow) { throw new Forbidden(); } @@ -116,7 +121,15 @@ class Service throw new NotFound(); } - if (!$user->isAdmin() && !$user->isRegular()) { + $allow = + $this->config->get('auth2FA') && + ( + $user->isAdmin() || + $user->isRegular() || + $user->isPortal() && $this->config->get('auth2FAInPortal') + ); + + if (!$allow) { throw new Forbidden(); } @@ -178,7 +191,12 @@ class Service throw new NotFound(); } - if (!$user->isAdmin() && !$user->isRegular()) { + $allow = + $user->isAdmin() || + $user->isRegular() || + $user->isPortal() && $this->config->get('auth2FAInPortal'); + + if (!$allow) { throw new Forbidden(); } @@ -219,7 +237,11 @@ class Service if ( $userData->get('auth2FA') && $userData->get('auth2FAMethod') && - ($userData->isAttributeChanged('auth2FA') || $userData->isAttributeChanged('auth2FAMethod')) + ($userData->isAttributeChanged('auth2FA') || $userData->isAttributeChanged('auth2FAMethod')) && + ( + !$user->isPortal() || + $this->config->get('auth2FAInPortal') + ) ) { $auth2FAMethod = $userData->get('auth2FAMethod'); diff --git a/application/Espo/Tools/UserSecurity/TwoFactor/EmailService.php b/application/Espo/Tools/UserSecurity/TwoFactor/EmailService.php index 93a43dc3e5..e8ffe63d0a 100644 --- a/application/Espo/Tools/UserSecurity/TwoFactor/EmailService.php +++ b/application/Espo/Tools/UserSecurity/TwoFactor/EmailService.php @@ -93,6 +93,10 @@ class EmailService throw new Forbidden("2FA is not enabled."); } + if ($this->user->isPortal() && !$this->config->get('auth2FAInPortal')) { + throw new Forbidden("2FA is not enabled in portals."); + } + $methodList = $this->config->get('auth2FAMethodList') ?? []; if (!in_array(EmailLogin::NAME, $methodList)) { diff --git a/application/Espo/Tools/UserSecurity/TwoFactor/SmsService.php b/application/Espo/Tools/UserSecurity/TwoFactor/SmsService.php index 6f76049bbe..071703b468 100644 --- a/application/Espo/Tools/UserSecurity/TwoFactor/SmsService.php +++ b/application/Espo/Tools/UserSecurity/TwoFactor/SmsService.php @@ -93,6 +93,10 @@ class SmsService throw new Forbidden("2FA is not enabled."); } + if ($this->user->isPortal() && !$this->config->get('auth2FAInPortal')) { + throw new Forbidden("2FA is not enabled in portals."); + } + $methodList = $this->config->get('auth2FAMethodList') ?? []; if (!in_array(SmsLogin::NAME, $methodList)) { diff --git a/client/src/views/admin/authentication.js b/client/src/views/admin/authentication.js index b84b29c822..9c169849ac 100644 --- a/client/src/views/admin/authentication.js +++ b/client/src/views/admin/authentication.js @@ -155,6 +155,7 @@ define('views/admin/authentication', ['views/settings/record/edit'], function (D if (this.model.get('auth2FA')) { this.showField('auth2FAForced'); this.showField('auth2FAMethodList'); + this.showField('auth2FAInPortal'); this.setFieldRequired('auth2FAMethodList'); return; @@ -162,6 +163,7 @@ define('views/admin/authentication', ['views/settings/record/edit'], function (D this.hideField('auth2FAForced'); this.hideField('auth2FAMethodList'); + this.hideField('auth2FAInPortal'); this.setFieldNotRequired('auth2FAMethodList'); }, diff --git a/client/src/views/user/record/detail.js b/client/src/views/user/record/detail.js index e48981d893..24ed173594 100644 --- a/client/src/views/user/record/detail.js +++ b/client/src/views/user/record/detail.js @@ -51,10 +51,16 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) { } } + let isPortalUser = this.model.isPortal() || + this.model.id === this.getUser().id && this.getUser().isPortal(); + if ( - (this.model.id == this.getUser().id || this.getUser().isAdmin()) && - (this.model.isRegular() || this.model.isAdmin()) && - this.getConfig().get('auth2FA') + (this.model.id === this.getUser().id || this.getUser().isAdmin()) && + this.getConfig().get('auth2FA') && + ( + (this.model.isRegular() || this.model.isAdmin()) || + isPortalUser && this.getConfig().get('auth2FAInPortal') + ) ) { this.addButton({ name: 'viewSecurity',