diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 8be0523b65..f4f0e23378 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -1,23 +1,24 @@ # Copyright (c) 2018 Jaime van Kessel, Ultimaker B.V. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. -from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot -from typing import Dict, Type, TYPE_CHECKING, List, Optional, cast - -from UM.PluginRegistry import PluginRegistry -from UM.Resources import Resources -from UM.Application import Application -from UM.Extension import Extension -from UM.Logger import Logger - import configparser # The script lists are stored in metadata as serialised config files. +import importlib.util import io # To allow configparser to write to a string. import os.path import pkgutil import sys -import importlib.util +from typing import Dict, Type, TYPE_CHECKING, List, Optional, cast +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot + +from UM.Application import Application +from UM.Extension import Extension +from UM.Logger import Logger +from UM.PluginRegistry import PluginRegistry +from UM.Resources import Resources +from UM.Trust import Trust from UM.i18n import i18nCatalog +from cura import ApplicationMetadata from cura.CuraApplication import CuraApplication i18n_catalog = i18nCatalog("cura") @@ -161,7 +162,13 @@ class PostProcessingPlugin(QObject, Extension): # Iterate over all scripts. if script_name not in sys.modules: try: - spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py")) + file_path = os.path.join(path, script_name + ".py") + if not self._isScriptAllowed(file_path): + Logger.warning("Skipped loading post-processing script {}: not trusted".format(file_path)) + continue + + spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, + file_path) loaded_script = importlib.util.module_from_spec(spec) if spec.loader is None: continue @@ -334,4 +341,26 @@ class PostProcessingPlugin(QObject, Extension): if global_container_stack is not None: global_container_stack.propertyChanged.emit("post_processing_plugin", "value") + @staticmethod + def _isScriptAllowed(file_path: str) -> bool: + """Checks whether the given file is allowed to be loaded""" + if not ApplicationMetadata.IsEnterpriseVersion: + # No signature needed + return True + + dir_path = os.path.split(file_path)[0] # type: str + plugin_path = PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin") + assert plugin_path is not None # appease mypy + bundled_path = os.path.join(plugin_path, "scripts") + if dir_path == bundled_path: + # Bundled scripts are trusted. + return True + + trust_instance = Trust.getInstanceOrNone() + if trust_instance is not None and Trust.signatureFileExistsFor(file_path): + if trust_instance.signedFileCheck(file_path): + return True + + return False # Default verdict should be False, being the most secure fallback + diff --git a/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py b/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py new file mode 100644 index 0000000000..360ea344ca --- /dev/null +++ b/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py @@ -0,0 +1,61 @@ + +import os +import sys +from unittest.mock import patch, MagicMock + +from UM.PluginRegistry import PluginRegistry +from UM.Resources import Resources +from UM.Trust import Trust +from ..PostProcessingPlugin import PostProcessingPlugin + +# not sure if needed +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) + +""" In this file, commnunity refers to regular Cura for makers.""" + +mock_plugin_registry = MagicMock() +mock_plugin_registry.getPluginPath = MagicMock(return_value = "mocked_plugin_path") + + +# noinspection PyProtectedMember +@patch("cura.ApplicationMetadata.IsEnterpriseVersion", False) +def test_community_user_script_allowed(): + assert PostProcessingPlugin._isScriptAllowed("blaat.py") + + +# noinspection PyProtectedMember +@patch("cura.ApplicationMetadata.IsEnterpriseVersion", False) +def test_community_bundled_script_allowed(): + assert PostProcessingPlugin._isScriptAllowed(_bundled_file_path()) + + +# noinspection PyProtectedMember +@patch("cura.ApplicationMetadata.IsEnterpriseVersion", True) +@patch.object(PluginRegistry, "getInstance", return_value=mock_plugin_registry) +def test_enterprise_unsigned_user_script_not_allowed(plugin_registry): + assert not PostProcessingPlugin._isScriptAllowed("blaat.py") + +# noinspection PyProtectedMember +@patch("cura.ApplicationMetadata.IsEnterpriseVersion", True) +@patch.object(PluginRegistry, "getInstance", return_value=mock_plugin_registry) +def test_enterprise_signed_user_script_allowed(plugin_registry): + mocked_trust = MagicMock() + mocked_trust.signedFileCheck = MagicMock(return_value=True) + + plugin_registry.getPluginPath = MagicMock(return_value="mocked_plugin_path") + + with patch.object(Trust, "signatureFileExistsFor", return_value = True): + with patch("UM.Trust.Trust.getInstanceOrNone", return_value=mocked_trust): + assert PostProcessingPlugin._isScriptAllowed("mocked_plugin_path/scripts/blaat.py") + + +# noinspection PyProtectedMember +@patch("cura.ApplicationMetadata.IsEnterpriseVersion", False) +def test_enterprise_bundled_script_allowed(): + assert PostProcessingPlugin._isScriptAllowed(_bundled_file_path()) + + +def _bundled_file_path(): + return os.path.join( + Resources.getStoragePath(Resources.Resources) + "scripts/blaat.py" + ) diff --git a/plugins/PostProcessingPlugin/tests/__init__.py b/plugins/PostProcessingPlugin/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2