Skip to content

Commit

Permalink
feat: Add browser authentication (#46)
Browse files Browse the repository at this point in the history
# Description
* Copy code over from target-snowflake
MeltanoLabs/target-snowflake#260, now there is
parity between how the target & tap can connect to snowflake (3 ways:
password, key pair & browser authentications)
* This refactor uses `Enum` for `auth_method` for greater type safety.

---------

Co-authored-by: Edgar Ramírez Mondragón <[email protected]>
  • Loading branch information
haleemur and edgarrmondragon authored Sep 20, 2024
1 parent 86a73b2 commit c519d8e
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 5 deletions.
29 changes: 24 additions & 5 deletions tap_snowflake/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ def patched_conform(
singer_sdk.helpers._typing._conform_primitive_property = patched_conform


class SnowflakeAuthMethod(Enum):
"""Supported methods to authenticate to snowflake"""

BROWSER = 1
PASSWORD = 2
KEY_PAIR = 3


class ProfileStats(Enum):
"""Profile Statistics Enum."""

Expand Down Expand Up @@ -111,14 +119,23 @@ def get_private_key(self):
)

@cached_property
def auth_method(self):
def auth_method(self) -> SnowflakeAuthMethod:
"""Validate & return the authentication method based on config."""
if self.config.get("use_browser_authentication"):
return SnowflakeAuthMethod.BROWSER

valid_auth_methods = {"private_key", "private_key_path", "password"}
config_auth_methods = [x for x in self.config if x in valid_auth_methods]
if len(config_auth_methods) != 1:
msg = f"One of {valid_auth_methods} must be specified"
msg = (
"Neither password nor private key was provided for "
"authentication. For password-less browser authentication via SSO, "
"set use_browser_authentication config option to True."
)
raise ConfigValidationError(msg)
return config_auth_methods[0]
if config_auth_methods[0] in ["private_key", "private_key_path"]:
return SnowflakeAuthMethod.KEY_PAIR
return SnowflakeAuthMethod.PASSWORD

def get_sqlalchemy_url(self, config: dict) -> str:
"""Concatenate a SQLAlchemy URL for use in connecting to the source."""
Expand All @@ -127,7 +144,9 @@ def get_sqlalchemy_url(self, config: dict) -> str:
"user": config["user"],
}

if self.auth_method == "password":
if self.auth_method == SnowflakeAuthMethod.BROWSER:
params["authenticator"] = "externalbrowser"
elif self.auth_method == SnowflakeAuthMethod.PASSWORD:
params["password"] = config["password"]

for option in ["database", "schema", "warehouse", "role"]:
Expand All @@ -143,7 +162,7 @@ def create_engine(self) -> sqlalchemy.engine.Engine:
A SQLAlchemy engine.
"""
connect_args = {}
if self.auth_method in ["private_key", "private_key_path"]:
if self.auth_method == SnowflakeAuthMethod.KEY_PAIR:
connect_args["private_key"] = self.get_private_key()
return sqlalchemy.create_engine(
self.sqlalchemy_url,
Expand Down
10 changes: 10 additions & 0 deletions tap_snowflake/tap.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ class TapSnowflake(SQLTap):
secret=True,
description="The passprhase used to protect the private key",
),
th.Property(
"use_browser_authentication",
th.BooleanType,
required=False,
default=False,
description=(
"If authentication should be done using SSO (via external browser). "
"See SSO browser authentication."
)
),
th.Property(
"account",
th.StringType,
Expand Down

0 comments on commit c519d8e

Please sign in to comment.