mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-06 15:27:05 +02:00
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:
@@ -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>
|
||||
|
||||
@@ -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
50
plogical/plugin_acl.py
Normal 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
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user