Skip to content
This repository was archived by the owner on Sep 23, 2024. It is now read-only.

Commit 1466fcb

Browse files
authored
enable-key-pair (#9)
1 parent 396f17b commit 1466fcb

4 files changed

Lines changed: 70 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
2.5.0 (2026-05-06)
2+
-------------------
3+
4+
*Changes*
5+
- Update dependencies:
6+
- snowflake-connector-python[pandas,secure-local-storage]
7+
- Add support for key pair authentication
8+
9+
110
2.4.0 (2024-06-05)
211
-------------------
312

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
python_requires='>=3.7',
2424
install_requires=[
2525
'pipelinewise-singer-python==1.*',
26-
'snowflake-connector-python[pandas,secure-local-storage]==3.10.1',
26+
'snowflake-connector-python[pandas,secure-local-storage]==4.4.0',
2727
'inflection==0.5.1',
2828
'joblib==1.2.0',
2929
'boto3==1.28.20',

target_snowflake/db_sync.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,16 @@ def validate_config(config):
4444
# Use table stage if none s3_bucket and stage defined
4545
elif not config.get('s3_bucket', None) and not config.get('stage', None):
4646
required_config_keys = snowflake_required_config_keys
47-
elif not(config.get("use_browser_authentication")) and not(config.get("password")):
48-
errors.append("'password' configuration was not provided and it is mandatory when "
49-
"SSO browser authentication is not intended ("
50-
"'use_browser_authentication' is 'False'). Please provide a value "
51-
"for 'password' for basic authentication or "
52-
"set 'use_browser_authentication' to True for SSO browser authentication.")
47+
if not(config.get("use_browser_authentication")) and not(config.get("password")) and not(config.get("private_key")):
48+
errors.append("'password' or 'private_key' configuration was not provided and it is mandatory when "
49+
"SSO browser authentication is not intended ("
50+
"'use_browser_authentication' is 'False'). Please provide a value "
51+
"for 'password' or 'private_key' for basic authentication or "
52+
"set 'use_browser_authentication' to True for SSO browser authentication.")
5353
else:
5454
errors.append("Only one of 's3_bucket' or 'stage' keys defined in config. "
5555
"Use both of them if you want to use an external stage when loading data into snowflake "
5656
"or don't use any of them if you want ot use table stages.")
57-
5857
# Check if mandatory keys exist
5958
for k in required_config_keys:
6059
if not config.get(k, None):
@@ -289,6 +288,39 @@ def __init__(self, connection_config, stream_schema_message=None, table_cache=No
289288
else:
290289
self.upload_client = SnowflakeUploadClient(connection_config, self)
291290

291+
292+
def _configure_private_key_auth(self, private_key: str):
293+
"""Configure private key authentication.
294+
295+
Args:
296+
private_key: RSA private key as raw PEM-encoded string
297+
298+
Raises:
299+
ValueError: If private key is invalid or cannot be loaded
300+
"""
301+
private_key_content = private_key.strip()
302+
303+
# Validate that the key is in PEM format
304+
if not private_key_content.startswith("-----BEGIN"):
305+
raise ValueError(
306+
"Private key must be in raw PEM format (starting with '-----BEGIN').",
307+
)
308+
309+
# Remove PEM headers/footers and whitespace to extract base64-encoded DER
310+
# PEM format is just: -----BEGIN...-----\n<base64 DER>\n-----END...-----
311+
key_base64 = re.sub(
312+
r"-----BEGIN.*?-----\s*|\s*-----END.*?-----\s*|\s+",
313+
"",
314+
private_key_content,
315+
)
316+
317+
if not key_base64:
318+
raise ValueError(
319+
"Private key appears to be empty after removing PEM headers.",
320+
)
321+
322+
return key_base64
323+
292324
def open_connection(self):
293325
"""Open snowflake connection"""
294326
stream = None
@@ -298,6 +330,7 @@ def open_connection(self):
298330
return snowflake.connector.connect(
299331
user=self.connection_config['user'],
300332
password=self.connection_config.get('password') if not self.connection_config.get('use_browser_authentication') else None,
333+
private_key=self._configure_private_key_auth(self.connection_config.get('private_key')) if not self.connection_config.get('use_browser_authentication') else None,
301334
account=self.connection_config['account'],
302335
database=self.connection_config['dbname'],
303336
warehouse=self.connection_config['warehouse'],

tests/unit/test_db_sync.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def test_config_validation(self):
4242
'dbname': "dummy-value",
4343
'user': "dummy-value",
4444
'password': "dummy-value",
45+
'private_key': "dummy-value",
4546
'warehouse': "dummy-value",
4647
'default_target_schema': "dummy-value",
4748
'file_format': "dummy-value"
@@ -92,15 +93,18 @@ def test_config_validation(self):
9293
config_with_archive_load_files['archive_load_files'] = True
9394
self.assertGreater(len(validator(config_with_external_stage)), 0)
9495

95-
# Configuration without password nor use_browser_authentication=True
96-
config_without_password_nor_browser_auth = minimal_config.copy()
97-
config_without_password_nor_browser_auth.pop("password")
98-
self.assertGreater(len(validator(minimal_config)), 0)
96+
# Configuration without password nor private_key nor use_browser_authentication=True
97+
config_without_password_nor_private_key_nor_browser_auth = minimal_config.copy()
98+
config_without_password_nor_private_key_nor_browser_auth.pop("password")
99+
config_without_password_nor_private_key_nor_browser_auth.pop("private_key")
100+
self.assertGreater(len(validator(config_without_password_nor_private_key_nor_browser_auth)), 0)
99101

100-
# Configuration without password nor use_browser_authentication=True
101-
config_without_password_with_browser_auth = minimal_config.copy()
102-
config_without_password_with_browser_auth["use_browser_authentication"] = True
103-
self.assertEqual(len(validator(minimal_config)), 0)
102+
# Configuration without password nor private_key and use_browser_authentication=True
103+
config_without_password_nor_private_key_nor_browser_auth = minimal_config.copy()
104+
config_without_password_nor_private_key_nor_browser_auth.pop("password")
105+
config_without_password_nor_private_key_nor_browser_auth.pop("private_key")
106+
config_without_password_nor_private_key_nor_browser_auth["use_browser_authentication"] = True
107+
self.assertEqual(len(validator(config_without_password_nor_private_key_nor_browser_auth)), 0)
104108

105109
def test_column_type_mapping(self):
106110
"""Test JSON type to Snowflake column type mappings"""
@@ -218,6 +222,7 @@ def test_parallelism(self, query_patch):
218222
'dbname': "dummy-value",
219223
'user': "dummy-value",
220224
'password': "dummy-value",
225+
'private_key': "dummy-value",
221226
'warehouse': "dummy-value",
222227
'default_target_schema': "dummy-value",
223228
'file_format': "dummy-value"
@@ -291,6 +296,7 @@ def test_record_primary_key_string(self, query_patch):
291296
'dbname': "dummy-value",
292297
'user': "dummy-value",
293298
'password': "dummy-value",
299+
'private_key': "dummy-value",
294300
'warehouse': "dummy-value",
295301
'default_target_schema': "dummy-value",
296302
'file_format': "dummy-value"
@@ -350,6 +356,7 @@ def test_merge_failure_message(self, load_file_merge_patch, query_patch):
350356
'dbname': "dummy_dbname",
351357
'user': "dummy_user",
352358
'password': "dummy_password",
359+
'private_key': "dummy_private_key",
353360
'warehouse': "dummy_warehouse",
354361
'default_target_schema': "dummy_default_target_schema",
355362
'file_format': "dummy_file_format",
@@ -388,6 +395,7 @@ def test_copy_failure_message(self, load_file_copy_patch, query_patch):
388395
'dbname': "dummy_dbname",
389396
'user': "dummy_user",
390397
'password': "dummy_password",
398+
'private_key': "dummy_private_key",
391399
'warehouse': "dummy_warehouse",
392400
'default_target_schema': "dummy_default_target_schema",
393401
'file_format': "dummy_file_format",
@@ -423,6 +431,7 @@ def test_sync_table_with_no_changes_to_pk(self, query_patch):
423431
'dbname': "dummy-db",
424432
'user': "dummy-user",
425433
'password': "dummy-passwd",
434+
'private_key': "dummy-private_key",
426435
'warehouse': "dummy-wh",
427436
'default_target_schema': "dummy-schema",
428437
'file_format': "dummy-file-format"
@@ -471,6 +480,7 @@ def test_sync_table_with_new_pk_in_stream(self, query_patch):
471480
'dbname': "dummy-db",
472481
'user': "dummy-user",
473482
'password': "dummy-passwd",
483+
'private_key': "dummy-private_key",
474484
'warehouse': "dummy-wh",
475485
'default_target_schema': "dummy-schema",
476486
'file_format': "dummy-file-format"
@@ -542,6 +552,7 @@ def test_sync_table_with_stream_that_changes_to_have_no_pk(self, query_patch):
542552
'dbname': "dummy-db",
543553
'user': "dummy-user",
544554
'password': "dummy-passwd",
555+
'private_key': "dummy-private_key",
545556
'warehouse': "dummy-wh",
546557
'default_target_schema': "dummy-schema",
547558
'file_format': "dummy-file-format"
@@ -591,6 +602,7 @@ def test_sync_table_with_stream_that_has_no_pk_but_get_a_new_one(self, query_pat
591602
'dbname': "dummy-db",
592603
'user': "dummy-user",
593604
'password': "dummy-passwd",
605+
'private_key': "dummy-private_key",
594606
'warehouse': "dummy-wh",
595607
'default_target_schema': "dummy-schema",
596608
'file_format': "dummy-file-format"

0 commit comments

Comments
 (0)