Skip to content

Commit 84deebc

Browse files
SNOW-1346234 add automated external browser tests (#1983)
1 parent c9bf0e7 commit 84deebc

File tree

13 files changed

+379
-1
lines changed

13 files changed

+379
-1
lines changed

.github/workflows/parameters_aws_auth_tests.json.gpg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
� �.T��Q�Q���_鞋ftS�o��C���e���ǜ�'�����W����"�#Z8\1j�^?+��h�qGR�,�=���������I�zc8K��=Z��p2��+�kU5�wxƳ
2+
R��W���ܗ*�p��W�\J�v���½|/u��^8��%���J���@L+��.������e��#Z��~���W� �^C�v�}�ے����]4pk�^r'
3+
�hŚ>���;X:�7"@�;~Z�Ezkv�2�nNS0��9��=�U��w�D0�8<����o�������f�o&��3��r�:_��"K�=����:= \9 ��X��(\��J\ʒ'�?��l�w��?�)�t�*[���pP8g�͠���\te�>q��z���4�?�gj�����78*u�-"�*'{ih|�/L�þ #S��b�蕮�i�J$�LF�c*A��B�n��&q=r�jbn�6Ih
4+
Rd�쌔���Di5��X��,���+�zh�ʻ��9��Oe<>[u�>����v��s��cf?c�l�Ջ,rS�rh�Ι�

Jenkinsfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ timestamps {
8989
}
9090

9191
jobDefinitions.put('JDBC-AIX-Unit', { build job: 'JDBC-AIX-UnitTests', parameters: [ string(name: 'BRANCH', value: scmInfo.GIT_BRANCH ) ] } )
92+
jobDefinitions.put('Test Authentication', {
93+
withCredentials([
94+
string(credentialsId: 'sfctest0-parameters-secret', variable: 'PARAMETERS_SECRET'),
95+
string(credentialsId: 'a791118f-a1ea-46cd-b876-56da1b9bc71c', variable: 'NEXUS_PASSWORD')
96+
]) {
97+
sh '''\
98+
|#!/bin/bash
99+
|set -e
100+
|ci/test_authentication.sh
101+
'''.stripMargin()
102+
}
103+
})
104+
92105
stage('Test') {
93106
parallel (jobDefinitions)
94107
}

ci/container/test_authentication.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash -e
2+
3+
set -o pipefail
4+
5+
export WORKSPACE=${WORKSPACE:-/mnt/workspace}
6+
export SOURCE_ROOT=${SOURCE_ROOT:-/mnt/host}
7+
MVNW_EXE=$SOURCE_ROOT/mvnw
8+
9+
AUTH_PARAMETER_FILE=./.github/workflows/parameters_aws_auth_tests.json
10+
eval $(jq -r '.authtestparams | to_entries | map("export \(.key)=\(.value|tostring)")|.[]' $AUTH_PARAMETER_FILE)
11+
12+
$MVNW_EXE -DjenkinsIT \
13+
-Djava.io.tmpdir=$WORKSPACE \
14+
-Djacoco.skip.instrument=true \
15+
-Dskip.unitTests=true \
16+
-DintegrationTestSuites=AuthenticationTestSuite \
17+
-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn \
18+
-Dnot-self-contained-jar \
19+
verify \
20+
--batch-mode --show-version

ci/test_authentication.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash -e
2+
3+
set -o pipefail
4+
THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
5+
export WORKSPACE=${WORKSPACE:-/tmp}
6+
export INTERNAL_REPO=nexus.int.snowflakecomputing.com:8086
7+
8+
source $THIS_DIR/scripts/login_internal_docker.sh
9+
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"
10+
11+
docker run \
12+
-v $(cd $THIS_DIR/.. && pwd):/mnt/host \
13+
-v $WORKSPACE:/mnt/workspace \
14+
--rm \
15+
nexus.int.snowflakecomputing.com:8086/docker/snowdrivers-test-external-browser-jdbc:1 \
16+
"/mnt/host/ci/container/test_authentication.sh"

parent-pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
<relocationBase>net/snowflake/client/jdbc/internal</relocationBase>
7979
<shadeBase>net.snowflake.client.jdbc.internal</shadeBase>
8080
<shadeNativeBase>net_snowflake_client_jdbc_internal</shadeNativeBase>
81+
<skip.unitTests>false</skip.unitTests>
8182
<slf4j.version>2.0.13</slf4j.version>
8283
<snowflake.common.version>5.1.4</snowflake.common.version>
8384
<integrationTestSuites>UnitTestSuite</integrationTestSuites>

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,7 @@
11621162
<plugin>
11631163
<artifactId>maven-surefire-plugin</artifactId>
11641164
<configuration>
1165+
<skipTests>${skip.unitTests}</skipTests>
11651166
<argLine>--add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/sun.util.calendar=ALL-UNNAMED --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED</argLine>
11661167
</configuration>
11671168
</plugin>
@@ -1214,6 +1215,7 @@
12141215
<artifactId>maven-surefire-plugin</artifactId>
12151216
<configuration>
12161217
<test>UnitTestSuite</test>
1218+
<skipTests>${skip.unitTests}</skipTests>
12171219
</configuration>
12181220
<dependencies>
12191221
<dependency>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package net.snowflake.client.authentication;
2+
3+
import static net.snowflake.client.jdbc.SnowflakeUtil.systemGetEnv;
4+
5+
import java.util.Properties;
6+
7+
public class AuthConnectionParameters {
8+
9+
static final String SSO_USER = systemGetEnv("SNOWFLAKE_AUTH_TEST_BROWSER_USER");
10+
static final String HOST = systemGetEnv("SNOWFLAKE_AUTH_TEST_HOST");
11+
static final String SSO_PASSWORD = systemGetEnv("SNOWFLAKE_AUTH_TEST_OKTA_PASS");
12+
13+
static Properties getBaseConnectionParameters() {
14+
Properties properties = new Properties();
15+
properties.put("host", HOST);
16+
properties.put("port", systemGetEnv("SNOWFLAKE_AUTH_TEST_PORT"));
17+
properties.put("role", systemGetEnv("SNOWFLAKE_AUTH_TEST_ROLE"));
18+
properties.put("account", systemGetEnv("SNOWFLAKE_AUTH_TEST_ACCOUNT"));
19+
properties.put("db", systemGetEnv("SNOWFLAKE_AUTH_TEST_DATABASE"));
20+
properties.put("schema", systemGetEnv("SNOWFLAKE_AUTH_TEST_SCHEMA"));
21+
properties.put("warehouse", systemGetEnv("SNOWFLAKE_AUTH_TEST_WAREHOUSE"));
22+
return properties;
23+
}
24+
25+
static Properties getExternalBrowserConnectionParameters() {
26+
Properties properties = getBaseConnectionParameters();
27+
properties.put("user", SSO_USER);
28+
properties.put("authenticator", "externalbrowser");
29+
return properties;
30+
}
31+
32+
static Properties getStoreIDTokenConnectionParameters() {
33+
Properties properties = getExternalBrowserConnectionParameters();
34+
properties.put("CLIENT_STORE_TEMPORARY_CREDENTIAL", true);
35+
return properties;
36+
}
37+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package net.snowflake.client.authentication;
2+
3+
import static org.hamcrest.CoreMatchers.is;
4+
import static org.hamcrest.CoreMatchers.nullValue;
5+
import static org.hamcrest.MatcherAssert.assertThat;
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import java.io.IOException;
10+
import java.sql.Connection;
11+
import java.sql.DriverManager;
12+
import java.sql.ResultSet;
13+
import java.sql.SQLException;
14+
import java.sql.Statement;
15+
import java.util.Properties;
16+
import java.util.concurrent.TimeUnit;
17+
import net.snowflake.client.core.SessionUtil;
18+
import net.snowflake.client.jdbc.SnowflakeConnectionV1;
19+
import net.snowflake.client.jdbc.SnowflakeSQLException;
20+
21+
public class AuthTest {
22+
23+
private Exception exception;
24+
private String idToken;
25+
private final boolean runAuthTestsManually;
26+
27+
public AuthTest() {
28+
this.runAuthTestsManually = Boolean.parseBoolean(System.getenv("RUN_AUTH_TESTS_MANUALLY"));
29+
}
30+
31+
public Thread getConnectAndExecuteSimpleQueryThread(Properties props, String sessionParameters) {
32+
return new Thread(() -> connectAndExecuteSimpleQuery(props, sessionParameters));
33+
}
34+
35+
public Thread getConnectAndExecuteSimpleQueryThread(Properties props) {
36+
return new Thread(() -> connectAndExecuteSimpleQuery(props, null));
37+
}
38+
39+
public void verifyExceptionIsThrown(String message) {
40+
assertThat("Expected exception not thrown", this.exception.getMessage(), is(message));
41+
}
42+
43+
public void verifyExceptionIsNotThrown() {
44+
assertThat("Unexpected exception thrown", this.exception, nullValue());
45+
}
46+
47+
public void connectAndProvideCredentials(Thread provideCredentialsThread, Thread connectThread)
48+
throws InterruptedException {
49+
if (runAuthTestsManually) {
50+
connectThread.start();
51+
connectThread.join();
52+
} else {
53+
provideCredentialsThread.start();
54+
connectThread.start();
55+
provideCredentialsThread.join();
56+
connectThread.join();
57+
}
58+
}
59+
60+
public void provideCredentials(String scenario, String login, String password) {
61+
try {
62+
String provideBrowserCredentialsPath = "/externalbrowser/provideBrowserCredentials.js";
63+
ProcessBuilder processBuilder =
64+
new ProcessBuilder("node", provideBrowserCredentialsPath, scenario, login, password);
65+
Process process = processBuilder.start();
66+
process.waitFor(15, TimeUnit.SECONDS);
67+
} catch (Exception e) {
68+
throw new RuntimeException(e);
69+
}
70+
}
71+
72+
public void cleanBrowserProcesses() {
73+
if (!runAuthTestsManually) {
74+
String cleanBrowserProcessesPath = "/externalbrowser/cleanBrowserProcesses.js";
75+
ProcessBuilder processBuilder = new ProcessBuilder("node", cleanBrowserProcessesPath);
76+
try {
77+
Process process = processBuilder.start();
78+
process.waitFor(15, TimeUnit.SECONDS);
79+
} catch (InterruptedException | IOException e) {
80+
throw new RuntimeException(e);
81+
}
82+
}
83+
}
84+
85+
public static void deleteIdToken() {
86+
SessionUtil.deleteIdTokenCache(
87+
AuthConnectionParameters.HOST, AuthConnectionParameters.SSO_USER);
88+
}
89+
90+
public void connectAndExecuteSimpleQuery(Properties props, String sessionParameters) {
91+
String url = String.format("jdbc:snowflake://%s:%s", props.get("host"), props.get("port"));
92+
if (sessionParameters != null) {
93+
url += "?" + sessionParameters;
94+
}
95+
try (Connection con = DriverManager.getConnection(url, props);
96+
Statement stmt = con.createStatement();
97+
ResultSet rs = stmt.executeQuery("select 1")) {
98+
assertTrue(rs.next());
99+
assertEquals(1, rs.getInt(1));
100+
saveToken(con);
101+
} catch (SQLException e) {
102+
this.exception = e;
103+
}
104+
}
105+
106+
private void saveToken(Connection con) throws SnowflakeSQLException {
107+
SnowflakeConnectionV1 sfcon = (SnowflakeConnectionV1) con;
108+
this.idToken = sfcon.getSfSession().getIdToken();
109+
}
110+
111+
public String getIdToken() {
112+
return idToken;
113+
}
114+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package net.snowflake.client.authentication;
2+
3+
import static net.snowflake.client.authentication.AuthConnectionParameters.getExternalBrowserConnectionParameters;
4+
5+
import java.io.IOException;
6+
import java.util.Properties;
7+
import net.snowflake.client.category.TestTags;
8+
import org.junit.jupiter.api.AfterEach;
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Tag;
11+
import org.junit.jupiter.api.Test;
12+
13+
@Tag(TestTags.AUTHENTICATION)
14+
class ExternalBrowserIT {
15+
16+
String login = AuthConnectionParameters.SSO_USER;
17+
String password = AuthConnectionParameters.SSO_PASSWORD;
18+
AuthTest authTest = new AuthTest();
19+
20+
@BeforeEach
21+
public void setUp() throws IOException {
22+
AuthTest.deleteIdToken();
23+
}
24+
25+
@AfterEach
26+
public void tearDown() {
27+
authTest.cleanBrowserProcesses();
28+
AuthTest.deleteIdToken();
29+
}
30+
31+
@Test
32+
void shouldAuthenticateUsingExternalBrowser() throws InterruptedException {
33+
Thread provideCredentialsThread =
34+
new Thread(() -> authTest.provideCredentials("success", login, password));
35+
Thread connectThread =
36+
authTest.getConnectAndExecuteSimpleQueryThread(getExternalBrowserConnectionParameters());
37+
38+
authTest.connectAndProvideCredentials(provideCredentialsThread, connectThread);
39+
authTest.verifyExceptionIsNotThrown();
40+
}
41+
42+
@Test
43+
void shouldThrowErrorForMismatchedUsername() throws InterruptedException {
44+
Properties properties = getExternalBrowserConnectionParameters();
45+
properties.put("user", "differentUsername");
46+
Thread provideCredentialsThread =
47+
new Thread(() -> authTest.provideCredentials("success", login, password));
48+
Thread connectThread = authTest.getConnectAndExecuteSimpleQueryThread(properties);
49+
50+
authTest.connectAndProvideCredentials(provideCredentialsThread, connectThread);
51+
authTest.verifyExceptionIsThrown(
52+
"The user you were trying to authenticate as differs from the user currently logged in at the IDP.");
53+
}
54+
55+
@Test
56+
void shouldThrowErrorForWrongCredentials() throws InterruptedException {
57+
String login = "itsnotanaccount.com";
58+
String password = "fakepassword";
59+
Thread provideCredentialsThread =
60+
new Thread(() -> authTest.provideCredentials("fail", login, password));
61+
Thread connectThread =
62+
authTest.getConnectAndExecuteSimpleQueryThread(
63+
getExternalBrowserConnectionParameters(), "BROWSER_RESPONSE_TIMEOUT=10");
64+
65+
authTest.connectAndProvideCredentials(provideCredentialsThread, connectThread);
66+
authTest.verifyExceptionIsThrown(
67+
"JDBC driver encountered communication error. Message: External browser authentication failed within timeout of 10000 milliseconds.");
68+
}
69+
70+
@Test
71+
void shouldThrowErrorForBrowserTimeout() throws InterruptedException {
72+
Thread provideCredentialsThread =
73+
new Thread(() -> authTest.provideCredentials("timeout", login, password));
74+
Thread connectThread =
75+
authTest.getConnectAndExecuteSimpleQueryThread(
76+
getExternalBrowserConnectionParameters(), "BROWSER_RESPONSE_TIMEOUT=1");
77+
78+
authTest.connectAndProvideCredentials(provideCredentialsThread, connectThread);
79+
authTest.verifyExceptionIsThrown(
80+
"JDBC driver encountered communication error. Message: External browser authentication failed within timeout of 1000 milliseconds.");
81+
}
82+
}

0 commit comments

Comments
 (0)