From 5b045f89b174e63763b787e5de40868c393ecf26 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 24 Mar 2020 16:24:24 +0100 Subject: [PATCH] Finish postprocessing script signature checking CURA-7319 --- .../PostProcessingPlugin.py | 53 ++++++++++----- .../tests/TestPostProcessingPlugin.py | 65 +++++++++++++++++++ .../PostProcessingPlugin/tests/__init__.py | 0 3 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py create mode 100644 plugins/PostProcessingPlugin/tests/__init__.py diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 826b655988..8cfe730d95 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -1,24 +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.Trust import Trust -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") @@ -162,12 +162,13 @@ class PostProcessingPlugin(QObject, Extension): # Iterate over all scripts. if script_name not in sys.modules: try: - file_location = os.path.join(path, script_name + ".py") - trust_instance = Trust.getInstanceOrNone() - if trust_instance is not None and Trust.signatureFileExistsFor(file_location): - if not trust_instance.signedFileCheck(file_location): - raise Exception("Can't validate script {0}".format(file_location)) - spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, file_location) + 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 @@ -340,4 +341,22 @@ 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) -> bool: + """Checks whether the given file is allowed to be loaded""" + if not ApplicationMetadata.IsEnterpriseVersion: + # No signature needed + return True + + if os.path.split(file_path) == os.path.join(Resources.getStoragePath(Resources.Resources), "scripts"): + # 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..241b7ef07d --- /dev/null +++ b/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py @@ -0,0 +1,65 @@ + +import os +import sys +from unittest.mock import patch, MagicMock + +from pytest import fixture + +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.""" + + +# 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) +def test_enterprise_unsigned_user_script_not_allowed(): + assert not PostProcessingPlugin._isScriptAllowed("blaat.py") + +@fixture +def mocked_get_instance_or_none(): + mocked_trust = MagicMock() + mocked_trust.signedFileCheck = MagicMock(return_value=True) + return mocked_trust + +@fixture +def mocked_get_signature_file_exists_for(): + return MagicMock(return_value=True) + +# noinspection PyProtectedMember +@patch("cura.ApplicationMetadata.IsEnterpriseVersion", True) +@patch("UM.Trust", "signatureFileExistsFor") +@patch("UM.Trust.Trust.getInstanceOrNone") +def test_enterprise_signed_user_script_allowed(mocked_instance_or_none, mocked_get_instance_or_none): + file_path = "blaat.py" + realSignatureFileExistsFor = Trust.signatureFileExistsFor + Trust.signatureFileExistsFor = MagicMock(return_value=True) + assert PostProcessingPlugin._isScriptAllowed(file_path) + + # cleanup + Trust.signatureFileExistsFor = realSignatureFileExistsFor + +# 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