diff --git a/.github/workflows/parameters_aws_auth_tests.json.gpg b/.github/workflows/parameters_aws_auth_tests.json.gpg new file mode 100644 index 000000000..bb9689577 Binary files /dev/null and b/.github/workflows/parameters_aws_auth_tests.json.gpg differ diff --git a/.github/workflows/rsa_keys/rsa_key.p8.gpg b/.github/workflows/rsa_keys/rsa_key.p8.gpg new file mode 100644 index 000000000..e90253cd3 Binary files /dev/null and b/.github/workflows/rsa_keys/rsa_key.p8.gpg differ diff --git a/.github/workflows/rsa_keys/rsa_key_invalid.p8.gpg b/.github/workflows/rsa_keys/rsa_key_invalid.p8.gpg new file mode 100644 index 000000000..3d2442a7c Binary files /dev/null and b/.github/workflows/rsa_keys/rsa_key_invalid.p8.gpg differ diff --git a/Jenkinsfile b/Jenkinsfile index dcba1007f..d82269fd6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -18,9 +18,26 @@ timestamps { string(name: 'parent_job', value: env.JOB_NAME), string(name: 'parent_build_number', value: env.BUILD_NUMBER) ] - stage('Test') { - build job: 'RT-LanguageGo-PC',parameters: params - } + parallel( + 'Test': { + stage('Test') { + build job: 'RT-LanguageGo-PC', parameters: params + } + }, + 'Test Authentication': { + stage('Test Authentication') { + withCredentials([ + string(credentialsId: 'a791118f-a1ea-46cd-b876-56da1b9bc71c', variable: 'NEXUS_PASSWORD'), + string(credentialsId: 'sfctest0-parameters-secret', variable: 'PARAMETERS_SECRET') + ]) { + sh '''\ + |#!/bin/bash -e + |$WORKSPACE/ci/test_authentication.sh + '''.stripMargin() + } + } + } + ) } } diff --git a/auth_generic_test_methods_test.go b/auth_generic_test_methods_test.go new file mode 100644 index 000000000..02c1cf483 --- /dev/null +++ b/auth_generic_test_methods_test.go @@ -0,0 +1,27 @@ +package gosnowflake + +import ( + "fmt" + "testing" +) + +func getAuthTestConfigFromEnv() (*Config, error) { + return GetConfigFromEnv([]*ConfigParam{ + {Name: "Account", EnvName: "SNOWFLAKE_TEST_ACCOUNT", FailOnMissing: true}, + {Name: "User", EnvName: "SNOWFLAKE_AUTH_TEST_OKTA_USER", FailOnMissing: true}, + {Name: "Password", EnvName: "SNOWFLAKE_AUTH_TEST_OKTA_PASS", FailOnMissing: true}, + {Name: "Host", EnvName: "SNOWFLAKE_TEST_HOST", FailOnMissing: false}, + {Name: "Port", EnvName: "SNOWFLAKE_TEST_PORT", FailOnMissing: false}, + {Name: "Protocol", EnvName: "SNOWFLAKE_AUTH_TEST_PROTOCOL", FailOnMissing: false}, + {Name: "Role", EnvName: "SNOWFLAKE_TEST_ROLE", FailOnMissing: false}, + }) +} + +func getAuthTestsConfig(t *testing.T, authMethod AuthType) (*Config, error) { + cfg, err := getAuthTestConfigFromEnv() + assertNilF(t, err, fmt.Sprintf("failed to get config: %v", err)) + + cfg.Authenticator = authMethod + + return cfg, nil +} diff --git a/auth_with_external_browser_test.go b/auth_with_external_browser_test.go new file mode 100644 index 000000000..53fc976f4 --- /dev/null +++ b/auth_with_external_browser_test.go @@ -0,0 +1,178 @@ +package gosnowflake + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log" + "os/exec" + "sync" + "testing" + "time" +) + +func TestExternalBrowserSuccessful(t *testing.T) { + cfg := setupExternalBrowserTest(t) + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + provideExternalBrowserCredentials(t, externalBrowserType.Success, cfg.User, cfg.Password) + }() + go func() { + defer wg.Done() + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + assertNilE(t, err, fmt.Sprintf("Connection failed due to %v", err)) + }() + wg.Wait() +} + +func TestExternalBrowserFailed(t *testing.T) { + cfg := setupExternalBrowserTest(t) + cfg.ExternalBrowserTimeout = time.Duration(10) * time.Second + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + provideExternalBrowserCredentials(t, externalBrowserType.Fail, "FakeAccount", "NotARealPassword") + }() + go func() { + defer wg.Done() + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + assertEqualE(t, err.Error(), "authentication timed out") + }() + wg.Wait() +} + +func TestExternalBrowserTimeout(t *testing.T) { + cfg := setupExternalBrowserTest(t) + cfg.ExternalBrowserTimeout = time.Duration(1) * time.Second + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + provideExternalBrowserCredentials(t, externalBrowserType.Timeout, cfg.User, cfg.Password) + }() + go func() { + defer wg.Done() + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + assertEqualE(t, err.Error(), "authentication timed out") + }() + wg.Wait() +} + +func TestExternalBrowserMismatchUser(t *testing.T) { + cfg := setupExternalBrowserTest(t) + correctUsername := cfg.User + cfg.User = "fakeAccount" + var wg sync.WaitGroup + + wg.Add(2) + go func() { + defer wg.Done() + provideExternalBrowserCredentials(t, externalBrowserType.Success, correctUsername, cfg.Password) + }() + go func() { + defer wg.Done() + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + var snowflakeErr *SnowflakeError + assertTrueF(t, errors.As(err, &snowflakeErr)) + assertEqualE(t, snowflakeErr.Number, 390191, fmt.Sprintf("Expected 390191, but got %v", snowflakeErr.Number)) + }() + wg.Wait() +} + +func TestClientStoreCredentials(t *testing.T) { + cfg := setupExternalBrowserTest(t) + cfg.ClientStoreTemporaryCredential = 1 + cfg.ExternalBrowserTimeout = time.Duration(10) * time.Second + + t.Run("Obtains the ID token from the server and saves it on the local storage", func(t *testing.T) { + cleanupBrowserProcesses(t) + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + provideExternalBrowserCredentials(t, externalBrowserType.Success, cfg.User, cfg.Password) + }() + go func() { + defer wg.Done() + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + assertNilE(t, err, fmt.Sprintf("Connection failed: err %v", err)) + }() + wg.Wait() + }) + + t.Run("Verify validation of ID token if option enabled", func(t *testing.T) { + cleanupBrowserProcesses(t) + cfg.ClientStoreTemporaryCredential = 1 + db := getDbHandlerFromConfig(t, cfg) + conn, err := db.Conn(context.Background()) + assertNilE(t, err, fmt.Sprintf("Failed to connect to Snowflake. err: %v", err)) + defer conn.Close() + rows, err := conn.QueryContext(context.Background(), "SELECT 1") + assertNilE(t, err, fmt.Sprintf("Failed to run a query. err: %v", err)) + rows.Close() + }) + + t.Run("Verify validation of IDToken if option disabled", func(t *testing.T) { + cleanupBrowserProcesses(t) + cfg.ClientStoreTemporaryCredential = 0 + db := getDbHandlerFromConfig(t, cfg) + _, err := db.Conn(context.Background()) + assertEqualE(t, err.Error(), "authentication timed out", fmt.Sprintf("Expected timeout, but got %v", err)) + }) +} + +type ExternalBrowserProcessResult struct { + Success string + Fail string + Timeout string +} + +var externalBrowserType = ExternalBrowserProcessResult{ + Success: "success", + Fail: "fail", + Timeout: "timeout", +} + +func cleanupBrowserProcesses(t *testing.T) { + const cleanBrowserProcessesPath = "/externalbrowser/cleanBrowserProcesses.js" + _, err := exec.Command("node", cleanBrowserProcessesPath).Output() + assertNilE(t, err, fmt.Sprintf("failed to execute command: %v", err)) +} + +func provideExternalBrowserCredentials(t *testing.T, ExternalBrowserProcess string, user string, password string) { + const provideBrowserCredentialsPath = "/externalbrowser/provideBrowserCredentials.js" + _, err := exec.Command("node", provideBrowserCredentialsPath, ExternalBrowserProcess, user, password).Output() + assertNilE(t, err, fmt.Sprintf("failed to execute command: %v", err)) +} + +func verifyConnectionToSnowflakeAuthTests(t *testing.T, cfg *Config) (err error) { + dsn, err := DSN(cfg) + assertNilE(t, err, "failed to create DSN from Config") + + db, err := sql.Open("snowflake", dsn) + assertNilE(t, err, "failed to open Snowflake DB connection") + defer db.Close() + + rows, err := db.Query("SELECT 1") + if err != nil { + log.Printf("failed to run a query. 'SELECT 1', err: %v", err) + return err + } + + defer rows.Close() + assertTrueE(t, rows.Next(), "failed to get result", "There were no results for query: ") + + return err +} + +func setupExternalBrowserTest(t *testing.T) *Config { + runOnlyOnDockerContainer(t, "Running only on Docker container") + cleanupBrowserProcesses(t) + cfg, err := getAuthTestsConfig(t, AuthTypeExternalBrowser) + assertNilF(t, err, fmt.Sprintf("failed to get config: %v", err)) + return cfg +} diff --git a/auth_with_keypair_test.go b/auth_with_keypair_test.go new file mode 100644 index 000000000..3e9897b5e --- /dev/null +++ b/auth_with_keypair_test.go @@ -0,0 +1,49 @@ +package gosnowflake + +import ( + "crypto/rsa" + "errors" + "fmt" + "golang.org/x/crypto/ssh" + "os" + "testing" +) + +func TestKeypairSuccessful(t *testing.T) { + cfg := setupKeyPairTest(t) + cfg.PrivateKey = loadRsaPrivateKeyForKeyPair(t, "SNOWFLAKE_AUTH_TEST_PRIVATE_KEY_PATH") + + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + assertNilE(t, err, fmt.Sprintf("failed to connect. err: %v", err)) +} + +func TestKeypairInvalidKey(t *testing.T) { + cfg := setupKeyPairTest(t) + cfg.PrivateKey = loadRsaPrivateKeyForKeyPair(t, "SNOWFLAKE_AUTH_TEST_INVALID_PRIVATE_KEY_PATH") + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + var snowflakeErr *SnowflakeError + assertTrueF(t, errors.As(err, &snowflakeErr)) + assertEqualE(t, snowflakeErr.Number, 390144, fmt.Sprintf("Expected 390144, but got %v", snowflakeErr.Number)) +} + +func setupKeyPairTest(t *testing.T) *Config { + runOnlyOnDockerContainer(t, "Running only on Docker container") + + cfg, err := getAuthTestsConfig(t, AuthTypeJwt) + assertEqualE(t, err, nil, fmt.Sprintf("failed to get config: %v", err)) + + return cfg +} + +func loadRsaPrivateKeyForKeyPair(t *testing.T, envName string) *rsa.PrivateKey { + filePath, err := GetFromEnv(envName, true) + assertNilF(t, err, fmt.Sprintf("failed to get env: %v", err)) + + bytes, err := os.ReadFile(filePath) + assertNilF(t, err, fmt.Sprintf("failed to read file: %v", err)) + + key, err := ssh.ParseRawPrivateKey(bytes) + assertNilF(t, err, fmt.Sprintf("failed to parse private key: %v", err)) + + return key.(*rsa.PrivateKey) +} diff --git a/auth_with_oauth_test.go b/auth_with_oauth_test.go new file mode 100644 index 000000000..2c5f4056a --- /dev/null +++ b/auth_with_oauth_test.go @@ -0,0 +1,109 @@ +package gosnowflake + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "testing" +) + +func TestOauthSuccessful(t *testing.T) { + cfg := setupOauthTest(t) + token, err := getOauthTestToken(t, cfg) + assertNilE(t, err, fmt.Sprintf("failed to get token. err: %v", err)) + cfg.Token = token + err = verifyConnectionToSnowflakeAuthTests(t, cfg) + assertNilE(t, err, fmt.Sprintf("failed to connect. err: %v", err)) +} + +func TestOauthInvalidToken(t *testing.T) { + cfg := setupOauthTest(t) + cfg.Token = "invalid_token" + + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + + var snowflakeErr *SnowflakeError + assertTrueF(t, errors.As(err, &snowflakeErr)) + assertEqualE(t, snowflakeErr.Number, 390303, fmt.Sprintf("Expected 390303, but got %v", snowflakeErr.Number)) +} + +func TestOauthMismatchedUser(t *testing.T) { + cfg := setupOauthTest(t) + token, err := getOauthTestToken(t, cfg) + assertNilE(t, err, fmt.Sprintf("failed to get token. err: %v", err)) + cfg.Token = token + cfg.User = "fakeaccount" + + err = verifyConnectionToSnowflakeAuthTests(t, cfg) + + var snowflakeErr *SnowflakeError + assertTrueF(t, errors.As(err, &snowflakeErr)) + assertEqualE(t, snowflakeErr.Number, 390309, fmt.Sprintf("Expected 390309, but got %v", snowflakeErr.Number)) +} + +func setupOauthTest(t *testing.T) *Config { + runOnlyOnDockerContainer(t, "Running only on Docker container") + + cfg, err := getAuthTestsConfig(t, AuthTypeOAuth) + assertNilF(t, err, fmt.Sprintf("failed to connect. err: %v", err)) + + return cfg +} + +func getOauthTestToken(t *testing.T, cfg *Config) (string, error) { + + client := &http.Client{} + + authURL, err := GetFromEnv("SNOWFLAKE_AUTH_TEST_OAUTH_URL", true) + assertNilF(t, err, "SNOWFLAKE_AUTH_TEST_OAUTH_URL is not set") + + oauthClientID, err := GetFromEnv("SNOWFLAKE_AUTH_TEST_OAUTH_CLIENT_ID", true) + assertNilF(t, err, "SNOWFLAKE_AUTH_TEST_OAUTH_CLIENT_ID is not set") + + oauthClientSecret, err := GetFromEnv("SNOWFLAKE_AUTH_TEST_OAUTH_CLIENT_SECRET", true) + assertNilF(t, err, "SNOWFLAKE_AUTH_TEST_OAUTH_CLIENT_SECRET is not set") + + inputData := formData(cfg) + + req, err := http.NewRequest("POST", authURL, strings.NewReader(inputData.Encode())) + assertNilF(t, err, fmt.Sprintf("Request failed %v", err)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") + req.SetBasicAuth(oauthClientID, oauthClientSecret) + resp, err := client.Do(req) + + assertNilF(t, err, fmt.Sprintf("Response failed %v", err)) + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to get access token, status code: %d", resp.StatusCode) + } + + defer resp.Body.Close() + + var response OAuthTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return "", fmt.Errorf("failed to decode response: %v", err) + } + + return response.Token, err +} + +func formData(cfg *Config) url.Values { + data := url.Values{} + data.Set("username", cfg.User) + data.Set("password", cfg.Password) + data.Set("grant_type", "password") + data.Set("scope", fmt.Sprintf("session:role:%s", strings.ToLower(cfg.Role))) + + return data + +} + +type OAuthTokenResponse struct { + Type string `json:"token_type"` + Expiration int `json:"expires_in"` + Token string `json:"access_token"` + Scope string `json:"scope"` +} diff --git a/auth_with_okta_test.go b/auth_with_okta_test.go new file mode 100644 index 000000000..c73a10c24 --- /dev/null +++ b/auth_with_okta_test.go @@ -0,0 +1,52 @@ +package gosnowflake + +import ( + "errors" + "fmt" + "net/url" + "testing" +) + +func TestOktaSuccessful(t *testing.T) { + cfg := setupOktaTest(t) + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + assertNilE(t, err, fmt.Sprintf("failed to connect. err: %v", err)) +} + +func TestOktaWrongCredentials(t *testing.T) { + cfg := setupOktaTest(t) + cfg.Password = "fakePassword" + err := verifyConnectionToSnowflakeAuthTests(t, cfg) + + var snowflakeErr *SnowflakeError + assertTrueF(t, errors.As(err, &snowflakeErr)) + assertEqualE(t, snowflakeErr.Number, 261006, fmt.Sprintf("Expected 261006, but got %v", snowflakeErr.Number)) +} + +func TestOktaWrongAuthenticator(t *testing.T) { + cfg := setupOktaTest(t) + invalidAddress, err := url.Parse("https://fake-account-0000.okta.com") + assertNilF(t, err, fmt.Sprintf("failed to parse: %v", err)) + + cfg.OktaURL = invalidAddress + err = verifyConnectionToSnowflakeAuthTests(t, cfg) + + var snowflakeErr *SnowflakeError + assertTrueF(t, errors.As(err, &snowflakeErr)) + assertEqualE(t, snowflakeErr.Number, 390139, fmt.Sprintf("Expected 390139, but got %v", snowflakeErr.Number)) +} + +func setupOktaTest(t *testing.T) *Config { + runOnlyOnDockerContainer(t, "Running only on Docker container") + + urlEnv, err := GetFromEnv("SNOWFLAKE_AUTH_TEST_OKTA_AUTH", true) + assertNilF(t, err, fmt.Sprintf("failed to get env: %v", err)) + + cfg, err := getAuthTestsConfig(t, AuthTypeOkta) + assertNilF(t, err, fmt.Sprintf("failed to get config: %v", err)) + + cfg.OktaURL, err = url.Parse(urlEnv) + assertNilF(t, err, fmt.Sprintf("failed to parse: %v", err)) + + return cfg +} diff --git a/authexternalbrowser_test.go b/authexternalbrowser_test.go index 22e285ce8..4546da461 100644 --- a/authexternalbrowser_test.go +++ b/authexternalbrowser_test.go @@ -131,9 +131,7 @@ func TestAuthenticationTimeout(t *testing.T) { TokenAccessor: getSimpleTokenAccessor(), } _, _, err := authenticateByExternalBrowser(context.Background(), sr, authenticator, application, account, user, password, timeout, ConfigBoolTrue) - if err.Error() != "authentication timed out" { - t.Fatal("should have timed out") - } + assertEqualE(t, err.Error(), "authentication timed out", err.Error()) } func Test_createLocalTCPListener(t *testing.T) { diff --git a/ci/container/test_authentication.sh b/ci/container/test_authentication.sh new file mode 100755 index 000000000..1a2735612 --- /dev/null +++ b/ci/container/test_authentication.sh @@ -0,0 +1,15 @@ +#!/bin/bash -e + +set -o pipefail + +export AUTH_PARAMETER_FILE=./.github/workflows/parameters_aws_auth_tests.json +eval $(jq -r '.authtestparams | to_entries | map("export \(.key)=\(.value|tostring)")|.[]' $AUTH_PARAMETER_FILE) + +export SNOWFLAKE_AUTH_TEST_PRIVATE_KEY_PATH=./.github/workflows/rsa_keys/rsa_key.p8 +export SNOWFLAKE_AUTH_TEST_INVALID_PRIVATE_KEY_PATH=./.github/workflows/rsa_keys/rsa_key_invalid.p8 + +go test -run TestExternalBrowser* +go test -run TestClientStoreCredentials +go test -run TestOkta* +go test -run TestOauth* +go test -run TestKeypair* \ No newline at end of file diff --git a/ci/test_authentication.sh b/ci/test_authentication.sh new file mode 100755 index 000000000..f41db0ece --- /dev/null +++ b/ci/test_authentication.sh @@ -0,0 +1,31 @@ +#!/bin/bash -e + +set -o pipefail + + +export THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +export WORKSPACE=${WORKSPACE:-/tmp} + +CI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +if [[ -n "$JENKINS_HOME" ]]; then + ROOT_DIR="$(cd "${CI_DIR}/.." && pwd)" + export WORKSPACE=${WORKSPACE:-/tmp} + + source $CI_DIR/_init.sh + source $CI_DIR/scripts/login_internal_docker.sh + + echo "Use /sbin/ip" + IP_ADDR=$(/sbin/ip -4 addr show scope global dev eth0 | grep inet | awk '{print $2}' | cut -d / -f 1) + +fi + +gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/parameters_aws_auth_tests.json "$THIS_DIR/../.github/workflows/parameters_aws_auth_tests.json.gpg" +gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/rsa_keys/rsa_key.p8 "$THIS_DIR/../.github/workflows/rsa_keys/rsa_key.p8.gpg" +gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/rsa_keys/rsa_key_invalid.p8 "$THIS_DIR/../.github/workflows/rsa_keys/rsa_key_invalid.p8.gpg" + +docker run \ + -v $(cd $THIS_DIR/.. && pwd):/mnt/host \ + -v $WORKSPACE:/mnt/workspace \ + --rm \ + nexus.int.snowflakecomputing.com:8086/docker/snowdrivers-test-external-browser-golang:3 \ + "/mnt/host/ci/container/test_authentication.sh" diff --git a/driver_test.go b/driver_test.go index d6245653c..e866767bf 100644 --- a/driver_test.go +++ b/driver_test.go @@ -465,6 +465,16 @@ func runSnowflakeConnTest(t *testing.T, test func(sct *SCTest)) { test(sct) } +func getDbHandlerFromConfig(t *testing.T, cfg *Config) *sql.DB { + dsn, err := DSN(cfg) + assertNilF(t, err, "failed to create DSN from Config") + + db, err := sql.Open("snowflake", dsn) + assertNilF(t, err, "failed to open database") + + return db +} + func runningOnAWS() bool { return os.Getenv("CLOUD_PROVIDER") == "AWS" } diff --git a/util_test.go b/util_test.go index bed222559..807123214 100644 --- a/util_test.go +++ b/util_test.go @@ -391,6 +391,12 @@ func skipOnJenkins(t *testing.T, message string) { } } +func runOnlyOnDockerContainer(t *testing.T, message string) { + if os.Getenv("AUTHENTICATION_TESTS_ENV") == "" { + t.Skip("Running only on Docker container: " + message) + } +} + func randomString(n int) string { r := rand.New(rand.NewSource(time.Now().UnixNano())) alpha := []rune("abcdefghijklmnopqrstuvwxyz")