Skip to content

Commit

Permalink
Final changes after testing
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-tmathew committed Sep 1, 2023
1 parent 03f2406 commit 48870ce
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
### Changed
- Fixed authentication workflow to check for authenticator type first, then for Key pair and finally default to password authentication.
- Fixed the `Dockerfile-src` configuration to build a docker image from source code.
- Updated README file.

## [3.5.3] - 2023-07-18
### Changed
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,16 @@ CREATE TABLE IF NOT EXISTS SCHEMACHANGE.CHANGE_HISTORY
Schemachange supports snowflake's default authenticator, External Oauth, Browswer based SSO and Programmatic SSO options supported by the [Snowflake Python Connector](https://docs.snowflake.com/en/user-guide/python-connector-example.html#connecting-to-snowflake). Set the environment variable `SNOWFLAKE_AUTHENTICATOR` to one of the following
Authentication Option | Expected Value
--- | ---
Default [Password](https://docs.snowflake.com/en/user-guide/python-connector-example.html#connecting-using-the-default-authenticator) Authenticator or [Key Pair](https://docs.snowflake.com/en/user-guide/python-connector-example.html#using-key-pair-authentication) Authenticator| `snowflake`
[External Oauth](https://docs.snowflake.com/en/user-guide/oauth-external.html) `oauth`
Default [Password](https://docs.snowflake.com/en/user-guide/python-connector-example.html#connecting-using-the-default-authenticator) Authenticator | `snowflake`
[Key Pair](https://docs.snowflake.com/en/user-guide/python-connector-example.html#using-key-pair-authentication) Authenticator| `snowflake`
[External Oauth](https://docs.snowflake.com/en/user-guide/oauth-external.html) | `oauth`
[Browser based SSO](https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use.html#setting-up-browser-based-sso) | `externalbrowser`
[Programmatic SSO](https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use.html#native-sso-okta-only) (Okta Only) | Okta URL endpoing for your Okta account typically in the form `https://<okta_account_name>.okta.com` OR `https://<okta_account_name>.oktapreview.com`

In the event both authentication criteria for the default authenticator are provided, schemachange will prioritize password authentication over key pair authentication.
If an authenticator is unsupported, then schemachange will default to `snowflake`. If the authenticator is `snowflake`, and both password and key pair values are provided then schemachange will use the password over the key pair values.

### Password Authentication
The Snowflake user password for `SNOWFLAKE_USER` is required to be set in the environment variable `SNOWFLAKE_PASSWORD` prior to calling the script. schemachange will fail if the `SNOWFLAKE_PASSWORD` environment variable is not set.
The Snowflake user password for `SNOWFLAKE_USER` is required to be set in the environment variable `SNOWFLAKE_PASSWORD` prior to calling the script. schemachange will fail if the `SNOWFLAKE_PASSWORD` environment variable is not set. The environment variable `SNOWFLAKE_AUTHENTICATOR` will be set to `snowflake` if it not explicitly set.

_**DEPRECATION NOTICE**: The `SNOWSQL_PWD` environment variable is deprecated but currently still supported. Support for it will be removed in a later version of schemachange. Please use `SNOWFLAKE_PASSWORD` instead._

Expand Down
93 changes: 51 additions & 42 deletions schemachange/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

#region Global Variables
# metadata
_schemachange_version = '3.5.3'
_schemachange_version = '3.5.4'
_config_file_name = 'schemachange-config.yml'
_metadata_database_name = 'METADATA'
_metadata_schema_name = 'SCHEMACHANGE'
Expand All @@ -35,7 +35,7 @@
_err_oauth_tk_err = '\n error description: {desc}'
_err_no_auth_mthd = "Unable to find connection credentials for Okta, private key, " \
+ "password, Oauth or Browser authentication"
_err_unsupported_auth_mthd = "'{snowflake_authenticator}' is not supported authenticator option. " \
_err_unsupported_auth_mthd = "'{unsupported_authenticator}' is not supported authenticator option. " \
+ "Choose from externalbrowser, oauth, https://<subdomain>.okta.com. Using default value = 'snowflake'"
_warn_password = "The SNOWSQL_PWD environment variable is deprecated and will" \
+ " be removed in a later version of schemachange. Please use SNOWFLAKE_PASSWORD instead."
Expand Down Expand Up @@ -231,9 +231,12 @@ def __init__(self, config):
self.oauth_config = config['oauth_config']
self.autocommit = config['autocommit']
self.verbose = config['verbose']
self.con = self.authenticate()
if not self.autocommit:
self.con.autocommit(False)
if self.set_connection_args():
self.con = snowflake.connector.connect(**self.conArgs)
if not self.autocommit:
self.con.autocommit(False)
else:
print(_err_env_missing)

def __del__(self):
if hasattr(self, 'con'):
Expand All @@ -259,9 +262,10 @@ def get_oauth_token(self):
errormessage += _err_oauth_tk_err.format(desc=resJsonDict['error_description'])
raise KeyError( errormessage )

def authenticate(self):
def set_connection_args(self):
# Password authentication is the default
snowflake_password = None
default_authenticator = 'snowflake'
if os.getenv("SNOWFLAKE_PASSWORD") is not None and os.getenv("SNOWFLAKE_PASSWORD"):
snowflake_password = os.getenv("SNOWFLAKE_PASSWORD")

Expand All @@ -274,6 +278,7 @@ def authenticate(self):
snowflake_password = os.getenv("SNOWSQL_PWD")

snowflake_authenticator = os.getenv("SNOWFLAKE_AUTHENTICATOR")

if snowflake_authenticator:
# Determine the type of Authenticator
# OAuth based authentication
Expand All @@ -298,49 +303,53 @@ def authenticate(self):

self.conArgs['password'] = snowflake_password
self.conArgs['authenticator'] = snowflake_authenticator.lower()

elif snowflake_authenticator.lower() == 'snowflake':
self.conArgs['authenticator'] = default_authenticator
# if authenticator is not a supported method or the authenticator variable is defined but not specified
else:
# defaulting to snowflake as authenticator
if self.verbose:
print(_err_unsupported_auth_mthd % snowflake_authenticator )
print(_log_auth_type % 'snowflake')
self.conArgs['authenticator'] = 'snowflake'
# If no Authenticator specified, Check private key authentication
elif os.getenv("SNOWFLAKE_PRIVATE_KEY_PATH", ''):
if self.verbose:
print( _log_auth_type % 'private key')

private_key_password = os.getenv("SNOWFLAKE_PRIVATE_KEY_PASSPHRASE", '')
if private_key_password:
private_key_password = private_key_password.encode()
else:
private_key_password = None
if self.verbose:
print(_log_pk_enc)
with open(os.environ["SNOWFLAKE_PRIVATE_KEY_PATH"], "rb") as key:
p_key= serialization.load_pem_private_key(
key.read(),
password = private_key_password,
backend = default_backend()
)

pkb = p_key.private_bytes(
encoding = serialization.Encoding.DER,
format = serialization.PrivateFormat.PKCS8,
encryption_algorithm = serialization.NoEncryption())

self.conArgs['private_key'] = pkb
self.conArgs['authenticator'] = 'snowflake'

elif snowflake_password:
print(_err_unsupported_auth_mthd.format(unsupported_authenticator=snowflake_authenticator) )
self.conArgs['authenticator'] = default_authenticator
else:
# default authenticator to snowflake
self.conArgs['authenticator'] = default_authenticator

if self.conArgs['authenticator'].lower() == default_authenticator:
# Giving preference to password based authentication when both private key and password are specified.
if snowflake_password:
if self.verbose:
print(_log_auth_type % 'password' )
self.conArgs['password'] = snowflake_password
self.conArgs['authenticator'] = 'snowflake'
else:
raise NameError(_err_no_auth_mthd)

elif os.getenv("SNOWFLAKE_PRIVATE_KEY_PATH", ''):
if self.verbose:
print( _log_auth_type % 'private key')

private_key_password = os.getenv("SNOWFLAKE_PRIVATE_KEY_PASSPHRASE", '')
if private_key_password:
private_key_password = private_key_password.encode()
else:
private_key_password = None
if self.verbose:
print(_log_pk_enc)
with open(os.environ["SNOWFLAKE_PRIVATE_KEY_PATH"], "rb") as key:
p_key= serialization.load_pem_private_key(
key.read(),
password = private_key_password,
backend = default_backend()
)

pkb = p_key.private_bytes(
encoding = serialization.Encoding.DER,
format = serialization.PrivateFormat.PKCS8,
encryption_algorithm = serialization.NoEncryption())

self.conArgs['private_key'] = pkb
else:
raise NameError(_err_no_auth_mthd)

return snowflake.connector.connect(**self.conArgs)
return True

def execute_snowflake_query(self, query):
if self.verbose:
Expand Down

0 comments on commit 48870ce

Please sign in to comment.