Add config option to fully disable installing plugins (#6535)

* [FR] Add config option to fully disable installing plugins
Fixes #6531

* also restrict uninstalling

* Added test

* diff cleanup

* extend api to show if install was disabled

* PUI disable install buttons

* CUI disable install button if not available

* add config option

* Rephrase
This commit is contained in:
Matthias Mair 2024-02-26 12:44:31 +01:00 committed by GitHub
parent 85225538e6
commit 75c24fb8f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 46 additions and 5 deletions

View File

@ -152,6 +152,7 @@ class InfoView(AjaxView):
'worker_running': is_worker_running(),
'worker_pending_tasks': self.worker_pending_tasks(),
'plugins_enabled': settings.PLUGINS_ENABLED,
'plugins_install_disabled': settings.PLUGINS_INSTALL_DISABLED,
'active_plugins': plugins_info(),
'email_configured': is_email_configured(),
'debug_mode': settings.DEBUG,

View File

@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 175
INVENTREE_API_VERSION = 176
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v176 - 2024-02-26 : https://github.com/inventree/InvenTree/pull/6535
- Adds the field "plugins_install_disabled" to the Server info API endpoint
v175 - 2024-02-21 : https://github.com/inventree/InvenTree/pull/6538
- Adds "parts" count to PartParameterTemplate serializer

View File

@ -1136,6 +1136,9 @@ MAINTENANCE_MODE_STATE_BACKEND = 'InvenTree.backends.InvenTreeMaintenanceModeBac
PLUGINS_ENABLED = get_boolean_setting(
'INVENTREE_PLUGINS_ENABLED', 'plugins_enabled', False
)
PLUGINS_INSTALL_DISABLED = get_boolean_setting(
'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False
)
PLUGIN_FILE = config.get_plugin_file()

View File

@ -155,6 +155,12 @@ def plugins_enabled(*args, **kwargs):
return djangosettings.PLUGINS_ENABLED
@register.simple_tag()
def plugins_install_disabled(*args, **kwargs):
"""Return True if plugin install is disabled for the server instance."""
return djangosettings.PLUGINS_INSTALL_DISABLED
@register.simple_tag()
def plugins_info(*args, **kwargs):
"""Return information about activated plugins."""

View File

@ -152,6 +152,7 @@ sentry_enabled: False
# Set this variable to True to enable InvenTree Plugins
# Alternatively, use the environment variable INVENTREE_PLUGINS_ENABLED
plugins_enabled: False
#plugin_noinstall: True
#plugin_file: '/path/to/plugins.txt'
#plugin_dir: '/path/to/plugins/'

View File

@ -55,6 +55,10 @@ class TemplateTagTest(InvenTreeTestCase):
"""Test the plugins_enabled tag."""
self.assertEqual(inventree_extras.plugins_enabled(), True)
def test_plugins_install_disabled(self):
"""Test the plugins_install_disabled tag."""
self.assertEqual(inventree_extras.plugins_install_disabled(), False)
def test_inventree_instance_name(self):
"""Test the 'instance name' setting."""
self.assertEqual(inventree_extras.inventree_instance_name(), 'InvenTree')

View File

@ -193,6 +193,9 @@ def install_plugin(url=None, packagename=None, user=None, version=None):
if user and not user.is_staff:
raise ValidationError(_('Only staff users can administer plugins'))
if settings.PLUGINS_INSTALL_DISABLED:
raise ValidationError(_('Plugin installation is disabled'))
logger.info('install_plugin: %s, %s', url, packagename)
# Check if we are running in a virtual environment
@ -292,6 +295,9 @@ def uninstall_plugin(cfg: plugin.models.PluginConfig, user=None, delete_config=T
"""
from plugin.registry import registry
if settings.PLUGINS_INSTALL_DISABLED:
raise ValidationError(_('Plugin uninstalling is disabled'))
if cfg.active:
raise ValidationError(
_('Plugin cannot be uninstalled as it is currently active')

View File

@ -1,5 +1,6 @@
"""Tests for general API tests for the plugin app."""
from django.conf import settings
from django.urls import reverse
from rest_framework.exceptions import NotFound
@ -81,6 +82,11 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
data['confirm'][0].title().upper(), 'Installation not confirmed'.upper()
)
# install disabled
settings.PLUGINS_INSTALL_DISABLED = True
self.post(url, {}, expected_code=400)
settings.PLUGINS_INSTALL_DISABLED = False
def test_plugin_activate(self):
"""Test the plugin activate."""
test_plg = self.plugin_confs.first()

View File

@ -29,6 +29,7 @@
</div>
{% plugins_enabled as plug %}
{% plugins_install_disabled as plug_disabled %}
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
@ -38,7 +39,7 @@
{% admin_url user "plugin.pluginconfig" None as url %}
{% include "admin_button.html" with url=url %}
{% if plug %}
<button class="btn btn-success" id="install-plugin" title="{% trans 'Install Plugin' %}">
<button class="btn btn-success" id="install-plugin" title="{% trans 'Install Plugin' %}"{% if plug_disabled %}disabled{% endif %}>
<span class='fas fa-plus-circle'></span> {% trans "Install Plugin" %}
</button>
<button class='btn btn-success' id='reload-plugins' title='{% trans "Reload Plugins" %}'>

View File

@ -324,6 +324,7 @@ The following [plugin](../extend/plugins.md) configuration options are available
| Environment Variable | Configuration File | Description | Default |
| --- | --- | --- | --- |
| INVENTREE_PLUGINS_ENABLED | plugins_enabled | Enable plugin support | False |
| INVENTREE_PLUGIN_NOINSTALL | plugin_noinstall | Disable Plugin installation via API - only use plugins.txt file | False |
| INVENTREE_PLUGIN_FILE | plugins_plugin_file | Location of plugin installation file | *Not specified* |
| INVENTREE_PLUGIN_DIR | plugins_plugin_dir | Location of external plugin directory | *Not specified* |

View File

@ -8,6 +8,7 @@ export const emptyServerAPI = {
worker_running: null,
worker_pending_tasks: null,
plugins_enabled: null,
plugins_install_disabled: null,
active_plugins: [],
email_configured: null,
debug_mode: null,

View File

@ -34,6 +34,7 @@ export interface ServerAPIProps {
worker_running: null | boolean;
worker_pending_tasks: null | number;
plugins_enabled: null | boolean;
plugins_install_disabled: null | boolean;
active_plugins: PluginProps[];
email_configured: null | boolean;
debug_mode: null | boolean;

View File

@ -14,6 +14,7 @@ export type RowAction = {
icon: ReactNode;
onClick?: () => void;
hidden?: boolean;
disabled?: boolean;
};
// Component for duplicating a row in a table
@ -129,6 +130,7 @@ export function RowActions({
setOpened(false);
}}
disabled={action.disabled || false}
>
{action.title}
</Menu.Item>

View File

@ -271,8 +271,11 @@ export default function PluginListTable() {
const navigate = useNavigate();
const user = useUserState();
const pluginsEnabled = useServerApiState(
(state) => state.server.plugins_enabled
const [pluginsEnabled, plugins_install_disabled] = useServerApiState(
(state) => [
state.server.plugins_enabled,
state.server.plugins_install_disabled
]
);
const pluginTableColumns: TableColumn[] = useMemo(
@ -457,7 +460,8 @@ export default function PluginListTable() {
onClick: () => {
setSelectedPlugin(record.pk);
uninstallPluginModal.open();
}
},
disabled: plugins_install_disabled || false
});
}
@ -592,6 +596,7 @@ export default function PluginListTable() {
setPluginPackage('');
installPluginModal.open();
}}
disabled={plugins_install_disabled || false}
/>
);
}