CVE-2023-41319: Protection Mechanism Failure (CWE-693)
value: 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`.
CWE description: The product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product.
CVSS v3 base score: 7.2
Published: 2023-09-06T18:15Z
Repository: https://github.com/ethyca/fides
Language: Python
Files changed: clients/admin-ui/src/features/connector-templates/ConnectorTemplateUploadModal.tsx, docker-compose.yml, requirements.txt, src/fides/api/schemas/saas/connector_template.py, src/fides/api/service/connectors/saas/connector_registry_service.py, src/fides/config/security_settings.py, tests/ops/api/v1/endpoints/test_saas_config_endpoints.py, tests/ops/models/test_custom_connector_template.py, tests/ops/service/connectors/test_connector_template_loaders.py
Commit date: 2023-09-05 12:51:11 -0700
Commit message: Merge pull request from GHSA-p6p2-qq95-vq5h

* Removing custom connector template functions

* Updating connector template modal description

* Adding function upload tests
Diff: diff --git a/clients/admin-ui/src/features/connector-templates/ConnectorTemplateUploadModal.tsx b/clients/admin-ui/src/features/connector-templates/ConnectorTemplateUploadModal.tsx
index 58d7e1c23..1368079f9 100644
--- a/clients/admin-ui/src/features/connector-templates/ConnectorTemplateUploadModal.tsx
+++ b/clients/admin-ui/src/features/connector-templates/ConnectorTemplateUploadModal.tsx
@@ -120,12 +120,11 @@ const ConnectorTemplateUploadModal: React.FC<RequestModalProps> = ({
             <input {...getInputProps()} />
             {renderFileText()}
           </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>
           <ButtonGroup
             size="sm"
diff --git a/docker-compose.yml b/docker-compose.yml
index 17e30caca..919b6cebe 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -26,11 +26,10 @@ services:
       FIDES__CLI__SERVER_PORT: "8080"
       FIDES__DATABASE__SERVER: "fides-db"
       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-}
       VAULT_ADDR: ${VAULT_ADDR-}
       VAULT_NAMESPACE: ${VAULT_NAMESPACE-}
diff --git a/requirements.txt b/requirements.txt
index bf8a3771a..29c152050 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,5 @@
-AccessControl==6.0
 alembic==1.8.1
 APScheduler==3.9.1.post1
 asyncpg==0.27.0
 boto3==1.26.1
 celery[pytest]==5.2.7
@@ -40,11 +39,10 @@ pymongo==3.13.0
 PyMySQL==1.0.2
 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
 snowflake-sqlalchemy==1.4.3
 sqlalchemy[asyncio]==1.4.27
diff --git a/src/fides/api/schemas/saas/connector_template.py b/src/fides/api/schemas/saas/connector_template.py
index 6dcbe6f70..22835889b 100644
--- a/src/fides/api/schemas/saas/connector_template.py
+++ b/src/fides/api/schemas/saas/connector_template.py
@@ -11,17 +11,16 @@ 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")
     def validate_config(cls, config: str) -> str:
         """Validates the config at the given path"""
diff --git a/src/fides/api/service/connectors/saas/connector_registry_service.py b/src/fides/api/service/connectors/saas/connector_registry_service.py
index 4e55c7e32..a3bc7c92a 100644
--- a/src/fides/api/service/connectors/saas/connector_registry_service.py
+++ b/src/fides/api/service/connectors/saas/connector_registry_service.py
@@ -1,24 +1,19 @@
 # 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,
     ConnectionConfig,
     ConnectionType,
@@ -39,11 +34,10 @@ from fides.api.util.saas_util import (
     replace_config_placeholders,
     replace_dataset_placeholders,
     replace_version,
 )
 from fides.api.util.unsafe_file_util import verify_svg, verify_zip
-from fides.config import CONFIG
 
 
 class ConnectorTemplateLoader(ABC):
     _instance: Optional["ConnectorTemplateLoader"] = None
 
@@ -94,11 +88,10 @@ class FileConnectorTemplateLoader(ConnectorTemplateLoader):
                         config=load_yaml_as_string(config_file),
                         dataset=load_yaml_as_string(
                             f"data/saas/dataset/{connector_type}_dataset.yml"
                         ),
                         icon=icon,
-                        functions=None,
                         human_readable=human_readable,
                     )
                 except Exception:
                     logger.exception("Unable to load {} connector", connector_type)
 
@@ -149,28 +142,20 @@ class CustomConnectorTemplateLoader(ConnectorTemplateLoader):
     def _register_template(
         cls,
         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
         ] = connector_template
 
@@ -218,17 +203,10 @@ class CustomConnectorTemplateLoader(ConnectorTemplateLoader):
                     icon_contents = str_to_b64_str(file_contents)
                 else:
                     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.")
 
         if not dataset_contents:
@@ -264,11 +242,10 @@ class CustomConnectorTemplateLoader(ConnectorTemplateLoader):
             key=connector_type,
             name=human_readable,
             config=config_contents,
             dataset=dataset_contents,
             icon=icon_contents,
-            functions=function_contents,
             replaceable=replaceable,
         )
 
         # attempt to register the template, raises an exception if validation fails
         CustomConnectorTemplateLoader._register_template(template)
@@ -445,82 +422,5 @@ 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)
diff --git a/src/fides/config/security_settings.py b/src/fides/config/security_settings.py
index c27410dd9..39d82482b 100644
--- a/src/fides/config/security_settings.py
+++ b/src/fides/config/security_settings.py
@@ -116,14 +116,10 @@ class SecuritySettings(FidesSettings):
     )
     subject_request_download_link_ttl_seconds: int = Field(
         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",
     )
 
diff --git a/tests/ops/api/v1/endpoints/test_saas_config_endpoints.py b/tests/ops/api/v1/endpoints/test_saas_config_endpoints.py
index d4e98c5ef..39de52cba 100644
--- a/tests/ops/api/v1/endpoints/test_saas_config_endpoints.py
+++ b/tests/ops/api/v1/endpoints/test_saas_config_endpoints.py
@@ -466,215 +466,160 @@ class TestRegisterConnectorTemplate:
     @pytest.fixture
     def complete_connector_template(
         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,
             }
         )
 
     @pytest.fixture
     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,
             }
         )
 
     @pytest.fixture
     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,
             }
         )
 
     @pytest.fixture
     def connector_template_invalid_config(
         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,
             }
         )
 
     @pytest.fixture
     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,
             }
         )
 
     @pytest.fixture
     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,
             }
         )
 
     @pytest.fixture
     def connector_template_invalid_dataset(
         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,
             }
         )
 
     @pytest.fixture
     def connector_template_no_icon(
         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,
             }
         )
 
     @pytest.fixture
     def connector_template_duplicate_configs(
         self,
         planet_express_config,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "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,
             }
         )
 
     @pytest.fixture
     def connector_template_duplicate_datasets(
         self,
         planet_express_config,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ):
         return create_zip_file(
             {
                 "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,
             }
         )
 
     @pytest.fixture
     def connector_template_duplicate_icons(
         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,
             }
         )
 
@@ -683,11 +628,10 @@ class TestRegisterConnectorTemplate:
         api_client: TestClient,
         register_connector_template_url,
         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,
             headers=auth_header,
             files={
@@ -744,15 +688,10 @@ class TestRegisterConnectorTemplate:
                 400,
                 {
                     "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,
                 {"message": "Connector template successfully registered."},
             ),
@@ -768,80 +707,27 @@ class TestRegisterConnectorTemplate:
                 400,
                 {
                     "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,
                 {"detail": "Multiple svg files found, only one is allowed."},
             ),
         ],
     )
-    @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,
         generate_auth_header,
         zip_file,
         status_code,
         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,
             headers=auth_header,
             files={
diff --git a/tests/ops/models/test_custom_connector_template.py b/tests/ops/models/test_custom_connector_template.py
index 5c40be627..3b7fdb773 100644
--- a/tests/ops/models/test_custom_connector_template.py
+++ b/tests/ops/models/test_custom_connector_template.py
@@ -10,19 +10,17 @@ class TestCustomConnectorTemplate:
         self,
         db: Session,
         planet_express_config,
         planet_express_dataset,
         planet_express_icon,
-        planet_express_functions,
     ) -> None:
         template = CustomConnectorTemplate(
             key="planet_express",
             name="Planet Express",
             config=planet_express_config,
             dataset=planet_express_dataset,
             icon=planet_express_icon,
-            functions=planet_express_functions,
         )
         template.save(db=db)
 
         # assert we can retrieve a connector template by key and
         # that the values are the same as what we persisted
@@ -34,6 +32,5 @@ class TestCustomConnectorTemplate:
         assert custom_connector
         assert custom_connector.name == "Planet Express"
         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
diff --git a/tests/ops/service/connectors/test_connector_template_loaders.py b/tests/ops/service/connectors/test_connector_template_loaders.py
index 7caf2486a..b37c6e5a1 100644
--- a/tests/ops/service/connectors/test_connector_template_loaders.py
+++ b/tests/ops/service/connectors/test_connector_template_loaders.py
@@ -14,11 +14,10 @@ from fides.api.service.authentication.authentication_strategy import (
 )
 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,
     SaaSRequestType,
 )
@@ -111,19 +110,17 @@ class TestCustomConnectorTemplateLoader:
     @pytest.fixture
     def replaceable_planet_express_zip(
         self,
         replaceable_planet_express_config,
         planet_express_dataset,
-        planet_express_functions,
         planet_express_icon,
     ) -> BytesIO:
         return create_zip_file(
             {
                 "config.yml": replaceable_planet_express_config,
                 "dataset.yml": planet_express_dataset,
                 "icon.svg": planet_express_icon,
-                "functions.py": planet_express_functions,
             }
         )
 
     @pytest.fixture
     def non_replaceable_zendesk_zip(self, zendesk_config, zendesk_dataset) -> BytesIO:
@@ -133,12 +130,10 @@ class TestCustomConnectorTemplateLoader:
                 "dataset.yml": zendesk_dataset,
             }
         )
 
     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 == {}
 
     @mock.patch(
         "fides.api.models.custom_connector_template.CustomConnectorTemplate.all"
@@ -146,168 +141,41 @@ class TestCustomConnectorTemplateLoader:
     def test_custom_connector_template_loader_invalid_template(
         self,
         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",
                 name="Planet Express",
                 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"
     )
     def test_custom_connector_template_loader(
         self,
         mock_all: MagicMock,
         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",
                 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()
@@ -316,47 +184,31 @@ class TestCustomConnectorTemplateLoader:
         assert connector_templates == {
             "planet_express": ConnectorTemplate(
                 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"
     )
     def test_loaders_have_separate_instances(
         self,
         mock_all: MagicMock,
         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",
                 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
         file_connector_templates = FileConnectorTemplateLoader.get_connector_templates()
@@ -373,22 +225,20 @@ class TestCustomConnectorTemplateLoader:
         self,
         mock_create_or_update: MagicMock,
         planet_express_config,
         planet_express_dataset,
         planet_express_icon,
-        planet_express_functions,
     ):
         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,
                     }
                 )
             ),
         )
@@ -399,41 +249,59 @@ class TestCustomConnectorTemplateLoader:
             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 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"
     )
     @mock.patch(
@@ -612,18 +480,5 @@ class TestCustomConnectorTemplateLoader:
         )
         assert not mock_create_or_update.call_args.kwargs["data"]["replaceable"]
         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())

Vulnerable code:             dataset, but may also contain an icon (.svg) and custom functions
            (.py) as optional files.
      FIDES__SECURITY__ALLOW_CUSTOM_CONNECTOR_FUNCTIONS: "True"
AccessControl==6.0
RestrictedPython==6.0.0
    SaaS connector (SaaS config, dataset, icon, functions, etc.)
    functions: Optional[str]
from ast import AST, AnnAssign
from operator import getitem
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type
from AccessControl.ZopeGuards import safe_builtins
from RestrictedPython import compile_restricted
from RestrictedPython.transformer import RestrictingNodeTransformer
from fides.api.common_exceptions import FidesopsException, ValidationError
from fides.config import CONFIG
                        functions=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.
            functions=template.functions,
        # 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}'"
            )

            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."
                    )
            functions=function_contents,


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)
    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.",
    )
        planet_express_functions,
                "functions.py": planet_express_functions,
        planet_express_functions,
                "functions.py": planet_express_functions,
        planet_express_functions,
                "functions.py": planet_express_functions,
        planet_express_functions,
                "functions.py": planet_express_functions,
        planet_express_functions,
                "functions.py": planet_express_functions,
        planet_express_functions,
                "functions.py": planet_express_functions,
        planet_express_functions,
                "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,
        planet_express_functions,
                "functions.py": planet_express_functions,
        planet_express_functions,
                "functions.py": planet_express_functions,
        planet_express_functions,
                "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,
        planet_express_functions,
                "functions.py": planet_express_functions,
        CONFIG.security.allow_custom_connector_functions = True
            (
                "connector_template_no_functions",
                200,
                {"message": "Connector template successfully registered."},
            ),
            (
                "connector_template_duplicate_functions",
                400,
                {"detail": "Multiple Python (.py) files found, only one is allowed."},
            ),
    @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(
        CONFIG.security.allow_custom_connector_functions = False
        planet_express_functions,
            functions=planet_express_functions,
        assert custom_connector.functions == planet_express_functions
    register_custom_functions,
        planet_express_functions,
                "functions.py": planet_express_functions,
        CONFIG.security.allow_custom_connector_functions = True

        planet_express_functions,
        CONFIG.security.allow_custom_connector_functions = True

                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
        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",
            )
        }

        planet_express_functions,
        CONFIG.security.allow_custom_connector_functions = True

                functions=planet_express_functions,
                functions=planet_express_functions,
        # 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
        ]

        planet_express_functions,
        CONFIG.security.allow_custom_connector_functions = True

                functions=planet_express_functions,
        planet_express_functions,
                        "functions.py": planet_express_functions,
                        "functions.py": planet_express_functions,
    def test_custom_connector_template_loader_disallowed_modules(
        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,
                        }
                    )
                ),
        assert "Import of 'os' module is not allowed." == str(exc.value)


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())
Fixed code:             dataset, but may also contain an icon (.svg) as an optional file.
    SaaS connector (SaaS config, dataset, icon, etc.)
from typing import Dict, Iterable, List, Optional, Type
from fides.api.common_exceptions import ValidationError
        Registers a custom connector template by converting it to a ConnectorTemplate
        and adding it to the loader's template dictionary.
    def test_register_connector_template(
    @mock.patch(
        "fides.api.models.custom_connector_template.CustomConnectorTemplate.create_or_update"
    )
    def test_custom_connector_save_template_with_functions(
        mock_create_or_update: MagicMock,
        planet_express_functions,
        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 (
            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
        ]