CVE-2023-41319 (published 2023-09-06T18:15Z)

Fides is an open-source privacy engineering platform for managing the fulfillment of data privacy requests in a runtime environment, and the enforcement of privacy regulations in code. The Fides webserver API allows custom integrations to be uploaded as a ZIP file. This ZIP file must contain YAML files, but Fides can be configured to also accept the inclusion of custom Python code in it. The custom code is executed in a restricted, sandboxed environment, but the sandbox can be bypassed to execute any arbitrary code. The vulnerability allows the execution of arbitrary code on the target system within the context of the webserver python process owner on the webserver container, which by default is `root`, and leverage that access to attack underlying infrastructure and integrated systems. This vulnerability affects Fides versions `2.11.0` through `2.19.0`. Exploitation is limited to API clients with the `CONNECTOR_TEMPLATE_REGISTER` authorization scope. In the Fides Admin UI this scope is restricted to highly privileged users, specifically root users and users with the owner role.  Exploitation is only possible if the security configuration parameter `allow_custom_connector_functions` is enabled by the user deploying the Fides webserver container, either in `fides.toml` or by setting the env var `FIDES__SECURITY__ALLOW_CUSTOM_CONNECTOR_FUNCTIONS=True`. By default this configuration parameter is disabled. The vulnerability has been patched in Fides version `2.19.0`. Users are advised to upgrade to this version or later to secure their systems against this threat. Users unable to upgrade should ensure that `allow_custom_connector_functions` in `fides.toml` and the `FIDES__SECURITY__ALLOW_CUSTOM_CONNECTOR_FUNCTIONS` are both either unset or explicit set to `False`.

CVSS v3 base score: 7.2, HIGH (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-94
cwe_name: Improper Control of Generation of Code ('Code Injection')
description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
extended_description: 
            When a product allows a user's input to contain code syntax, it might be possible for an attacker to craft the code in such a way that it will alter the intended control flow of the product. Such an alteration could lead to arbitrary code execution.
            Injection problems encompass a wide variety of issues -- all mitigated in very different ways. For this reason, the most effective way to discuss these weaknesses is to note the distinct features which classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed. The most classic instantiations of this category of weakness are SQL injection and format string vulnerabilities.
         
         
cwe_id: CWE-693
cwe_name: Protection Mechanism Failure
description: The product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product.
extended_description: This weakness covers three distinct situations. A "missing" protection mechanism occurs when the application does not define any mechanism against a certain class of attack. An "insufficient" protection mechanism might provide some defenses - for example, against the most common attacks - but it does not protect against everything that is intended. Finally, an "ignored" mechanism occurs when a mechanism is available and in active use within the product, but the developer has not applied it in some code path.
         

Fix information:
repo_url: https://github.com/ethyca/fides
commit_details: repo_url: https://github.com/ethyca/fides
author: Adrian Galvan
author_date: 2023-09-05 12:51:11-07:00
msg: Merge pull request from GHSA-p6p2-qq95-vq5h

* Removing custom connector template functions

* Updating connector template modal description

* Adding function upload tests
repository: repo_url: https://github.com/ethyca/fides
repo_name: ethyca/fides
description: The Privacy Engineering & Compliance Framework
repo_language: Python
owner: ethyca
file_changes: filename: ConnectorTemplateUploadModal.tsx
change_type: ModificationType.MODIFY
diff: @@ -122,8 +122,7 @@ const ConnectorTemplateUploadModal: React.FC<RequestModalProps> = ({
           </Box>
           <Text fontSize="sm" mt={4}>
             An integration template zip file must include a SaaS config and
-            dataset, but may also contain an icon (.svg) and custom functions
-            (.py) as optional files.
+            dataset, but may also contain an icon (.svg) as an optional file.
           </Text>
         </ModalBody>
         <ModalFooter>

programming_language: TypeScript
filename: docker-compose.yml
change_type: ModificationType.MODIFY
diff: @@ -28,7 +28,6 @@ services:
       FIDES__DEV_MODE: "True"
       FIDES__LOGGING__COLORIZE: "True"
       FIDES__USER__ANALYTICS_OPT_OUT: "True"
-      FIDES__SECURITY__ALLOW_CUSTOM_CONNECTOR_FUNCTIONS: "True"
       FIDES__SECURITY__BASTION_SERVER_HOST: ${FIDES__SECURITY__BASTION_SERVER_HOST-}
       FIDES__SECURITY__BASTION_SERVER_SSH_USERNAME: ${FIDES__SECURITY__BASTION_SERVER_SSH_USERNAME-}
       FIDES__SECURITY__BASTION_SERVER_SSH_PRIVATE_KEY: ${FIDES__SECURITY__BASTION_SERVER_SSH_PRIVATE_KEY-}

programming_language: CoffeeScript
filename: requirements.txt
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,3 @@
-AccessControl==6.0
 alembic==1.8.1
 APScheduler==3.9.1.post1
 asyncpg==0.27.0
@@ -42,7 +41,6 @@ pymssql==2.2.8
 python-jose[cryptography]==3.3.0
 pyyaml==6.0.1
 redis==3.5.3
-RestrictedPython==6.0.0
 rich-click==1.6.1
 sendgrid==6.9.7
 slowapi==0.1.8

programming_language: Batchfile
filename: connector_template.py
change_type: ModificationType.MODIFY
diff: @@ -13,13 +13,12 @@ from fides.api.util.saas_util import load_config_from_string, load_dataset_from_
 class ConnectorTemplate(BaseModel):
     """
     A collection of artifacts that make up a complete
-    SaaS connector (SaaS config, dataset, icon, functions, etc.)
+    SaaS connector (SaaS config, dataset, icon, etc.)
     """
 
     config: str
     dataset: str
     icon: Optional[str]
-    functions: Optional[str]
     human_readable: str
 
     @validator("config")

programming_language: Python
filename: connector_registry_service.py
change_type: ModificationType.MODIFY
diff: @@ -1,22 +1,17 @@
 # pylint: disable=protected-access
 import os
 from abc import ABC, abstractmethod
-from ast import AST, AnnAssign
-from operator import getitem
-from typing import Any, Dict, Iterable, List, Optional, Tuple, Type
+from typing import Dict, Iterable, List, Optional, Type
 from zipfile import ZipFile
 
-from AccessControl.ZopeGuards import safe_builtins
 from fideslang.models import Dataset
 from loguru import logger
 from packaging.version import Version
 from packaging.version import parse as parse_version
-from RestrictedPython import compile_restricted
-from RestrictedPython.transformer import RestrictingNodeTransformer
 from sqlalchemy.orm import Session
 
 from fides.api.api.deps import get_api_session
-from fides.api.common_exceptions import FidesopsException, ValidationError
+from fides.api.common_exceptions import ValidationError
 from fides.api.cryptography.cryptographic_util import str_to_b64_str
 from fides.api.models.connectionconfig import (
     AccessLevel,
@@ -41,7 +36,6 @@ from fides.api.util.saas_util import (
     replace_version,
 )
 from fides.api.util.unsafe_file_util import verify_svg, verify_zip
-from fides.config import CONFIG
 
 
 class ConnectorTemplateLoader(ABC):
@@ -96,7 +90,6 @@ class FileConnectorTemplateLoader(ConnectorTemplateLoader):
                             f"data/saas/dataset/{connector_type}_dataset.yml"
                         ),
                         icon=icon,
-                        functions=None,
                         human_readable=human_readable,
                     )
                 except Exception:
@@ -151,24 +144,16 @@ class CustomConnectorTemplateLoader(ConnectorTemplateLoader):
         template: CustomConnectorTemplate,
     ) -> None:
         """
-        Registers a custom connector template by converting it to a ConnectorTemplate,
-        registering any custom functions, and adding it to the loader's template dictionary.
+        Registers a custom connector template by converting it to a ConnectorTemplate
+        and adding it to the loader's template dictionary.
         """
         connector_template = ConnectorTemplate(
             config=template.config,
             dataset=template.dataset,
             icon=template.icon,
-            functions=template.functions,
             human_readable=template.name,
         )
 
-        # register custom functions if available
-        if template.functions:
-            register_custom_functions(template.functions)
-            logger.info(
-                f"Loaded functions from the custom connector template '{template.key}'"
-            )
-
         # register the template in the loader's template dictionary
         CustomConnectorTemplateLoader.get_connector_templates()[
             template.key
@@ -220,13 +205,6 @@ class CustomConnectorTemplateLoader(ConnectorTemplateLoader):
                     raise ValidationError(
                         "Multiple svg files found, only one is allowed."
                     )
-            elif info.filename.endswith(".py"):
-                if not function_contents:
-                    function_contents = file_contents
-                else:
-                    raise ValidationError(
-                        "Multiple Python (.py) files found, only one is allowed."
-                    )
 
         if not config_contents:
             raise ValidationError("Zip file does not contain a config.yml file.")
@@ -266,7 +244,6 @@ class CustomConnectorTemplateLoader(ConnectorTemplateLoader):
             config=config_contents,
             dataset=dataset_contents,
             icon=icon_contents,
-            functions=function_contents,
             replaceable=replaceable,
         )
 
@@ -447,80 +424,3 @@ def update_saas_instance(
     connection_config.update_saas_config(db, SaaSConfig(**config_from_template))
 
     upsert_dataset_config_from_template(db, connection_config, template, template_vals)
-
-
-def register_custom_functions(script: str) -> None:
-    """
-    Registers custom functions by executing the given script in a restricted environment.
-
-    The script is compiled and executed with RestrictedPython, which is designed to reduce
-    the risk of executing untrusted code. It provides a set of safe builtins to prevent
-    malicious or unintended behavior.
-
-    Args:
-        script (str): The Python script containing the custom functions to be registered.
-
-    Raises:
-        FidesopsException: If allow_custom_connector_functions is disabled.
-        SyntaxError: If the script contains a syntax error or uses restricted language features.
-        Exception: If an exception occurs during the execution of the script.
-    """
-
-    if CONFIG.security.allow_custom_connector_functions:
-        restricted_code = compile_restricted(
-            script, "<string>", "exec", policy=CustomRestrictingNodeTransformer
-        )
-        safe_builtins["__import__"] = custom_guarded_import
-        safe_builtins["_getitem_"] = getitem
-        safe_builtins["staticmethod"] = staticmethod
-
-        # pylint: disable=exec-used
-        exec(
-            restricted_code,
-            {
-                "__metaclass__": type,
-                "__name__": "restricted_module",
-                "__builtins__": safe_builtins,
-            },
-        )
-    else:
-        raise FidesopsException(
-            message="The import of connector templates with custom functions is disabled by the 'security.allow_custom_connector_functions' setting."
-        )
-
-
-class CustomRestrictingNodeTransformer(RestrictingNodeTransformer):
-    """
-    Custom node transformer class that extends RestrictedPython's RestrictingNodeTransformer
-    to allow the use of type annotations (AnnAssign) in restricted code.
-    """
-
-    def visit_AnnAssign(self, node: AnnAssign) -> AST:
-        return self.node_contents_visit(node)
-
-
-def custom_guarded_import(
-    name: str,
-    _globals: Optional[dict] = None,
-    _locals: Optional[dict] = None,
-    fromlist: Optional[Tuple[str, ...]] = None,
-    level: int = 0,
-) -> Any:
-    """
-    A custom import function that prevents the import of certain potentially unsafe modules.
-    """
-    if name in [
-        "os",
-        "sys",
-        "subprocess",
-        "shutil",
-        "socket",
-        "importlib",
-        "tempfile",
-        "glob",
-    ]:
-        # raising SyntaxError to be consistent with exceptions thrown from other guarded functions
-        raise SyntaxError(f"Import of '{name}' module is not allowed.")
-    if fromlist is None:
-        fromlist = ()
-    return __import__(name, _globals, _locals, fromlist, level)

programming_language: Python
filename: security_settings.py
change_type: ModificationType.MODIFY
diff: @@ -118,10 +118,6 @@ class SecuritySettings(FidesSettings):
         default=432000,
         description="The number of seconds that a pre-signed download URL when using S3 storage will be valid. The default is equal to 5 days.",
     )
-    allow_custom_connector_functions: Optional[bool] = Field(
-        default=False,
-        description="Enables or disables the ability to import connector templates with custom functions. When enabled, custom functions which will be loaded in a restricted environment to minimize security risks.",
-    )
     enable_audit_log_resource_middleware: Optional[bool] = Field(
         default=False,
         description="Either enables the collection of audit log resource data or bypasses the middleware",

programming_language: Python
filename: test_saas_config_endpoints.py
change_type: ModificationType.MODIFY
diff: @@ -468,14 +468,12 @@ class TestRegisterConnectorTemplate:
         self,
         planet_express_config,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "config.yml": planet_express_config,
                 "dataset.yml": planet_express_dataset,
-                "functions.py": planet_express_functions,
                 "icon.svg": planet_express_icon,
             }
         )
@@ -484,13 +482,11 @@ class TestRegisterConnectorTemplate:
     def connector_template_missing_config(
         self,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "dataset.yml": planet_express_dataset,
-                "functions.py": planet_express_functions,
                 "icon.svg": planet_express_icon,
             }
         )
@@ -499,14 +495,12 @@ class TestRegisterConnectorTemplate:
     def connector_template_wrong_contents_config(
         self,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "config.yml": "planet_express_config",
                 "dataset.yml": planet_express_dataset,
-                "functions.py": planet_express_functions,
                 "icon.svg": planet_express_icon,
             }
         )
@@ -516,14 +510,12 @@ class TestRegisterConnectorTemplate:
         self,
         planet_express_invalid_config,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "config.yml": planet_express_invalid_config,
                 "dataset.yml": planet_express_dataset,
-                "functions.py": planet_express_functions,
                 "icon.svg": planet_express_icon,
             }
         )
@@ -532,13 +524,11 @@ class TestRegisterConnectorTemplate:
     def connector_template_missing_dataset(
         self,
         planet_express_config,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "config.yml": planet_express_config,
-                "functions.py": planet_express_functions,
                 "icon.svg": planet_express_icon,
             }
         )
@@ -547,14 +537,12 @@ class TestRegisterConnectorTemplate:
     def connector_template_wrong_contents_dataset(
         self,
         planet_express_config,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "config.yml": planet_express_config,
                 "dataset.yml": "planet_express_dataset",
-                "functions.py": planet_express_functions,
                 "icon.svg": planet_express_icon,
             }
         )
@@ -564,29 +552,12 @@ class TestRegisterConnectorTemplate:
         self,
         planet_express_config,
         planet_express_invalid_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "config.yml": planet_express_config,
                 "dataset.yml": planet_express_invalid_dataset,
-                "functions.py": planet_express_functions,
-                "icon.svg": planet_express_icon,
-            }
-        )
-
-    @pytest.fixture
-    def connector_template_no_functions(
-        self,
-        planet_express_config,
-        planet_express_dataset,
-        planet_express_icon,
-    ):
-        return create_zip_file(
-            {
-                "config.yml": planet_express_config,
-                "dataset.yml": planet_express_dataset,
                 "icon.svg": planet_express_icon,
             }
         )
@@ -596,13 +567,11 @@ class TestRegisterConnectorTemplate:
         self,
         planet_express_config,
         planet_express_dataset,
-        planet_express_functions,
     ):
         return create_zip_file(
             {
                 "config.yml": planet_express_config,
                 "dataset.yml": planet_express_dataset,
-                "functions.py": planet_express_functions,
             }
         )
 
@@ -611,7 +580,6 @@ class TestRegisterConnectorTemplate:
         self,
         planet_express_config,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
@@ -619,7 +587,6 @@ class TestRegisterConnectorTemplate:
                 "1_config.yml": planet_express_config,
                 "2_config.yml": planet_express_config,
                 "dataset.yml": planet_express_dataset,
-                "functions.py": planet_express_functions,
                 "icon.svg": planet_express_icon,
             }
         )
@@ -629,7 +596,6 @@ class TestRegisterConnectorTemplate:
         self,
         planet_express_config,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
@@ -637,25 +603,6 @@ class TestRegisterConnectorTemplate:
                 "config.yml": planet_express_config,
                 "1_dataset.yml": planet_express_dataset,
                 "2_dataset.yml": planet_express_dataset,
-                "functions.py": planet_express_functions,
-                "icon.svg": planet_express_icon,
-            }
-        )
-
-    @pytest.fixture
-    def connector_template_duplicate_functions(
-        self,
-        planet_express_config,
-        planet_express_dataset,
-        planet_express_functions,
-        planet_express_icon,
-    ):
-        return create_zip_file(
-            {
-                "config.yml": planet_express_config,
-                "dataset.yml": planet_express_dataset,
-                "1_functions.py": planet_express_functions,
-                "2_functions.py": planet_express_functions,
                 "icon.svg": planet_express_icon,
             }
         )
@@ -665,14 +612,12 @@ class TestRegisterConnectorTemplate:
         self,
         planet_express_config,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "config.yml": planet_express_config,
                 "dataset.yml": planet_express_dataset,
-                "functions.py": planet_express_functions,
                 "1_icon.svg": planet_express_icon,
                 "2_icon.svg": planet_express_icon,
             }
@@ -685,7 +630,6 @@ class TestRegisterConnectorTemplate:
         generate_auth_header,
         complete_connector_template,
     ):
-        CONFIG.security.allow_custom_connector_functions = True
         auth_header = generate_auth_header(scopes=[CLIENT_READ])
         response = api_client.post(
             register_connector_template_url,
@@ -746,11 +690,6 @@ class TestRegisterConnectorTemplate:
                     "detail": "1 validation error for Dataset\ncollections -> 0 -> name\n  field required (type=value_error.missing)"
                 },
             ),
-            (
-                "connector_template_no_functions",
-                200,
-                {"message": "Connector template successfully registered."},
-            ),
             (
                 "connector_template_no_icon",
                 200,
@@ -770,11 +709,6 @@ class TestRegisterConnectorTemplate:
                     "detail": "Multiple files ending with dataset.yml found, only one is allowed."
                 },
             ),
-            (
-                "connector_template_duplicate_functions",
-                400,
-                {"detail": "Multiple Python (.py) files found, only one is allowed."},
-            ),
             (
                 "connector_template_duplicate_icons",
                 400,
@@ -782,54 +716,7 @@ class TestRegisterConnectorTemplate:
             ),
         ],
     )
-    @mock.patch(
-        "fides.api.service.connectors.saas.connector_registry_service.register_custom_functions"
-    )  # prevent functions from being registered to avoid test conflicts
-    def test_register_connector_template_allow_custom_connector_functions(
-        self,
-        mock_register_custom_functions: MagicMock,
-        api_client: TestClient,
-        register_connector_template_url,
-        generate_auth_header,
-        zip_file,
-        status_code,
-        details,
-        request,
-    ):
-        CONFIG.security.allow_custom_connector_functions = True
-        auth_header = generate_auth_header(scopes=[CONNECTOR_TEMPLATE_REGISTER])
-        response = api_client.post(
-            register_connector_template_url,
-            headers=auth_header,
-            files={
-                "file": (
-                    "template.zip",
-                    request.getfixturevalue(zip_file).read(),
-                    "application/zip",
-                )
-            },
-        )
-        assert response.status_code == status_code
-        assert response.json() == details
-
-    @pytest.mark.parametrize(
-        "zip_file, status_code, details",
-        [
-            (
-                "complete_connector_template",
-                400,
-                {
-                    "detail": "The import of connector templates with custom functions is disabled by the 'security.allow_custom_connector_functions' setting."
-                },
-            ),
-            (
-                "connector_template_no_functions",
-                200,
-                {"message": "Connector template successfully registered."},
-            ),
-        ],
-    )
-    def test_register_connector_template_disallow_custom_connector_functions(
+    def test_register_connector_template(
         self,
         api_client: TestClient,
         register_connector_template_url,
@@ -839,7 +726,6 @@ class TestRegisterConnectorTemplate:
         details,
         request,
     ):
-        CONFIG.security.allow_custom_connector_functions = False
         auth_header = generate_auth_header(scopes=[CONNECTOR_TEMPLATE_REGISTER])
         response = api_client.post(
             register_connector_template_url,

programming_language: Python
filename: test_custom_connector_template.py
change_type: ModificationType.MODIFY
diff: @@ -12,7 +12,6 @@ class TestCustomConnectorTemplate:
         planet_express_config,
         planet_express_dataset,
         planet_express_icon,
-        planet_express_functions,
     ) -> None:
         template = CustomConnectorTemplate(
             key="planet_express",
@@ -20,7 +19,6 @@ class TestCustomConnectorTemplate:
             config=planet_express_config,
             dataset=planet_express_dataset,
             icon=planet_express_icon,
-            functions=planet_express_functions,
         )
         template.save(db=db)
 
@@ -36,4 +34,3 @@ class TestCustomConnectorTemplate:
         assert custom_connector.config == planet_express_config
         assert custom_connector.dataset == planet_express_dataset
         assert custom_connector.icon == planet_express_icon
-        assert custom_connector.functions == planet_express_functions

programming_language: Python
filename: test_connector_template_loaders.py
change_type: ModificationType.MODIFY
diff: @@ -16,7 +16,6 @@ from fides.api.service.connectors.saas.connector_registry_service import (
     ConnectorRegistry,
     CustomConnectorTemplateLoader,
     FileConnectorTemplateLoader,
-    register_custom_functions,
 )
 from fides.api.service.saas_request.saas_request_override_factory import (
     SaaSRequestOverrideFactory,
@@ -113,7 +112,6 @@ class TestCustomConnectorTemplateLoader:
         self,
         replaceable_planet_express_config,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ) -> BytesIO:
         return create_zip_file(
@@ -121,7 +119,6 @@ class TestCustomConnectorTemplateLoader:
                 "config.yml": replaceable_planet_express_config,
                 "dataset.yml": planet_express_dataset,
                 "icon.svg": planet_express_icon,
-                "functions.py": planet_express_functions,
             }
         )
 
@@ -135,8 +132,6 @@ class TestCustomConnectorTemplateLoader:
         )
 
     def test_custom_connector_template_loader_no_templates(self):
-        CONFIG.security.allow_custom_connector_functions = True
-
         connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
         assert connector_templates == {}
 
@@ -148,10 +143,7 @@ class TestCustomConnectorTemplateLoader:
         mock_all: MagicMock,
         planet_express_dataset,
         planet_express_icon,
-        planet_express_functions,
     ):
-        CONFIG.security.allow_custom_connector_functions = True
-
         mock_all.return_value = [
             CustomConnectorTemplate(
                 key="planet_express",
@@ -159,132 +151,12 @@ class TestCustomConnectorTemplateLoader:
                 config="planet_express_config",
                 dataset=planet_express_dataset,
                 icon=planet_express_icon,
-                functions=planet_express_functions,
-            )
-        ]
-
-        # verify the custom functions aren't loaded if the template is invalid
-        connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
-        assert connector_templates == {}
-
-        with pytest.raises(NoSuchSaaSRequestOverrideException):
-            SaaSRequestOverrideFactory.get_override(
-                "planet_express_user_access", SaaSRequestType.READ
-            )
-
-        # assert the strategy was not registered
-        authentication_strategies = AuthenticationStrategy.get_strategies()
-        assert "planet_express" not in [
-            strategy.name for strategy in authentication_strategies
-        ]
-
-    @mock.patch(
-        "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
-    )
-    def test_custom_connector_template_loader_invalid_functions(
-        self,
-        mock_all: MagicMock,
-        planet_express_config,
-        planet_express_dataset,
-        planet_express_icon,
-    ):
-        CONFIG.security.allow_custom_connector_functions = True
-
-        # save custom connector template to the database
-        mock_all.return_value = [
-            CustomConnectorTemplate(
-                key="planet_express",
-                name="Planet Express",
-                config=planet_express_config,
-                dataset=planet_express_dataset,
-                icon=planet_express_icon,
-                functions="planet_express_functions",
-            )
-        ]
-
-        # verify nothing is loaded if the custom functions fail to load
-        connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
-        assert connector_templates == {}
-
-    @mock.patch(
-        "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
-    )
-    def test_custom_connector_template_loader_custom_connector_functions_disabled(
-        self,
-        mock_all: MagicMock,
-        planet_express_config,
-        planet_express_dataset,
-        planet_express_icon,
-        planet_express_functions,
-    ):
-        CONFIG.security.allow_custom_connector_functions = False
-
-        mock_all.return_value = [
-            CustomConnectorTemplate(
-                key="planet_express",
-                name="Planet Express",
-                config=planet_express_config,
-                dataset=planet_express_dataset,
-                icon=planet_express_icon,
-                functions=planet_express_functions,
             )
         ]
 
-        # load custom connector templates from the database
         connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
         assert connector_templates == {}
 
-        with pytest.raises(NoSuchSaaSRequestOverrideException):
-            SaaSRequestOverrideFactory.get_override(
-                "planet_express_user_access", SaaSRequestType.READ
-            )
-
-        # assert the strategy was not registered
-        authentication_strategies = AuthenticationStrategy.get_strategies()
-        assert "planet_express" not in [
-            strategy.name for strategy in authentication_strategies
-        ]
-
-    @mock.patch(
-        "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
-    )
-    def test_custom_connector_template_loader_custom_connector_functions_disabled_custom_functions(
-        self,
-        mock_all: MagicMock,
-        planet_express_config,
-        planet_express_dataset,
-        planet_express_icon,
-    ):
-        """
-        A connector template with no custom functions should still be loaded
-        even if allow_custom_connector_functions is set to false
-        """
-
-        CONFIG.security.allow_custom_connector_functions = False
-
-        # save custom connector template to the database
-        mock_all.return_value = [
-            CustomConnectorTemplate(
-                key="planet_express",
-                name="Planet Express",
-                config=planet_express_config,
-                dataset=planet_express_dataset,
-                icon=planet_express_icon,
-                functions=None,
-            )
-        ]
-
-        # load custom connector templates from the database
-        connector_templates = CustomConnectorTemplateLoader.get_connector_templates()
-        assert connector_templates == {
-            "planet_express": ConnectorTemplate(
-                config=planet_express_config,
-                dataset=planet_express_dataset,
-                icon=planet_express_icon,
-                human_readable="Planet Express",
-            )
-        }
-
     @mock.patch(
         "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
     )
@@ -294,10 +166,7 @@ class TestCustomConnectorTemplateLoader:
         planet_express_config,
         planet_express_dataset,
         planet_express_icon,
-        planet_express_functions,
     ):
-        CONFIG.security.allow_custom_connector_functions = True
-
         mock_all.return_value = [
             CustomConnectorTemplate(
                 key="planet_express",
@@ -305,7 +174,6 @@ class TestCustomConnectorTemplateLoader:
                 config=planet_express_config,
                 dataset=planet_express_dataset,
                 icon=planet_express_icon,
-                functions=planet_express_functions,
             )
         ]
 
@@ -318,22 +186,10 @@ class TestCustomConnectorTemplateLoader:
                 config=planet_express_config,
                 dataset=planet_express_dataset,
                 icon=planet_express_icon,
-                functions=planet_express_functions,
                 human_readable="Planet Express",
             )
         }
 
-        # assert the request override was registered
-        SaaSRequestOverrideFactory.get_override(
-            "planet_express_user_access", SaaSRequestType.READ
-        )
-
-        # assert the strategy was registered
-        authentication_strategies = AuthenticationStrategy.get_strategies()
-        assert "planet_express" in [
-            strategy.name for strategy in authentication_strategies
-        ]
-
     @mock.patch(
         "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
     )
@@ -343,10 +199,7 @@ class TestCustomConnectorTemplateLoader:
         planet_express_config,
         planet_express_dataset,
         planet_express_icon,
-        planet_express_functions,
     ):
-        CONFIG.security.allow_custom_connector_functions = True
-
         mock_all.return_value = [
             CustomConnectorTemplate(
                 key="planet_express",
@@ -354,7 +207,6 @@ class TestCustomConnectorTemplateLoader:
                 config=planet_express_config,
                 dataset=planet_express_dataset,
                 icon=planet_express_icon,
-                functions=planet_express_functions,
             )
         ]
 
@@ -375,7 +227,6 @@ class TestCustomConnectorTemplateLoader:
         planet_express_config,
         planet_express_dataset,
         planet_express_icon,
-        planet_express_functions,
     ):
         db = MagicMock()
 
@@ -386,7 +237,6 @@ class TestCustomConnectorTemplateLoader:
                     {
                         "config.yml": planet_express_config,
                         "dataset.yml": planet_express_dataset,
-                        "functions.py": planet_express_functions,
                         "icon.svg": planet_express_icon,
                     }
                 )
@@ -401,7 +251,6 @@ class TestCustomConnectorTemplateLoader:
                     {
                         "config.yml": planet_express_config,
                         "dataset.yml": planet_express_dataset,
-                        "functions.py": planet_express_functions,
                         "icon.svg": planet_express_icon,
                     }
                 )
@@ -409,29 +258,48 @@ class TestCustomConnectorTemplateLoader:
         )
         assert mock_create_or_update.call_count == 2
 
-    def test_custom_connector_template_loader_disallowed_modules(
+    @mock.patch(
+        "fides.api.models.custom_connector_template.CustomConnectorTemplate.create_or_update"
+    )
+    def test_custom_connector_save_template_with_functions(
         self,
+        mock_create_or_update: MagicMock,
         planet_express_config,
         planet_express_dataset,
+        planet_express_functions,
         planet_express_icon,
     ):
-        CONFIG.security.allow_custom_connector_functions = True
-
-        with pytest.raises(SyntaxError) as exc:
-            CustomConnectorTemplateLoader.save_template(
-                MagicMock(),
-                ZipFile(
-                    create_zip_file(
-                        {
-                            "config.yml": planet_express_config,
-                            "dataset.yml": planet_express_dataset,
-                            "functions.py": "import os",
-                            "icon.svg": planet_express_icon,
-                        }
-                    )
-                ),
+        db = MagicMock()
+
+        CustomConnectorTemplateLoader.save_template(
+            db,
+            ZipFile(
+                create_zip_file(
+                    {
+                        "config.yml": planet_express_config,
+                        "dataset.yml": planet_express_dataset,
+                        "functions.py": planet_express_functions,
+                        "icon.svg": planet_express_icon,
+                    }
+                )
+            ),
+        )
+
+        # assert the request override was ignored
+        with pytest.raises(NoSuchSaaSRequestOverrideException) as exc:
+            SaaSRequestOverrideFactory.get_override(
+                "planet_express_user_access", SaaSRequestType.UPDATE
             )
-        assert "Import of 'os' module is not allowed." == str(exc.value)
+        assert (
+            f"Custom SaaS override 'planet_express_user_access' does not exist."
+            in str(exc.value)
+        )
+
+        # assert the strategy was ignored
+        authentication_strategies = AuthenticationStrategy.get_strategies()
+        assert "planet_express" not in [
+            strategy.name for strategy in authentication_strategies
+        ]
 
     @mock.patch(
         "fides.api.models.custom_connector_template.CustomConnectorTemplate.delete"
@@ -614,16 +482,3 @@ class TestCustomConnectorTemplateLoader:
         config_contents = mock_create_or_update.call_args.kwargs["data"]["config"]
         custom_config = load_config_from_string(config_contents)
         assert custom_config["version"] == "0.0.0"
-
-
-class TestRegisterCustomFunctions:
-    def test_function_loader(self):
-        """Verify that all override implementations can be loaded by RestrictedPython"""
-
-        overrides_path = "src/fides/api/service/saas_request/override_implementations"
-
-        for filename in os.listdir(overrides_path):
-            if filename.endswith(".py") and filename != "__init__.py":
-                file_path = os.path.join(overrides_path, filename)
-                with open(file_path, "r") as file:
-                    register_custom_functions(file.read())

programming_language: Python