CVE-2023-3293: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in GitHub repository salesagility/suitecrm-core prior to 8.3.0.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 4.8
Published: 2023-06-16T11:15Z
Repository: https://github.com/salesagility/suitecrm-core
Language: PHP
Files changed: .env, README.md, VERSION, config/core_services.yaml, config/packages/security.php, config/services/system/admin_only_module_actions.yaml, core/app/common/package.json, core/app/core/package.json, core/app/core/src/lib/store/record-list/record-list.store.ts, core/app/core/src/lib/views/login/components/login/login.component.ts, core/backend/Engine/LegacyHandler/AclHandler.php, core/backend/Install/Service/Installation/InstallActionHandler.php, core/backend/Languages/LegacyHandler/SetSessionLanguage.php, core/backend/Migrations/Version20230420135520.php, core/backend/Module/LegacyHandler/Favorites/UpdateFavorite.php, core/backend/Module/LegacyHandler/RecentlyViewed/AddRecentlyViewed.php, core/backend/Module/LegacyHandler/RecentlyViewed/RecentlyViewedHandler.php, core/backend/Process/DataPersister/ProcessDataPersister.php, core/backend/Process/LegacyHandler/ACL/UserACLHandler.php, core/backend/Process/LegacyHandler/AddContactsToTargetListBulkActionHandler.php, core/backend/Process/LegacyHandler/AddRecordsToTargetListBulkActionHandler.php, core/backend/Process/LegacyHandler/LinkRelationHandler.php, core/backend/Process/LegacyHandler/ResetPasswordHandler.php, core/backend/Process/LegacyHandler/UnlinkRelationHandler.php, core/backend/Process/Service/BulkActions/CsvExportBulkAction.php, core/backend/Process/Service/BulkActions/DeleteRecordsBulkAction.php, core/backend/Process/Service/BulkActions/MassUpdateBulkAction.php, core/backend/Process/Service/BulkActions/MergeRecordsBulkAction.php, core/backend/Process/Service/BulkActions/PrintAsPdfBulkAction.php, core/backend/Process/Service/ProcessHandlerInterface.php, core/backend/Process/Service/RecordActions/ChangeLogAction.php, core/backend/Process/Service/RecordActions/ConvertLeadAction.php, core/backend/Process/Service/RecordActions/DeleteRecordAction.php, core/backend/Process/Service/RecordActions/DuplicateRecordAction.php, core/backend/Process/Service/RecordActions/EditAction.php, core/backend/Process/Service/RecordActions/MergeDuplicateAction.php, core/backend/Process/Service/RecordActions/PrintAsPdfAction.php, core/backend/Process/Service/RecordThreadItemActions/DeleteRecordThreadItemAction.php, core/backend/Process/Service/RecordThreadItemActions/DismissAlertAction.php, core/backend/Process/Service/RecordThreadItemActions/SnoozeRecordThreadItemAction.php, core/backend/Process/Service/RecordThreadListActions/DeleteAllRecordThreadListAction.php, core/backend/Process/Service/RecordThreadListActions/DismissAllAlertsRecordThreadListAction.php, core/backend/Process/Service/RecordThreadListActions/MarkAsReadRecordThreadListAction.php, core/backend/UserPreferences/LegacyHandler/SaveUiPreferences.php, package.json, public/legacy/data/SugarBean.php, public/legacy/include/MVC/SugarApplication.php, public/legacy/include/Smarty/plugins/function.sugarvar.php, public/legacy/include/Smarty/plugins/modifier.htmlpurify.php, public/legacy/include/SugarFields/Fields/Text/SugarFieldText.php, public/legacy/include/modules.php, public/legacy/include/portability/ApiBeanMapper/ApiBeanMapper.php, public/legacy/include/portability/ApiBeanMapper/TypeMappers/HtmlMapper.php, public/legacy/include/portability/ApiBeanMapper/TypeMappers/TextMapper.php, public/legacy/include/portability/ListView/ListViewDataPort.php, public/legacy/include/portability/Services/Relationships/AddContactsToTargetListService.php, public/legacy/include/utils.php, public/legacy/modules/SugarFeed/Dashlets/SugarFeedDashlet/SugarFeedDashlet.php
Commit date: 2023-05-15 13:23:12 +0100
Commit message: SuiteCRM 8.3.0 Release

Diff: diff --git a/.env b/.env
index 516c1f82..a1838395 100644
--- a/.env
+++ b/.env
@@ -59,5 +59,10 @@ LOCK_DSN=flock
 ###< symfony/lock ###
 
 ###> login throttling ###
 LOGIN_THROTTLING_MAX_ATTEMPTS=5
 ###< login throttling ###
+
+###> login throttling ###
+LOGIN_THROTTLING_IP_LOGIN_MAX_ATTEMPTS=50
+LOGIN_THROTTLING_INTERVAL="30 minutes"
+###< login throttling ###
diff --git a/README.md b/README.md
index 4d28a9a0..5aad4bf4 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
 <a href="https://suitecrm.com">
   <img width="180px" height="41px" src="https://suitecrm.com/wp-content/uploads/2017/12/logo.png" align="right" />
 </a>
 
-# SuiteCRM 8.3-beta
+# SuiteCRM 8.3.0
 
 [![LICENSE](https://img.shields.io/github/license/suitecrm/suitecrm.svg)](https://github.com/salesagility/suitecrm/blob/hotfix/LICENSE.txt)
 [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/salesagility/SuiteCRM-Core/issues)
 [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/suitecrm/Lobby)
 [![Twitter](https://img.shields.io/twitter/follow/suitecrm.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=suitecrm)
diff --git a/VERSION b/VERSION
index 9d33d64d..2bf50aaf 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.3-beta
+8.3.0
diff --git a/config/core_services.yaml b/config/core_services.yaml
index d68a8b4b..e27b21c0 100644
--- a/config/core_services.yaml
+++ b/config/core_services.yaml
@@ -60,10 +60,11 @@ services:
       $subpanelTopButtons: '%module.subpanel.top_buttons%'
       $ldapAutoCreateExtraFieldsMap: '%ldap.autocreate.extra_fields_map%'
       $samlAutoCreateAttributesMap: '%saml.autocreate.attributes_map%'
       $logoutConfig: '%auth.logout%'
       $sessionExpiredConfig: '%auth.session-expired%'
+      $adminOnlyModuleActions: '%system.admin_only_module_actions%'
       $navbarAdministrationOverrides: '%navbar.administration_override%'
   _instanceof:
     App\Process\Service\ProcessHandlerInterface:
       tags: [ 'app.process.handler' ]
     App\UserPreferences\LegacyHandler\UserPreferencesMapperInterface:
diff --git a/config/packages/security.php b/config/packages/security.php
index d6faf129..45fb388f 100644
--- a/config/packages/security.php
+++ b/config/packages/security.php
@@ -30,11 +30,13 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
 use App\Module\Users\Entity\User;
 use App\Security\Ldap\AppLdapUserProvider;
 use App\Security\Saml\AppSamlAuthenticator;
 use App\Security\UserChecker;
 use Symfony\Component\DependencyInjection\Container;
+use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\Ldap\Ldap;
+use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter;
 
 /** @var $container Container */
 if (!isset($container)) {
     return;
 }
@@ -42,11 +44,40 @@ if (!isset($container)) {
 return static function (ContainerConfigurator $containerConfig) {
 
     $env = $_ENV ?? [];
     $authType = $env['AUTH_TYPE'] ?? 'native';
 
-    $maxAttempts = (int)($env['LOGIN_THROTTLING_MAX_ATTEMPTS'] ?? 3);
+    $maxAttempts = (int)($env['LOGIN_THROTTLING_MAX_ATTEMPTS'] ?? 5);
+    $ipLoginMaxAttempts = (int)($env['LOGIN_THROTTLING_IP_LOGIN_MAX_ATTEMPTS'] ?? 50);
+    $loginThrottlingInterval = (string)($env['LOGIN_THROTTLING_INTERVAL'] ?? '30 minutes');
+
+    $containerConfig->extension('framework', [
+        'rate_limiter' => [
+            // define 2 rate limiters (one for username+IP, the other for IP)
+            'username_ip_login' => [
+                'policy' => 'token_bucket',
+                'limit' => $maxAttempts,
+                'rate' => [ 'interval' => $loginThrottlingInterval ],
+            ],
+            'ip_login' => [
+                'policy' => 'sliding_window',
+                'limit' => $ipLoginMaxAttempts,
+                'interval' => $loginThrottlingInterval,
+            ],
+        ],
+    ]);
+
+    $containerConfig->services()->set('app.login_rate_limiter')
+        ->class(DefaultLoginRateLimiter::class)
+        ->args(
+            [
+                // 1st argument is the limiter for IP
+                new Reference('limiter.ip_login'),
+                // 2nd argument is the limiter for username+IP
+                new Reference('limiter.username_ip_login'),
+            ]
+        );
 
     $baseFirewall = [
         'dev' => [
             'pattern' => '^/(_(profiler|wdt)|css|images|js)/',
             'user_checker' => UserChecker::class,
@@ -82,11 +113,11 @@ return static function (ContainerConfigurator $containerConfig) {
                 'main' => [
                     'json_login' => [
                         'check_path' => 'app_login',
                     ],
                     'login_throttling' => [
-                        'max_attempts' => $maxAttempts
+                        'limiter' => 'app.login_rate_limiter'
                     ],
                     'logout' => [
                         'path' => 'app_logout'
                     ],
                 ],
@@ -168,11 +199,11 @@ return static function (ContainerConfigurator $containerConfig) {
             'firewalls' => array_merge_recursive($baseFirewall, [
                 'main' => [
                     'json_login_ldap' => $baseLdapConfig,
                     'provider' => $baseLdapConfig['provider'],
                     'login_throttling' => [
-                        'max_attempts' => $maxAttempts
+                        'limiter' => 'app.login_rate_limiter',
                     ],
                     'logout' => [
                         'path' => 'app_logout'
                     ]
                 ],
@@ -237,11 +268,11 @@ return static function (ContainerConfigurator $containerConfig) {
                     'json_login' => [
                         'provider' => 'app_user_provider',
                         'check_path' => 'native_auth_login',
                     ],
                     'login_throttling' => [
-                        'max_attempts' => $maxAttempts,
+                        'limiter' => 'app.login_rate_limiter',
                     ],
                     'logout' => [
                         'path' => 'native_auth_logout'
                     ]
                 ],
@@ -253,11 +284,11 @@ return static function (ContainerConfigurator $containerConfig) {
                     'json_login' => [
                         'provider' => 'app_user_provider',
                         'check_path' => 'native_auth_login',
                     ],
                     'login_throttling' => [
-                        'max_attempts' => $maxAttempts,
+                        'limiter' => 'app.login_rate_limiter',
                     ],
                     'logout' => [
                         'path' => 'native_auth_logout'
                     ]
                 ],
diff --git a/config/services/system/admin_only_module_actions.yaml b/config/services/system/admin_only_module_actions.yaml
new file mode 100644
index 00000000..977b3e60
--- /dev/null
+++ b/config/services/system/admin_only_module_actions.yaml
@@ -0,0 +1,17 @@
+parameters:
+  system.admin_only_module_actions:
+    administration:
+      '*': true
+    users:
+      'list': true
+      'index': true
+    'sugar-feed':
+      'adminsettings': true
+    'address-cache':
+      '*': true
+    maps:
+      'geocoded_counts': true
+      'geocoding_test': true
+      'config': true
+    external-oauth-connection:
+      '*': true
diff --git a/core/app/common/package.json b/core/app/common/package.json
index 7330df81..3e441a1d 100644
--- a/core/app/common/package.json
+++ b/core/app/common/package.json
@@ -1,8 +1,8 @@
 {
   "name": "common",
-  "version": "8.3-beta",
+  "version": "8.3.0",
   "peerDependencies": {
     "@angular/common": "^12.1.0",
     "@angular/core": "^12.1.0",
     "@angular/forms": "^12.1.0",
     "rxjs": "^6.6.3",
diff --git a/core/app/core/package.json b/core/app/core/package.json
index 222a18c4..859d3f04 100644
--- a/core/app/core/package.json
+++ b/core/app/core/package.json
@@ -1,8 +1,8 @@
 {
   "name": "core",
-  "version": "8.3-beta",
+  "version": "8.3.0",
   "peerDependencies": {
     "@angular/common": "^12.1.0",
     "@angular/core": "^12.1.0",
     "rxjs": "^6.6.3",
     "apollo-angular": "^2.2.0",
diff --git a/core/app/core/src/lib/store/record-list/record-list.store.ts b/core/app/core/src/lib/store/record-list/record-list.store.ts
index 880b60e1..ba070249 100644
--- a/core/app/core/src/lib/store/record-list/record-list.store.ts
+++ b/core/app/core/src/lib/store/record-list/record-list.store.ts
@@ -775,11 +775,11 @@ export class RecordListStore implements StateStore, DataSource<Record>, Selectio
      *
      * @param {} pageSizePreference to use
      * @param {string} pageSizeConfig to use
      */
     protected determinePageSize(pageSizePreference: any, pageSizeConfig: string): void {
-        let size = 0;
+        let size = 20;
 
         if (pageSizePreference) {
             size = pageSizePreference;
         } else if (pageSizeConfig) {
             size = parseInt(pageSizeConfig, 10);
diff --git a/core/app/core/src/lib/views/login/components/login/login.component.ts b/core/app/core/src/lib/views/login/components/login/login.component.ts
index 89aa10f4..1a904b64 100644
--- a/core/app/core/src/lib/views/login/components/login/login.component.ts
+++ b/core/app/core/src/lib/views/login/components/login/login.component.ts
@@ -206,11 +206,11 @@ export class LoginUiComponent implements OnInit {
         const defaultTooManyFailedMessage = 'Too many failed login attempts, please try again later.';
         let message = this.languageStore.getFieldLabel('LOGIN_INCORRECT');
 
         const errorMessage = httpError?.error?.error ?? '';
 
-        if (errorMessage === 'Too many failed login attempts, please try again in 1 minute.') {
+        if (typeof errorMessage === 'string' && errorMessage.includes('Too many failed login attempts, please try again in')) {
             message = this.getTooManyFailedMessage(defaultTooManyFailedMessage);
         }
 
         if (!message) {
             message = defaultMessage
diff --git a/core/backend/Engine/LegacyHandler/AclHandler.php b/core/backend/Engine/LegacyHandler/AclHandler.php
index b8be2ab2..c4017a6a 100644
--- a/core/backend/Engine/LegacyHandler/AclHandler.php
+++ b/core/backend/Engine/LegacyHandler/AclHandler.php
@@ -98,10 +98,14 @@ class AclHandler extends LegacyHandler implements AclManagerInterface
 
         $legacyName = $this->moduleNameMapper->toLegacy($module);
 
         $hasAccess = ACLController::checkAccess($legacyName, $action, $isOwner, $type, $in_group);
 
+        if (check_default_module_access($legacyName) === false) {
+            $hasAccess = false;
+        }
+
         $this->close();
 
         return $hasAccess;
     }
 
diff --git a/core/backend/Install/Service/Installation/InstallActionHandler.php b/core/backend/Install/Service/Installation/InstallActionHandler.php
index aba45b77..c0497c9d 100644
--- a/core/backend/Install/Service/Installation/InstallActionHandler.php
+++ b/core/backend/Install/Service/Installation/InstallActionHandler.php
@@ -75,10 +75,18 @@ class InstallActionHandler implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return '';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        return [];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Languages/LegacyHandler/SetSessionLanguage.php b/core/backend/Languages/LegacyHandler/SetSessionLanguage.php
index 62eced96..6b43cdca 100644
--- a/core/backend/Languages/LegacyHandler/SetSessionLanguage.php
+++ b/core/backend/Languages/LegacyHandler/SetSessionLanguage.php
@@ -98,10 +98,18 @@ class SetSessionLanguage extends LegacyHandler implements ProcessHandlerInterfac
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        return [];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Migrations/Version20230420135520.php b/core/backend/Migrations/Version20230420135520.php
new file mode 100644
index 00000000..06272138
--- /dev/null
+++ b/core/backend/Migrations/Version20230420135520.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2023 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+
+declare(strict_types=1);
+
+namespace App\Migrations;
+
+use Doctrine\DBAL\Schema\Schema;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+final class Version20230420135520 extends BaseMigration implements ContainerAwareInterface
+{
+    use EnvHandlingMigrationTrait;
+
+    /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    /**
+     * @var LoggerInterface
+     */
+    protected $upgradeLogger;
+
+    public function getDescription(): string
+    {
+        return 'Add login throttling interval to .env';
+    }
+
+    public function isTransactional(): bool
+    {
+        return false;
+    }
+
+    public function up(Schema $schema): void
+    {
+        $envFile = $this->getProjectDir() . "/.env";
+
+        if (!file_exists($envFile)) {
+            return;
+        }
+
+        $envContents = file_get_contents($envFile);
+
+        $this->addLoginThrottlingConfig($envContents, $envFile);
+
+    }
+
+    public function down(Schema $schema): void
+    {
+    }
+
+    /**
+     * Check and add missing login throttling config
+     * @param $envContents
+     * @param string $envFile
+     */
+    protected function addLoginThrottlingConfig(&$envContents, string $envFile): void
+    {
+        $properties = [
+            'LOGIN_THROTTLING_IP_LOGIN_MAX_ATTEMPTS' => '50',
+            'LOGIN_THROTTLING_INTERVAL' => '"30 minutes"'
+        ];
+
+        $wrapperStart = '###> login throttling ###';
+        $wrapperEnd = '###< login throttling ###';
+
+        $propertiesToAdd = $this->getContentToAdd($envContents, $properties, $wrapperStart, $wrapperEnd);
+        if (!empty($propertiesToAdd)) {
+            $envContents .= $propertiesToAdd;
+            file_put_contents($envFile, $envContents);
+            $this->log('Added LOGIN_THROTTLING_INTERVAL to .env.');
+
+            return;
+        }
+
+        $this->log('LOGIN_THROTTLING_INTERVAL already in .env, skipping.');
+    }
+}
diff --git a/core/backend/Module/LegacyHandler/Favorites/UpdateFavorite.php b/core/backend/Module/LegacyHandler/Favorites/UpdateFavorite.php
index b5a635cd..f41179fa 100644
--- a/core/backend/Module/LegacyHandler/Favorites/UpdateFavorite.php
+++ b/core/backend/Module/LegacyHandler/Favorites/UpdateFavorite.php
@@ -99,10 +99,30 @@ class UpdateFavorite extends LegacyHandler implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        ['favorite' => $favorite] = $process->getOptions();
+
+        $module = $favorite['attributes']['parent_type'] ?? '';
+        $id = $favorite['attributes']['parent_id'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'view',
+                    'record' => $id
+                ]
+            ]
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
@@ -119,10 +139,19 @@ class UpdateFavorite extends LegacyHandler implements ProcessHandlerInterface
     public function validate(Process $process): void
     {
         if (empty($process->getOptions())) {
             throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
         }
+
+        ['favorite' => $favorite, 'action' => $action] = $process->getOptions();
+
+        $module = $favorite['attributes']['parent_type'] ?? '';
+        $id = $favorite['attributes']['parent_id'] ?? '';
+
+        if (empty($module) || empty($id) || empty($action)) {
+            throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
+        }
     }
 
     /**
      * @inheritDoc
      * @throws Exception
diff --git a/core/backend/Module/LegacyHandler/RecentlyViewed/AddRecentlyViewed.php b/core/backend/Module/LegacyHandler/RecentlyViewed/AddRecentlyViewed.php
index 53359e7f..54ce1255 100644
--- a/core/backend/Module/LegacyHandler/RecentlyViewed/AddRecentlyViewed.php
+++ b/core/backend/Module/LegacyHandler/RecentlyViewed/AddRecentlyViewed.php
@@ -105,10 +105,29 @@ class AddRecentlyViewed extends LegacyHandler implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        ['recentlyViewed' => $recentlyViewed] = $process->getOptions();
+        $itemId = $recentlyViewed['attributes']['item_id'] ?? '';
+        $itemModule = $recentlyViewed['attributes']['module_name'] ?? '';
+
+        return [
+            $itemModule => [
+                [
+                    'action' => 'view',
+                    'record' => $itemId,
+                ]
+            ]
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
@@ -125,10 +144,20 @@ class AddRecentlyViewed extends LegacyHandler implements ProcessHandlerInterface
     public function validate(Process $process): void
     {
         if (empty($process->getOptions())) {
             throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
         }
+
+        ['recentlyViewed' => $recentlyViewed] = $process->getOptions();
+        $itemModule = $recentlyViewed['attributes']['module_name'] ?? '';
+        $itemModule = $this->moduleNameMapper->toLegacy($itemModule);
+        $action = $recentlyViewed['attributes']['action'] ?? '';
+
+
+        if (empty($itemModule) || empty($action)) {
+            throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
+        }
     }
 
     /**
      * @inheritDoc
      * @throws Exception
diff --git a/core/backend/Module/LegacyHandler/RecentlyViewed/RecentlyViewedHandler.php b/core/backend/Module/LegacyHandler/RecentlyViewed/RecentlyViewedHandler.php
index 26287b4f..83b58ef5 100644
--- a/core/backend/Module/LegacyHandler/RecentlyViewed/RecentlyViewedHandler.php
+++ b/core/backend/Module/LegacyHandler/RecentlyViewed/RecentlyViewedHandler.php
@@ -1,6 +1,31 @@
 <?php
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2021 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
 
 namespace App\Module\LegacyHandler\RecentlyViewed;
 
 use App\Engine\LegacyHandler\LegacyHandler;
 use App\Engine\LegacyHandler\LegacyScopeState;
diff --git a/core/backend/Process/DataPersister/ProcessDataPersister.php b/core/backend/Process/DataPersister/ProcessDataPersister.php
index 20ba1f9d..529ce474 100644
--- a/core/backend/Process/DataPersister/ProcessDataPersister.php
+++ b/core/backend/Process/DataPersister/ProcessDataPersister.php
@@ -28,36 +28,45 @@
 
 namespace App\Process\DataPersister;
 
 use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
 use ApiPlatform\Core\Exception\InvalidResourceException;
+use App\Engine\Service\AclManagerInterface;
 use App\Process\Entity\Process;
-use App\Process\Service\ProcessHandlerRegistry;
 use App\Process\Service\ProcessHandlerInterface;
+use App\Process\Service\ProcessHandlerRegistry;
 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 use Symfony\Component\Security\Core\Security;
 
 class ProcessDataPersister implements ContextAwareDataPersisterInterface
 {
     /**
      * @var ProcessHandlerRegistry
      */
     private $registry;
+
     /**
      * @var Security
      */
     private $security;
 
+    /**
+     * @var AclManagerInterface
+     */
+    private $acl;
+
     /**
      * ProcessDataPersister constructor.
      * @param ProcessHandlerRegistry $registry
      * @param Security $security
+     * @param AclManagerInterface $acl
      */
-    public function __construct(ProcessHandlerRegistry $registry, Security $security)
+    public function __construct(ProcessHandlerRegistry $registry, Security $security, AclManagerInterface $acl)
     {
         $this->registry = $registry;
         $this->security = $security;
+        $this->acl = $acl;
     }
 
     /**
      * @inheritDoc
      */
@@ -77,12 +86,22 @@ class ProcessDataPersister implements ContextAwareDataPersisterInterface
         $processHandler = $this->registry->get($process->getType());
 
         $this->checkAuthentication($processHandler);
 
         $processHandler->validate($process);
+
+        $hasAccess = $this->checkACLAccess($processHandler, $process);
+
         $processHandler->configure($process);
 
+        if (!$hasAccess) {
+            $process->setMessages(['LBL_ACCESS_DENIED']);
+            $process->setStatus('error');
+
+            return $process;
+        }
+
         if ($process->getAsync() === true) {
             // Store process for background processing
             // Not supported yet
         } else {
             $processHandler->run($process);
@@ -106,10 +125,67 @@ class ProcessDataPersister implements ContextAwareDataPersisterInterface
         }
 
         throw new AccessDeniedException();
     }
 
+    /**
+     * Check acl access
+     * @param ProcessHandlerInterface $processHandler
+     * @param Process $process
+     */
+    protected function checkACLAccess(ProcessHandlerInterface $processHandler, Process $process): bool
+    {
+        $modulesACLs = $processHandler->getRequiredACLs($process) ?? [];
+        if (empty($modulesACLs)) {
+            return true;
+        }
+
+        $hasAccess = true;
+        foreach ($modulesACLs as $module => $requiredACLs) {
+            if (empty($requiredACLs)) {
+                continue;
+            }
+            if (empty($module)) {
+                continue;
+            }
+
+            if ($hasAccess === false) {
+                return false;
+            }
+
+            foreach ($requiredACLs as $requiredACL) {
+                $record = $requiredACL['record'] ?? '';
+                $ids = $requiredACL['ids'] ?? '';
+                $action = $requiredACL['action'] ?? '';
+
+                if (empty($action)) {
+                    continue;
+                }
+
+                if ($hasAccess === false) {
+                    return false;
+                }
+
+                if (empty($record) && empty($ids)) {
+                    $hasAccess &= $this->acl->checkAccess($module, $action, true, 'module', true);
+                    continue;
+                }
+
+                if (!empty($record)) {
+                    $hasAccess &= $this->acl->checkRecordAccess($module, $action, $record);
+                    continue;
+                }
+
+                if (!empty($ids)) {
+                    $hasAccess &= $this->checkRecordsAccess($ids, $module, $action);
+                }
+            }
+        }
+
+        return $hasAccess;
+    }
+
     /**
      * Handler process deletion request
      * @param $data
      * @param array $context
      * @throws InvalidResourceException
@@ -117,6 +193,28 @@ class ProcessDataPersister implements ContextAwareDataPersisterInterface
     public function remove($data, array $context = [])
     {
         // Deleting processes is not supported
         throw new InvalidResourceException();
     }
+
+    /**
+     * @param array $ids
+     * @param string $module
+     * @param string $action
+     * @return bool
+     */
+    protected function checkRecordsAccess(array $ids, string $module, string $action): bool
+    {
+        if (empty($ids)) {
+            return true;
+        }
+
+        foreach ($ids as $id) {
+            $hasAccess = $this->acl->checkRecordAccess($module, $action, $id);
+            if ($hasAccess === false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
 }
diff --git a/core/backend/Process/LegacyHandler/ACL/UserACLHandler.php b/core/backend/Process/LegacyHandler/ACL/UserACLHandler.php
index ab33411c..627a8acc 100644
--- a/core/backend/Process/LegacyHandler/ACL/UserACLHandler.php
+++ b/core/backend/Process/LegacyHandler/ACL/UserACLHandler.php
@@ -28,13 +28,13 @@
 namespace App\Process\LegacyHandler\ACL;
 
 use ApiPlatform\Core\Exception\InvalidArgumentException;
 use App\Engine\LegacyHandler\LegacyHandler;
 use App\Engine\LegacyHandler\LegacyScopeState;
-use App\Engine\Service\AclManagerInterface;
 use App\Module\Service\ModuleNameMapperInterface;
 use App\Process\Entity\Process;
+use App\Process\Service\ActionNameMapperInterface;
 use App\Process\Service\BaseActionDefinitionProviderInterface;
 use App\Process\Service\LegacyActionResolverInterface;
 use App\Process\Service\ProcessHandlerInterface;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
@@ -63,14 +63,20 @@ class UserACLHandler extends LegacyHandler implements ProcessHandlerInterface, L
 
     /**
      * @var LegacyActionResolverInterface
      */
     private $legacyActionResolver;
+
+    /**
+     * @var array
+     */
+    private $adminOnlyModuleActions;
+
     /**
-     * @var AclManagerInterface
+     * @var ActionNameMapperInterface
      */
-    private $acl;
+    private $actionNameMapper;
 
     /**
      * UserACLHandler constructor.
      * @param string $projectDir
      * @param string $legacyDir
@@ -79,11 +85,11 @@ class UserACLHandler extends LegacyHandler implements ProcessHandlerInterface, L
      * @param LegacyScopeState $legacyScopeState
      * @param SessionInterface $session
      * @param ModuleNameMapperInterface $moduleNameMapper
      * @param BaseActionDefinitionProviderInterface $baseActionDefinitionProvider
      * @param LegacyActionResolverInterface $legacyActionResolver
-     * @param AclManagerInterface $acl
+     * @param array $adminOnlyModuleActions
      */
     public function __construct(
         string $projectDir,
         string $legacyDir,
         string $legacySessionName,
@@ -91,11 +97,12 @@ class UserACLHandler extends LegacyHandler implements ProcessHandlerInterface, L
         LegacyScopeState $legacyScopeState,
         SessionInterface $session,
         ModuleNameMapperInterface $moduleNameMapper,
         BaseActionDefinitionProviderInterface $baseActionDefinitionProvider,
         LegacyActionResolverInterface $legacyActionResolver,
-        AclManagerInterface $acl
+        ActionNameMapperInterface $actionNameMapper,
+        array $adminOnlyModuleActions
     ) {
         parent::__construct(
             $projectDir,
             $legacyDir,
             $legacySessionName,
@@ -104,11 +111,12 @@ class UserACLHandler extends LegacyHandler implements ProcessHandlerInterface, L
             $session
         );
         $this->moduleNameMapper = $moduleNameMapper;
         $this->baseActionDefinitionProvider = $baseActionDefinitionProvider;
         $this->legacyActionResolver = $legacyActionResolver;
-        $this->acl = $acl;
+        $this->actionNameMapper = $actionNameMapper;
+        $this->adminOnlyModuleActions = $adminOnlyModuleActions;
     }
 
     /**
      * @inheritDoc
      */
@@ -131,10 +139,18 @@ class UserACLHandler extends LegacyHandler implements ProcessHandlerInterface, L
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        return [];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(
         Process $process
@@ -207,10 +223,18 @@ class UserACLHandler extends LegacyHandler implements ProcessHandlerInterface, L
             && ($this->baseActionDefinitionProvider->isActionAccessible($frontEndModuleName, $actionKey, $context))
         ) {
             $hasAccess = true;
         }
 
+        global $current_user;
+        $isAdmin = is_admin($current_user);
+        $isActionAdminOnly = $this->isAdminOnlyAction($frontEndModuleName, $legacyModuleName, $actionKey, $queryParams);
+
+        if ($isActionAdminOnly && !$isAdmin) {
+            $hasAccess = false;
+        }
+
         $service = new UserACLService();
         $result = $service->run($legacyModuleName, $payload['routeURL'], $hasAccess);
 
         $process->setStatus('success');
 
@@ -243,22 +267,26 @@ class UserACLHandler extends LegacyHandler implements ProcessHandlerInterface, L
         $actionModuleIdentifierKey = $this->legacyActionResolver->get($primaryAction, $secondaryAction);
 
         if (empty($actionModuleIdentifierKey)) {
             return '';
         }
+
         return $actionModuleIdentifierKey;
     }
 
     /**
      * Get list of modules the user has access to
      * @param string $primaryAction
      * @param string $secondaryAction
      * @param array $queryParams
      * @return string
      */
-    protected function getResolvedLegacyModule(string $primaryAction, string $secondaryAction, array $queryParams): string
-    {
+    protected function getResolvedLegacyModule(
+        string $primaryAction,
+        string $secondaryAction,
+        array $queryParams
+    ): string {
         $actionModuleIdentifierKey = $this->entryExistsInLegacyActionMapper($primaryAction, $secondaryAction);
 
         if (empty($actionModuleIdentifierKey)) {
             return '';
         }
@@ -271,6 +299,39 @@ class UserACLHandler extends LegacyHandler implements ProcessHandlerInterface, L
      */
     public function setLogger(LoggerInterface $logger): void
     {
         $this->logger = $logger;
     }
+
+    /**
+     * @param string $module
+     * @param string $legacyModule
+     * @param string $actionKey
+     * @param array $queryParams
+     * @return bool
+     */
+    protected function isAdminOnlyAction(string $module, string $legacyModule,  string $actionKey, array $queryParams): bool
+    {
+        if (!empty($queryParams['import_module']) && strtolower($module) === 'import') {
+            $module = $this->moduleNameMapper->toFrontEnd($queryParams['import_module']);
+            $legacyModule = $queryParams['import_module'];
+            $actionKey = 'import';
+        }
+
+        $adminOnlyList = [];
+
+        /* @noinspection PhpIncludeInspection */
+        require_once 'include/modules.php';
+        $legacyActionName = strtolower($this->actionNameMapper->toLegacy($actionKey));
+        $adminOnlyActions = $adminOnlyList[$legacyModule] ?? [];
+        $adminOnlyAction = $adminOnlyActions[$legacyActionName] ?? $adminOnlyActions['all'] ?? false;
+        $isActionAdminOnly = !empty($adminOnlyAction) && $adminOnlyAction !== 'allow';
+
+        if ($isActionAdminOnly) {
+            return true;
+        }
+
+        $adminOnlyActions = $this->adminOnlyModuleActions[$module] ?? [];
+
+        return $adminOnlyActions[strtolower($actionKey)] ?? $adminOnlyActions['*'] ?? false;
+    }
 }
diff --git a/core/backend/Process/LegacyHandler/AddContactsToTargetListBulkActionHandler.php b/core/backend/Process/LegacyHandler/AddContactsToTargetListBulkActionHandler.php
index 00455da8..46e7ccfc 100644
--- a/core/backend/Process/LegacyHandler/AddContactsToTargetListBulkActionHandler.php
+++ b/core/backend/Process/LegacyHandler/AddContactsToTargetListBulkActionHandler.php
@@ -106,10 +106,38 @@ class AddContactsToTargetListBulkActionHandler extends LegacyHandler implements
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $baseModule = $options['module'] ?? '';
+        $baseIds = $options['ids'] ?? [];
+        $modalRecord = $options['modalRecord'] ?? [];
+        $modalModule = $modalRecord['module'] ?? '';
+        $modalRecordId = $modalRecord['id'] ?? '';
+
+        return [
+            $baseModule => [
+                [
+                    'action' => 'view',
+                    'ids' => $baseIds
+                ]
+            ],
+            $modalModule => [
+                [
+                    'action' => 'view',
+                    'record' => $modalRecordId,
+                ]
+            ],
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
@@ -150,10 +178,11 @@ class AddContactsToTargetListBulkActionHandler extends LegacyHandler implements
      * @inheritDoc
      */
     public function run(Process $process)
     {
         $this->init();
+        $this->startLegacyApp();
 
         /* @noinspection PhpIncludeInspection */
         require_once 'include/portability/Services/Relationships/AddContactsToTargetListService.php';
 
         $options = $process->getOptions();
diff --git a/core/backend/Process/LegacyHandler/AddRecordsToTargetListBulkActionHandler.php b/core/backend/Process/LegacyHandler/AddRecordsToTargetListBulkActionHandler.php
index edd9c79b..8876f266 100644
--- a/core/backend/Process/LegacyHandler/AddRecordsToTargetListBulkActionHandler.php
+++ b/core/backend/Process/LegacyHandler/AddRecordsToTargetListBulkActionHandler.php
@@ -106,10 +106,34 @@ class AddRecordsToTargetListBulkActionHandler extends LegacyHandler implements P
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $baseModule = $options['module'] ?? '';
+        $baseIds = $options['ids'] ?? [];
+        $modalRecord = $options['modalRecord'] ?? [];
+        $modalModule = $modalRecord['module'] ?? '';
+
+        return [
+            $baseModule => [
+                [
+                    'action' => 'view',
+                    'ids' => $baseIds
+                ]
+            ],
+            $modalModule => [
+                ['action' => 'view']
+            ],
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/LegacyHandler/LinkRelationHandler.php b/core/backend/Process/LegacyHandler/LinkRelationHandler.php
index 7504d2bd..1d4dc2e6 100644
--- a/core/backend/Process/LegacyHandler/LinkRelationHandler.php
+++ b/core/backend/Process/LegacyHandler/LinkRelationHandler.php
@@ -112,10 +112,38 @@ class LinkRelationHandler extends LegacyHandler implements ProcessHandlerInterfa
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $baseModule = $options['module'] ?? '';
+        $baseModuleId = $options['id'] ?? '';
+        $payload = $options['payload'] ?? [];
+        $relateModule = $payload['relateModule'] ?? '';
+        $relateRecordIds = $payload['relateRecordIds'] ?? [];
+
+        return [
+            $baseModule => [
+                [
+                    'action' => 'view',
+                    'record' => $baseModuleId
+                ]
+            ],
+            $relateModule => [
+                [
+                    'action' => 'view',
+                    'ids' => $relateRecordIds
+                ]
+            ]
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/LegacyHandler/ResetPasswordHandler.php b/core/backend/Process/LegacyHandler/ResetPasswordHandler.php
index 04e23fa2..9ca09fab 100644
--- a/core/backend/Process/LegacyHandler/ResetPasswordHandler.php
+++ b/core/backend/Process/LegacyHandler/ResetPasswordHandler.php
@@ -28,12 +28,12 @@
 
 namespace App\Process\LegacyHandler;
 
 
 use ApiPlatform\Core\Exception\InvalidArgumentException;
-use App\Process\Entity\Process;
 use App\Engine\LegacyHandler\LegacyHandler;
+use App\Process\Entity\Process;
 use App\Process\Service\ProcessHandlerInterface;
 use BadFunctionCallException;
 use Exception;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
@@ -71,10 +71,19 @@ class ResetPasswordHandler extends LegacyHandler implements ProcessHandlerInterf
     public function requiredAuthRole(): string
     {
         return '';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        return [];
+    }
+
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
@@ -120,11 +129,11 @@ class ResetPasswordHandler extends LegacyHandler implements ProcessHandlerInterf
             'LBL_RECOVER_PASSWORD_SUCCESS'
         ]);
 
         try {
             $service->sendResetLink($username, $useremail);
-        } catch (BadFunctionCallException | \InvalidArgumentException $e) {
+        } catch (BadFunctionCallException|\InvalidArgumentException $e) {
             //logged by suite 7
         } catch (Exception $unknownException) {
             $this->logger->error($unknownException->getMessage(), ['exception' => $unknownException]);
             $process->setStatus('error');
             $process->setMessages([
diff --git a/core/backend/Process/LegacyHandler/UnlinkRelationHandler.php b/core/backend/Process/LegacyHandler/UnlinkRelationHandler.php
index 5d847946..9a3e5ade 100644
--- a/core/backend/Process/LegacyHandler/UnlinkRelationHandler.php
+++ b/core/backend/Process/LegacyHandler/UnlinkRelationHandler.php
@@ -104,10 +104,52 @@ class UnlinkRelationHandler extends LegacyHandler implements ProcessHandlerInter
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $payload = $options['payload'] ?? [];
+        $baseModule = $payload['baseModule'] ?? '';
+        $relateModule = $payload['relateModule'] ?? '';
+        $relateModuleId = $payload['relateRecordId'] ?? '';
+
+        $acls = [
+            $baseModule => [
+                [
+                    'action' => 'view',
+                    'record' => $payload['baseRecordId'] ?? ''
+                ],
+                [
+                    'action' => 'edit'
+                ],
+            ],
+            $relateModule => [
+                [
+                    'action' => 'view',
+                    'record' => $relateModuleId
+                ]
+            ]
+        ];
+
+        if (!empty($relateModule) && !empty($relateModuleId)) {
+            $acls[$relateModule] = [
+                [
+                    'action' => 'view',
+                    'record' =>  $relateModuleId ?? ''
+                ]
+            ];
+        }
+
+        return $acls;
+    }
+
+
     /**
      * @inheritDoc
      */
     public function configure(
         Process $process
diff --git a/core/backend/Process/Service/BulkActions/CsvExportBulkAction.php b/core/backend/Process/Service/BulkActions/CsvExportBulkAction.php
index a4b15bdb..db84725b 100644
--- a/core/backend/Process/Service/BulkActions/CsvExportBulkAction.php
+++ b/core/backend/Process/Service/BulkActions/CsvExportBulkAction.php
@@ -27,13 +27,13 @@
 
 
 namespace App\Process\Service\BulkActions;
 
 use ApiPlatform\Core\Exception\InvalidArgumentException;
-use App\Process\Entity\Process;
 use App\Data\LegacyHandler\FilterMapper\LegacyFilterMapper;
 use App\Module\Service\ModuleNameMapperInterface;
+use App\Process\Entity\Process;
 use App\Process\Service\ProcessHandlerInterface;
 
 class CsvExportBulkAction implements ProcessHandlerInterface
 {
     protected const MSG_OPTIONS_NOT_FOUND = 'Process options is not defined';
@@ -74,10 +74,29 @@ class CsvExportBulkAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+        $ids = $options['ids'] ?? [];
+
+        return [
+            $module => [
+                [
+                    'action' => 'export',
+                    'ids' => $ids
+                ]
+            ]
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/BulkActions/DeleteRecordsBulkAction.php b/core/backend/Process/Service/BulkActions/DeleteRecordsBulkAction.php
index d1fb740b..113d38f0 100644
--- a/core/backend/Process/Service/BulkActions/DeleteRecordsBulkAction.php
+++ b/core/backend/Process/Service/BulkActions/DeleteRecordsBulkAction.php
@@ -84,10 +84,29 @@ class DeleteRecordsBulkAction implements ProcessHandlerInterface, LoggerAwareInt
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+        $ids = $options['ids'] ?? [];
+
+        return [
+            $module => [
+                [
+                    'action' => 'delete',
+                    'ids' => $ids
+                ]
+            ]
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/BulkActions/MassUpdateBulkAction.php b/core/backend/Process/Service/BulkActions/MassUpdateBulkAction.php
index 3276c2fd..e1cd2e79 100644
--- a/core/backend/Process/Service/BulkActions/MassUpdateBulkAction.php
+++ b/core/backend/Process/Service/BulkActions/MassUpdateBulkAction.php
@@ -84,10 +84,33 @@ class MassUpdateBulkAction implements ProcessHandlerInterface, LoggerAwareInterf
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+        $ids = $options['ids'] ?? [];
+
+        return [
+            $module => [
+                [
+                    'action' => 'edit',
+                    'ids' => $ids
+                ],
+                [
+                    'action' => 'massupdate'
+                ]
+            ],
+        ];
+    }
+
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/BulkActions/MergeRecordsBulkAction.php b/core/backend/Process/Service/BulkActions/MergeRecordsBulkAction.php
index a0c1486e..a8aae151 100644
--- a/core/backend/Process/Service/BulkActions/MergeRecordsBulkAction.php
+++ b/core/backend/Process/Service/BulkActions/MergeRecordsBulkAction.php
@@ -27,12 +27,12 @@
 
 
 namespace App\Process\Service\BulkActions;
 
 use ApiPlatform\Core\Exception\InvalidArgumentException;
-use App\Process\Entity\Process;
 use App\Module\Service\ModuleNameMapperInterface;
+use App\Process\Entity\Process;
 use App\Process\Service\ProcessHandlerInterface;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
 
 class MergeRecordsBulkAction implements ProcessHandlerInterface, LoggerAwareInterface
@@ -73,10 +73,32 @@ class MergeRecordsBulkAction implements ProcessHandlerInterface, LoggerAwareInte
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+        $ids = $options['ids'] ?? [];
+
+        return [
+            $module => [
+                [
+                    'action' => 'view'
+                ],
+                [
+                    'action' => 'edit',
+                    'ids' => $ids
+                ]
+            ]
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/BulkActions/PrintAsPdfBulkAction.php b/core/backend/Process/Service/BulkActions/PrintAsPdfBulkAction.php
index 1bba6baf..5fd568de 100644
--- a/core/backend/Process/Service/BulkActions/PrintAsPdfBulkAction.php
+++ b/core/backend/Process/Service/BulkActions/PrintAsPdfBulkAction.php
@@ -27,12 +27,12 @@
 
 
 namespace App\Process\Service\BulkActions;
 
 use ApiPlatform\Core\Exception\InvalidArgumentException;
-use App\Process\Entity\Process;
 use App\Module\Service\ModuleNameMapperInterface;
+use App\Process\Entity\Process;
 use App\Process\Service\ProcessHandlerInterface;
 
 class PrintAsPdfBulkAction implements ProcessHandlerInterface
 {
     protected const MSG_OPTIONS_NOT_FOUND = 'Process options is not defined';
@@ -66,10 +66,33 @@ class PrintAsPdfBulkAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+        $ids = $options['ids'] ?? [];
+
+
+        return [
+            $module => [
+                [
+                    'action' => 'view'
+                ],
+                [
+                    'action' => 'export',
+                    'ids' => $ids
+                ]
+            ]
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/ProcessHandlerInterface.php b/core/backend/Process/Service/ProcessHandlerInterface.php
index b45953a2..2e0bb44e 100644
--- a/core/backend/Process/Service/ProcessHandlerInterface.php
+++ b/core/backend/Process/Service/ProcessHandlerInterface.php
@@ -43,10 +43,18 @@ interface ProcessHandlerInterface
      *
      * @return string
      */
     public function requiredAuthRole(): string;
 
+    /**
+     * Get required acls list per module, empty array means no acl check needed
+     *
+     * @param Process $process
+     * @return array
+     */
+    public function getRequiredACLs(Process $process): array;
+
     /**
      * Configure process for given type
      * @param Process $process
      */
     public function configure(Process $process): void;
diff --git a/core/backend/Process/Service/RecordActions/ChangeLogAction.php b/core/backend/Process/Service/RecordActions/ChangeLogAction.php
index 0c2587d8..ade82eff 100644
--- a/core/backend/Process/Service/RecordActions/ChangeLogAction.php
+++ b/core/backend/Process/Service/RecordActions/ChangeLogAction.php
@@ -65,10 +65,29 @@ class ChangeLogAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+        $recordId = $options['id'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'view',
+                    'record' => $recordId
+                ]
+            ],
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordActions/ConvertLeadAction.php b/core/backend/Process/Service/RecordActions/ConvertLeadAction.php
index 28c48962..dfe68304 100644
--- a/core/backend/Process/Service/RecordActions/ConvertLeadAction.php
+++ b/core/backend/Process/Service/RecordActions/ConvertLeadAction.php
@@ -26,12 +26,12 @@
  */
 
 namespace App\Process\Service\RecordActions;
 
 use ApiPlatform\Core\Exception\InvalidArgumentException;
-use App\Process\Entity\Process;
 use App\Module\Service\ModuleNameMapperInterface;
+use App\Process\Entity\Process;
 use App\Process\Service\ProcessHandlerInterface;
 
 class ConvertLeadAction implements ProcessHandlerInterface
 {
     protected const MSG_OPTIONS_NOT_FOUND = 'Process options are not defined';
@@ -65,10 +65,30 @@ class ConvertLeadAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+        $id = $options['id'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'view',
+                    'record' => $id
+                ],
+            ],
+        ];
+
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordActions/DeleteRecordAction.php b/core/backend/Process/Service/RecordActions/DeleteRecordAction.php
index fd275691..facc8a08 100644
--- a/core/backend/Process/Service/RecordActions/DeleteRecordAction.php
+++ b/core/backend/Process/Service/RecordActions/DeleteRecordAction.php
@@ -77,10 +77,28 @@ class DeleteRecordAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'delete',
+                    'record' => $options['id'] ?? ''
+                ]
+            ],
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordActions/DuplicateRecordAction.php b/core/backend/Process/Service/RecordActions/DuplicateRecordAction.php
index a36f56a8..c652b858 100644
--- a/core/backend/Process/Service/RecordActions/DuplicateRecordAction.php
+++ b/core/backend/Process/Service/RecordActions/DuplicateRecordAction.php
@@ -66,10 +66,28 @@ class DuplicateRecordAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'edit',
+                    'record' => $options['id'] ?? ''
+                ]
+            ],
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordActions/EditAction.php b/core/backend/Process/Service/RecordActions/EditAction.php
index 90889e0b..6ac4f87a 100644
--- a/core/backend/Process/Service/RecordActions/EditAction.php
+++ b/core/backend/Process/Service/RecordActions/EditAction.php
@@ -65,10 +65,37 @@ class EditAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+
+        $baseModule = $options['payload']['baseModule'] ?? '';
+        $baseRecord = $options['payload']['baseRecordId'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'edit',
+                    'record' => $options['id'] ?? ''
+                ]
+            ],
+            $baseModule => [
+                [
+                    'action' => 'view',
+                    'record' => $baseRecord
+                ]
+            ],
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordActions/MergeDuplicateAction.php b/core/backend/Process/Service/RecordActions/MergeDuplicateAction.php
index f8ec319a..adad6a47 100644
--- a/core/backend/Process/Service/RecordActions/MergeDuplicateAction.php
+++ b/core/backend/Process/Service/RecordActions/MergeDuplicateAction.php
@@ -65,10 +65,28 @@ class MergeDuplicateAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'edit',
+                    'record' => $options['id'] ?? ''
+                ]
+            ],
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordActions/PrintAsPdfAction.php b/core/backend/Process/Service/RecordActions/PrintAsPdfAction.php
index b71b81e9..9a5da4cc 100644
--- a/core/backend/Process/Service/RecordActions/PrintAsPdfAction.php
+++ b/core/backend/Process/Service/RecordActions/PrintAsPdfAction.php
@@ -66,10 +66,50 @@ class PrintAsPdfAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+
+        $modalRecord = $options['params']['modalRecord'] ?? [];
+        $modalRecordModule = $modalRecord['module'] ?? '';
+        $modalRecordId = $modalRecord['id'] ?? '';
+
+        $acls = [
+            $module => [
+                [
+                    'action' => 'view',
+                    'record' => $options['id'] ?? ''
+                ],
+                [
+                    'action' => 'export',
+                    'record' => $options['id'] ?? ''
+                ]
+            ],
+        ];
+
+        if ($modalRecordModule !== '') {
+            $acls[$modalRecordModule] = [
+                [
+                    [
+                        'action' => 'view',
+                        'record' => $modalRecordId
+                    ]
+                ]
+            ];
+        }
+
+        return $acls;
+
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordThreadItemActions/DeleteRecordThreadItemAction.php b/core/backend/Process/Service/RecordThreadItemActions/DeleteRecordThreadItemAction.php
index a448c552..90f0757b 100644
--- a/core/backend/Process/Service/RecordThreadItemActions/DeleteRecordThreadItemAction.php
+++ b/core/backend/Process/Service/RecordThreadItemActions/DeleteRecordThreadItemAction.php
@@ -76,10 +76,28 @@ class DeleteRecordThreadItemAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'delete',
+                    'record' => $options['id'] ?? ''
+                ]
+            ],
+        ];
+    }
+
     /**
      * {@inheritDoc}
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordThreadItemActions/DismissAlertAction.php b/core/backend/Process/Service/RecordThreadItemActions/DismissAlertAction.php
index 57a2cefd..0714b8d6 100644
--- a/core/backend/Process/Service/RecordThreadItemActions/DismissAlertAction.php
+++ b/core/backend/Process/Service/RecordThreadItemActions/DismissAlertAction.php
@@ -68,10 +68,28 @@ class DismissAlertAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'delete',
+                    'record' => $options['id'] ?? ''
+                ]
+            ],
+        ];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordThreadItemActions/SnoozeRecordThreadItemAction.php b/core/backend/Process/Service/RecordThreadItemActions/SnoozeRecordThreadItemAction.php
index 37d8c770..da7a113e 100644
--- a/core/backend/Process/Service/RecordThreadItemActions/SnoozeRecordThreadItemAction.php
+++ b/core/backend/Process/Service/RecordThreadItemActions/SnoozeRecordThreadItemAction.php
@@ -76,10 +76,28 @@ class SnoozeRecordThreadItemAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+
+        return [
+            $module => [
+                [
+                    'action' => 'view',
+                    'record' => $options['id'] ?? ''
+                ]
+            ],
+        ];
+    }
+
     /**
      * {@inheritDoc}
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordThreadListActions/DeleteAllRecordThreadListAction.php b/core/backend/Process/Service/RecordThreadListActions/DeleteAllRecordThreadListAction.php
index 77f83943..6c02aa37 100644
--- a/core/backend/Process/Service/RecordThreadListActions/DeleteAllRecordThreadListAction.php
+++ b/core/backend/Process/Service/RecordThreadListActions/DeleteAllRecordThreadListAction.php
@@ -72,10 +72,29 @@ class DeleteAllRecordThreadListAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+        $ids = $options['ids'] ?? [];
+
+        return [
+            $module => [
+                [
+                    'action' => 'delete',
+                    'ids' => $ids
+                ]
+            ],
+        ];
+    }
+
     /**
      * {@inheritDoc}
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordThreadListActions/DismissAllAlertsRecordThreadListAction.php b/core/backend/Process/Service/RecordThreadListActions/DismissAllAlertsRecordThreadListAction.php
index d1559cd5..7d60e6d0 100644
--- a/core/backend/Process/Service/RecordThreadListActions/DismissAllAlertsRecordThreadListAction.php
+++ b/core/backend/Process/Service/RecordThreadListActions/DismissAllAlertsRecordThreadListAction.php
@@ -67,10 +67,28 @@ class DismissAllAlertsRecordThreadListAction implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $ids = $options['ids'] ?? [];
+
+        return [
+            'alerts' => [
+                [
+                    'action' => 'delete',
+                    'ids' => $ids
+                ]
+            ],
+        ];
+    }
+
     /**
      * {@inheritDoc}
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/Process/Service/RecordThreadListActions/MarkAsReadRecordThreadListAction.php b/core/backend/Process/Service/RecordThreadListActions/MarkAsReadRecordThreadListAction.php
index 3c74e084..0ddb20a9 100644
--- a/core/backend/Process/Service/RecordThreadListActions/MarkAsReadRecordThreadListAction.php
+++ b/core/backend/Process/Service/RecordThreadListActions/MarkAsReadRecordThreadListAction.php
@@ -110,10 +110,29 @@ class MarkAsReadRecordThreadListAction extends LegacyHandler implements ProcessH
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        $options = $process->getOptions();
+        $module = $options['module'] ?? '';
+        $ids = $options['ids'] ?? [];
+
+        return [
+            $module => [
+                [
+                    'action' => 'view',
+                    'ids' => $ids
+                ]
+            ],
+        ];
+    }
+
     /**
      * {@inheritDoc}
      */
     public function configure(Process $process): void
     {
diff --git a/core/backend/UserPreferences/LegacyHandler/SaveUiPreferences.php b/core/backend/UserPreferences/LegacyHandler/SaveUiPreferences.php
index 280b7804..eca58a0c 100644
--- a/core/backend/UserPreferences/LegacyHandler/SaveUiPreferences.php
+++ b/core/backend/UserPreferences/LegacyHandler/SaveUiPreferences.php
@@ -67,10 +67,18 @@ class SaveUiPreferences extends LegacyHandler implements ProcessHandlerInterface
     public function requiredAuthRole(): string
     {
         return 'ROLE_USER';
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getRequiredACLs(Process $process): array
+    {
+        return [];
+    }
+
     /**
      * @inheritDoc
      */
     public function configure(Process $process): void
     {
diff --git a/package.json b/package.json
index ce21af7d..36faf809 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
   "name": "suitecrm",
-  "version": "8.3-beta",
+  "version": "8.3.0",
   "scripts": {
     "ng": "ng",
     "start": "npm run start:shell",
     "start:shell": "ng serve shell  -o --port 5000",
     "start:all": "concurrently \"npm run start:shell\"",
diff --git a/public/legacy/data/SugarBean.php b/public/legacy/data/SugarBean.php
index 8ab1c29e..8436ce59 100755
--- a/public/legacy/data/SugarBean.php
+++ b/public/legacy/data/SugarBean.php
@@ -6201,10 +6201,15 @@ class SugarBean
     {
         global $current_user;
         if ($current_user->isAdmin() || !$this->bean_implements('ACL')) {
             return true;
         }
+
+        if (check_default_module_access($this->module_name ?? '') === false) {
+            return false;
+        }
+
         $view = strtolower($view);
         switch ($view) {
             case 'list':
             case 'index':
             case 'listview':
diff --git a/public/legacy/include/MVC/SugarApplication.php b/public/legacy/include/MVC/SugarApplication.php
index 760aa8bb..1a8985c9 100755
--- a/public/legacy/include/MVC/SugarApplication.php
+++ b/public/legacy/include/MVC/SugarApplication.php
@@ -299,11 +299,25 @@ class SugarApplication
             return;
         }
         if (!empty($_REQUEST['action']) && $_REQUEST['action'] == "RetrieveEmail") {
             return;
         }
-        if (!is_admin($GLOBALS['current_user']) && !empty($GLOBALS['adminOnlyList'][$this->controller->module]) && !empty($GLOBALS['adminOnlyList'][$this->controller->module]['all']) && (empty($GLOBALS['adminOnlyList'][$this->controller->module][$this->controller->action]) || $GLOBALS['adminOnlyList'][$this->controller->module][$this->controller->action] != 'allow')) {
+
+        $module = $this->controller->module ?? '';
+        $action = strtolower($this->controller->action ?? '');
+
+        if (!empty($_REQUEST['import_module'] ?? '') && strtolower($module) === 'import') {
+            $module = $_REQUEST['import_module'] ?? '';
+            $action = 'import';
+        }
+
+        $adminOnlyList = $GLOBALS['adminOnlyList'] ?? [];
+        $adminOnlyModuleActions = $adminOnlyList[$module] ?? [];
+        $adminOnlyAction = $adminOnlyModuleActions[$action] ?? $adminOnlyModuleActions['all'] ?? false;
+        $isAdminOnly = !empty($adminOnlyAction) && $adminOnlyAction !== 'allow';
+
+        if ($isAdminOnly && !is_admin($GLOBALS['current_user'])) {
             $this->controller->hasAccess = false;
             return;
         }
 
         // Bug 20916 - Special case for check ACL access rights for Subpanel QuickCreates
@@ -627,11 +641,11 @@ class SugarApplication
 
     public function endSession()
     {
         session_destroy();
     }
-    
+
     /**
      * Redirect to another URL
      *
      * @access	public
      * @param	string	$url	The URL to redirect to
@@ -641,11 +655,11 @@ class SugarApplication
     ) {
         /*
          * If the headers have been sent, then we cannot send an additional location header
          * so we will output a javascript redirect statement.
          */
-        
+
         if (!empty($_REQUEST['ajax_load'])) {
             ob_get_clean();
             $ajax_ret = array(
                 'content' => "<script>SUGAR.ajaxUI.loadContent('$url');</script>\n",
                 'menu' => array(
diff --git a/public/legacy/include/Smarty/plugins/function.sugarvar.php b/public/legacy/include/Smarty/plugins/function.sugarvar.php
index f6ed00ea..f66b8611 100755
--- a/public/legacy/include/Smarty/plugins/function.sugarvar.php
+++ b/public/legacy/include/Smarty/plugins/function.sugarvar.php
@@ -15,11 +15,11 @@ r56965 - 2010-06-15 10:57:35 -0700 (Tue, 15 Jun 2010) - jenny - Merging with Win
 
 r55980 - 2010-04-19 13:31:28 -0700 (Mon, 19 Apr 2010) - kjing - create Mango (6.1) based on windex
 
 r52439 - 2009-11-12 17:05:52 -0800 (Thu, 12 Nov 2009) - clee - Updated to allow Rich Text Editor to resize and render HTML content on detailview.
 
-r51719 - 2009-10-22 10:18:00 -0700 (Thu, 22 Oct 2009) - mitani - Converted to Build 3  tags and updated the build system 
+r51719 - 2009-10-22 10:18:00 -0700 (Thu, 22 Oct 2009) - mitani - Converted to Build 3  tags and updated the build system
 
 r51634 - 2009-10-19 13:32:22 -0700 (Mon, 19 Oct 2009) - mitani - Windex is the branch for Sugar Sales 1.0 development
 
 r50375 - 2009-08-24 18:07:43 -0700 (Mon, 24 Aug 2009) - dwong - branch kobe2 from tokyo r50372
 
@@ -40,11 +40,11 @@ r22184 - 2007-04-23 17:47:51 -0700 (Mon, 23 Apr 2007) - clee - Latest updates as
 
 r22175 - 2007-04-23 16:43:00 -0700 (Mon, 23 Apr 2007) - clee - Latest updates as we continue 5.0 framework development.
 
 r22125 - 2007-04-20 17:02:26 -0700 (Fri, 20 Apr 2007) - majed - makes it so you don't need to pass in as many variables
 
-r22124 - 2007-04-20 16:54:53 -0700 (Fri, 20 Apr 2007) - clee - 
+r22124 - 2007-04-20 16:54:53 -0700 (Fri, 20 Apr 2007) - clee -
 
 */
 
 
 /**
@@ -59,56 +59,62 @@ r22124 - 2007-04-20 16:54:53 -0700 (Fri, 20 Apr 2007) - clee -
  *
  * Type:     function<br>
  * Name:     sugarvar<br>
  * Purpose:  creates a smarty variable from the parameters
  *
- * @author Wayne Pan {wayne at sugarcrm.com}
  * @param array
  * @param Smarty
+ * @author Wayne Pan {wayne at sugarcrm.com}
  */
 
 function smarty_function_sugarvar($params, &$smarty)
 {
-	if(empty($params['key']))  {
-	    $smarty->trigger_error("sugarvar: missing 'key' parameter");
-	    return;
-	}
-
-	$object = (empty($params['objectName']))?$smarty->get_template_vars('parentFieldArray'): $params['objectName'];
-	$displayParams = $smarty->get_template_vars('displayParams');
-
-
-	if(empty($params['memberName'])){
-		$member = $smarty->get_template_vars('vardef');
-		$member = $member['name'];
-	}else{
-		$members = explode('.', $params['memberName']);
-		$member =  $smarty->get_template_vars($members[0]);
-		for($i = 1; $i < count($members); $i++){
-			$member = $member[$members[$i]];
-		}
-	}
-
-    $_contents =  '$'. $object . '.' . $member . '.' . $params['key'];
-	if(empty($params['stringFormat']) && empty($params['string'])) {
-		$_contents = '{' . $_contents;
-		if(!empty($displayParams['htmlescape'])){
-			$_contents .= '|escape:\'html\'';
-		}
-		if(!empty($params['htmlentitydecode'])){
-			$_contents .= '|escape:\'html_entity_decode\'';
-		}
-		if(!empty($displayParams['strip_tags'])){
-			$_contents .= '|strip_tags';
-		}
-		if(!empty($displayParams['url2html'])){
-			$_contents .= '|url2html';
-		}
-		if(!empty($displayParams['nl2br'])){
-			$_contents .= '|nl2br';
-		}
-
-		$_contents .= '}';
+    if (empty($params['key'])) {
+        $smarty->trigger_error("sugarvar: missing 'key' parameter");
+
+        return;
     }
+
+    $object = (empty($params['objectName'])) ? $smarty->get_template_vars('parentFieldArray') : $params['objectName'];
+    $displayParams = $smarty->get_template_vars('displayParams');
+
+
+    if (empty($params['memberName'])) {
+        $member = $smarty->get_template_vars('vardef');
+        $member = $member['name'];
+    } else {
+        $members = explode('.', $params['memberName']);
+        $member = $smarty->get_template_vars($members[0]);
+        for ($i = 1; $i < count($members); $i++) {
+            $member = $member[$members[$i]];
+        }
+    }
+
+    $_contents = '$' . $object . '.' . $member . '.' . $params['key'];
+    if (empty($params['stringFormat']) && empty($params['string'])) {
+        $_contents = '{' . $_contents;
+        if (!empty($displayParams['htmlescape'])) {
+            $_contents .= '|escape:\'html\'';
+        }
+        if (!empty($params['htmlentitydecode'])) {
+            $_contents .= '|escape:\'html_entity_decode\'';
+        }
+        if (!empty($displayParams['strip_tags'])) {
+            $_contents .= '|strip_tags';
+        }
+        if (!empty($displayParams['url2html'])) {
+            $_contents .= '|url2html';
+        }
+        if (!empty($displayParams['nl2br'])) {
+            $_contents .= '|nl2br';
+        }
+        if (!empty($displayParams['purifyHtml'])) {
+            $_contents .= '|htmlpurify';
+        }
+
+        $_contents .= '}';
+    }
+
     return $_contents;
 }
+
 ?>
diff --git a/public/legacy/include/Smarty/plugins/modifier.htmlpurify.php b/public/legacy/include/Smarty/plugins/modifier.htmlpurify.php
new file mode 100644
index 00000000..4111c46c
--- /dev/null
+++ b/public/legacy/include/Smarty/plugins/modifier.htmlpurify.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2023 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+
+/**
+ * Smarty plugin
+ * @package Smarty
+ * @subpackage plugins
+ */
+
+/**
+ * Smarty HTML purifier modifier plugin
+ *
+ * Type:     modifier<br>
+ * Name:     htmlpurify<br>
+ * Purpose:  purify html to display
+ * @param string
+ * @return string
+ */
+
+function smarty_modifier_htmlpurify($string)
+{
+    $string = purify_html(securexss($string));
+
+    return $string;
+}
diff --git a/public/legacy/include/SugarFields/Fields/Text/SugarFieldText.php b/public/legacy/include/SugarFields/Fields/Text/SugarFieldText.php
index 97b5d75b..9af4ae69 100755
--- a/public/legacy/include/SugarFields/Fields/Text/SugarFieldText.php
+++ b/public/legacy/include/SugarFields/Fields/Text/SugarFieldText.php
@@ -54,10 +54,14 @@ class SugarFieldText extends SugarFieldBase
 
         if (!isset($displayParams['url2html'])) {
             $displayParams['url2html'] = true;
         }
 
+        if (isset($vardef['editor']) && $vardef['editor'] === "html") {
+            $displayParams['purifyHtml'] = $displayParams['purifyHtml'] ?? true;
+        }
+
         return parent::getDetailViewSmarty($parentFieldArray, $vardef, $displayParams, $tabindex);
     }
 
     public function getClassicEditView($field_id='description', $value='', $prefix='', $rich_text=false, $maxlength='', $tabindex=1, $cols=80, $rows=4)
     {
@@ -96,12 +100,15 @@ class SugarFieldText extends SugarFieldBase
 
         if (isset($vardef['editor']) && $vardef['editor'] == "html") {
             if (!isset($displayParams['htmlescape'])) {
                 $displayParams['htmlescape'] = false;
             }
+            if (isset($vardef['editor']) && $vardef['editor'] === "html") {
+                $displayParams['purifyHtml'] = $displayParams['purifyHtml'] ?? true;
+            }
 
-            if ($_REQUEST['action'] == "EditView") {
+            if ($_REQUEST['action'] === "EditView") {
                 require_once(__DIR__ . "/../../../../include/SugarTinyMCE.php");
                 $tiny = new SugarTinyMCE();
                 $editor = $tiny->getInstance($vardef['name'], 'email_compose_light');
             }
         }
diff --git a/public/legacy/include/modules.php b/public/legacy/include/modules.php
index fab178fa..62ea557b 100755
--- a/public/legacy/include/modules.php
+++ b/public/legacy/include/modules.php
@@ -279,11 +279,11 @@ $modInvisList = [
     'AOBH_BusinessHours',
 ];
 
 $adminOnlyList = [
     //module => list of actions  (all says all actions are admin only)
-    //'Administration' => ['all'=>1, 'SupportPortal'=>'allow'],
+    'Administration' => ['all'=>1, 'supportportal'=>'allow'],
     'Dropdown' => ['all' => 1],
     'Dynamic' => ['all' => 1],
     'DynamicFields' => ['all' => 1],
     'Currencies' => ['all' => 1],
     'EditCustomFields' => ['all' => 1],
@@ -293,10 +293,14 @@ $adminOnlyList = [
     'ACLActions' => ['all' => 1],
     'ACLRoles' => ['all' => 1],
     'UpgradeWizard' => ['all' => 1],
     'Studio' => ['all' => 1],
     'Schedulers' => ['all' => 1],
+    'ExternalOAuthConnection' => ['all' => 1],
+    'jjwg_Address_Cache' => ['all' => 1],
+    'SugarFeed' => ['adminsettings' => 1],
+    'jjwg_Maps' => ['geocoded_counts' => 1, 'geocoding_test' => 1, 'config' => 1],
 ];
 
 $modInvisList[] = 'ACL';
 $modInvisList[] = 'ACLRoles';
 $modInvisList[] = 'Configurator';
diff --git a/public/legacy/include/portability/ApiBeanMapper/ApiBeanMapper.php b/public/legacy/include/portability/ApiBeanMapper/ApiBeanMapper.php
index 8e1f1287..554e6a4d 100644
--- a/public/legacy/include/portability/ApiBeanMapper/ApiBeanMapper.php
+++ b/public/legacy/include/portability/ApiBeanMapper/ApiBeanMapper.php
@@ -34,10 +34,12 @@ require_once __DIR__ . '/TypeMappers/ParentMapper.php';
 require_once __DIR__ . '/TypeMappers/DateMapper.php';
 require_once __DIR__ . '/TypeMappers/DateTimeMapper.php';
 require_once __DIR__ . '/TypeMappers/DateTimeComboMapper.php';
 require_once __DIR__ . '/TypeMappers/MultiEnumMapper.php';
 require_once __DIR__ . '/TypeMappers/BooleanMapper.php';
+require_once __DIR__ . '/TypeMappers/HtmlMapper.php';
+require_once __DIR__ . '/TypeMappers/TextMapper.php';
 require_once __DIR__ . '/ApiBeanModuleMappers.php';
 require_once __DIR__ . '/ModuleMappers/SavedSearch/SavedSearchMappers.php';
 require_once __DIR__ . '/ModuleMappers/AOP_Case_Updates/CaseUpdatesMappers.php';
 require_once __DIR__ . '/ModuleMappers/Alerts/AlertsMappers.php';
 require_once __DIR__ . '/../Bean/Field/Validation/FieldValidatorRegistry.php';
@@ -73,10 +75,12 @@ class ApiBeanMapper
         $this->typeMappers[DateMapper::getType()] = new DateMapper();
         $this->typeMappers[DateTimeMapper::getType()] = new DateTimeMapper();
         $this->typeMappers[MultiEnumMapper::getType()] = new MultiEnumMapper();
         $this->typeMappers[BooleanMapper::getType()] = new BooleanMapper();
         $this->typeMappers['boolean'] = $this->typeMappers[BooleanMapper::getType()];
+        $this->typeMappers[HtmlMapper::getType()] = new HtmlMapper();
+        $this->typeMappers[TextMapper::getType()] = new TextMapper();
         $this->moduleMappers[SavedSearchMappers::getModule()] = new SavedSearchMappers();
         $this->typeMappers[DateTimeComboMapper::getType()] = new DateTimeMapper();
         $this->linkMappers[EmailAddressLinkMapper::getRelateModule()] = [];
         $this->linkMappers[EmailAddressLinkMapper::getRelateModule()]['all'] = new EmailAddressLinkMapper();
         $this->moduleMappers[CaseUpdatesMappers::getModule()] = new CaseUpdatesMappers();
diff --git a/public/legacy/include/portability/ApiBeanMapper/TypeMappers/HtmlMapper.php b/public/legacy/include/portability/ApiBeanMapper/TypeMappers/HtmlMapper.php
new file mode 100644
index 00000000..016136b9
--- /dev/null
+++ b/public/legacy/include/portability/ApiBeanMapper/TypeMappers/HtmlMapper.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2023 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see http://www.gnu.org/licenses.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+
+require_once __DIR__ . '/TypeMapperInterface.php';
+
+class HtmlMapper implements TypeMapperInterface
+{
+    /**
+     * @inheritDoc
+     */
+    public static function getType(): string
+    {
+        return 'html';
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function toApi(SugarBean $bean, array &$container, string $name, string $alternativeName = ''): void
+    {
+        $newName = $name;
+
+        if (!empty($alternativeName)) {
+            $newName = $alternativeName;
+        }
+
+        if (empty($bean->$name)) {
+            $container[$newName] = '';
+
+            return;
+        }
+
+        $container[$newName] = $bean->$name;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function toBean(SugarBean $bean, array &$container, string $name, string $alternativeName = ''): void
+    {
+        $newName = $name;
+
+        if (!empty($alternativeName)) {
+            $newName = $alternativeName;
+        }
+
+        $fieldDef = $bean->field_defs[$newName] ?? [];
+
+        $purifyHtml = $fieldDef['metadata']['purifyHtml'] ?? true;
+        $value = $container[$newName] ?? '';
+
+        if ($purifyHtml === false || empty($value) || !is_string($value)) {
+            return;
+        }
+
+        $container[$newName] = purify_html(securexss($value));
+    }
+}
diff --git a/public/legacy/include/portability/ApiBeanMapper/TypeMappers/TextMapper.php b/public/legacy/include/portability/ApiBeanMapper/TypeMappers/TextMapper.php
new file mode 100644
index 00000000..a6e6d252
--- /dev/null
+++ b/public/legacy/include/portability/ApiBeanMapper/TypeMappers/TextMapper.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2023 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see http://www.gnu.org/licenses.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+
+require_once __DIR__ . '/TypeMapperInterface.php';
+
+class TextMapper implements TypeMapperInterface
+{
+    /**
+     * @inheritDoc
+     */
+    public static function getType(): string
+    {
+        return 'text';
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function toApi(SugarBean $bean, array &$container, string $name, string $alternativeName = ''): void
+    {
+        $newName = $name;
+
+        if (!empty($alternativeName)) {
+            $newName = $alternativeName;
+        }
+
+        if (empty($bean->$name)) {
+            $container[$newName] = '';
+
+            return;
+        }
+
+        $container[$newName] = $bean->$name;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function toBean(SugarBean $bean, array &$container, string $name, string $alternativeName = ''): void
+    {
+        $newName = $name;
+
+        if (!empty($alternativeName)) {
+            $newName = $alternativeName;
+        }
+
+        $fieldDef = $bean->field_defs[$newName] ?? [];
+
+        $editor = $fieldDef['editor'] ?? '';
+        $purifyHtml = $fieldDef['metadata']['purifyHtml'] ?? true;
+
+        if (empty($editor) || $editor !== 'html' || $purifyHtml === false || !is_string($container[$newName] ?? '')) {
+            return;
+        }
+
+        $container[$newName] = purify_html(securexss($container[$newName] ?? ''));
+    }
+}
diff --git a/public/legacy/include/portability/ListView/ListViewDataPort.php b/public/legacy/include/portability/ListView/ListViewDataPort.php
index d1be4833..e96cf3f6 100644
--- a/public/legacy/include/portability/ListView/ListViewDataPort.php
+++ b/public/legacy/include/portability/ListView/ListViewDataPort.php
@@ -616,11 +616,16 @@ class ListViewDataPort extends ListViewData
 
         if ($count >= $limit && $totalCounted) {
             $totalCount = $this->getTotalCount($main_query);
         }
 
-        $endOffset = (floor(($totalCount - 1) / $limit)) * $limit;
+        if ($limit > 0) {
+            $endOffset = (floor(($totalCount - 1) / $limit)) * $limit;
+        } else {
+            $endOffset = 0;
+        }
+
         $pageData['ordering'] = $order;
         $pageData['ordering']['sortOrder'] = $this->getReverseSortOrder($pageData['ordering']['sortOrder']);
         //get url parameters as an array
         $pageData['queries'] = $this->generateQueries(
             $pageData['ordering']['sortOrder'],
diff --git a/public/legacy/include/portability/Services/Relationships/AddContactsToTargetListService.php b/public/legacy/include/portability/Services/Relationships/AddContactsToTargetListService.php
index c2fa9824..de0a1f04 100644
--- a/public/legacy/include/portability/Services/Relationships/AddContactsToTargetListService.php
+++ b/public/legacy/include/portability/Services/Relationships/AddContactsToTargetListService.php
@@ -107,10 +107,20 @@ class AddContactsToTargetListService
         $sanitizedModuleName = str_replace('-', '_', $modalModule);
         foreach ($baseIds as $id) {
             $bean->retrieve($id);
             $contacts = $bean->get_linked_beans('contacts', 'Contacts3');
             foreach ($contacts as $contact) {
+
+                if (empty($contact)) {
+                    continue;
+                }
+
+                $hasAccess = $contact->ACLAccess('view');
+                if (!$hasAccess) {
+                    continue;
+                }
+
                 $relationship = $contact->load_relationship($sanitizedModuleName);
 
                 if (!$relationship) {
                     return false;
                 }
diff --git a/public/legacy/include/utils.php b/public/legacy/include/utils.php
index 683ecbe3..a0cf3574 100755
--- a/public/legacy/include/utils.php
+++ b/public/legacy/include/utils.php
@@ -290,10 +290,14 @@ function make_sugar_config(&$sugar_config)
             'min_cron_interval' => 30, // minimal interval between cron jobs
         ),
         'strict_id_validation' => false,
         'legacy_email_behaviour' => false,
         'snooze_alert_timer' => 600,
+        'default_module_access' => [
+            'SecurityGroups' => false,
+            'AOW_WorkFlow' => false
+        ],
     );
 }
 
 /**
  * Used for getting base values for array style config.php
@@ -590,10 +594,14 @@ function get_sugar_config_defaults(): array
             'gc_probability' => 1,
             'gc_divisor' => 100,
         ],
         'legacy_email_behaviour' => false,
         'snooze_alert_timer' => 600,
+        'default_module_access' => [
+            'SecurityGroups' => false,
+            'AOW_WorkFlow' => false,
+        ],
     ];
 
     if (!is_object($locale)) {
         $locale = new Localization();
     }
@@ -6215,16 +6223,62 @@ function has_group_action_acls_defined(string $module, string $action): bool
     }
 
     return $hasGroupActionAcls;
 }
 
+/**
+ * Check if user has action acls defined
+ * @param string $module
+ * @param string $action
+ * @return bool
+ */
+function has_action_acls_defined(string $module, string $action): bool
+{
+    global $current_user;
+
+    $hasGroupActionAcls = true;
+
+    $aclActions = ACLAction::getUserActions($current_user->id, false, $module, 'module', $action);
+    $isDefaultListACL = !empty($aclActions['isDefault']) && isTrue($aclActions['isDefault']);
+
+    if (empty($aclActions) || $isDefaultListACL) {
+        $hasGroupActionAcls = false;
+    }
+
+    return $hasGroupActionAcls;
+}
+
+/**
+ * Check default module access
+ * @param string $module
+ * @return bool
+ */
+function check_default_module_access(string $module): bool
+{
+    global $sugar_config, $current_user;
+
+    if (empty($module) || is_admin($current_user)) {
+        return true;
+    }
+
+    $hasActionAclsDefined = has_action_acls_defined($module, 'access');
+    $defaultModuleAccessConfig = $sugar_config['default_module_access'] ?? [];
+    $defaultModuleAccess = $defaultModuleAccessConfig[$module] ?? true;
+    if ($defaultModuleAccess === false && isFalse($hasActionAclsDefined ?? false)) {
+        return false;
+    }
+
+    return true;
+}
+
 /**
  * Check if is value is smtp in a case-insensitive way
  * @param $value
  * @return bool
  */
-function isSmtp($value): bool {
+function isSmtp($value): bool
+{
     if (empty($value) || !is_string($value)) {
         return false;
     }
 
     return strtolower($value)  === 'smtp';
diff --git a/public/legacy/modules/SugarFeed/Dashlets/SugarFeedDashlet/SugarFeedDashlet.php b/public/legacy/modules/SugarFeed/Dashlets/SugarFeedDashlet/SugarFeedDashlet.php
index fd2529e2..9bb296f9 100644
--- a/public/legacy/modules/SugarFeed/Dashlets/SugarFeedDashlet/SugarFeedDashlet.php
+++ b/public/legacy/modules/SugarFeed/Dashlets/SugarFeedDashlet/SugarFeedDashlet.php
@@ -280,17 +280,17 @@ class SugarFeedDashlet extends DashletGeneric
                         $securitygroup_where .= " AND
         (
             '".$current_user->id."' = (select assigned_user_id from ".$mod_bean->table_name." where id = sugarfeed.related_id)
         OR  EXISTS (SELECT  1
                   FROM    securitygroups secg
-                          INNER JOIN securitygroups_users secu 
-                            ON secg.id = secu.securitygroup_id 
-                               AND secu.deleted = 0 
+                          INNER JOIN securitygroups_users secu
+                            ON secg.id = secu.securitygroup_id
+                               AND secu.deleted = 0
                                AND secu.user_id = '".$current_user->id."'
-                          INNER JOIN securitygroups_records secr 
-                            ON secg.id = secr.securitygroup_id 
-                               AND secr.deleted = 0 
+                          INNER JOIN securitygroups_records secr
+                            ON secg.id = secr.securitygroup_id
+                               AND secr.deleted = 0
                                AND secr.module = '".$module."'
                        WHERE   secr.record_id = sugarfeed.related_id
                                AND secg.deleted = 0)
         ) ";
                     }
@@ -592,13 +592,27 @@ enableQS(false);
 
         //now process each token to create the proper url and image tags in feed, leaving a string for the alt to be replaced in next step
         /* BEGIN - SECURITY GROUPS */
         //hide links for those that shouldn't have one
         $listview = preg_replace('/\[(\w+)\:([\w\-\d]*)\:([^\]]*)\]\[HIDELINK\]/', '$3', $listview);
-        /* END - SECURITY GROUPS */ 
-        $listview = preg_replace('/\[(\w+)\:([\w\-\d]*)\:([^\]]*)\]/', '<a href="index.php?module=$1&action=DetailView&record=$2"><img src="themes/default/images/$1.gif" border=0 REPLACE_ALT>$3</a>', $listview); /*SKIP_IMAGE_TAG*/
+        /* END - SECURITY GROUPS */
 
+        require_once 'include/portability/RouteConverter.php';
+        $routeConverter = new RouteConverter();
+
+        $listview = preg_replace_callback(
+            '/\[(\w+)\:([\w\-\d]*)\:([^\]]*)\]/',
+            static function (&$matches) use ($routeConverter) {
+                $module = $matches[1] ?? '';
+                $record = $matches[2] ?? '';
+                $name = $matches[3] ?? '';
+                $uiLink = str_replace('./', '../', $routeConverter->convert($module, 'DetailView', $record, []));
+
+                return '<a href="' . $uiLink . '">' . $name . '</a>';
+            },
+            $listview
+        );/*SKIP_IMAGE_TAG*/
 
         //process each module for the singular version so we can populate the alt tag on the image
         $altStrings = array();
         foreach ($alt_modules[1] as $alt) {
             //create the alt string and replace the alt token
@@ -657,11 +671,11 @@ enableQS(false);
 
         if (!empty($this->selectedCategories) && !array_key_exists('UserFeed', $this->categories)) {
             // The user feed system isn't enabled, don't let them post notes
             return '';
         }
-        
+
         $user_name = ucfirst($GLOBALS['current_user']->user_name);
         $moreimg = SugarThemeRegistry::current()->getImage('advanced_search', 'onclick="toggleDisplay(\'more_' . $this->id . '\'); toggleDisplay(\'more_img_'.$this->id.'\'); toggleDisplay(\'less_img_'.$this->id.'\');"', null, null, '.gif', translate('LBL_SHOW_MORE_OPTIONS', 'SugarFeed'));
         $lessimg = SugarThemeRegistry::current()->getImage('basic_search', 'onclick="toggleDisplay(\'more_' . $this->id . '\'); toggleDisplay(\'more_img_'.$this->id.'\'); toggleDisplay(\'less_img_'.$this->id.'\');"', null, null, '.gif', translate('LBL_HIDE_OPTIONS', 'SugarFeed'));
         $ss = new Sugar_Smarty();
         $ss->assign('LBL_TO', translate('LBL_TO', 'SugarFeed'));

Vulnerable code: # SuiteCRM 8.3-beta
8.3-beta
    $maxAttempts = (int)($env['LOGIN_THROTTLING_MAX_ATTEMPTS'] ?? 3);
                        'max_attempts' => $maxAttempts
                        'max_attempts' => $maxAttempts
                        'max_attempts' => $maxAttempts,
                        'max_attempts' => $maxAttempts,
  "version": "8.3-beta",
  "version": "8.3-beta",
        let size = 0;
        if (errorMessage === 'Too many failed login attempts, please try again in 1 minute.') {
use App\Process\Service\ProcessHandlerRegistry;
    public function __construct(ProcessHandlerRegistry $registry, Security $security)
use App\Engine\Service\AclManagerInterface;
     * @var AclManagerInterface
    private $acl;
     * @param AclManagerInterface $acl
        AclManagerInterface $acl
        $this->acl = $acl;
    protected function getResolvedLegacyModule(string $primaryAction, string $secondaryAction, array $queryParams): string
    {
use App\Process\Entity\Process;
        } catch (BadFunctionCallException | \InvalidArgumentException $e) {
use App\Process\Entity\Process;
use App\Process\Entity\Process;
use App\Process\Entity\Process;
use App\Process\Entity\Process;
  "version": "8.3-beta",
        if (!is_admin($GLOBALS['current_user']) && !empty($GLOBALS['adminOnlyList'][$this->controller->module]) && !empty($GLOBALS['adminOnlyList'][$this->controller->module]['all']) && (empty($GLOBALS['adminOnlyList'][$this->controller->module][$this->controller->action]) || $GLOBALS['adminOnlyList'][$this->controller->module][$this->controller->action] != 'allow')) {
    
        
r51719 - 2009-10-22 10:18:00 -0700 (Thu, 22 Oct 2009) - mitani - Converted to Build 3  tags and updated the build system 
r22124 - 2007-04-20 16:54:53 -0700 (Fri, 20 Apr 2007) - clee - 
 * @author Wayne Pan {wayne at sugarcrm.com}
	if(empty($params['key']))  {
	    $smarty->trigger_error("sugarvar: missing 'key' parameter");
	    return;
	}

	$object = (empty($params['objectName']))?$smarty->get_template_vars('parentFieldArray'): $params['objectName'];
	$displayParams = $smarty->get_template_vars('displayParams');


	if(empty($params['memberName'])){
		$member = $smarty->get_template_vars('vardef');
		$member = $member['name'];
	}else{
		$members = explode('.', $params['memberName']);
		$member =  $smarty->get_template_vars($members[0]);
		for($i = 1; $i < count($members); $i++){
			$member = $member[$members[$i]];
		}
	}

    $_contents =  '$'. $object . '.' . $member . '.' . $params['key'];
	if(empty($params['stringFormat']) && empty($params['string'])) {
		$_contents = '{' . $_contents;
		if(!empty($displayParams['htmlescape'])){
			$_contents .= '|escape:\'html\'';
		}
		if(!empty($params['htmlentitydecode'])){
			$_contents .= '|escape:\'html_entity_decode\'';
		}
		if(!empty($displayParams['strip_tags'])){
			$_contents .= '|strip_tags';
		}
		if(!empty($displayParams['url2html'])){
			$_contents .= '|url2html';
		}
		if(!empty($displayParams['nl2br'])){
			$_contents .= '|nl2br';
		}

		$_contents .= '}';
            if ($_REQUEST['action'] == "EditView") {
    //'Administration' => ['all'=>1, 'SupportPortal'=>'allow'],
        $endOffset = (floor(($totalCount - 1) / $limit)) * $limit;
function isSmtp($value): bool {
                          INNER JOIN securitygroups_users secu 
                            ON secg.id = secu.securitygroup_id 
                               AND secu.deleted = 0 
                          INNER JOIN securitygroups_records secr 
                            ON secg.id = secr.securitygroup_id 
                               AND secr.deleted = 0 
        /* END - SECURITY GROUPS */ 
        $listview = preg_replace('/\[(\w+)\:([\w\-\d]*)\:([^\]]*)\]/', '<a href="index.php?module=$1&action=DetailView&record=$2"><img src="themes/default/images/$1.gif" border=0 REPLACE_ALT>$3</a>', $listview); /*SKIP_IMAGE_TAG*/
        
Fixed code: 
###> login throttling ###
LOGIN_THROTTLING_IP_LOGIN_MAX_ATTEMPTS=50
LOGIN_THROTTLING_INTERVAL="30 minutes"
###< login throttling ###
# SuiteCRM 8.3.0
8.3.0
      $adminOnlyModuleActions: '%system.admin_only_module_actions%'
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter;
    $maxAttempts = (int)($env['LOGIN_THROTTLING_MAX_ATTEMPTS'] ?? 5);
    $ipLoginMaxAttempts = (int)($env['LOGIN_THROTTLING_IP_LOGIN_MAX_ATTEMPTS'] ?? 50);
    $loginThrottlingInterval = (string)($env['LOGIN_THROTTLING_INTERVAL'] ?? '30 minutes');

    $containerConfig->extension('framework', [
        'rate_limiter' => [
            // define 2 rate limiters (one for username+IP, the other for IP)
            'username_ip_login' => [
                'policy' => 'token_bucket',
                'limit' => $maxAttempts,
                'rate' => [ 'interval' => $loginThrottlingInterval ],
            ],
            'ip_login' => [
                'policy' => 'sliding_window',
                'limit' => $ipLoginMaxAttempts,
                'interval' => $loginThrottlingInterval,
            ],
        ],
    ]);

    $containerConfig->services()->set('app.login_rate_limiter')
        ->class(DefaultLoginRateLimiter::class)
        ->args(
            [
                // 1st argument is the limiter for IP
                new Reference('limiter.ip_login'),
                // 2nd argument is the limiter for username+IP
                new Reference('limiter.username_ip_login'),
            ]
        );
                        'limiter' => 'app.login_rate_limiter'
                        'limiter' => 'app.login_rate_limiter',
                        'limiter' => 'app.login_rate_limiter',
                        'limiter' => 'app.login_rate_limiter',
parameters:
  system.admin_only_module_actions:
    administration:
      '*': true
    users:
      'list': true
      'index': true
    'sugar-feed':
      'adminsettings': true
    'address-cache':
      '*': true
    maps:
      'geocoded_counts': true
      'geocoding_test': true
      'config': true
    external-oauth-connection:
      '*': true
  "version": "8.3.0",
  "version": "8.3.0",
        let size = 20;
        if (typeof errorMessage === 'string' && errorMessage.includes('Too many failed login attempts, please try again in')) {
        if (check_default_module_access($legacyName) === false) {
            $hasAccess = false;
        }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        return [];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        return [];
    }

<?php
/**
 * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
 * Copyright (C) 2023 SalesAgility Ltd.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by the
 * Free Software Foundation with the addition of the following permission added
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Supercharged by SuiteCRM".
 */

declare(strict_types=1);

namespace App\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

final class Version20230420135520 extends BaseMigration implements ContainerAwareInterface
{
    use EnvHandlingMigrationTrait;

    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * @var LoggerInterface
     */
    protected $upgradeLogger;

    public function getDescription(): string
    {
        return 'Add login throttling interval to .env';
    }

    public function isTransactional(): bool
    {
        return false;
    }

    public function up(Schema $schema): void
    {
        $envFile = $this->getProjectDir() . "/.env";

        if (!file_exists($envFile)) {
            return;
        }

        $envContents = file_get_contents($envFile);

        $this->addLoginThrottlingConfig($envContents, $envFile);

    }

    public function down(Schema $schema): void
    {
    }

    /**
     * Check and add missing login throttling config
     * @param $envContents
     * @param string $envFile
     */
    protected function addLoginThrottlingConfig(&$envContents, string $envFile): void
    {
        $properties = [
            'LOGIN_THROTTLING_IP_LOGIN_MAX_ATTEMPTS' => '50',
            'LOGIN_THROTTLING_INTERVAL' => '"30 minutes"'
        ];

        $wrapperStart = '###> login throttling ###';
        $wrapperEnd = '###< login throttling ###';

        $propertiesToAdd = $this->getContentToAdd($envContents, $properties, $wrapperStart, $wrapperEnd);
        if (!empty($propertiesToAdd)) {
            $envContents .= $propertiesToAdd;
            file_put_contents($envFile, $envContents);
            $this->log('Added LOGIN_THROTTLING_INTERVAL to .env.');

            return;
        }

        $this->log('LOGIN_THROTTLING_INTERVAL already in .env, skipping.');
    }
}
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        ['favorite' => $favorite] = $process->getOptions();

        $module = $favorite['attributes']['parent_type'] ?? '';
        $id = $favorite['attributes']['parent_id'] ?? '';

        return [
            $module => [
                [
                    'action' => 'view',
                    'record' => $id
                ]
            ]
        ];
    }


        ['favorite' => $favorite, 'action' => $action] = $process->getOptions();

        $module = $favorite['attributes']['parent_type'] ?? '';
        $id = $favorite['attributes']['parent_id'] ?? '';

        if (empty($module) || empty($id) || empty($action)) {
            throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
        }
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        ['recentlyViewed' => $recentlyViewed] = $process->getOptions();
        $itemId = $recentlyViewed['attributes']['item_id'] ?? '';
        $itemModule = $recentlyViewed['attributes']['module_name'] ?? '';

        return [
            $itemModule => [
                [
                    'action' => 'view',
                    'record' => $itemId,
                ]
            ]
        ];
    }


        ['recentlyViewed' => $recentlyViewed] = $process->getOptions();
        $itemModule = $recentlyViewed['attributes']['module_name'] ?? '';
        $itemModule = $this->moduleNameMapper->toLegacy($itemModule);
        $action = $recentlyViewed['attributes']['action'] ?? '';


        if (empty($itemModule) || empty($action)) {
            throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
        }
/**
 * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
 * Copyright (C) 2021 SalesAgility Ltd.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by the
 * Free Software Foundation with the addition of the following permission added
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Supercharged by SuiteCRM".
 */
use App\Engine\Service\AclManagerInterface;
use App\Process\Service\ProcessHandlerRegistry;

    /**
     * @var AclManagerInterface
     */
    private $acl;

     * @param AclManagerInterface $acl
    public function __construct(ProcessHandlerRegistry $registry, Security $security, AclManagerInterface $acl)
        $this->acl = $acl;

        $hasAccess = $this->checkACLAccess($processHandler, $process);

        if (!$hasAccess) {
            $process->setMessages(['LBL_ACCESS_DENIED']);
            $process->setStatus('error');

            return $process;
        }

    /**
     * Check acl access
     * @param ProcessHandlerInterface $processHandler
     * @param Process $process
     */
    protected function checkACLAccess(ProcessHandlerInterface $processHandler, Process $process): bool
    {
        $modulesACLs = $processHandler->getRequiredACLs($process) ?? [];
        if (empty($modulesACLs)) {
            return true;
        }

        $hasAccess = true;
        foreach ($modulesACLs as $module => $requiredACLs) {
            if (empty($requiredACLs)) {
                continue;
            }
            if (empty($module)) {
                continue;
            }

            if ($hasAccess === false) {
                return false;
            }

            foreach ($requiredACLs as $requiredACL) {
                $record = $requiredACL['record'] ?? '';
                $ids = $requiredACL['ids'] ?? '';
                $action = $requiredACL['action'] ?? '';

                if (empty($action)) {
                    continue;
                }

                if ($hasAccess === false) {
                    return false;
                }

                if (empty($record) && empty($ids)) {
                    $hasAccess &= $this->acl->checkAccess($module, $action, true, 'module', true);
                    continue;
                }

                if (!empty($record)) {
                    $hasAccess &= $this->acl->checkRecordAccess($module, $action, $record);
                    continue;
                }

                if (!empty($ids)) {
                    $hasAccess &= $this->checkRecordsAccess($ids, $module, $action);
                }
            }
        }

        return $hasAccess;
    }


    /**
     * @param array $ids
     * @param string $module
     * @param string $action
     * @return bool
     */
    protected function checkRecordsAccess(array $ids, string $module, string $action): bool
    {
        if (empty($ids)) {
            return true;
        }

        foreach ($ids as $id) {
            $hasAccess = $this->acl->checkRecordAccess($module, $action, $id);
            if ($hasAccess === false) {
                return false;
            }
        }

        return true;
    }
use App\Process\Service\ActionNameMapperInterface;

    /**
     * @var array
     */
    private $adminOnlyModuleActions;

     * @var ActionNameMapperInterface
    private $actionNameMapper;
     * @param array $adminOnlyModuleActions
        ActionNameMapperInterface $actionNameMapper,
        array $adminOnlyModuleActions
        $this->actionNameMapper = $actionNameMapper;
        $this->adminOnlyModuleActions = $adminOnlyModuleActions;
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        return [];
    }

        global $current_user;
        $isAdmin = is_admin($current_user);
        $isActionAdminOnly = $this->isAdminOnlyAction($frontEndModuleName, $legacyModuleName, $actionKey, $queryParams);

        if ($isActionAdminOnly && !$isAdmin) {
            $hasAccess = false;
        }


    protected function getResolvedLegacyModule(
        string $primaryAction,
        string $secondaryAction,
        array $queryParams
    ): string {

    /**
     * @param string $module
     * @param string $legacyModule
     * @param string $actionKey
     * @param array $queryParams
     * @return bool
     */
    protected function isAdminOnlyAction(string $module, string $legacyModule,  string $actionKey, array $queryParams): bool
    {
        if (!empty($queryParams['import_module']) && strtolower($module) === 'import') {
            $module = $this->moduleNameMapper->toFrontEnd($queryParams['import_module']);
            $legacyModule = $queryParams['import_module'];
            $actionKey = 'import';
        }

        $adminOnlyList = [];

        /* @noinspection PhpIncludeInspection */
        require_once 'include/modules.php';
        $legacyActionName = strtolower($this->actionNameMapper->toLegacy($actionKey));
        $adminOnlyActions = $adminOnlyList[$legacyModule] ?? [];
        $adminOnlyAction = $adminOnlyActions[$legacyActionName] ?? $adminOnlyActions['all'] ?? false;
        $isActionAdminOnly = !empty($adminOnlyAction) && $adminOnlyAction !== 'allow';

        if ($isActionAdminOnly) {
            return true;
        }

        $adminOnlyActions = $this->adminOnlyModuleActions[$module] ?? [];

        return $adminOnlyActions[strtolower($actionKey)] ?? $adminOnlyActions['*'] ?? false;
    }
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $baseModule = $options['module'] ?? '';
        $baseIds = $options['ids'] ?? [];
        $modalRecord = $options['modalRecord'] ?? [];
        $modalModule = $modalRecord['module'] ?? '';
        $modalRecordId = $modalRecord['id'] ?? '';

        return [
            $baseModule => [
                [
                    'action' => 'view',
                    'ids' => $baseIds
                ]
            ],
            $modalModule => [
                [
                    'action' => 'view',
                    'record' => $modalRecordId,
                ]
            ],
        ];
    }

        $this->startLegacyApp();
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $baseModule = $options['module'] ?? '';
        $baseIds = $options['ids'] ?? [];
        $modalRecord = $options['modalRecord'] ?? [];
        $modalModule = $modalRecord['module'] ?? '';

        return [
            $baseModule => [
                [
                    'action' => 'view',
                    'ids' => $baseIds
                ]
            ],
            $modalModule => [
                ['action' => 'view']
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $baseModule = $options['module'] ?? '';
        $baseModuleId = $options['id'] ?? '';
        $payload = $options['payload'] ?? [];
        $relateModule = $payload['relateModule'] ?? '';
        $relateRecordIds = $payload['relateRecordIds'] ?? [];

        return [
            $baseModule => [
                [
                    'action' => 'view',
                    'record' => $baseModuleId
                ]
            ],
            $relateModule => [
                [
                    'action' => 'view',
                    'ids' => $relateRecordIds
                ]
            ]
        ];
    }

use App\Process\Entity\Process;
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        return [];
    }


        } catch (BadFunctionCallException|\InvalidArgumentException $e) {
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $payload = $options['payload'] ?? [];
        $baseModule = $payload['baseModule'] ?? '';
        $relateModule = $payload['relateModule'] ?? '';
        $relateModuleId = $payload['relateRecordId'] ?? '';

        $acls = [
            $baseModule => [
                [
                    'action' => 'view',
                    'record' => $payload['baseRecordId'] ?? ''
                ],
                [
                    'action' => 'edit'
                ],
            ],
            $relateModule => [
                [
                    'action' => 'view',
                    'record' => $relateModuleId
                ]
            ]
        ];

        if (!empty($relateModule) && !empty($relateModuleId)) {
            $acls[$relateModule] = [
                [
                    'action' => 'view',
                    'record' =>  $relateModuleId ?? ''
                ]
            ];
        }

        return $acls;
    }


use App\Process\Entity\Process;
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $ids = $options['ids'] ?? [];

        return [
            $module => [
                [
                    'action' => 'export',
                    'ids' => $ids
                ]
            ]
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $ids = $options['ids'] ?? [];

        return [
            $module => [
                [
                    'action' => 'delete',
                    'ids' => $ids
                ]
            ]
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $ids = $options['ids'] ?? [];

        return [
            $module => [
                [
                    'action' => 'edit',
                    'ids' => $ids
                ],
                [
                    'action' => 'massupdate'
                ]
            ],
        ];
    }


use App\Process\Entity\Process;
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $ids = $options['ids'] ?? [];

        return [
            $module => [
                [
                    'action' => 'view'
                ],
                [
                    'action' => 'edit',
                    'ids' => $ids
                ]
            ]
        ];
    }

use App\Process\Entity\Process;
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $ids = $options['ids'] ?? [];


        return [
            $module => [
                [
                    'action' => 'view'
                ],
                [
                    'action' => 'export',
                    'ids' => $ids
                ]
            ]
        ];
    }

    /**
     * Get required acls list per module, empty array means no acl check needed
     *
     * @param Process $process
     * @return array
     */
    public function getRequiredACLs(Process $process): array;

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $recordId = $options['id'] ?? '';

        return [
            $module => [
                [
                    'action' => 'view',
                    'record' => $recordId
                ]
            ],
        ];
    }

use App\Process\Entity\Process;
    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $id = $options['id'] ?? '';

        return [
            $module => [
                [
                    'action' => 'view',
                    'record' => $id
                ],
            ],
        ];

    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';

        return [
            $module => [
                [
                    'action' => 'delete',
                    'record' => $options['id'] ?? ''
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';

        return [
            $module => [
                [
                    'action' => 'edit',
                    'record' => $options['id'] ?? ''
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';

        $baseModule = $options['payload']['baseModule'] ?? '';
        $baseRecord = $options['payload']['baseRecordId'] ?? '';

        return [
            $module => [
                [
                    'action' => 'edit',
                    'record' => $options['id'] ?? ''
                ]
            ],
            $baseModule => [
                [
                    'action' => 'view',
                    'record' => $baseRecord
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';

        return [
            $module => [
                [
                    'action' => 'edit',
                    'record' => $options['id'] ?? ''
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';

        $modalRecord = $options['params']['modalRecord'] ?? [];
        $modalRecordModule = $modalRecord['module'] ?? '';
        $modalRecordId = $modalRecord['id'] ?? '';

        $acls = [
            $module => [
                [
                    'action' => 'view',
                    'record' => $options['id'] ?? ''
                ],
                [
                    'action' => 'export',
                    'record' => $options['id'] ?? ''
                ]
            ],
        ];

        if ($modalRecordModule !== '') {
            $acls[$modalRecordModule] = [
                [
                    [
                        'action' => 'view',
                        'record' => $modalRecordId
                    ]
                ]
            ];
        }

        return $acls;

    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';

        return [
            $module => [
                [
                    'action' => 'delete',
                    'record' => $options['id'] ?? ''
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';

        return [
            $module => [
                [
                    'action' => 'delete',
                    'record' => $options['id'] ?? ''
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';

        return [
            $module => [
                [
                    'action' => 'view',
                    'record' => $options['id'] ?? ''
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $ids = $options['ids'] ?? [];

        return [
            $module => [
                [
                    'action' => 'delete',
                    'ids' => $ids
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $ids = $options['ids'] ?? [];

        return [
            'alerts' => [
                [
                    'action' => 'delete',
                    'ids' => $ids
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $ids = $options['ids'] ?? [];

        return [
            $module => [
                [
                    'action' => 'view',
                    'ids' => $ids
                ]
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        return [];
    }

  "version": "8.3.0",

        if (check_default_module_access($this->module_name ?? '') === false) {
            return false;
        }


        $module = $this->controller->module ?? '';
        $action = strtolower($this->controller->action ?? '');

        if (!empty($_REQUEST['import_module'] ?? '') && strtolower($module) === 'import') {
            $module = $_REQUEST['import_module'] ?? '';
            $action = 'import';
        }

        $adminOnlyList = $GLOBALS['adminOnlyList'] ?? [];
        $adminOnlyModuleActions = $adminOnlyList[$module] ?? [];
        $adminOnlyAction = $adminOnlyModuleActions[$action] ?? $adminOnlyModuleActions['all'] ?? false;
        $isAdminOnly = !empty($adminOnlyAction) && $adminOnlyAction !== 'allow';

        if ($isAdminOnly && !is_admin($GLOBALS['current_user'])) {


r51719 - 2009-10-22 10:18:00 -0700 (Thu, 22 Oct 2009) - mitani - Converted to Build 3  tags and updated the build system
r22124 - 2007-04-20 16:54:53 -0700 (Fri, 20 Apr 2007) - clee -
 * @author Wayne Pan {wayne at sugarcrm.com}
    if (empty($params['key'])) {
        $smarty->trigger_error("sugarvar: missing 'key' parameter");

        return;

    $object = (empty($params['objectName'])) ? $smarty->get_template_vars('parentFieldArray') : $params['objectName'];
    $displayParams = $smarty->get_template_vars('displayParams');


    if (empty($params['memberName'])) {
        $member = $smarty->get_template_vars('vardef');
        $member = $member['name'];
    } else {
        $members = explode('.', $params['memberName']);
        $member = $smarty->get_template_vars($members[0]);
        for ($i = 1; $i < count($members); $i++) {
            $member = $member[$members[$i]];
        }
    }

    $_contents = '$' . $object . '.' . $member . '.' . $params['key'];
    if (empty($params['stringFormat']) && empty($params['string'])) {
        $_contents = '{' . $_contents;
        if (!empty($displayParams['htmlescape'])) {
            $_contents .= '|escape:\'html\'';
        }
        if (!empty($params['htmlentitydecode'])) {
            $_contents .= '|escape:\'html_entity_decode\'';
        }
        if (!empty($displayParams['strip_tags'])) {
            $_contents .= '|strip_tags';
        }
        if (!empty($displayParams['url2html'])) {
            $_contents .= '|url2html';
        }
        if (!empty($displayParams['nl2br'])) {
            $_contents .= '|nl2br';
        }
        if (!empty($displayParams['purifyHtml'])) {
            $_contents .= '|htmlpurify';
        }

        $_contents .= '}';
    }


<?php
/**
 * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
 * Copyright (C) 2023 SalesAgility Ltd.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by the
 * Free Software Foundation with the addition of the following permission added
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Supercharged by SuiteCRM".
 */

/**
 * Smarty plugin
 * @package Smarty
 * @subpackage plugins
 */

/**
 * Smarty HTML purifier modifier plugin
 *
 * Type:     modifier<br>
 * Name:     htmlpurify<br>
 * Purpose:  purify html to display
 * @param string
 * @return string
 */

function smarty_modifier_htmlpurify($string)
{
    $string = purify_html(securexss($string));

    return $string;
}
        if (isset($vardef['editor']) && $vardef['editor'] === "html") {
            $displayParams['purifyHtml'] = $displayParams['purifyHtml'] ?? true;
        }

            if (isset($vardef['editor']) && $vardef['editor'] === "html") {
                $displayParams['purifyHtml'] = $displayParams['purifyHtml'] ?? true;
            }
            if ($_REQUEST['action'] === "EditView") {
    'Administration' => ['all'=>1, 'supportportal'=>'allow'],
    'ExternalOAuthConnection' => ['all' => 1],
    'jjwg_Address_Cache' => ['all' => 1],
    'SugarFeed' => ['adminsettings' => 1],
    'jjwg_Maps' => ['geocoded_counts' => 1, 'geocoding_test' => 1, 'config' => 1],
require_once __DIR__ . '/TypeMappers/HtmlMapper.php';
require_once __DIR__ . '/TypeMappers/TextMapper.php';
        $this->typeMappers[HtmlMapper::getType()] = new HtmlMapper();
        $this->typeMappers[TextMapper::getType()] = new TextMapper();
<?php
/**
 * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
 * Copyright (C) 2023 SalesAgility Ltd.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by the
 * Free Software Foundation with the addition of the following permission added
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Supercharged by SuiteCRM".
 */

require_once __DIR__ . '/TypeMapperInterface.php';

class HtmlMapper implements TypeMapperInterface
{
    /**
     * @inheritDoc
     */
    public static function getType(): string
    {
        return 'html';
    }

    /**
     * @inheritDoc
     */
    public function toApi(SugarBean $bean, array &$container, string $name, string $alternativeName = ''): void
    {
        $newName = $name;

        if (!empty($alternativeName)) {
            $newName = $alternativeName;
        }

        if (empty($bean->$name)) {
            $container[$newName] = '';

            return;
        }

        $container[$newName] = $bean->$name;
    }

    /**
     * @inheritDoc
     */
    public function toBean(SugarBean $bean, array &$container, string $name, string $alternativeName = ''): void
    {
        $newName = $name;

        if (!empty($alternativeName)) {
            $newName = $alternativeName;
        }

        $fieldDef = $bean->field_defs[$newName] ?? [];

        $purifyHtml = $fieldDef['metadata']['purifyHtml'] ?? true;
        $value = $container[$newName] ?? '';

        if ($purifyHtml === false || empty($value) || !is_string($value)) {
            return;
        }

        $container[$newName] = purify_html(securexss($value));
    }
}
<?php
/**
 * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
 * Copyright (C) 2023 SalesAgility Ltd.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by the
 * Free Software Foundation with the addition of the following permission added
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Supercharged by SuiteCRM".
 */

require_once __DIR__ . '/TypeMapperInterface.php';

class TextMapper implements TypeMapperInterface
{
    /**
     * @inheritDoc
     */
    public static function getType(): string
    {
        return 'text';
    }

    /**
     * @inheritDoc
     */
    public function toApi(SugarBean $bean, array &$container, string $name, string $alternativeName = ''): void
    {
        $newName = $name;

        if (!empty($alternativeName)) {
            $newName = $alternativeName;
        }

        if (empty($bean->$name)) {
            $container[$newName] = '';

            return;
        }

        $container[$newName] = $bean->$name;
    }

    /**
     * @inheritDoc
     */
    public function toBean(SugarBean $bean, array &$container, string $name, string $alternativeName = ''): void
    {
        $newName = $name;

        if (!empty($alternativeName)) {
            $newName = $alternativeName;
        }

        $fieldDef = $bean->field_defs[$newName] ?? [];

        $editor = $fieldDef['editor'] ?? '';
        $purifyHtml = $fieldDef['metadata']['purifyHtml'] ?? true;

        if (empty($editor) || $editor !== 'html' || $purifyHtml === false || !is_string($container[$newName] ?? '')) {
            return;
        }

        $container[$newName] = purify_html(securexss($container[$newName] ?? ''));
    }
}
        if ($limit > 0) {
            $endOffset = (floor(($totalCount - 1) / $limit)) * $limit;
        } else {
            $endOffset = 0;
        }


                if (empty($contact)) {
                    continue;
                }

                $hasAccess = $contact->ACLAccess('view');
                if (!$hasAccess) {
                    continue;
                }

        'default_module_access' => [
            'SecurityGroups' => false,
            'AOW_WorkFlow' => false
        ],
        'default_module_access' => [
            'SecurityGroups' => false,
            'AOW_WorkFlow' => false,
        ],
/**
 * Check if user has action acls defined
 * @param string $module
 * @param string $action
 * @return bool
 */
function has_action_acls_defined(string $module, string $action): bool
{
    global $current_user;

    $hasGroupActionAcls = true;

    $aclActions = ACLAction::getUserActions($current_user->id, false, $module, 'module', $action);
    $isDefaultListACL = !empty($aclActions['isDefault']) && isTrue($aclActions['isDefault']);

    if (empty($aclActions) || $isDefaultListACL) {
        $hasGroupActionAcls = false;
    }

    return $hasGroupActionAcls;
}

/**
 * Check default module access
 * @param string $module
 * @return bool
 */
function check_default_module_access(string $module): bool
{
    global $sugar_config, $current_user;

    if (empty($module) || is_admin($current_user)) {
        return true;
    }

    $hasActionAclsDefined = has_action_acls_defined($module, 'access');
    $defaultModuleAccessConfig = $sugar_config['default_module_access'] ?? [];
    $defaultModuleAccess = $defaultModuleAccessConfig[$module] ?? true;
    if ($defaultModuleAccess === false && isFalse($hasActionAclsDefined ?? false)) {
        return false;
    }

    return true;
}

function isSmtp($value): bool
{
                          INNER JOIN securitygroups_users secu
                            ON secg.id = secu.securitygroup_id
                               AND secu.deleted = 0
                          INNER JOIN securitygroups_records secr
                            ON secg.id = secr.securitygroup_id
                               AND secr.deleted = 0
        /* END - SECURITY GROUPS */
        require_once 'include/portability/RouteConverter.php';
        $routeConverter = new RouteConverter();

        $listview = preg_replace_callback(
            '/\[(\w+)\:([\w\-\d]*)\:([^\]]*)\]/',
            static function (&$matches) use ($routeConverter) {
                $module = $matches[1] ?? '';
                $record = $matches[2] ?? '';
                $name = $matches[3] ?? '';
                $uiLink = str_replace('./', '../', $routeConverter->convert($module, 'DetailView', $record, []));

                return '<a href="' . $uiLink . '">' . $name . '</a>';
            },
            $listview
        );/*SKIP_IMAGE_TAG*/