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

[SNOW-1346233] Tests for authentication methods (external browser, oauth, okta, keypair) #1264

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ba5a0c3
Additional external browser tests
sfc-gh-pcyrek Dec 4, 2024
efc3ecc
add err handling, docker container change
sfc-gh-pcyrek Dec 4, 2024
7f31099
skip github actions check
sfc-gh-pcyrek Dec 5, 2024
65e7cdd
docker repo setup2
sfc-gh-pcyrek Dec 5, 2024
719f724
test default configuration
sfc-gh-pcyrek Dec 5, 2024
298a08a
test default configuration, only IPADDR
sfc-gh-pcyrek Dec 5, 2024
9d63c1d
test default configuration, only IPADDR2
sfc-gh-pcyrek Dec 5, 2024
29b8f2b
test default configuration3
sfc-gh-pcyrek Dec 5, 2024
5540296
adding okta tests, solution refactor
sfc-gh-pcyrek Dec 5, 2024
707c64b
adding oauth tests, small refactor
sfc-gh-pcyrek Dec 6, 2024
c77b081
linter fix
sfc-gh-pcyrek Dec 6, 2024
3053d2d
linter fix2
sfc-gh-pcyrek Dec 10, 2024
0c6d4b9
review 1
sfc-gh-pcyrek Dec 13, 2024
26d3ff2
fix usernames
sfc-gh-pcyrek Dec 13, 2024
ff49f15
logging for flaky tc
sfc-gh-pcyrek Dec 16, 2024
245ade2
after review session
sfc-gh-pcyrek Dec 17, 2024
bf866bf
after review session2
sfc-gh-pcyrek Dec 17, 2024
ca5c1d7
linters fix
sfc-gh-pcyrek Dec 17, 2024
833a75f
Merge branch 'master' into pcyrek-golang-external-browser-tests
sfc-gh-pcyrek Dec 17, 2024
e78d18f
review - round 2
sfc-gh-pcyrek Dec 18, 2024
361984d
Merge remote-tracking branch 'origin/pcyrek-golang-external-browser-t…
sfc-gh-pcyrek Dec 18, 2024
fd3dc3c
Merge branch 'master' into pcyrek-golang-external-browser-tests
sfc-gh-pcyrek Dec 18, 2024
a26313a
linters fix
sfc-gh-pcyrek Dec 18, 2024
03adfa1
linters fix2
sfc-gh-pcyrek Dec 18, 2024
b6ac5ba
Merge remote-tracking branch 'origin/pcyrek-golang-external-browser-t…
sfc-gh-pcyrek Dec 18, 2024
4959876
linters fix3
sfc-gh-pcyrek Dec 18, 2024
b1073b1
linterfix4
sfc-gh-pcyrek Dec 18, 2024
d2f480b
linterfix5
sfc-gh-pcyrek Dec 18, 2024
a52cb60
errorhandling
sfc-gh-pcyrek Dec 18, 2024
c3ef9e5
review - round 3
sfc-gh-pcyrek Dec 19, 2024
cf0a112
lintersfix1
sfc-gh-pcyrek Dec 19, 2024
a95755d
Merge branch 'master' into pcyrek-golang-external-browser-tests
sfc-gh-pcyrek Jan 2, 2025
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
Binary file not shown.
Binary file added .github/workflows/rsa_keys/rsa_key.p8.gpg
Binary file not shown.
Binary file not shown.
23 changes: 20 additions & 3 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
)
}
}

Expand Down
27 changes: 27 additions & 0 deletions auth_generic_test_methods_test.go
Original file line number Diff line number Diff line change
@@ -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
}
178 changes: 178 additions & 0 deletions auth_with_external_browser_test.go
Original file line number Diff line number Diff line change
@@ -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
sfc-gh-pcyrek marked this conversation as resolved.
Show resolved Hide resolved

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
}
49 changes: 49 additions & 0 deletions auth_with_keypair_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
109 changes: 109 additions & 0 deletions auth_with_oauth_test.go
Original file line number Diff line number Diff line change
@@ -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"`
sfc-gh-pcyrek marked this conversation as resolved.
Show resolved Hide resolved
Scope string `json:"scope"`
}
Loading
Loading