116 changes: 1 addition & 115 deletions tests/ops/api/v1/endpoints/test_saas_config_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,14 +468,12 @@ 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,
}
)
Expand All @@ -484,13 +482,11 @@ def complete_connector_template(
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,
}
)
Expand All @@ -499,14 +495,12 @@ def connector_template_missing_config(
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,
}
)
Expand All @@ -516,14 +510,12 @@ 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,
}
)
Expand All @@ -532,13 +524,11 @@ def connector_template_invalid_config(
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,
}
)
Expand All @@ -547,14 +537,12 @@ def connector_template_missing_dataset(
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,
}
)
Expand All @@ -564,29 +552,12 @@ 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,
}
)
Expand All @@ -596,13 +567,11 @@ 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,
}
)

Expand All @@ -611,15 +580,13 @@ 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,
}
)
Expand All @@ -629,33 +596,13 @@ 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,
}
)
Expand All @@ -665,14 +612,12 @@ 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,
}
Expand All @@ -685,7 +630,6 @@ def test_register_connector_template_wrong_scope(
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,
Expand Down Expand Up @@ -746,11 +690,6 @@ def test_register_connector_template_wrong_scope(
"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,
Expand All @@ -770,66 +709,14 @@ def test_register_connector_template_wrong_scope(
"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,
Expand All @@ -839,7 +726,6 @@ def test_register_connector_template_disallow_custom_connector_functions(
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,
Expand Down
3 changes: 0 additions & 3 deletions tests/ops/models/test_custom_connector_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ def test_create_custom_connector_template(
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)

Expand All @@ -36,4 +34,3 @@ def test_create_custom_connector_template(
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
217 changes: 36 additions & 181 deletions tests/ops/service/connectors/test_connector_template_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
ConnectorRegistry,
CustomConnectorTemplateLoader,
FileConnectorTemplateLoader,
register_custom_functions,
)
from fides.api.service.saas_request.saas_request_override_factory import (
SaaSRequestOverrideFactory,
Expand Down Expand Up @@ -113,15 +112,13 @@ 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,
}
)

Expand All @@ -135,8 +132,6 @@ def non_replaceable_zendesk_zip(self, zendesk_config, zendesk_dataset) -> BytesI
)

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 == {}

Expand All @@ -148,143 +143,20 @@ def test_custom_connector_template_loader_invalid_template(
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"
)
Expand All @@ -294,18 +166,14 @@ def test_custom_connector_template_loader(
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,
)
]

Expand All @@ -318,22 +186,10 @@ def test_custom_connector_template_loader(
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"
)
Expand All @@ -343,18 +199,14 @@ def test_loaders_have_separate_instances(
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,
)
]

Expand All @@ -375,7 +227,6 @@ def test_custom_connector_save_template(
planet_express_config,
planet_express_dataset,
planet_express_icon,
planet_express_functions,
):
db = MagicMock()

Expand All @@ -386,7 +237,6 @@ def test_custom_connector_save_template(
{
"config.yml": planet_express_config,
"dataset.yml": planet_express_dataset,
"functions.py": planet_express_functions,
"icon.svg": planet_express_icon,
}
)
Expand All @@ -401,37 +251,55 @@ def test_custom_connector_save_template(
{
"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"
Expand Down Expand Up @@ -614,16 +482,3 @@ def test_non_replaceable_template(
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())