From 8ec4be684882147ff4e758f3eb1a2cd5ec9c0393 Mon Sep 17 00:00:00 2001 From: Artur Kyryliuk Date: Sun, 30 Aug 2026 00:01:14 +0200 Subject: [PATCH] feat(manager): Ability to restrict roles to run specific modules --- ..._000000_create_site_module_roles_table.php | 79 ++++++++ core/lang/az/global.php | 7 + core/lang/be/global.php | 7 + core/lang/bg/global.php | 7 + core/lang/cs/global.php | 7 + core/lang/da/global.php | 7 + core/lang/de/global.php | 7 + core/lang/en/global.php | 7 + core/lang/es/global.php | 7 + core/lang/fa/global.php | 7 + core/lang/fi/global.php | 7 + core/lang/fr/global.php | 7 + core/lang/he/global.php | 7 + core/lang/it/global.php | 7 + core/lang/ja/global.php | 7 + core/lang/nl/global.php | 7 + core/lang/nn/global.php | 7 + core/lang/pl/global.php | 7 + core/lang/pt/global.php | 7 + core/lang/ru/global.php | 7 + core/lang/sv/global.php | 7 + core/lang/uk/global.php | 7 + core/lang/zh/global.php | 7 + core/src/Console/SiteUpdateCommand.php | 6 +- core/src/Controllers/Frame.php | 33 +++- core/src/Controllers/Modules.php | 8 +- core/src/Controllers/Resources/Modules.php | 18 +- core/src/Controllers/UserRoles/UserRole.php | 106 +++++++++++ core/src/Models/SiteModule.php | 67 ++++++- core/src/Models/SiteModuleRole.php | 66 +++++++ .../Services/Store/LegacyDeleteService.php | 1 + core/src/Support/ModuleAccess.php | 118 ++++++++++++ .../Feature/ModuleRoleAclDatabaseTest.php | 177 ++++++++++++++++++ .../Unit/Manager/ModuleRoleAccessTest.php | 75 ++++++++ .../Unit/Security/ModuleExecutionAclTest.php | 148 +++++++++++++++ core/vendor/composer/autoload_classmap.php | 3 + core/vendor/composer/autoload_static.php | 3 + install/cli-install.php | 4 +- install/src/controllers/install.php | 4 +- ..._000000_create_site_module_roles_table.php | 79 ++++++++ .../processors/delete_module.processor.php | 2 + manager/processors/delete_role.processor.php | 3 + .../processors/duplicate_module.processor.php | 8 + .../processors/execute_module.processor.php | 54 ++++-- .../views/page/user_roles/user_role.blade.php | 39 ++++ 45 files changed, 1218 insertions(+), 37 deletions(-) create mode 100644 core/database/migrations/2026_08_29_000000_create_site_module_roles_table.php create mode 100644 core/src/Models/SiteModuleRole.php create mode 100644 core/src/Support/ModuleAccess.php create mode 100644 core/tests/Feature/ModuleRoleAclDatabaseTest.php create mode 100644 core/tests/Unit/Manager/ModuleRoleAccessTest.php create mode 100644 core/tests/Unit/Security/ModuleExecutionAclTest.php create mode 100644 install/stubs/migrations/2026_08_29_000000_create_site_module_roles_table.php diff --git a/core/database/migrations/2026_08_29_000000_create_site_module_roles_table.php b/core/database/migrations/2026_08_29_000000_create_site_module_roles_table.php new file mode 100644 index 0000000000..15a40f3916 --- /dev/null +++ b/core/database/migrations/2026_08_29_000000_create_site_module_roles_table.php @@ -0,0 +1,79 @@ +comment('Module role access control - defines which user roles may run specific modules'); + $table->increments('id'); + $table->unsignedInteger('module')->default(0); + $table->unsignedInteger('role')->default(0); + $table->unique(['module', 'role'], 'site_module_roles_module_role'); + }); + } + + $this->applyDefaultRestrictions(); + } + + public function down() + { + Schema::dropIfExists('site_module_roles'); + } + + /** + * Restrict bundled modules on sites that already have them installed. + * + * Only modules with no restriction at all are touched, so an administrator + * who has opened a module up keeps that decision. + */ + protected function applyDefaultRestrictions(): void + { + if (!Schema::hasTable('site_modules') || !Schema::hasTable('user_roles')) { + return; + } + + foreach (ModuleAccess::DEFAULT_RESTRICTIONS as $name => $roleIds) { + $moduleId = (int) DB::table('site_modules')->where('name', $name)->value('id'); + if ($moduleId <= 0) { + continue; + } + + if (DB::table('site_module_roles')->where('module', $moduleId)->exists()) { + continue; + } + + foreach (ModuleAccess::normalizeRoleIds($roleIds) as $roleId) { + if (!DB::table('user_roles')->where('id', $roleId)->exists()) { + continue; + } + + DB::table('site_module_roles')->insert([ + 'module' => $moduleId, + 'role' => $roleId, + ]); + } + } + } +} diff --git a/core/lang/az/global.php b/core/lang/az/global.php index a3fb7d4349..f1f71bc677 100644 --- a/core/lang/az/global.php +++ b/core/lang/az/global.php @@ -80,6 +80,10 @@ $_lang["tmplvars_type"] = 'Daxil Etmə Növü'; $_lang["tmplvars_widget"] = 'Widget'; $_lang["tmplvars_widget_prop"] = 'Widget Xüsusiyyətləri'; +$_lang["role_modules_tab"] = 'Modullar'; +$_lang["role_modules_msg"] = 'Bu rolun işə salmasına icazə verilən modullar. Heç kimin məhdudlaşdırmadığı modul bütün rollar üçün əlçatan qalır — buradan işarəni götürmək onu məhdudlaşdırır.'; +$_lang["role_modules_admin_msg"] = 'Administrator rolu bütün modulları işə sala bilər.'; +$_lang["role_modules_none"] = 'Hələ heç bir modul quraşdırılmayıb.'; $_lang["role_no_tv"] = 'Bu Rol üçün hələ dəyişənlər təyin olunmayıb.'; $_lang["role_notassigned_tv"] = 'Təyin edilə bilən dəyişənlər.'; $_lang["role_tv_msg"] = 'Bu Rola təyin olunmuş dəyişənlər aşağıda göstərilib.'; @@ -203,6 +207,9 @@ $_lang["module_desc"] = 'Təsvir'; $_lang["module_disabled"] = 'Modul deaktiv edilib'; $_lang["module_edit_click_title"] = 'Bu Modulu redaktə etmək üçün buraya klikləyin'; +$_lang["module_exec_no_privileges"] = 'Bu modulu işə salmaq üçün kifayət qədər icazəniz yoxdur.'; +$_lang["module_exec_not_found"] = '%s id-li modul tapılmadı.'; +$_lang["module_exec_disabled"] = 'Bu modul deaktivdir və işə salına bilməz.'; $_lang["module_group_access_msg"] = 'Bu Modulu İdarəetmə Panelində icra edə biləcək İstifadəçi Qruplarını seçin.'; $_lang["module_management"] = 'Modulları idarə et'; $_lang["module_management_msg"] = 'İcra etmək və ya dəyişdirmək istədiyiniz Modulu seçin. Modulu işə salmaq üçün şəbəkədəki ikona klikləyin. Modulu redaktə etmək üçün onun adına klikləyin.'; diff --git a/core/lang/be/global.php b/core/lang/be/global.php index 168a85087a..c944295d8a 100644 --- a/core/lang/be/global.php +++ b/core/lang/be/global.php @@ -68,6 +68,10 @@ $_lang["tmplvars_type"] = 'Тып уводу'; $_lang["tmplvars_widget"] = 'Віджэт'; $_lang["tmplvars_widget_prop"] = 'Уласцівасці віджэта'; +$_lang["role_modules_tab"] = 'Модулі'; +$_lang["role_modules_msg"] = 'Модулі, якія дазволена запускаць гэтай ролі. Модуль, які ніхто не абмежаваў, застаецца даступным усім ролям — зніміце адзнаку, каб абмежаваць яго.'; +$_lang["role_modules_admin_msg"] = 'Роля адміністратара можа запускаць любыя модулі.'; +$_lang["role_modules_none"] = 'Модулі яшчэ не ўсталяваны.'; $_lang["role_no_tv"] = 'Гэтай ролі яшчэ не было прызначана зменных.'; $_lang["role_notassigned_tv"] = 'Гэтыя зменныя даступныя для прызначэння.'; $_lang["role_tv_msg"] = 'Зменныя, прызначаныя гэтай ролі, пералічаны ніжэй.'; @@ -191,6 +195,9 @@ $_lang["module_desc"] = 'Апісанне'; $_lang["module_disabled"] = 'Модуль адключаны'; $_lang["module_edit_click_title"] = 'Націсніце тут, каб змяніць гэты модуль'; +$_lang["module_exec_no_privileges"] = 'У вас недастаткова правоў для запуску гэтага модуля.'; +$_lang["module_exec_not_found"] = 'Модуль з id %s не знойдзены.'; +$_lang["module_exec_disabled"] = 'Гэты модуль адключаны і не можа быць запушчаны.'; $_lang["module_group_access_msg"] = 'Выберыце групы карыстальнікаў, якім дазволена выконваць гэты модуль з Менеджара кантэнту.'; $_lang["module_management"] = 'Кіраванне модулямі'; $_lang["module_management_msg"] = 'Выберыце модуль, які вы хочаце выканаць або змяніць. Каб запусціць модуль, націсніце на значок у сетцы. Каб змяніць модуль, націсніце на назву модуля.'; diff --git a/core/lang/bg/global.php b/core/lang/bg/global.php index 12849c4650..78073ab066 100644 --- a/core/lang/bg/global.php +++ b/core/lang/bg/global.php @@ -504,6 +504,9 @@ $_lang["module_desc"] = 'Описание'; $_lang["module_disabled"] = 'Модулът е спрян'; $_lang["module_edit_click_title"] = 'Щракнете тук, за да редактирате този модул'; +$_lang["module_exec_no_privileges"] = 'Нямате достатъчно права да изпълните този модул.'; +$_lang["module_exec_not_found"] = 'Не е намерен модул с id %s.'; +$_lang["module_exec_disabled"] = 'Този модул е изключен и не може да бъде изпълнен.'; $_lang["module_group_access_msg"] = 'Изберете потребителските групи, имащи права да стартират този модул от Мениджъра.'; $_lang["module_management"] = 'Управление на Модули'; $_lang["module_management_msg"] = 'Тук можете да изберете модула, който желаете да стартирате или модифицирате. За да стартирате модул, щракнете върху иконата в таблицата. За да модифицирате модул, кликнете върху името на модула.'; @@ -1249,6 +1252,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = 'Модули'; +$_lang["role_modules_msg"] = 'Модули, които тази роля може да изпълнява. Модул, който никой не е ограничил, остава достъпен за всички роли — премахването на отметката тук го ограничава.'; +$_lang["role_modules_admin_msg"] = 'Ролята администратор може да изпълнява всички модули.'; +$_lang["role_modules_none"] = 'Все още няма инсталирани модули.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/cs/global.php b/core/lang/cs/global.php index c0432c5849..1cf881b082 100644 --- a/core/lang/cs/global.php +++ b/core/lang/cs/global.php @@ -504,6 +504,9 @@ $_lang["module_desc"] = 'Popis'; $_lang["module_disabled"] = 'Modul je zakázaný'; $_lang["module_edit_click_title"] = 'Pro úpravu tohoto modulu klikněte zde'; +$_lang["module_exec_no_privileges"] = 'Nemáte dostatečná oprávnění ke spuštění tohoto modulu.'; +$_lang["module_exec_not_found"] = 'Nebyl nalezen žádný modul s id %s.'; +$_lang["module_exec_disabled"] = 'Tento modul je zakázán a nelze jej spustit.'; $_lang["module_group_access_msg"] = 'Zvolte skupiny uživatelů, které mají oprávnění spustit tento modul ve Správci obsahu.'; $_lang["module_management"] = 'Správa modulů'; $_lang["module_management_msg"] = 'Zde můžete zvolit modul, který byste chtěli spustit nebo upravit. Ke spuštění modulu klikněte na ikonu kostky. Pro úpravu modulu klikněte na název modulu.'; @@ -1253,6 +1256,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = 'Moduly'; +$_lang["role_modules_msg"] = 'Moduly, které tato role smí spouštět. Modul, který nikdo neomezil, zůstává dostupný všem rolím — zrušením zaškrtnutí jej omezíte.'; +$_lang["role_modules_admin_msg"] = 'Role administrátora může spustit každý modul.'; +$_lang["role_modules_none"] = 'Zatím nejsou nainstalovány žádné moduly.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/da/global.php b/core/lang/da/global.php index 09aade39da..29d2602412 100644 --- a/core/lang/da/global.php +++ b/core/lang/da/global.php @@ -505,6 +505,9 @@ $_lang["module_desc"] = 'Beskrivelse'; $_lang["module_disabled"] = 'Modulet deaktiveret'; $_lang["module_edit_click_title"] = 'Klik her for at redigere dette modul'; +$_lang["module_exec_no_privileges"] = 'Du har ikke tilstrækkelige rettigheder til at køre dette modul.'; +$_lang["module_exec_not_found"] = 'Der blev ikke fundet noget modul med id %s.'; +$_lang["module_exec_disabled"] = 'Dette modul er deaktiveret og kan ikke køres.'; $_lang["module_group_access_msg"] = 'Vælg den eller de brugergrupper som har rettigheder til at eksekvere dette modul indefra CMS\'et.'; $_lang["module_management"] = 'Administrer moduler'; $_lang["module_management_msg"] = 'Vælg det modul du vil eksekvere eller redigere. For at køre modulet skal man klikke på ikonet i oversigten. For at redigere modulet skal man klikke på modulets navn.'; @@ -1252,6 +1255,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = 'Moduler'; +$_lang["role_modules_msg"] = 'Moduler, som denne rolle må køre. Et modul, som ingen har begrænset, er tilgængeligt for alle roller — det er fjernelsen af fluebenet her, der begrænser det.'; +$_lang["role_modules_admin_msg"] = 'Administratorrollen kan køre alle moduler.'; +$_lang["role_modules_none"] = 'Der er endnu ikke installeret nogen moduler.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/de/global.php b/core/lang/de/global.php index ea3e4f1f78..c5f6954dcd 100644 --- a/core/lang/de/global.php +++ b/core/lang/de/global.php @@ -541,6 +541,9 @@ $_lang["module_desc"] = 'Beschreibung'; $_lang["module_disabled"] = 'Modul deaktivieren'; $_lang["module_edit_click_title"] = 'Klicken Sie hier, um das Modul zu bearbeiten'; +$_lang["module_exec_no_privileges"] = 'Sie haben nicht genügend Rechte, um dieses Modul auszuführen.'; +$_lang["module_exec_not_found"] = 'Kein Modul mit der ID %s gefunden.'; +$_lang["module_exec_disabled"] = 'Dieses Modul ist deaktiviert und kann nicht ausgeführt werden.'; $_lang["module_group_access_msg"] = 'Wählen Sie die Benutzergruppen aus, die dieses Modul innerhalb des MODX-Managers ausführen dürfen.'; $_lang["module_management"] = 'Modul-Verwaltung'; $_lang["module_management_msg"] = 'In dieser Übersicht können Sie Module ausführen und bearbeiten. Um ein Modul zu starten, klicken Sie auf das Symbol des Moduls, für die Bearbeitung auf den Namen.'; @@ -1477,6 +1480,10 @@ $_lang['manager_menu_position_left'] = 'links'; $_lang['invalid_event_response'] = 'Der Event "%s" verursachte eine invalide Ausgabe.'; +$_lang["role_modules_tab"] = 'Module'; +$_lang["role_modules_msg"] = 'Module, die diese Rolle ausführen darf. Ein Modul, das niemand eingeschränkt hat, bleibt für alle Rollen verfügbar — das Häkchen hier zu entfernen schränkt es ein.'; +$_lang["role_modules_admin_msg"] = 'Die Administratorrolle kann jedes Modul ausführen.'; +$_lang["role_modules_none"] = 'Es sind noch keine Module installiert.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/en/global.php b/core/lang/en/global.php index 700353862d..0e18021d1d 100644 --- a/core/lang/en/global.php +++ b/core/lang/en/global.php @@ -560,6 +560,9 @@ $_lang["module_desc"] = 'Description'; $_lang["module_disabled"] = 'Module disabled'; $_lang["module_edit_click_title"] = 'Click here to edit this Module'; +$_lang["module_exec_no_privileges"] = 'You do not have sufficient privileges to execute this Module.'; +$_lang["module_exec_not_found"] = 'No Module found for id %s.'; +$_lang["module_exec_disabled"] = 'This Module is disabled and cannot be executed.'; $_lang["module_group_access_msg"] = 'Select the User Groups that are allowed to execute this Module from within the Content Manager.'; $_lang["module_management"] = 'Manage Modules'; $_lang["module_management_msg"] = 'Choose the Module you would like to execute or modify. To run the Module click on the icon in the grid. To modify the Module click on the name of the Module.'; @@ -1543,6 +1546,10 @@ $_lang["lang_key_desc"] = 'Key language from array $_lang'; $_lang["key_desc"] = 'Key for checked permission'; +$_lang["role_modules_tab"] = 'Modules'; +$_lang["role_modules_msg"] = 'Modules this role is allowed to run. A module nobody has restricted stays available to every role - unticking it here is what restricts it.'; +$_lang["role_modules_admin_msg"] = 'The administrator role can run every module.'; +$_lang["role_modules_none"] = 'No modules have been installed yet.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/es/global.php b/core/lang/es/global.php index 7405452e1e..73ef810cb6 100644 --- a/core/lang/es/global.php +++ b/core/lang/es/global.php @@ -543,6 +543,9 @@ $_lang["module_desc"] = 'Descripción'; $_lang["module_disabled"] = 'Modulo deshabilitado'; $_lang["module_edit_click_title"] = 'Haz clic aquí para editar este módulo'; +$_lang["module_exec_no_privileges"] = 'No tiene privilegios suficientes para ejecutar este módulo.'; +$_lang["module_exec_not_found"] = 'No se encontró ningún módulo con el id %s.'; +$_lang["module_exec_disabled"] = 'Este módulo está desactivado y no se puede ejecutar.'; $_lang["module_group_access_msg"] = 'Selecciona los grupos de usuarios que tienen permitido ejecutar este módulo desde el Administrador de Contenido.'; $_lang["module_management"] = 'Administrar Módulos'; $_lang["module_management_msg"] = 'Elige el módulo que quisieras ejecutar o modificar. Para correr el módulo haz clic en el icono en la cuadrícula. Para modificarlo ´haz clic en el nombre del módulo.'; @@ -1486,6 +1489,10 @@ $_lang['enable_mootools'] = 'Habilitar Mootools en el administrador'; $_lang['enable_mootools_message'] = 'Cargue Mootools.js en el administrador para compatibilidad con versiones anteriores'; +$_lang["role_modules_tab"] = 'Módulos'; +$_lang["role_modules_msg"] = 'Módulos que este rol puede ejecutar. Un módulo que nadie ha restringido sigue disponible para todos los roles: desmarcarlo aquí es lo que lo restringe.'; +$_lang["role_modules_admin_msg"] = 'El rol de administrador puede ejecutar cualquier módulo.'; +$_lang["role_modules_none"] = 'Todavía no hay módulos instalados.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/fa/global.php b/core/lang/fa/global.php index b79cc23ca2..957fc4a234 100644 --- a/core/lang/fa/global.php +++ b/core/lang/fa/global.php @@ -505,6 +505,9 @@ $_lang["module_desc"] = 'توضیحات'; $_lang["module_disabled"] = 'تعلیق ماژول'; $_lang["module_edit_click_title"] = 'برای ویرایش ماژول اینجا کلیک کنید'; +$_lang["module_exec_no_privileges"] = 'شما دسترسی کافی برای اجرای این ماژول را ندارید.'; +$_lang["module_exec_not_found"] = 'ماژولی با شناسه %s یافت نشد.'; +$_lang["module_exec_disabled"] = 'این ماژول غیرفعال است و نمی‌تواند اجرا شود.'; $_lang["module_group_access_msg"] = 'گروههای کاربری که مجاز به اجرای این ماژول در بخش مدیریت محتوا هستند را انتخاب کنید.'; $_lang["module_management"] = 'مدیریت ماژولها'; $_lang["module_management_msg"] = 'در اینجا شما میتوانید ماژول مورد نظر خود را برای اجرا یا تنظیم انتخاب کنید. برای اجرای ماژول روی نماد یا آیکون در داخل همان قسمت کلیک کنید. برای تنظیم ماژول روی اسم ان کلیک کنید.'; @@ -1250,6 +1253,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = 'ماژول‌ها'; +$_lang["role_modules_msg"] = 'ماژول‌هایی که این نقش اجازه اجرای آن‌ها را دارد. ماژولی که کسی آن را محدود نکرده باشد برای همه نقش‌ها در دسترس است؛ برداشتن تیک در اینجا آن را محدود می‌کند.'; +$_lang["role_modules_admin_msg"] = 'نقش مدیر می‌تواند همه ماژول‌ها را اجرا کند.'; +$_lang["role_modules_none"] = 'هنوز هیچ ماژولی نصب نشده است.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/fi/global.php b/core/lang/fi/global.php index 9fe6e6fea3..74cc25487b 100644 --- a/core/lang/fi/global.php +++ b/core/lang/fi/global.php @@ -504,6 +504,9 @@ $_lang["module_desc"] = 'Kuvaus'; $_lang["module_disabled"] = 'Ota moduuli pois käytöstä'; $_lang["module_edit_click_title"] = 'Muokkaa moduulia napsauttamalla tästä'; +$_lang["module_exec_no_privileges"] = 'Sinulla ei ole riittäviä oikeuksia tämän moduulin suorittamiseen.'; +$_lang["module_exec_not_found"] = 'Moduulia tunnuksella %s ei löytynyt.'; +$_lang["module_exec_disabled"] = 'Tämä moduuli on poistettu käytöstä eikä sitä voi suorittaa.'; $_lang["module_group_access_msg"] = 'Valitse käyttäjäryhmät, joilla on oikeus suorittaa moduuli ylläpidosta käsin.'; $_lang["module_management"] = 'Moduulit'; $_lang["module_management_msg"] = 'Valitse suoritettava tai muokattava moduuli. Suorita, muokkaa, tee kopio tai poista moduuli napsauttamalla moduulin kuvaketta. Muokkaa moduulia napsauttamalla sen nimeä.'; @@ -1248,6 +1251,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = 'Moduulit'; +$_lang["role_modules_msg"] = 'Moduulit, joita tämä rooli saa suorittaa. Moduuli, jota kukaan ei ole rajoittanut, on kaikkien roolien käytettävissä — valinnan poistaminen tässä rajoittaa sen.'; +$_lang["role_modules_admin_msg"] = 'Ylläpitäjän rooli voi suorittaa kaikki moduulit.'; +$_lang["role_modules_none"] = 'Moduuleja ei ole vielä asennettu.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/fr/global.php b/core/lang/fr/global.php index 7aadd9e2aa..dcade25fdb 100644 --- a/core/lang/fr/global.php +++ b/core/lang/fr/global.php @@ -507,6 +507,9 @@ $_lang["module_desc"] = 'Description'; $_lang["module_disabled"] = 'Le Module est désactivé'; $_lang["module_edit_click_title"] = 'Cliquez ici pour modifier le Module'; +$_lang["module_exec_no_privileges"] = 'Vous n\'avez pas les privilèges suffisants pour exécuter ce module.'; +$_lang["module_exec_not_found"] = 'Aucun module trouvé pour l\'id %s.'; +$_lang["module_exec_disabled"] = 'Ce module est désactivé et ne peut pas être exécuté.'; $_lang["module_group_access_msg"] = 'Sélectionnez les Groupes d\'Utilisateurs ayant le droit de lancer ce Module depuis le Gestionnaire de Contenu.'; $_lang["module_management"] = 'Gestion des Modules'; $_lang["module_management_msg"] = 'Vous pouvez choisir ici le Module à lancer ou à modifier. Pour lancer le Module, cliquer sur l\'icône dans la grille. Pour modifier le Module, cliquez sur le nom du Module.'; @@ -1219,6 +1222,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = 'Modules'; +$_lang["role_modules_msg"] = 'Modules que ce rôle est autorisé à exécuter. Un module que personne n\'a restreint reste accessible à tous les rôles : le décocher ici est ce qui le restreint.'; +$_lang["role_modules_admin_msg"] = 'Le rôle administrateur peut exécuter tous les modules.'; +$_lang["role_modules_none"] = 'Aucun module n\'est encore installé.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/he/global.php b/core/lang/he/global.php index e70634e842..79850cdf34 100644 --- a/core/lang/he/global.php +++ b/core/lang/he/global.php @@ -505,6 +505,9 @@ $_lang["module_desc"] = 'תיאור'; $_lang["module_disabled"] = 'מודול לא פעיל'; $_lang["module_edit_click_title"] = 'Click here to edit this module'; +$_lang["module_exec_no_privileges"] = 'אין לך הרשאות מספיקות להריץ מודול זה.'; +$_lang["module_exec_not_found"] = 'לא נמצא מודול עם המזהה %s.'; +$_lang["module_exec_disabled"] = 'מודול זה מושבת ולא ניתן להריץ אותו.'; $_lang["module_group_access_msg"] = 'Select the user groups that are allowed to execute this module from within the Content Manager.'; $_lang["module_management"] = 'ניהול מודולים'; $_lang["module_management_msg"] = 'Choose the module you would like to execute or modify. To run the module click on the icon in the grid. To modify the module click on the name of the module.'; @@ -1250,6 +1253,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = 'מודולים'; +$_lang["role_modules_msg"] = 'המודולים שתפקיד זה רשאי להריץ. מודול שאיש לא הגביל נשאר זמין לכל התפקידים — הסרת הסימון כאן היא שמגבילה אותו.'; +$_lang["role_modules_admin_msg"] = 'תפקיד המנהל יכול להריץ כל מודול.'; +$_lang["role_modules_none"] = 'עדיין לא הותקנו מודולים.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/it/global.php b/core/lang/it/global.php index d36be00f12..f203252742 100644 --- a/core/lang/it/global.php +++ b/core/lang/it/global.php @@ -543,6 +543,9 @@ $_lang["module_desc"] = 'Descrizione'; $_lang["module_disabled"] = 'Modulo disabilitato'; $_lang["module_edit_click_title"] = 'Cliccate qui per modificare il Modulo'; +$_lang["module_exec_no_privileges"] = 'Non hai privilegi sufficienti per eseguire questo modulo.'; +$_lang["module_exec_not_found"] = 'Nessun modulo trovato per l\'id %s.'; +$_lang["module_exec_disabled"] = 'Questo modulo è disattivato e non può essere eseguito.'; $_lang["module_group_access_msg"] = 'Selezionate i gruppi autorizzati ad eseguire il Modulo.'; $_lang["module_management"] = 'Gestione Moduli'; $_lang["module_management_msg"] = 'Qui potete scegliere il Modulo che desiderate eseguire o modificare. Per eseguire un Modulo, fate clic sull\'icona del Modulo a sinistra sulla griglia. Per modificare un Modulo, fate clic sul suo nome.'; @@ -1496,6 +1499,10 @@ $_lang['enable_mootools'] = 'Abilita Mootools nel manager'; $_lang['enable_mootools_message'] = 'Carica nel manager la libreria Mootools.js per la retrocompatibilità'; +$_lang["role_modules_tab"] = 'Moduli'; +$_lang["role_modules_msg"] = 'Moduli che questo ruolo può eseguire. Un modulo che nessuno ha limitato resta disponibile a tutti i ruoli: togliere la spunta qui è ciò che lo limita.'; +$_lang["role_modules_admin_msg"] = 'Il ruolo amministratore può eseguire qualsiasi modulo.'; +$_lang["role_modules_none"] = 'Nessun modulo è ancora installato.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/ja/global.php b/core/lang/ja/global.php index 924f7f4a1c..321b215bab 100644 --- a/core/lang/ja/global.php +++ b/core/lang/ja/global.php @@ -566,6 +566,9 @@ $_lang["module_desc"] = '説明'; $_lang["module_disabled"] = 'モジュールを停止'; $_lang["module_edit_click_title"] = 'このモジュールを編集'; +$_lang["module_exec_no_privileges"] = 'このモジュールを実行する権限がありません。'; +$_lang["module_exec_not_found"] = 'id %s のモジュールが見つかりません。'; +$_lang["module_exec_disabled"] = 'このモジュールは無効になっているため実行できません。'; $_lang["module_group_access_msg"] = '管理画面内でこのモジュールを実行できるユーザーグループを選択してください。'; $_lang["module_management"] = 'モジュール管理'; $_lang["module_management_msg"] = 'アイコンをクリックして操作を選択してください。モジュールは管理画面を拡張する形で機能を追加できます。追加する機能自体はプラグインやスニペットで作り、これをモジュール側のフォームでコントロールする実装を推奨します。'; @@ -1532,6 +1535,10 @@ $_lang["lang_key_desc"] = '配列$_langから言語のキーを設定'; $_lang["key_desc"] = 'チェックしたパーミッションに対するキー'; +$_lang["role_modules_tab"] = 'モジュール'; +$_lang["role_modules_msg"] = 'このロールが実行できるモジュールです。誰も制限していないモジュールはすべてのロールで利用できます。ここでチェックを外すと制限されます。'; +$_lang["role_modules_admin_msg"] = '管理者ロールはすべてのモジュールを実行できます。'; +$_lang["role_modules_none"] = 'モジュールはまだインストールされていません。'; $_lang["role_no_tv"] = 'このロールにはテンプレート変数がまだ割り当てられていません。'; $_lang["role_notassigned_tv"] = '割り当て可能なテンプレート変数です。'; $_lang["role_tv_msg"] = '以下のテンプレート変数にこのロールが割り当てられました。'; diff --git a/core/lang/nl/global.php b/core/lang/nl/global.php index 3e719ae7d5..da9b163930 100644 --- a/core/lang/nl/global.php +++ b/core/lang/nl/global.php @@ -532,6 +532,9 @@ $_lang["module_desc"] = 'Beschrijving'; $_lang["module_disabled"] = 'Module uitgeschakeld'; $_lang["module_edit_click_title"] = 'Klik hier om de Module te bewerken'; +$_lang["module_exec_no_privileges"] = 'U hebt onvoldoende rechten om deze module uit te voeren.'; +$_lang["module_exec_not_found"] = 'Geen module gevonden met id %s.'; +$_lang["module_exec_disabled"] = 'Deze module is uitgeschakeld en kan niet worden uitgevoerd.'; $_lang["module_group_access_msg"] = 'Selecteer de Gebruikersgroepen die deze Module binnen het Content Management Systeem mogen uitvoeren.'; $_lang["module_management"] = 'Modulebeheer'; $_lang["module_management_msg"] = 'Hier kunt u Modules toevoegen, bewerken en uitvoeren.
Klik op het icoon van de Module om de Module uit te voeren. Klik op de naam van de Module om de Module te bewerken.'; @@ -1446,6 +1449,10 @@ $_lang["viewopts_fontsize"] = 'Fontgrootte'; $_lang["viewopts_cb_alltabs"] = 'Alle tabs'; +$_lang["role_modules_tab"] = 'Modules'; +$_lang["role_modules_msg"] = 'Modules die deze rol mag uitvoeren. Een module die niemand heeft beperkt blijft voor elke rol beschikbaar — het vinkje hier weghalen is wat hem beperkt.'; +$_lang["role_modules_admin_msg"] = 'De beheerdersrol kan elke module uitvoeren.'; +$_lang["role_modules_none"] = 'Er zijn nog geen modules geïnstalleerd.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/nn/global.php b/core/lang/nn/global.php index f3e70d3a1f..a49c92d3f9 100644 --- a/core/lang/nn/global.php +++ b/core/lang/nn/global.php @@ -504,6 +504,9 @@ $_lang["module_desc"] = 'Beskrivelse'; $_lang["module_disabled"] = 'Modulen er slått av'; $_lang["module_edit_click_title"] = 'Klikk her for å redigere denne modulen'; +$_lang["module_exec_no_privileges"] = 'Du har ikkje tilstrekkelege rettar til å køyre denne modulen.'; +$_lang["module_exec_not_found"] = 'Fann ingen modul med id %s.'; +$_lang["module_exec_disabled"] = 'Denne modulen er deaktivert og kan ikkje køyrast.'; $_lang["module_group_access_msg"] = 'Velg brukergruppene som har tilgang til å kjøre denne modulen fra inneholdshåndtereren.'; $_lang["module_management"] = 'Håndtere moduler'; $_lang["module_management_msg"] = 'Her kan du velge modulen du vil kjøre eller endre. Klikk på ikonet i tabellen for å kjøre modulen. Klikk på modulnavnet for å redigere modulen.'; @@ -1216,6 +1219,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = 'Modular'; +$_lang["role_modules_msg"] = 'Modular som denne rolla har lov til å køyre. Ein modul som ingen har avgrensa, er tilgjengeleg for alle roller — å fjerne haken her er det som avgrensar han.'; +$_lang["role_modules_admin_msg"] = 'Administratorrolla kan køyre alle modular.'; +$_lang["role_modules_none"] = 'Ingen modular er installerte enno.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/pl/global.php b/core/lang/pl/global.php index fe8d9d5fb5..36aa7c8709 100644 --- a/core/lang/pl/global.php +++ b/core/lang/pl/global.php @@ -544,6 +544,9 @@ $_lang["module_desc"] = 'Opis'; $_lang["module_disabled"] = 'Moduł wyłączony'; $_lang["module_edit_click_title"] = 'Kliknij tutaj aby edytować moduł'; +$_lang["module_exec_no_privileges"] = 'Nie masz wystarczających uprawnień, aby uruchomić ten moduł.'; +$_lang["module_exec_not_found"] = 'Nie znaleziono modułu o id %s.'; +$_lang["module_exec_disabled"] = 'Ten moduł jest wyłączony i nie może zostać uruchomiony.'; $_lang["module_group_access_msg"] = 'Wybierz grupy użytkowników, które mogą uruchamiać ten moduł z panelu administracji.'; $_lang["module_management"] = 'Zarządzenie modułami'; $_lang["module_management_msg"] = 'Tutaj możesz wybrać moduł, który chcesz wykonać lub zmodyfikować. Aby uruchomić moduł kliknij ikonę na siatce. Aby zmodyfikować moduł kliknij w jego nazwę.'; @@ -1482,6 +1485,10 @@ $_lang['manager_menu_position_left'] = 'Lewa'; $_lang['invalid_event_response'] = 'Zdarzenie %s ma nieprawidłowy wynik'; +$_lang["role_modules_tab"] = 'Moduły'; +$_lang["role_modules_msg"] = 'Moduły, które ta rola może uruchamiać. Moduł, którego nikt nie ograniczył, pozostaje dostępny dla wszystkich ról — odznaczenie go tutaj wprowadza ograniczenie.'; +$_lang["role_modules_admin_msg"] = 'Rola administratora może uruchomić każdy moduł.'; +$_lang["role_modules_none"] = 'Nie zainstalowano jeszcze żadnych modułów.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/pt/global.php b/core/lang/pt/global.php index 4e6321a038..b3b277ec34 100644 --- a/core/lang/pt/global.php +++ b/core/lang/pt/global.php @@ -504,6 +504,9 @@ $_lang["module_desc"] = 'Descrição'; $_lang["module_disabled"] = 'Módulo desactivado'; $_lang["module_edit_click_title"] = 'Clique aqui para editar este Módulo'; +$_lang["module_exec_no_privileges"] = 'Não tem privilégios suficientes para executar este módulo.'; +$_lang["module_exec_not_found"] = 'Nenhum módulo encontrado para o id %s.'; +$_lang["module_exec_disabled"] = 'Este módulo está desativado e não pode ser executado.'; $_lang["module_group_access_msg"] = 'Seleccione os grupos de utilizadores que estão autorizados a executar este Módulo a partir do Gestor de Conteúdos.'; $_lang["module_management"] = 'Gerir Módulos'; $_lang["module_management_msg"] = 'Aqui pode escolher qual o módulo que deseja executar ou modificar. Para aceder às opções do módulo, clique no ícone ao lado do nome.'; @@ -1249,6 +1252,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = 'Módulos'; +$_lang["role_modules_msg"] = 'Módulos que este perfil pode executar. Um módulo que ninguém restringiu continua disponível para todos os perfis — desmarcá-lo aqui é o que o restringe.'; +$_lang["role_modules_admin_msg"] = 'O perfil de administrador pode executar qualquer módulo.'; +$_lang["role_modules_none"] = 'Ainda não há módulos instalados.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/ru/global.php b/core/lang/ru/global.php index 3b25b96197..e1e8209722 100644 --- a/core/lang/ru/global.php +++ b/core/lang/ru/global.php @@ -559,6 +559,9 @@ $_lang["module_desc"] = 'Описание'; $_lang["module_disabled"] = 'Модуль отключен'; $_lang["module_edit_click_title"] = 'Нажмите для редактирования модуля'; +$_lang["module_exec_no_privileges"] = 'У вас недостаточно прав для запуска этого модуля.'; +$_lang["module_exec_not_found"] = 'Модуль с id %s не найден.'; +$_lang["module_exec_disabled"] = 'Этот модуль отключён и не может быть запущен.'; $_lang["module_group_access_msg"] = 'Выберите группы пользователей, которые могут запускать этот модуль.'; $_lang["module_management"] = 'Модули'; $_lang["module_management_msg"] = 'Модуль - это приложение, основанное на архитектуре Evolution CMS и расширяющее возможности системы управления. Модуль может группировать набор элементов (сниппеты, чанки, данные), реализуя принцип инкапсуляции через разделение интерфейса и реализации.

Выберите модуль, который вы хотите запустить или изменить, или создайте новый модуль. Для запуска модуля нажмите на значок рядом с его именем, для редактирования модуля нажмите на его название.'; @@ -1537,6 +1540,10 @@ $_lang["lang_key_desc"] = 'Ключ языка из массива $_lang'; $_lang["key_desc"] = 'Ключь для проверки доступа'; +$_lang["role_modules_tab"] = 'Модули'; +$_lang["role_modules_msg"] = 'Модули, которые разрешено запускать этой роли. Модуль без ограничений доступен всем ролям — снимите галочку, чтобы ограничить его.'; +$_lang["role_modules_admin_msg"] = 'Роль администратора может запускать любые модули.'; +$_lang["role_modules_none"] = 'Модули ещё не установлены.'; $_lang["role_no_tv"] = 'Этой роли еще не присвоены параметры.'; $_lang["role_notassigned_tv"] = 'Эти параметры доступны для выбора.'; $_lang["role_tv_msg"] = 'Ниже выводятся параметры, назначенные этой роли.'; diff --git a/core/lang/sv/global.php b/core/lang/sv/global.php index 146da59e03..234cf609e0 100644 --- a/core/lang/sv/global.php +++ b/core/lang/sv/global.php @@ -505,6 +505,9 @@ $_lang["module_desc"] = 'Beskrivning'; $_lang["module_disabled"] = 'Modulen avstängd'; $_lang["module_edit_click_title"] = 'Klicka här för att redigera denna modul'; +$_lang["module_exec_no_privileges"] = 'Du har inte tillräcklig behörighet för att köra den här modulen.'; +$_lang["module_exec_not_found"] = 'Ingen modul hittades med id %s.'; +$_lang["module_exec_disabled"] = 'Den här modulen är inaktiverad och kan inte köras.'; $_lang["module_group_access_msg"] = 'Välj de användargrupper som har tillstånd att köra den här modulen innifrån innehållshanteraren.'; $_lang["module_management"] = 'Hantera moduler'; $_lang["module_management_msg"] = 'Välj den modul du vill köra eller ändra. Klicka på ikonen i tabellen för att köra modulen. Klicka på modulnamnet för att redigera modulen.'; @@ -1249,6 +1252,10 @@ $_lang["check_none"] = "Välj ingen"; $_lang["check_toggle"] = "Växla val"; +$_lang["role_modules_tab"] = 'Moduler'; +$_lang["role_modules_msg"] = 'Moduler som den här rollen får köra. En modul som ingen har begränsat är tillgänglig för alla roller — att avmarkera den här är det som begränsar den.'; +$_lang["role_modules_admin_msg"] = 'Administratörsrollen kan köra alla moduler.'; +$_lang["role_modules_none"] = 'Inga moduler är installerade ännu.'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/lang/uk/global.php b/core/lang/uk/global.php index 90776756ad..60226b392d 100644 --- a/core/lang/uk/global.php +++ b/core/lang/uk/global.php @@ -564,6 +564,9 @@ $_lang["module_desc"] = 'Опис'; $_lang["module_disabled"] = 'Модуль відключений'; $_lang["module_edit_click_title"] = 'Натисніть для редагування модуля'; +$_lang["module_exec_no_privileges"] = 'У вас недостатньо прав для запуску цього модуля.'; +$_lang["module_exec_not_found"] = 'Модуль з id %s не знайдено.'; +$_lang["module_exec_disabled"] = 'Цей модуль вимкнено, і його не можна запустити.'; $_lang["module_group_access_msg"] = 'Оберіть групи користувачів, які можуть запускати цей модуль.'; $_lang["module_management"] = 'Керування модулями'; $_lang["module_management_msg"] = 'Модуль - це додаток, засноване на архітектурі EVO і розширює можливості системи управління. Модуль може групувати набір елементів (сніппети, чанки, дані), реалізуючи принцип інкапсуляції через поділ інтерфейсу і реалізації.

Оберіть модуль, який ви хочете запустити або змінити, або створіть новий модуль. Для запуску модуля натисніть на значок поруч з його ім\'ям, для редагування модуля натисніть на його назву.'; @@ -1537,6 +1540,10 @@ $_lang['invalid_event_response'] = 'Подія %s повернула невірний результат'; $_lang['chunk_processor'] = 'Клас обробки чанків'; +$_lang["role_modules_tab"] = 'Модулі'; +$_lang["role_modules_msg"] = 'Модулі, які дозволено запускати цій ролі. Модуль без обмежень доступний усім ролям — зніміть позначку, щоб обмежити його.'; +$_lang["role_modules_admin_msg"] = 'Роль адміністратора може запускати будь-які модулі.'; +$_lang["role_modules_none"] = 'Модулі ще не встановлено.'; $_lang["role_no_tv"] = 'Цій ролі ще не призначено жодної змінної.'; $_lang["role_notassigned_tv"] = 'Ці змінні доступні для призначення.'; $_lang["role_tv_msg"] = 'Змінні, призначені цій ролі, перераховані нижче.'; diff --git a/core/lang/zh/global.php b/core/lang/zh/global.php index aeede52a16..cf4fa7c36d 100644 --- a/core/lang/zh/global.php +++ b/core/lang/zh/global.php @@ -505,6 +505,9 @@ $_lang["module_desc"] = '描述'; $_lang["module_disabled"] = '禁用模块'; $_lang["module_edit_click_title"] = '点这里运行模块'; +$_lang["module_exec_no_privileges"] = '您没有足够的权限运行此模块。'; +$_lang["module_exec_not_found"] = '未找到 id 为 %s 的模块。'; +$_lang["module_exec_disabled"] = '此模块已禁用,无法运行。'; $_lang["module_group_access_msg"] = '选择可以在后台执行模块的用户组.'; $_lang["module_management"] = '管理模块'; $_lang["module_management_msg"] = '在这里你可以选择你希望运行或编辑的模块,要运行模块的话,就在他的图标上点击,如果你要修改编辑的话,就点击他的名字。'; @@ -1250,6 +1253,10 @@ $_lang["check_none"] = "Select none"; $_lang["check_toggle"] = "Toggle selection"; +$_lang["role_modules_tab"] = '模块'; +$_lang["role_modules_msg"] = '此角色可以运行的模块。没有被任何人限制的模块对所有角色都可用——在此取消勾选即会对其加以限制。'; +$_lang["role_modules_admin_msg"] = '管理员角色可以运行所有模块。'; +$_lang["role_modules_none"] = '尚未安装任何模块。'; $_lang["role_no_tv"] = 'No Variables have been assigned to this Role yet.'; $_lang["role_notassigned_tv"] = 'These Variables are available for assigning.'; $_lang["role_tv_msg"] = 'The Variables assigned to this Role are listed below.'; diff --git a/core/src/Console/SiteUpdateCommand.php b/core/src/Console/SiteUpdateCommand.php index 32d1b5f978..7957adde49 100644 --- a/core/src/Console/SiteUpdateCommand.php +++ b/core/src/Console/SiteUpdateCommand.php @@ -2,6 +2,7 @@ use EvolutionCMS\Models\Category; use EvolutionCMS\Models\SiteModule; +use EvolutionCMS\Models\SiteModuleRole; use EvolutionCMS\Services\ComposerVersionSynchronizer; use Illuminate\Console\Command; @@ -402,7 +403,7 @@ protected function updateBundledExtrasModule(): void return; } - SiteModule::query()->create([ + $created = SiteModule::query()->create([ 'name' => 'Extras', 'description' => $description, 'modulecode' => $moduleCode, @@ -411,6 +412,9 @@ protected function updateBundledExtrasModule(): void 'enable_sharedparams' => (int) ($params['shareparams'] ?? 0), 'category' => $categoryId, ]); + + // a re-created Extras module keeps the shipped default: admins only + SiteModuleRole::applyDefaultsFor((int) $created->getKey(), 'Extras'); } /** diff --git a/core/src/Controllers/Frame.php b/core/src/Controllers/Frame.php index f3926f6beb..73f7d0eaed 100644 --- a/core/src/Controllers/Frame.php +++ b/core/src/Controllers/Frame.php @@ -3,6 +3,7 @@ use EvolutionCMS\Interfaces\ManagerTheme; use EvolutionCMS\Interfaces\ManagerThemeInterface; use EvolutionCMS\Models\SiteModule; +use EvolutionCMS\Support\ModuleAccess; class Frame extends AbstractController implements ManagerTheme\PageControllerInterface { @@ -685,18 +686,17 @@ protected function menuNewModule() protected function menuRunModules() { if ($this->managerTheme->getCore()->hasPermission('exec_module')) { - if ($_SESSION['mgrRole'] != 1 && $this->managerTheme->getCore()->getConfig('use_udperms') === true) { - $modules = SiteModule::select('site_modules.id', 'site_modules.name', 'site_modules.icon', 'member_groups.member') - ->withoutProtected() - ->lockedView() - ->where('site_modules.disabled', 0) - ->orderBy('site_modules.name')->get()->toArray(); - - } else { - $modules = SiteModule::where('disabled', '!=', 1)->orderBy('name')->get()->toArray(); - } + // withoutProtected() applies both ACL axes and is a no-op for the + // admin role, so the menu lists exactly what the user may run + $modules = SiteModule::select('site_modules.id', 'site_modules.name', 'site_modules.icon') + ->withoutProtected() + ->lockedView() + ->where('site_modules.disabled', '!=', 1) + ->orderBy('site_modules.name')->get()->toArray(); + $entries = 0; if (count($modules) > 0) { foreach ($modules as $row) { + $entries++; $this->sitemenu['module' . $row['id']] = [ 'module' . $row['id'], 'modules', @@ -712,11 +712,17 @@ protected function menuRunModules() ]; } } + $mgrRole = (int) get_by_key($_SESSION, 'mgrRole', 0); foreach ($this->managerTheme->getCore()->modulesFromFile as $module) { if (!empty($module['properties']['hidden'])) { continue; } + if (!ModuleAccess::canRunFileModule($mgrRole, $module)) { + continue; + } + + $entries++; $this->sitemenu['module' . $module['id']] = [ 'module' . $module['id'], 'modules', @@ -731,6 +737,13 @@ protected function menuRunModules() '' ]; } + + // Nothing this user may run: drop the parent node menuModules() + // added before the list was known, instead of rendering an empty + // Modules menu. Free - the listing above has already happened. + if ($entries === 0) { + unset($this->sitemenu['modules']); + } } return $this; diff --git a/core/src/Controllers/Modules.php b/core/src/Controllers/Modules.php index ed7165f059..22814f0c51 100644 --- a/core/src/Controllers/Modules.php +++ b/core/src/Controllers/Modules.php @@ -99,7 +99,13 @@ protected function getModules() : Collection */ protected function getCategories(): Collection { - return Category::with('modules') + // the eager load is constrained too, otherwise a category the user can + // see would list the modules inside it that they may not run + $roleId = (int) get_by_key($_SESSION, 'mgrRole', 0); + + return Category::with(['modules' => function ($builder) use ($roleId) { + return $builder->allowedForRole($roleId); + }]) ->whereHas('modules', function (Builder $builder) { return $builder->withoutProtected()->lockedView(); }) diff --git a/core/src/Controllers/Resources/Modules.php b/core/src/Controllers/Resources/Modules.php index bc2882af26..cb386d1c4c 100644 --- a/core/src/Controllers/Resources/Modules.php +++ b/core/src/Controllers/Resources/Modules.php @@ -67,17 +67,25 @@ public function getParameters(array $params = []) : array protected function parameterOutCategory() : Collection { - return Models\SiteModule::where('category', '=', 0) - ->orderBy('name', 'ASC') + return Models\SiteModule::query() + ->where('site_modules.category', '=', 0) + ->withoutProtected() + ->orderBy('site_modules.name', 'ASC') ->lockedView() ->get(); } protected function parameterCategories() : Collection { - return Models\Category::with('modules') - ->whereHas('modules', function (Eloquent\Builder $builder) { - return $builder->lockedView(); + // the eager load is constrained too, otherwise a category the user can + // see would list the modules inside it that they may not run + $roleId = (int) get_by_key($_SESSION, 'mgrRole', 0); + + return Models\Category::with(['modules' => function ($builder) use ($roleId) { + return $builder->allowedForRole($roleId); + }]) + ->whereHas('modules', function (Eloquent\Builder $builder) use ($roleId) { + return $builder->lockedView()->allowedForRole($roleId); })->orderBy('rank', 'ASC') ->get(); } diff --git a/core/src/Controllers/UserRoles/UserRole.php b/core/src/Controllers/UserRoles/UserRole.php index 8938c87a0a..b258df1709 100644 --- a/core/src/Controllers/UserRoles/UserRole.php +++ b/core/src/Controllers/UserRoles/UserRole.php @@ -2,6 +2,7 @@ use EvolutionCMS\Controllers\AbstractController; use EvolutionCMS\Models; +use EvolutionCMS\Support\ModuleAccess; use EvolutionCMS\Interfaces\ManagerTheme; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent; @@ -37,6 +38,7 @@ public function process(): bool if(isset($_GET['action']) && $_GET['action'] == 'delete' ){ $id = $this->getElementId(); Models\RolePermissions::query()->where('role_id', $id)->delete(); + Models\SiteModuleRole::query()->where('role', $id)->delete(); Models\UserRoleVar::where('roleid', $id)->delete(); Models\UserRole::query()->where('id', $id)->delete(); header('Location: index.php?a=86&tab=0'); @@ -80,6 +82,11 @@ public function updateOrCreate() Models\RolePermissions::create(['role_id' => $role->getKey(), 'permission' => $key]); } + $this->saveModuleAccess( + (int) $role->getKey(), + is_array($_POST['modules'] ?? null) ? array_keys($_POST['modules']) : [] + ); + if ($_POST['tvsDirty'] == 1) { // Preserve rankings of already assigned TVs $exists = Models\UserRoleVar::where('roleid', $role->id)->get()->toArray(); @@ -114,6 +121,101 @@ public function updateOrCreate() header('Location: index.php?a=35&id=' . $role->getKey() . '&r=9'); } + /** + * Store which modules this role may run. + * + * A module with no rows in site_module_roles is unrestricted, so allowing + * such a module means writing nothing at all. Denying one has to make it + * restricted first, by listing every other role. A module always keeps at + * least the admin role, otherwise dropping the last row would silently + * turn it back into an unrestricted module. + * + * @param int $roleId + * @param array $allowedModuleIds Module ids ticked on the role form. + * @return void + */ + protected function saveModuleAccess(int $roleId, array $allowedModuleIds): void + { + // the admin role bypasses the ACL, and the form renders read-only for it + if ($roleId <= 0 || $roleId === ModuleAccess::ADMIN_ROLE) { + return; + } + + if (!$this->managerTheme->getCore()->hasPermission('manage_module_permissions')) { + return; + } + + $allowed = ModuleAccess::normalizeRoleIds($allowedModuleIds); + $restricted = Models\SiteModuleRole::query()->get()->groupBy('module'); + $allRoleIds = Models\UserRole::query()->pluck('id')->all(); + + foreach (Models\SiteModule::query()->pluck('id') as $moduleId) { + $moduleId = (int) $moduleId; + $isRestricted = isset($restricted[$moduleId]); + + if (in_array($moduleId, $allowed, true)) { + if ($isRestricted) { + Models\SiteModuleRole::query()->firstOrCreate([ + 'module' => $moduleId, + 'role' => $roleId, + ]); + } + + continue; + } + + if (!$isRestricted) { + foreach (ModuleAccess::normalizeRoleIds($allRoleIds) as $otherRoleId) { + if ($otherRoleId === $roleId) { + continue; + } + + Models\SiteModuleRole::query()->firstOrCreate([ + 'module' => $moduleId, + 'role' => $otherRoleId, + ]); + } + + continue; + } + + Models\SiteModuleRole::query() + ->where('module', $moduleId) + ->where('role', $roleId) + ->delete(); + + $remaining = Models\SiteModuleRole::query()->where('module', $moduleId)->count(); + if ($remaining === 0) { + Models\SiteModuleRole::query()->firstOrCreate([ + 'module' => $moduleId, + 'role' => ModuleAccess::ADMIN_ROLE, + ]); + } + } + } + + /** + * Modules the role may run, keyed by module id. + * + * Unrestricted modules read as allowed: unticking one is what restricts it. + * + * @param int $roleId + * @return array + */ + protected function parameterModuleAccess(int $roleId): array + { + $restricted = Models\SiteModuleRole::query()->get()->groupBy('module'); + $selected = []; + + foreach (Models\SiteModule::query()->pluck('id') as $moduleId) { + $moduleId = (int) $moduleId; + $selected[$moduleId] = !isset($restricted[$moduleId]) + || $restricted[$moduleId]->contains('role', $roleId); + } + + return $selected; + } + public static function normalizePermissionsPayload(array $permissions, array $requiredPermissions = []): array { foreach ($requiredPermissions as $permission) { @@ -150,6 +252,10 @@ public function getParameters(array $params = []): array 'role' => $role, 'groups' => Models\PermissionsGroups::query()->get(), 'permissionsRole' => $permissionsRole, + 'modules' => Models\SiteModule::query()->orderBy('name')->get(), + 'moduleAccess' => $this->parameterModuleAccess($id), + 'canManageModuleAccess' => $this->managerTheme->getCore() + ->hasPermission('manage_module_permissions'), 'categories' => $this->parameterCategories(), 'tvSelected' => $this->parameterTvSelected(), 'categoriesWithTv' => $this->parameterCategoriesWithTv( diff --git a/core/src/Models/SiteModule.php b/core/src/Models/SiteModule.php index ccb9ffd8b2..a0334a238b 100644 --- a/core/src/Models/SiteModule.php +++ b/core/src/Models/SiteModule.php @@ -1,5 +1,6 @@ where('locked', '=', 0) : $builder; } + /** + * Roles this module is restricted to. No rows means every role may run it. + * + * @return Eloquent\Relations\HasMany + */ + public function roles() + { + return $this->hasMany(SiteModuleRole::class, 'module', 'id'); + } + + /** + * Hide modules the current manager user may not run. + * + * Two independent axes, both of which must pass: the user-group ACL + * (site_module_access, only when use_udperms is on) and the role ACL + * (site_module_roles, which is a property of the role and therefore + * applies whether or not user-document permissions are enabled). + */ public function scopeWithoutProtected(Eloquent\Builder $builder) { - if ($_SESSION['mgrRole'] != 1 && evo()->getConfig('use_udperms')) { + $roleId = (int) get_by_key($_SESSION, 'mgrRole', 0); + if ($roleId === ModuleAccess::ADMIN_ROLE) { + return $builder; + } + + if (evo()->getConfig('use_udperms')) { + // the joins below bring in columns of their own, so make sure a + // caller that did not name any gets the module row, not a mix of + // module and member_groups columns sharing the id name + if (empty($builder->getQuery()->columns)) { + $builder->select('site_modules.*'); + } + $builder->leftJoin('site_module_access', 'site_module_access.module', '=', 'site_modules.id') ->leftJoin('member_groups', 'member_groups.user_group', '=', 'site_module_access.usergroup') - ->whereNull('site_module_access.usergroup') - ->orWhere('member_groups.member', '=', (int)evo()->getLoginUserID('mgr')); + ->where(function (Eloquent\Builder $query) { + $query->whereNull('site_module_access.usergroup') + ->orWhere('member_groups.member', '=', (int)evo()->getLoginUserID('mgr')); + }); + } + + return $builder->allowedForRole($roleId); + } + + /** + * Restrict to modules the given role may run: those with no role + * restriction at all, plus those the role is explicitly listed on. + */ + public function scopeAllowedForRole(Eloquent\Builder $builder, int $roleId) + { + if ($roleId === ModuleAccess::ADMIN_ROLE) { + return $builder; } - return $builder; + return $builder->where(function (Eloquent\Builder $query) use ($roleId) { + $query->whereNotExists(function ($sub) { + $sub->selectRaw(1) + ->from('site_module_roles') + ->whereColumn('site_module_roles.module', 'site_modules.id'); + })->orWhereExists(function ($sub) use ($roleId) { + $sub->selectRaw(1) + ->from('site_module_roles') + ->whereColumn('site_module_roles.module', 'site_modules.id') + ->where('site_module_roles.role', '=', $roleId); + }); + }); } public static function getLockedElements() diff --git a/core/src/Models/SiteModuleRole.php b/core/src/Models/SiteModuleRole.php new file mode 100644 index 0000000000..bcef0c9801 --- /dev/null +++ b/core/src/Models/SiteModuleRole.php @@ -0,0 +1,66 @@ + 'int', + 'role' => 'int' + ]; + + protected $fillable = [ + 'module', + 'role' + ]; + + /** + * Apply the shipped default restriction for a freshly created bundled module. + * + * Called from every install path so a new site restricts Extras to the + * administrator role the same way the migration does for existing sites. + * Modules that already carry a restriction are left alone, so an + * administrator who opened one up keeps that decision across updates. + * + * @param int $moduleId + * @param string $moduleName + * @return void + */ + public static function applyDefaultsFor(int $moduleId, string $moduleName): void + { + $roleIds = \EvolutionCMS\Support\ModuleAccess::defaultRolesFor($moduleName); + if ($moduleId <= 0 || $roleIds === []) { + return; + } + + if (self::query()->where('module', $moduleId)->exists()) { + return; + } + + foreach ($roleIds as $roleId) { + if (!UserRole::query()->where('id', $roleId)->exists()) { + continue; + } + + self::query()->firstOrCreate([ + 'module' => $moduleId, + 'role' => (int) $roleId, + ]); + } + } +} diff --git a/core/src/Services/Store/LegacyDeleteService.php b/core/src/Services/Store/LegacyDeleteService.php index 354874df0f..02401d3ee8 100644 --- a/core/src/Services/Store/LegacyDeleteService.php +++ b/core/src/Services/Store/LegacyDeleteService.php @@ -200,6 +200,7 @@ public function runLegacyDelete($token, array $selection) continue; } \EvolutionCMS\Models\SiteModuleAccess::query()->where('module', $id)->delete(); + \EvolutionCMS\Models\SiteModuleRole::query()->where('module', $id)->delete(); $deletedDb += (int) \EvolutionCMS\Models\SiteModule::query()->where('id', $id)->delete(); } diff --git a/core/src/Support/ModuleAccess.php b/core/src/Support/ModuleAccess.php new file mode 100644 index 0000000000..bae2ef3db5 --- /dev/null +++ b/core/src/Support/ModuleAccess.php @@ -0,0 +1,118 @@ + + */ + public const DEFAULT_RESTRICTIONS = [ + 'Extras' => [self::ADMIN_ROLE], + ]; + + /** + * @param int $roleId Role of the user asking to run the module. + * @param array $allowedRoleIds Roles the module is restricted to; empty means unrestricted. + * @return bool + */ + public static function isAllowedForRole(int $roleId, array $allowedRoleIds): bool + { + if ($roleId === self::ADMIN_ROLE) { + return true; + } + + $allowed = self::normalizeRoleIds($allowedRoleIds); + if ($allowed === []) { + return true; + } + + return in_array($roleId, $allowed, true); + } + + /** + * @param array $roleIds + * @return int[] Positive, unique, re-indexed role ids. + */ + public static function normalizeRoleIds(array $roleIds): array + { + $normalized = []; + foreach ($roleIds as $roleId) { + if (is_array($roleId) || is_object($roleId) || !is_numeric($roleId)) { + continue; + } + $roleId = (int) $roleId; + if ($roleId > 0 && !in_array($roleId, $normalized, true)) { + $normalized[] = $roleId; + } + } + + return $normalized; + } + + /** + * Roles a file-based module (registerModule()) is restricted to. + * + * File modules have no database row, so they declare their restriction + * at registration time via $params['roles']. Nothing declared means + * unrestricted, which is how every file module behaved before. + * + * @param array $module Entry from Core::$modulesFromFile. + * @return int[] + */ + public static function fileModuleAllowedRoles(array $module): array + { + $properties = $module['properties'] ?? []; + if (!is_array($properties) || !isset($properties['roles'])) { + return []; + } + + $roles = $properties['roles']; + if (is_string($roles)) { + $roles = explode(',', $roles); + } + + return is_array($roles) ? self::normalizeRoleIds($roles) : []; + } + + /** + * @param int $roleId + * @param array $module Entry from Core::$modulesFromFile. + * @return bool + */ + public static function canRunFileModule(int $roleId, array $module): bool + { + return self::isAllowedForRole($roleId, self::fileModuleAllowedRoles($module)); + } + + /** + * Default role restriction for a bundled module, by module name. + * + * @param string $moduleName + * @return int[] + */ + public static function defaultRolesFor(string $moduleName): array + { + return self::DEFAULT_RESTRICTIONS[$moduleName] ?? []; + } +} diff --git a/core/tests/Feature/ModuleRoleAclDatabaseTest.php b/core/tests/Feature/ModuleRoleAclDatabaseTest.php new file mode 100644 index 0000000000..0c968e12a7 --- /dev/null +++ b/core/tests/Feature/ModuleRoleAclDatabaseTest.php @@ -0,0 +1,177 @@ +addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + $capsule->setAsGlobal(); + $capsule->bootEloquent(); + + $container = $capsule->getContainer(); + $container->instance('db', $capsule->getDatabaseManager()); + $container->bind('db.schema', fn () => $capsule->getConnection()->getSchemaBuilder()); + Facade::clearResolvedInstances(); + Facade::setFacadeApplication($container); + Model::setConnectionResolver($capsule->getDatabaseManager()); + + $schema = $capsule->getConnection()->getSchemaBuilder(); + $schema->create('user_roles', function (Blueprint $table) { + $table->increments('id'); + $table->string('name')->nullable(); + }); + $schema->create('site_modules', function (Blueprint $table) { + $table->increments('id'); + $table->string('name')->nullable(); + $table->integer('disabled')->default(0); + $table->integer('locked')->default(0); + $table->integer('category')->default(0); + }); + + $db = $capsule->getConnection(); + $db->table('user_roles')->insert([ + ['id' => 1, 'name' => 'Administrator'], + ['id' => 2, 'name' => 'Editor'], + ['id' => 3, 'name' => 'Publisher'], + ]); + $db->table('site_modules')->insert([ + ['id' => 1, 'name' => 'Extras'], + ['id' => 2, 'name' => 'Reports'], + ]); + + return $capsule; +} + +/** + * @return string[] Names of the modules the role is allowed to run. + */ +function moduleNamesForRole(int $roleId): array +{ + return SiteModule::query() + ->allowedForRole($roleId) + ->orderBy('name') + ->pluck('name') + ->all(); +} + +it('restricts an already installed Extras module to administrators when the table is created', function () { + $capsule = bootModuleRoleAclDatabase(); + + (new \CreateSiteModuleRolesTable())->up(); + + $rows = $capsule->getConnection()->table('site_module_roles')->get()->all(); + + expect($rows)->toHaveCount(1) + ->and((int) $rows[0]->module)->toBe(1) + ->and((int) $rows[0]->role)->toBe(ModuleAccess::ADMIN_ROLE); +}); + +it('hides a restricted module from the roles it is not granted to', function () { + bootModuleRoleAclDatabase(); + (new \CreateSiteModuleRolesTable())->up(); + + // Editor and Publisher lose Extras but keep every unrestricted module + expect(moduleNamesForRole(2))->toBe(['Reports']) + ->and(moduleNamesForRole(3))->toBe(['Reports']) + ->and(moduleNamesForRole(ModuleAccess::ADMIN_ROLE))->toBe(['Extras', 'Reports']); +}); + +it('grants a restricted module to a role that is listed on it', function () { + bootModuleRoleAclDatabase(); + (new \CreateSiteModuleRolesTable())->up(); + + SiteModuleRole::query()->create(['module' => 1, 'role' => 3]); + + expect(moduleNamesForRole(3))->toBe(['Extras', 'Reports']) + ->and(moduleNamesForRole(2))->toBe(['Reports']); +}); + +it('keeps a module with no rows available to every role', function () { + bootModuleRoleAclDatabase(); + (new \CreateSiteModuleRolesTable())->up(); + + SiteModuleRole::query()->where('module', 1)->delete(); + + expect(moduleNamesForRole(2))->toBe(['Extras', 'Reports']); +}); + +it('counts a module as restricted even when only other roles are listed', function () { + bootModuleRoleAclDatabase(); + (new \CreateSiteModuleRolesTable())->up(); + + // Reports denied to the Editor by listing everybody else + SiteModuleRole::query()->create(['module' => 2, 'role' => 1]); + SiteModuleRole::query()->create(['module' => 2, 'role' => 3]); + + expect(moduleNamesForRole(2))->toBe([]) + ->and(moduleNamesForRole(3))->toBe(['Reports']) + ->and(moduleNamesForRole(ModuleAccess::ADMIN_ROLE))->toBe(['Extras', 'Reports']); +}); + +it('applies the shipped default when an install path creates a bundled module', function () { + $capsule = bootModuleRoleAclDatabase(); + (new \CreateSiteModuleRolesTable())->up(); + $capsule->getConnection()->table('site_module_roles')->delete(); + + SiteModuleRole::applyDefaultsFor(1, 'Extras'); + SiteModuleRole::applyDefaultsFor(2, 'Reports'); + + expect(SiteModuleRole::query()->where('module', 1)->pluck('role')->all())->toBe([1]) + ->and(SiteModuleRole::query()->where('module', 2)->count())->toBe(0); +}); + +it('leaves a bundled module alone once an administrator has changed its restriction', function () { + bootModuleRoleAclDatabase(); + (new \CreateSiteModuleRolesTable())->up(); + + // an administrator opens Extras up to the Publisher role, then a later + // update runs the same default again + SiteModuleRole::query()->create(['module' => 1, 'role' => 3]); + SiteModuleRole::applyDefaultsFor(1, 'Extras'); + (new \CreateSiteModuleRolesTable())->up(); + + expect(SiteModuleRole::query()->where('module', 1)->orderBy('role')->pluck('role')->all())->toBe([1, 3]); +}); + +it('skips a default for a role that does not exist on the site', function () { + $capsule = bootModuleRoleAclDatabase(); + (new \CreateSiteModuleRolesTable())->up(); + $capsule->getConnection()->table('site_module_roles')->delete(); + $capsule->getConnection()->table('user_roles')->where('id', 1)->delete(); + + SiteModuleRole::applyDefaultsFor(1, 'Extras'); + + expect(SiteModuleRole::query()->count())->toBe(0); +}); diff --git a/core/tests/Unit/Manager/ModuleRoleAccessTest.php b/core/tests/Unit/Manager/ModuleRoleAccessTest.php new file mode 100644 index 0000000000..75cb0e00e1 --- /dev/null +++ b/core/tests/Unit/Manager/ModuleRoleAccessTest.php @@ -0,0 +1,75 @@ +toBeTrue() + ->and(ModuleAccess::isAllowedForRole(3, []))->toBeTrue() + ->and(ModuleAccess::isAllowedForRole(0, []))->toBeTrue(); +}); + +it('only lets the listed roles run a restricted module', function () { + expect(ModuleAccess::isAllowedForRole(2, [2, 3]))->toBeTrue() + ->and(ModuleAccess::isAllowedForRole(3, [2, 3]))->toBeTrue() + ->and(ModuleAccess::isAllowedForRole(4, [2, 3]))->toBeFalse(); +}); + +it('always lets the administrator role run a restricted module', function () { + expect(ModuleAccess::isAllowedForRole(ModuleAccess::ADMIN_ROLE, [2]))->toBeTrue(); +}); + +it('compares role ids numerically, not by string shape', function () { + // ids arrive from $_POST and from the database as strings + expect(ModuleAccess::isAllowedForRole(2, ['2']))->toBeTrue() + ->and(ModuleAccess::isAllowedForRole(2, ['02']))->toBeTrue() + ->and(ModuleAccess::isAllowedForRole(20, ['2']))->toBeFalse(); +}); + +it('drops junk and duplicates when normalizing role ids', function () { + expect(ModuleAccess::normalizeRoleIds(['3', 3, 0, -1, 'x', null, [], 4]))->toBe([3, 4]); +}); + +it('restricts the bundled Extras module to the administrator role by default', function () { + expect(ModuleAccess::defaultRolesFor('Extras'))->toBe([ModuleAccess::ADMIN_ROLE]) + ->and(ModuleAccess::isAllowedForRole(2, ModuleAccess::defaultRolesFor('Extras')))->toBeFalse() + ->and(ModuleAccess::isAllowedForRole(3, ModuleAccess::defaultRolesFor('Extras')))->toBeFalse() + ->and(ModuleAccess::isAllowedForRole(1, ModuleAccess::defaultRolesFor('Extras')))->toBeTrue(); +}); + +it('leaves modules without a shipped default unrestricted', function () { + expect(ModuleAccess::defaultRolesFor('Some Third Party Module'))->toBe([]); +}); + +it('keeps file based modules unrestricted unless they declare roles', function () { + $module = ['id' => 'abc', 'name' => 'Report', 'file' => 'report.php', 'properties' => []]; + + expect(ModuleAccess::fileModuleAllowedRoles($module))->toBe([]) + ->and(ModuleAccess::canRunFileModule(2, $module))->toBeTrue() + ->and(ModuleAccess::canRunFileModule(0, $module))->toBeTrue(); +}); + +it('enforces the roles a file based module declares', function () { + $module = [ + 'id' => 'abc', + 'name' => 'Report', + 'file' => 'report.php', + 'properties' => ['roles' => [1, 3]], + ]; + + expect(ModuleAccess::canRunFileModule(3, $module))->toBeTrue() + ->and(ModuleAccess::canRunFileModule(2, $module))->toBeFalse() + ->and(ModuleAccess::canRunFileModule(ModuleAccess::ADMIN_ROLE, $module))->toBeTrue(); +}); + +it('accepts a comma separated role list from a file based module', function () { + $module = ['properties' => ['roles' => '1, 3']]; + + expect(ModuleAccess::fileModuleAllowedRoles($module))->toBe([1, 3]) + ->and(ModuleAccess::canRunFileModule(2, $module))->toBeFalse(); +}); + +it('treats an unusable roles declaration as unrestricted rather than locking everyone out', function () { + expect(ModuleAccess::fileModuleAllowedRoles(['properties' => ['roles' => 5]]))->toBe([]) + ->and(ModuleAccess::fileModuleAllowedRoles(['properties' => 'nonsense']))->toBe([]) + ->and(ModuleAccess::fileModuleAllowedRoles([]))->toBe([]); +}); diff --git a/core/tests/Unit/Security/ModuleExecutionAclTest.php b/core/tests/Unit/Security/ModuleExecutionAclTest.php new file mode 100644 index 0000000000..b4396dd8d3 --- /dev/null +++ b/core/tests/Unit/Security/ModuleExecutionAclTest.php @@ -0,0 +1,148 @@ + (string) file_get_contents($path); + +it('refuses to execute a file based module that is not registered', function () use ($basePath, $read) { + // the id is user input; before this guard an unknown key read an + // arbitrary path out of an undefined registry entry + $processor = $read($basePath . '/manager/processors/execute_module.processor.php'); + + expect($processor)->toContain('!isset($modx->modulesFromFile[$id])') + ->and($processor)->toContain('is_file($content[\'file\'])'); +}); + +it('applies the role ACL to file based modules instead of skipping them', function () use ($basePath, $read) { + $processor = $read($basePath . '/manager/processors/execute_module.processor.php'); + + expect($processor)->toContain('ModuleAccess::canRunFileModule($mgrRole, $content)') + // the old guard skipped every non numeric id outright + ->and($processor)->not->toContain("if (\$_SESSION['mgrRole'] != 1 && is_numeric(\$id))"); +}); + +it('still checks the database ACL before running a stored module', function () use ($basePath, $read) { + $processor = $read($basePath . '/manager/processors/execute_module.processor.php'); + + expect($processor)->toContain("hasPermission('exec_module')") + ->and($processor)->toContain('->withoutProtected()'); +}); + +it('reports module execution failures through the lexicon, with the id escaped', function () use ($basePath, $read) { + $processor = $read($basePath . '/manager/processors/execute_module.processor.php'); + + foreach (['module_exec_no_privileges', 'module_exec_not_found', 'module_exec_disabled'] as $key) { + expect($processor)->toContain('$_lang["' . $key . '"]'); + } + + // webAlertAndQuit() echoes its message into the page unescaped, and the + // file module branch puts a raw $_GET value in it + expect($processor)->toContain('sprintf($_lang["module_exec_not_found"], e($id))') + ->and($processor)->not->toContain('No record found for id') + ->and($processor)->not->toContain('You do not sufficient privileges'); +}); + +it('carries the module execution messages in every bundled language', function () use ($basePath) { + $locales = glob($basePath . '/core/lang/*/global.php'); + expect($locales)->not->toBeEmpty(); + + foreach ($locales as $file) { + $_lang = []; + include $file; + + foreach ([ + 'module_exec_no_privileges', + 'module_exec_not_found', + 'module_exec_disabled', + 'role_modules_tab', + 'role_modules_msg', + 'role_modules_admin_msg', + 'role_modules_none', + ] as $key) { + $locale = basename(dirname($file)); + expect($_lang)->toHaveKey($key); + expect(trim((string) $_lang[$key]))->not->toBe('', $locale . ' has an empty ' . $key); + } + + // the id is substituted with sprintf() + expect($_lang['module_exec_not_found'])->toContain('%s'); + } +}); + +it('filters the module menu by role, including file based modules', function () use ($basePath, $read) { + $frame = $read($basePath . '/core/src/Controllers/Frame.php'); + + expect($frame)->toContain('ModuleAccess::canRunFileModule($mgrRole, $module)') + // the menu used to skip the ACL entirely when use_udperms was off + ->and($frame)->not->toContain("\$_SESSION['mgrRole'] != 1 && \$this->managerTheme->getCore()->getConfig('use_udperms') === true"); +}); + +it('applies the role axis in the shared module scope so every listing inherits it', function () use ($basePath, $read) { + $model = $read($basePath . '/core/src/Models/SiteModule.php'); + + expect($model)->toContain('function scopeAllowedForRole') + ->and($model)->toContain('->allowedForRole($roleId)') + ->and($model)->toContain('site_module_roles') + // the group ACL branch must stay grouped, or the added role + // condition would be swallowed by its trailing orWhere + ->and($model)->toContain('->where(function (Eloquent\Builder $query) {'); +}); + +it('constrains the modules listed inside a category, not just the categories', function () use ($basePath, $read) { + foreach (['/core/src/Controllers/Modules.php', '/core/src/Controllers/Resources/Modules.php'] as $file) { + expect($read($basePath . $file)) + ->toContain("Category::with(['modules' => function (\$builder) use (\$roleId) {"); + } + + // the elements tab listed every module regardless of either ACL axis + expect($read($basePath . '/core/src/Controllers/Resources/Modules.php')) + ->toContain('->withoutProtected()'); +}); + +it('drops the Modules menu when the user may not run a single module', function () use ($basePath, $read) { + $frame = $read($basePath . '/core/src/Controllers/Frame.php'); + + // the parent node is added before the list is known, so the listing pass + // is what removes it again - no second query, no extra permission lookup + expect($frame)->toContain("unset(\$this->sitemenu['modules']);") + ->and(strpos($frame, '->menuModules()')) + ->toBeLessThan(strpos($frame, '->menuRunModules()')); +}); + +it('cleans up role rows wherever module and role rows are removed or copied', function () use ($basePath, $read) { + $files = [ + '/manager/processors/delete_module.processor.php', + '/manager/processors/duplicate_module.processor.php', + '/manager/processors/delete_role.processor.php', + '/core/src/Services/Store/LegacyDeleteService.php', + '/core/src/Controllers/UserRoles/UserRole.php', + ]; + + foreach ($files as $file) { + expect($read($basePath . $file))->toContain('SiteModuleRole'); + } +}); + +it('ships the role ACL table through every install and update path', function () use ($basePath) { + $migration = '/2026_08_29_000000_create_site_module_roles_table.php'; + + expect(is_file($basePath . '/core/database/migrations' . $migration))->toBeTrue() + ->and(is_file($basePath . '/install/stubs/migrations' . $migration))->toBeTrue(); + + $installers = [ + '/install/src/controllers/install.php', + '/install/cli-install.php', + '/core/src/Console/SiteUpdateCommand.php', + ]; + + foreach ($installers as $file) { + expect((string) file_get_contents($basePath . $file))->toContain('applyDefaultsFor('); + } +}); diff --git a/core/vendor/composer/autoload_classmap.php b/core/vendor/composer/autoload_classmap.php index 3f6eeefb02..c02ac402ad 100644 --- a/core/vendor/composer/autoload_classmap.php +++ b/core/vendor/composer/autoload_classmap.php @@ -482,6 +482,7 @@ 'Composer\\XdebugHandler\\Status' => $vendorDir . '/composer/xdebug-handler/src/Status.php', 'Composer\\XdebugHandler\\XdebugHandler' => $vendorDir . '/composer/xdebug-handler/src/XdebugHandler.php', 'CreateFileGroupsTable' => $baseDir . '/database/migrations/2026_03_29_000000_create_file_groups_table.php', + 'CreateSiteModuleRolesTable' => $baseDir . '/database/migrations/2026_08_29_000000_create_site_module_roles_table.php', 'CreateSystemCliTasksTables' => $baseDir . '/database/migrations/2026_04_12_000000_create_system_cli_tasks_tables.php', 'Cron\\AbstractField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/AbstractField.php', 'Cron\\CronExpression' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/CronExpression.php', @@ -1302,6 +1303,7 @@ 'EvolutionCMS\\Models\\SiteModule' => $baseDir . '/src/Models/SiteModule.php', 'EvolutionCMS\\Models\\SiteModuleAccess' => $baseDir . '/src/Models/SiteModuleAccess.php', 'EvolutionCMS\\Models\\SiteModuleDepobj' => $baseDir . '/src/Models/SiteModuleDepobj.php', + 'EvolutionCMS\\Models\\SiteModuleRole' => $baseDir . '/src/Models/SiteModuleRole.php', 'EvolutionCMS\\Models\\SitePlugin' => $baseDir . '/src/Models/SitePlugin.php', 'EvolutionCMS\\Models\\SitePluginEvent' => $baseDir . '/src/Models/SitePluginEvent.php', 'EvolutionCMS\\Models\\SiteSnippet' => $baseDir . '/src/Models/SiteSnippet.php', @@ -1403,6 +1405,7 @@ 'EvolutionCMS\\Support\\MailTestSmtp' => $baseDir . '/src/Support/MailTestSmtp.php', 'EvolutionCMS\\Support\\MakeTable' => $baseDir . '/src/Support/MakeTable.php', 'EvolutionCMS\\Support\\Menu' => $baseDir . '/src/Support/Menu.php', + 'EvolutionCMS\\Support\\ModuleAccess' => $baseDir . '/src/Support/ModuleAccess.php', 'EvolutionCMS\\Support\\MoveDocumentTargetGuard' => $baseDir . '/src/Support/MoveDocumentTargetGuard.php', 'EvolutionCMS\\Support\\MysqlDumper' => $baseDir . '/src/Support/MysqlDumper.php', 'EvolutionCMS\\Support\\Paginate' => $baseDir . '/src/Support/Paginate.php', diff --git a/core/vendor/composer/autoload_static.php b/core/vendor/composer/autoload_static.php index 7e5476db4b..c7908dbb43 100644 --- a/core/vendor/composer/autoload_static.php +++ b/core/vendor/composer/autoload_static.php @@ -1159,6 +1159,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Composer\\XdebugHandler\\Status' => __DIR__ . '/..' . '/composer/xdebug-handler/src/Status.php', 'Composer\\XdebugHandler\\XdebugHandler' => __DIR__ . '/..' . '/composer/xdebug-handler/src/XdebugHandler.php', 'CreateFileGroupsTable' => __DIR__ . '/../..' . '/database/migrations/2026_03_29_000000_create_file_groups_table.php', + 'CreateSiteModuleRolesTable' => __DIR__ . '/../..' . '/database/migrations/2026_08_29_000000_create_site_module_roles_table.php', 'CreateSystemCliTasksTables' => __DIR__ . '/../..' . '/database/migrations/2026_04_12_000000_create_system_cli_tasks_tables.php', 'Cron\\AbstractField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/AbstractField.php', 'Cron\\CronExpression' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/CronExpression.php', @@ -1979,6 +1980,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'EvolutionCMS\\Models\\SiteModule' => __DIR__ . '/../..' . '/src/Models/SiteModule.php', 'EvolutionCMS\\Models\\SiteModuleAccess' => __DIR__ . '/../..' . '/src/Models/SiteModuleAccess.php', 'EvolutionCMS\\Models\\SiteModuleDepobj' => __DIR__ . '/../..' . '/src/Models/SiteModuleDepobj.php', + 'EvolutionCMS\\Models\\SiteModuleRole' => __DIR__ . '/../..' . '/src/Models/SiteModuleRole.php', 'EvolutionCMS\\Models\\SitePlugin' => __DIR__ . '/../..' . '/src/Models/SitePlugin.php', 'EvolutionCMS\\Models\\SitePluginEvent' => __DIR__ . '/../..' . '/src/Models/SitePluginEvent.php', 'EvolutionCMS\\Models\\SiteSnippet' => __DIR__ . '/../..' . '/src/Models/SiteSnippet.php', @@ -2080,6 +2082,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'EvolutionCMS\\Support\\MailTestSmtp' => __DIR__ . '/../..' . '/src/Support/MailTestSmtp.php', 'EvolutionCMS\\Support\\MakeTable' => __DIR__ . '/../..' . '/src/Support/MakeTable.php', 'EvolutionCMS\\Support\\Menu' => __DIR__ . '/../..' . '/src/Support/Menu.php', + 'EvolutionCMS\\Support\\ModuleAccess' => __DIR__ . '/../..' . '/src/Support/ModuleAccess.php', 'EvolutionCMS\\Support\\MoveDocumentTargetGuard' => __DIR__ . '/../..' . '/src/Support/MoveDocumentTargetGuard.php', 'EvolutionCMS\\Support\\MysqlDumper' => __DIR__ . '/../..' . '/src/Support/MysqlDumper.php', 'EvolutionCMS\\Support\\Paginate' => __DIR__ . '/../..' . '/src/Support/Paginate.php', diff --git a/install/cli-install.php b/install/cli-install.php index 335a50634a..3a3cb2be63 100644 --- a/install/cli-install.php +++ b/install/cli-install.php @@ -857,7 +857,9 @@ public function installModulesAndPlugins() \EvolutionCMS\Models\SiteModule::query()->where('name', $name)->update(['modulecode' => $module, 'description' => $desc, 'properties' => $props, 'enable_sharedparams' => $shared]); } else { $props = parseProperties($properties, true); - \EvolutionCMS\Models\SiteModule::query()->create(['name' => $name, 'guid' => $guid, 'category' => $category, 'modulecode' => $module, 'description' => $desc, 'properties' => $props, 'enable_sharedparams' => $shared]); + $newModule = \EvolutionCMS\Models\SiteModule::query()->create(['name' => $name, 'guid' => $guid, 'category' => $category, 'modulecode' => $module, 'description' => $desc, 'properties' => $props, 'enable_sharedparams' => $shared]); + // bundled modules ship with a default role restriction + \EvolutionCMS\Models\SiteModuleRole::applyDefaultsFor((int)$newModule->getKey(), $name); } } } diff --git a/install/src/controllers/install.php b/install/src/controllers/install.php index e261952465..fff3b05579 100644 --- a/install/src/controllers/install.php +++ b/install/src/controllers/install.php @@ -503,9 +503,11 @@ } else { $installDataLevel['modules'][$moduleModule[0]]['type'] = 'create'; $properties = parseProperties($properties, true); - \EvolutionCMS\Models\SiteModule::create(['name' => $name, + $newModule = \EvolutionCMS\Models\SiteModule::create(['name' => $name, 'description' => $desc, 'modulecode' => $module, 'properties' => $properties, 'guid' => $guid, 'enable_sharedparams' => (int)$shared, 'category' => $category]); + // bundled modules ship with a default role restriction + \EvolutionCMS\Models\SiteModuleRole::applyDefaultsFor((int)$newModule->getKey(), $name); } } } else { diff --git a/install/stubs/migrations/2026_08_29_000000_create_site_module_roles_table.php b/install/stubs/migrations/2026_08_29_000000_create_site_module_roles_table.php new file mode 100644 index 0000000000..15a40f3916 --- /dev/null +++ b/install/stubs/migrations/2026_08_29_000000_create_site_module_roles_table.php @@ -0,0 +1,79 @@ +comment('Module role access control - defines which user roles may run specific modules'); + $table->increments('id'); + $table->unsignedInteger('module')->default(0); + $table->unsignedInteger('role')->default(0); + $table->unique(['module', 'role'], 'site_module_roles_module_role'); + }); + } + + $this->applyDefaultRestrictions(); + } + + public function down() + { + Schema::dropIfExists('site_module_roles'); + } + + /** + * Restrict bundled modules on sites that already have them installed. + * + * Only modules with no restriction at all are touched, so an administrator + * who has opened a module up keeps that decision. + */ + protected function applyDefaultRestrictions(): void + { + if (!Schema::hasTable('site_modules') || !Schema::hasTable('user_roles')) { + return; + } + + foreach (ModuleAccess::DEFAULT_RESTRICTIONS as $name => $roleIds) { + $moduleId = (int) DB::table('site_modules')->where('name', $name)->value('id'); + if ($moduleId <= 0) { + continue; + } + + if (DB::table('site_module_roles')->where('module', $moduleId)->exists()) { + continue; + } + + foreach (ModuleAccess::normalizeRoleIds($roleIds) as $roleId) { + if (!DB::table('user_roles')->where('id', $roleId)->exists()) { + continue; + } + + DB::table('site_module_roles')->insert([ + 'module' => $moduleId, + 'role' => $roleId, + ]); + } + } + } +} diff --git a/manager/processors/delete_module.processor.php b/manager/processors/delete_module.processor.php index 0cd6aaaaa0..99dc73d7d2 100755 --- a/manager/processors/delete_module.processor.php +++ b/manager/processors/delete_module.processor.php @@ -27,6 +27,8 @@ EvolutionCMS\Models\SiteModuleDepobj::where('module',$id)->delete(); // delete the module user group access. EvolutionCMS\Models\SiteModuleAccess::where('module',$id)->delete(); +// delete the module role access. +EvolutionCMS\Models\SiteModuleRole::where('module',$id)->delete(); // invoke OnModFormDelete event EvolutionCMS()->invokeEvent("OnModFormDelete", diff --git a/manager/processors/delete_role.processor.php b/manager/processors/delete_role.processor.php index e6edfcbbfd..6e132c8a14 100755 --- a/manager/processors/delete_role.processor.php +++ b/manager/processors/delete_role.processor.php @@ -24,6 +24,9 @@ $name = EvolutionCMS\Models\UserRole::select('name')->where('id',$id)->first()->name; $_SESSION['itemname'] = $name; +// delete the module access rows granted to this role +EvolutionCMS\Models\SiteModuleRole::where('role',$id)->delete(); + // delete the attributes EvolutionCMS\Models\UserRole::select('name')->where('id',$id)->delete(); diff --git a/manager/processors/duplicate_module.processor.php b/manager/processors/duplicate_module.processor.php index dce24b28a3..1c7942771d 100755 --- a/manager/processors/duplicate_module.processor.php +++ b/manager/processors/duplicate_module.processor.php @@ -46,6 +46,14 @@ $item->replicate()->save(); }); +// duplicate module role access +EvolutionCMS\Models\SiteModuleRole::select("module", "role") + ->where('module', $id)->get() + ->each(function ($item, $key) use ($newid) { + $item->module = $newid; + $item->replicate()->save(); + }); + // Set the item name for logger $name = EvolutionCMS\Models\SiteModule::select('name')->findOrFail($newid)->name; $_SESSION['itemname'] = $name; diff --git a/manager/processors/execute_module.processor.php b/manager/processors/execute_module.processor.php index 15e6535cbb..50e075deaa 100755 --- a/manager/processors/execute_module.processor.php +++ b/manager/processors/execute_module.processor.php @@ -1,6 +1,7 @@ INCLUDE_ORDERING_ERROR

Please use the EVO Content Manager instead of accessing this file directly."); @@ -11,35 +12,60 @@ if (isset($_GET['id'])) { if (is_numeric($_GET['id'])) { $id = (int)$_GET['id']; - } else { + } elseif (is_string($_GET['id'])) { + // the key of a module registered from a file $id = $_GET['id']; + } else { + // ?id[]=... and friends: not an id at all + $modx->webAlertAndQuit($_lang["error_no_id"]); } } else { $modx->webAlertAndQuit($_lang["error_no_id"]); } -// check if user has access permission, except admins -if ($_SESSION['mgrRole'] != 1 && is_numeric($id)) { - $moduleAccess = SiteModule::query() - ->withoutProtected() - ->where('site_modules.id', $id) - ->first(); - - if (empty($moduleAccess)) { - $modx->webAlertAndQuit("You do not sufficient privileges to execute this module.", "index.php?a=76&tab=5"); - } -} +$mgrRole = (int)get_by_key($_SESSION, 'mgrRole', 0); + if (is_numeric($id)) { + // check if user has access permission, except admins + if ($mgrRole !== ModuleAccess::ADMIN_ROLE) { + $moduleAccess = SiteModule::query() + ->select('site_modules.id') + ->withoutProtected() + ->where('site_modules.id', $id) + ->first(); + + if (empty($moduleAccess)) { + $modx->webAlertAndQuit($_lang["module_exec_no_privileges"], "index.php?a=76&tab=5"); + } + } + // get module data $content = \EvolutionCMS\Models\SiteModule::find($id); if (is_null($content)) { - $modx->webAlertAndQuit("No record found for id {$id}.", "index.php?a=76&tab=5"); + $modx->webAlertAndQuit(sprintf($_lang["module_exec_not_found"], e($id)), "index.php?a=76&tab=5"); } $content = $content->toArray(); if ($content['disabled']) { - $modx->webAlertAndQuit("This module is disabled and cannot be executed.", "index.php?a=76&tab=5"); + $modx->webAlertAndQuit($_lang["module_exec_disabled"], "index.php?a=76&tab=5"); } } else { + // file based modules: the id is a registry key, never a path - reject + // anything that is not registered instead of reading an arbitrary file + if (!is_string($id) || !isset($modx->modulesFromFile[$id])) { + $modx->webAlertAndQuit(sprintf($_lang["module_exec_not_found"], e($id)), "index.php?a=76&tab=5"); + } + $content = $modx->modulesFromFile[$id]; + + // file based modules carry their role restriction in the registration + // params; the group ACL cannot apply because they have no database row + if (!ModuleAccess::canRunFileModule($mgrRole, $content)) { + $modx->webAlertAndQuit($_lang["module_exec_no_privileges"], "index.php?a=76&tab=5"); + } + + if (!is_file($content['file']) || !is_readable($content['file'])) { + $modx->webAlertAndQuit(sprintf($_lang["module_exec_not_found"], e($id)), "index.php?a=76&tab=5"); + } + $content['modulecode'] = file_get_contents($content['file']); $content["guid"] = ''; } diff --git a/manager/views/page/user_roles/user_role.blade.php b/manager/views/page/user_roles/user_role.blade.php index 343c30b729..599c239be1 100644 --- a/manager/views/page/user_roles/user_role.blade.php +++ b/manager/views/page/user_roles/user_role.blade.php @@ -82,6 +82,45 @@ + @if($canManageModuleAccess) +

+

{{ ManagerTheme::getLexicon('role_modules_tab', 'Modules') }}

+ + + +
+ @php($isAdminRole = (int)($role->id ?? 0) === 1) +

{{ $isAdminRole + ? ManagerTheme::getLexicon('role_modules_admin_msg', 'The administrator role can run every module.') + : ManagerTheme::getLexicon('role_modules_msg', 'Modules this role is allowed to run.') }}

+ + @if($modules->count() === 0) + {{ ManagerTheme::getLexicon('role_modules_none', 'No modules have been installed yet.') }} + @else +
+ @foreach($modules as $module) +
+ +
+ @endforeach +
+ @endif +
+
+ @endif +

{{ ManagerTheme::getLexicon('template_assignedtv_tab') }}