Skip to content

Commit b5a4440

Browse files
committed
Cherry picking SSL mode for MySQL
1 parent bb14848 commit b5a4440

8 files changed

Lines changed: 122 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o
2525
- Migrate `Cookies` resources to `Asset` resources of type `Cookie` [#5776](https://github.com/ethyca/fides/pull/5776) https://github.com/ethyca/fides/labels/db-migration https://github.com/ethyca/fides/labels/high-risk
2626
- Added support for selecting TCF Publisher Override configuration when configuring Privacy Experience [#6033](https://github.com/ethyca/fides/pull/6033)
2727
- Added Google Cloud Storage as a storage option [#6006](https://github.com/ethyca/fides/pull/6006)
28+
- Added SSL Mode field for MySQL connections [#6048](https://github.com/ethyca/fides/pull/6048)
29+
- Added PostgreSQL connection config form to the "integrations" page to support use with discovery monitors [#6018](https://github.com/ethyca/fides/pull/6018)
2830
- Update the Datahub Permissions section to include required permissions from Datahub [#6052](https://github.com/ethyca/fides/pull/6052)
2931
- Added the ability to create new TCF Experiences within Admin UI [#6055](https://github.com/ethyca/fides/pull/6055)
3032
- PostgreSQL connection config now supports SSL Mode [#6068](https://github.com/ethyca/fides/pull/6068)

clients/admin-ui/src/features/integrations/integration-type-info/mySQLInfo.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ export const MYSQL_TAGS = ["DSR Automation", "Discovery", "Detection"];
2222
export const MySQLOverview = () => (
2323
<>
2424
<InfoHeading text="Overview" />
25-
<InfoText>Add infotext</InfoText>
25+
<InfoText>
26+
Continuously monitor MySQL databases to detect and track schema-level
27+
changes, automatically discover and label data categories as well as
28+
automatically process DSR (privacy requests) and consent enforcement to
29+
proactively manage data governance risks.
30+
</InfoText>
2631
<ShowMoreContent>
2732
<InfoHeading text="Categories" />
2833
<InfoUnorderedList>

clients/admin-ui/src/types/api/models/MySQLDocsSchema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,8 @@ export type MySQLDocsSchema = {
3030
* Indicates whether an SSH tunnel is required for the connection. Enable this option if your MySQL server is behind a firewall and requires SSH tunneling for remote connections.
3131
*/
3232
ssh_required?: boolean;
33+
/**
34+
* The SSL mode to use for the connection. Valid values are 'required', 'preferred', and 'disabled'.
35+
*/
36+
ssl_mode?: string;
3337
};

src/fides/api/schemas/connection_configuration/connection_secrets_mysql.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from enum import Enum
12
from typing import ClassVar, List, Optional
23

34
from pydantic import Field
@@ -8,6 +9,12 @@
89
)
910

1011

12+
class MySQLSSLMode(str, Enum):
13+
preferred = "preferred"
14+
required = "required"
15+
disabled = "disabled"
16+
17+
1118
class MySQLSchema(ConnectionConfigSecretsSchema):
1219
"""Schema to validate the secrets needed to connect to a MySQL Database"""
1320

@@ -40,6 +47,11 @@ class MySQLSchema(ConnectionConfigSecretsSchema):
4047
title="SSH required",
4148
description="Indicates whether an SSH tunnel is required for the connection. Enable this option if your MySQL server is behind a firewall and requires SSH tunneling for remote connections.",
4249
)
50+
ssl_mode: Optional[MySQLSSLMode] = Field(
51+
None, # TODO: support for verify-ca and verify-full
52+
title="SSL Mode",
53+
description="The SSL mode to use for the connection. Valid values are 'required', 'preferred', and 'disabled'.",
54+
)
4355

4456
_required_components: ClassVar[List[str]] = ["host", "dbname"]
4557

src/fides/api/service/connectors/mysql_connector.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from typing import List
1+
from typing import Dict, List
22

33
from sqlalchemy.engine import Engine, LegacyCursorResult, create_engine # type: ignore
44

55
from fides.api.graph.execution import ExecutionNode
66
from fides.api.schemas.connection_configuration.connection_secrets_mysql import (
77
MySQLSchema,
8+
MySQLSSLMode,
89
)
910
from fides.api.service.connectors.query_configs.mysql_query_config import (
1011
MySQLQueryConfig,
@@ -69,16 +70,27 @@ def create_client(self) -> Engine:
6970
uri = self.build_ssh_uri(local_address=self.ssh_server.local_bind_address)
7071
else:
7172
uri = (self.configuration.secrets or {}).get("url") or self.build_uri()
73+
connect_args = self.get_connect_args()
7274
return create_engine(
7375
uri,
7476
hide_parameters=self.hide_parameters,
7577
echo=not self.hide_parameters,
78+
connect_args=connect_args,
7679
)
7780

7881
def query_config(self, node: ExecutionNode) -> SQLQueryConfig:
7982
"""Query wrapper corresponding to the input execution_node."""
8083
return MySQLQueryConfig(node)
8184

85+
def get_connect_args(self) -> Dict[str, Dict[str, MySQLSSLMode]]:
86+
"""Get connection arguments for the engine"""
87+
ssl_mode = self.configuration.secrets.get("ssl_mode", MySQLSSLMode.preferred)
88+
return {
89+
"ssl": {
90+
"mode": ssl_mode,
91+
}
92+
}
93+
8294
@staticmethod
8395
def cursor_result_to_rows(results: LegacyCursorResult) -> List[Row]:
8496
"""

tests/ops/api/v1/endpoints/test_connection_template_endpoints.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,8 +1065,20 @@ def test_get_connection_secret_schema_mysql(
10651065
"default": False,
10661066
"type": "boolean",
10671067
},
1068+
"ssl_mode": {
1069+
"title": "SSL Mode",
1070+
"description": "The SSL mode to use for the connection. Valid values are 'required', 'preferred', and 'disabled'.",
1071+
"allOf": [{"$ref": "#/definitions/MySQLSSLMode"}],
1072+
},
10681073
},
10691074
"required": ["host", "dbname"],
1075+
"definitions": {
1076+
"MySQLSSLMode": {
1077+
"title": "MySQLSSLMode",
1078+
"type": "string",
1079+
"enum": ["preferred", "required", "disabled"],
1080+
}
1081+
},
10701082
}
10711083

10721084
def test_get_connection_secret_schema_postgres(

tests/ops/integration_tests/test_connection_configuration_integration.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ def test_mysql_db_connection_incorrect_secrets(
311311
"username": None,
312312
"password": None,
313313
"ssh_required": False,
314+
"ssl_mode": None,
314315
}
315316
assert connection_config_mysql.last_test_timestamp is not None
316317
assert connection_config_mysql.last_test_succeeded is False
@@ -353,6 +354,51 @@ def test_mysql_db_connection_connect_with_components(
353354
"password": "mysql_pw",
354355
"port": 3306,
355356
"ssh_required": False,
357+
"ssl_mode": None,
358+
}
359+
assert connection_config_mysql.last_test_timestamp is not None
360+
assert connection_config_mysql.last_test_succeeded is True
361+
362+
def test_mysql_db_connection_connect_with_ssl(
363+
self,
364+
url,
365+
api_client: TestClient,
366+
db: Session,
367+
generate_auth_header,
368+
connection_config_mysql,
369+
) -> None:
370+
payload = {
371+
"host": "mysql_example",
372+
"dbname": "mysql_example",
373+
"username": "mysql_user",
374+
"password": "mysql_pw",
375+
"ssl_mode": "preferred",
376+
}
377+
378+
auth_header = generate_auth_header(scopes=[CONNECTION_CREATE_OR_UPDATE])
379+
resp = api_client.put(
380+
url,
381+
headers=auth_header,
382+
json=payload,
383+
)
384+
assert resp.status_code == 200
385+
body = resp.json()
386+
387+
assert (
388+
body["msg"]
389+
== f"Secrets updated for ConnectionConfig with key: {connection_config_mysql.key}."
390+
)
391+
assert body["test_status"] == "succeeded"
392+
assert body["failure_reason"] is None
393+
db.refresh(connection_config_mysql)
394+
assert connection_config_mysql.secrets == {
395+
"host": "mysql_example",
396+
"dbname": "mysql_example",
397+
"username": "mysql_user",
398+
"password": "mysql_pw",
399+
"port": 3306,
400+
"ssh_required": False,
401+
"ssl_mode": "preferred",
356402
}
357403
assert connection_config_mysql.last_test_timestamp is not None
358404
assert connection_config_mysql.last_test_succeeded is True

tests/ops/service/connection_config/test_mysql_connector.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pytest
2+
from pydantic import ValidationError
13
from sqlalchemy.orm import Session
24

35
from fides.api.service.connectors.mysql_connector import MySQLConnector
@@ -54,3 +56,28 @@ def test_mysql_connector_build_uri(connection_config_mysql, db: Session):
5456
connector.build_uri()
5557
== "mysql+pymysql://host.docker.internal:3306/mysql_example"
5658
)
59+
60+
61+
def test_get_connect_args(connection_config_mysql):
62+
connector = MySQLConnector(configuration=connection_config_mysql)
63+
64+
# Default ssl_mode
65+
connection_config_mysql.secrets = {
66+
"username": "mysql_user",
67+
"password": "mysql_pw",
68+
"host": "host.docker.internal",
69+
"dbname": "mysql_example",
70+
}
71+
connect_args = connector.get_connect_args()
72+
assert connect_args == {"ssl": {"mode": "preferred"}}
73+
74+
# Custom ssl_mode
75+
connection_config_mysql.secrets = {
76+
"username": "mysql_user",
77+
"password": "mysql_pw",
78+
"host": "host.docker.internal",
79+
"dbname": "mysql_example",
80+
"ssl_mode": "required",
81+
}
82+
connect_args = connector.get_connect_args()
83+
assert connect_args == {"ssl": {"mode": "required"}}

0 commit comments

Comments
 (0)