Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add browser authentication #46

Merged
merged 2 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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