Add managePlugins ACL + enforce pluginHolder actions

Ensures delegated admins with plugin management permission can access `/plugins/installed` UI and all plugin action endpoints consistently.
This commit is contained in:
master3395
2026-03-25 10:12:48 +01:00
parent ab3d8bfd19
commit b3ae20cba0
8 changed files with 114 additions and 6 deletions

View File

@@ -2130,7 +2130,11 @@
</a>
{% endif %}
</div>
{% endif %}
{% if admin or managePlugins %}
{% if managePlugins and not admin %}
<div class="section-header">{% trans "PLUGINS" %}</div>
{% endif %}
<a href="#" class="menu-item" onclick="toggleSubmenu('plugins-submenu', this); return false;">
<div class="icon-wrapper">
<i class="fas fa-plug"></i>

View File

@@ -283,6 +283,17 @@ class ACLManager:
finalResponse['mailServerSSL'] = config['mailServerSSL']
finalResponse['sslReconcile'] = config.get('sslReconcile', 0)
## Plugin management (Plugin Store / installed plugins UI and APIs)
_mpv = config.get('managePlugins', 0)
if _mpv in (1, True, '1', 'true'):
finalResponse['managePlugins'] = 1
else:
try:
finalResponse['managePlugins'] = 1 if int(_mpv) else 0
except (TypeError, ValueError):
finalResponse['managePlugins'] = 0
return finalResponse
@staticmethod

50
plogical/plugin_acl.py Normal file
View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
"""Shared ACL checks for CyberPanel plugin management (core + store plugins)."""
from functools import wraps
from django.http import JsonResponse
def user_can_manage_plugins(request):
"""True if session user is full admin or has managePlugins ACL."""
try:
user_id = request.session['userID']
except KeyError:
return False
try:
from plogical.acl import ACLManager
acl = ACLManager.loadedACL(user_id)
if acl.get('admin') == 1:
return True
try:
return int(acl.get('managePlugins', 0) or 0) == 1
except (TypeError, ValueError):
return False
except BaseException:
return False
def deny_plugin_manage_json_response(request):
"""401 if no session, else 403 JSON for plugin management APIs."""
try:
request.session['userID']
except KeyError:
return JsonResponse({
'success': False,
'error_message': 'Authentication required.',
'error': 'Authentication required.',
}, status=401)
return JsonResponse({
'success': False,
'error_message': 'You are not authorized to manage plugins.',
'error': 'You are not authorized to manage plugins.',
}, status=403)
def require_manage_plugins_api(view_func):
"""Decorator: JSON 401/403 if user cannot manage plugins (use after login/session check)."""
@wraps(view_func)
def _wrapped(request, *args, **kwargs):
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
return view_func(request, *args, **kwargs)
return _wrapped

View File

@@ -12,6 +12,7 @@ import json
from datetime import datetime, timedelta
from xml.etree import ElementTree
from plogical.httpProc import httpProc
from plogical.plugin_acl import user_can_manage_plugins, deny_plugin_manage_json_response
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
import sys
import urllib.request
@@ -206,7 +207,7 @@ def _set_plugin_state(plugin_name, enabled):
def help_page(request):
"""Display plugin development help page"""
mailUtilities.checkHome()
proc = httpProc(request, 'pluginHolder/help.html', {}, 'admin')
proc = httpProc(request, 'pluginHolder/help.html', {}, 'managePlugins')
return proc.render()
def installed(request):
@@ -611,7 +612,7 @@ def installed(request):
proc = httpProc(request, 'pluginHolder/plugins.html',
{'plugins': pluginList, 'error_plugins': errorPlugins,
'installed_count': installed_count, 'active_count': active_count,
'cache_expiry_timestamp': cache_expiry_timestamp}, 'admin')
'cache_expiry_timestamp': cache_expiry_timestamp}, 'managePlugins')
return proc.render()
@csrf_exempt
@@ -619,6 +620,8 @@ def installed(request):
def install_plugin(request, plugin_name):
"""Install a plugin"""
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
# Check if plugin source exists (in any configured source path)
pluginSource = _get_plugin_source_path(plugin_name)
if not pluginSource:
@@ -724,6 +727,8 @@ def install_plugin(request, plugin_name):
def uninstall_plugin(request, plugin_name):
"""Uninstall a plugin - but keep source files and settings"""
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
# Check if plugin is installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if not os.path.exists(pluginInstalled):
@@ -778,6 +783,8 @@ def uninstall_plugin(request, plugin_name):
def enable_plugin(request, plugin_name):
"""Enable a plugin"""
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
# Check if plugin is installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if not os.path.exists(pluginInstalled):
@@ -810,6 +817,8 @@ def enable_plugin(request, plugin_name):
def disable_plugin(request, plugin_name):
"""Disable a plugin"""
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
# Check if plugin is installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if not os.path.exists(pluginInstalled):
@@ -1394,6 +1403,8 @@ def _fetch_plugins_from_github():
def fetch_plugin_store(request):
"""Fetch plugins from the plugin store with caching"""
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
mailUtilities.checkHome()
except Exception as e:
logging.writeToFile(f"fetch_plugin_store: checkHome failed: {str(e)}")
@@ -1461,6 +1472,8 @@ def upgrade_plugin(request, plugin_name):
mailUtilities.checkHome()
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
# Check if plugin is installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if not os.path.exists(pluginInstalled):
@@ -1623,6 +1636,8 @@ def get_plugin_backups(request, plugin_name):
mailUtilities.checkHome()
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
backups = _get_plugin_backups(plugin_name)
return JsonResponse({
'success': True,
@@ -1643,6 +1658,8 @@ def revert_plugin(request, plugin_name):
mailUtilities.checkHome()
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
# Get backup path from request
data = json.loads(request.body)
backup_path = data.get('backup_path')
@@ -1707,6 +1724,8 @@ def install_from_store(request, plugin_name):
mailUtilities.checkHome()
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
# Check if already installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if os.path.exists(pluginInstalled):
@@ -1888,6 +1907,8 @@ def install_from_store(request, plugin_name):
def debug_loaded_plugins(request):
"""Return which plugins have URL routes loaded and which failed (for diagnosing 404s)."""
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
import pluginHolder.urls as urls_mod
loaded = list(getattr(urls_mod, '_loaded_plugins', []))
failed = dict(getattr(urls_mod, '_failed_plugins', {}))
@@ -1909,6 +1930,9 @@ def plugin_settings_proxy(request, plugin_name):
the plugin was installed after the worker started (dynamic URL list is built at import time).
"""
mailUtilities.checkHome()
if not user_can_manage_plugins(request):
from django.http import HttpResponseForbidden
return HttpResponseForbidden('You are not authorized to manage plugins.')
plugin_path = '/usr/local/CyberCP/' + plugin_name
urls_py = os.path.join(plugin_path, 'urls.py')
if not plugin_name or not os.path.isdir(plugin_path) or not os.path.exists(urls_py):
@@ -1945,7 +1969,7 @@ def plugin_help(request, plugin_name):
if not os.path.exists(plugin_path) or not os.path.exists(meta_xml_path):
proc = httpProc(request, 'pluginHolder/plugin_not_found.html', {
'plugin_name': plugin_name
}, 'admin')
}, 'managePlugins')
return proc.render()
# Parse meta.xml
@@ -1967,7 +1991,7 @@ def plugin_help(request, plugin_name):
logging.writeToFile(f"Error parsing meta.xml for {plugin_name}: {str(e)}")
proc = httpProc(request, 'pluginHolder/plugin_not_found.html', {
'plugin_name': plugin_name
}, 'admin')
}, 'managePlugins')
return proc.render()
# Look for help content files (README.md, CHANGELOG.md, HELP.md, etc.)
@@ -2118,7 +2142,7 @@ def plugin_help(request, plugin_name):
'help_content': help_content,
}
proc = httpProc(request, 'pluginHolder/plugin_help.html', context, 'admin')
proc = httpProc(request, 'pluginHolder/plugin_help.html', context, 'managePlugins')
return proc.render()
@csrf_exempt
@@ -2140,6 +2164,8 @@ def check_plugin_subscription(request, plugin_name):
}
"""
try:
if not user_can_manage_plugins(request):
return deny_plugin_manage_json_response(request)
# Check if user is authenticated
if not request.user or not request.user.is_authenticated:
return JsonResponse({

View File

@@ -773,6 +773,7 @@ app.controller('createACLCTRL', function ($scope, $http) {
//
$scope.versionManagement = false;
$scope.managePlugins = false;
// User Management
@@ -854,6 +855,7 @@ app.controller('createACLCTRL', function ($scope, $http) {
//
versionManagement: $scope.versionManagement,
managePlugins: $scope.managePlugins,
// User Management
@@ -977,6 +979,7 @@ app.controller('createACLCTRL', function ($scope, $http) {
//
$scope.versionManagement = true;
$scope.managePlugins = true;
// User Management
@@ -1048,6 +1051,7 @@ app.controller('createACLCTRL', function ($scope, $http) {
//
$scope.versionManagement = false;
$scope.managePlugins = false;
// User Management
@@ -1232,6 +1236,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) {
//
$scope.versionManagement = Boolean(response.data.versionManagement);
$scope.managePlugins = Boolean(response.data.managePlugins);
// User Management
@@ -1333,6 +1338,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) {
adminStatus: $scope.makeAdmin,
//
versionManagement: $scope.versionManagement,
managePlugins: $scope.managePlugins,
// User Management
@@ -1456,6 +1462,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) {
//
$scope.versionManagement = true;
$scope.managePlugins = true;
// User Management
@@ -1527,6 +1534,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) {
//
$scope.versionManagement = false;
$scope.managePlugins = false;
// User Management

View File

@@ -162,6 +162,9 @@
<div class="checkbox-wrapper">
<label><input ng-model="versionManagement" type="checkbox" value=""> {% trans "Version Management" %}</label>
</div>
<div class="checkbox-wrapper">
<label><input ng-model="managePlugins" type="checkbox" value=""> {% trans "Plugin management" %} ({% trans "Plugin Store and installed plugins" %})</label>
</div>
<div class="checkbox-wrapper">
<div class="form-label" style="margin-bottom: 0.5rem;">{% trans "User Management" %}</div>
<label><input ng-model="createNewUser" type="checkbox" value=""> {% trans "Create New User" %}</label>

View File

@@ -374,6 +374,10 @@
<input ng-model="versionManagement" type="checkbox" id="versionManagement">
<label for="versionManagement" class="checkbox-label">{% trans "Version Management" %}</label>
</div>
<div class="checkbox-wrapper">
<input ng-model="managePlugins" type="checkbox" id="managePlugins">
<label for="managePlugins" class="checkbox-label">{% trans "Plugin management" %} ({% trans "Plugin Store and installed plugins" %})</label>
</div>
</div>
</div>

View File

@@ -124,6 +124,7 @@ class TestUserManagement(TestCase):
data_ret = {'aclName': 'hello', 'makeAdmin':1,
'createNewUser': 1,
'versionManagement': 1,
'managePlugins': 0,
'listUsers': 1,
'resellerCenter': 1,
'deleteUser': 1,
@@ -190,6 +191,7 @@ class TestUserManagement(TestCase):
'adminStatus':1,
'createNewUser': 1,
'versionManagement': 1,
'managePlugins': 0,
'listUsers': 1,
'resellerCenter': 1,
'deleteUser': 1,