Skip to content

Commit 9ef8d11

Browse files
committed
Use Snowflake key pair auth, quote identifiers
Switch to key pair authentication in the Snowflake workflows, as password-only authentication will be discontinued later this year. In the process, work out and document how to set the appropriate ODBC parameters in an `odbc.ini` file in preference to the connection URL. Refactor `snowflake.sh` to∑ set `WORKSPACE` when installing SnowSQL, which is required to upgrade to v1.3. Use the variable instead of heard-coded paths throughout, and fix the location of the configuration file (it has to go into the `.snowsql` subdirectory of the workspace). Copy and update an `odbc.ini` and `.snowsql/config` as needed for key pair auth. Fix the quoting of the role and schema names on connecting to Snowflake, which were silently failing and therefore failing to properly use the registry schema, which lead to a failure to find the registry. Broken in 33291bd (#853). While at it, remove support for the `SNOWSQL_PORT` environment variable, which has long been deprecated by Snowflake and likely never did anything. Also add redaction of passwords from Snowflake URL query parameters in the display URL. Any query parameter matching `pwd` will now be shown as "REDACTED".
1 parent 558651c commit 9ef8d11

9 files changed

Lines changed: 110 additions & 35 deletions

File tree

.github/ubuntu/snowflake.sh

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,37 @@ if [ -z "$SKIP_DEPENDS" ]; then
99
cat t/odbc/odbcinst.ini | sudo tee -a /etc/odbcinst.ini
1010
fi
1111

12+
# Set the SnowSQL workspace.
13+
export WORKSPACE=/opt/snowflake
14+
1215
# https://docs.snowflake.net/manuals/release-notes/client-change-log-snowsql.html
1316
# https://sfc-repo.snowflakecomputing.com/index.html
14-
curl -sSLo snowsql.bash https://sfc-repo.snowflakecomputing.com/snowsql/bootstrap/1.2/linux_x86_64/snowsql-1.2.21-linux_x86_64.bash
17+
curl -sSLo snowsql.bash https://sfc-repo.snowflakecomputing.com/snowsql/bootstrap/1.3/linux_x86_64/snowsql-1.3.2-linux_x86_64.bash
1518
curl -sSLo snowdbc.tgz https://sfc-repo.snowflakecomputing.com/odbc/linux/latest/snowflake_linux_x8664_odbc-3.5.0.tgz
1619

1720
# Install and configure ODBC.
18-
mkdir -p /opt/snowflake
19-
sudo tar --strip-components 1 -C /opt/snowflake -xzf snowdbc.tgz
20-
sudo mv /opt/snowflake/ErrorMessages/en-US /opt/snowflake/lib/
21+
mkdir -p "$WORKSPACE/.snowsql"
22+
sudo tar --strip-components 1 -C "$WORKSPACE" -xzf snowdbc.tgz
23+
sudo mv "$WORKSPACE/ErrorMessages/en-US" "$WORKSPACE/lib/"
24+
25+
# Set up the DSN for key pair auth.
26+
perl -npE 's/KEY_PASSWORD/$ENV{SNOWFLAKE_KEY_PASSWORD}/g' t/odbc/snowflake.ini | sudo tee -a /etc/odbc.ini
27+
printf "%s" "${SNOWFLAKE_KEY_FILE}" > "$WORKSPACE/rsa_key.p8"
2128

2229
# Install, update, and configure SnowSQL.
23-
sed -e '1,/^exit$/d' snowsql.bash | sudo tar -C /opt/snowflake -zxf -
24-
/opt/snowflake/snowsql -Uv
25-
printf "[options]\nnoup = true\n" > /opt/snowflake/config
30+
sed -e '1,/^exit$/d' snowsql.bash | sudo tar -C "$WORKSPACE" -zxf -
31+
"$WORKSPACE/snowsql" -Uv
32+
(
33+
printf "[connections]\nprivate_key_path=%s/rsa_key.p8\n\n" "$WORKSPACE"
34+
printf "[options]\nnoup = true\n"
35+
) > "$WORKSPACE/.snowsql/config"
2636

2737
# Add to the path.
2838
if [[ -n "$GITHUB_PATH" ]]; then
29-
echo "/opt/snowflake" >> "$GITHUB_PATH"
39+
echo "$WORKSPACE" >> "$GITHUB_PATH"
3040
fi
3141

3242
# Tell SnowSQL where to find the config.
3343
if [[ -n "$GITHUB_ENV" ]]; then
34-
echo "WORKSPACE=/opt/snowflake" >> "$GITHUB_ENV"
44+
echo "WORKSPACE=$WORKSPACE" >> "$GITHUB_ENV"
3545
fi

.github/workflows/coverage.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ jobs:
6565
- name: Setup Clients
6666
env:
6767
SKIP_DEPENDS: true
68+
SNOWFLAKE_KEY_PASSWORD: ${{ secrets.SNOWFLAKE_KEY_PASSWORD }}
69+
SNOWFLAKE_KEY_FILE: ${{ secrets.SNOWFLAKE_KEY_FILE }}
6870
run: |
6971
.github/ubuntu/all-apt-prereqs.sh
7072
.github/ubuntu/exasol.sh
@@ -92,7 +94,8 @@ jobs:
9294
LIVE_PG_REQUIRED: true
9395
SQITCH_TEST_PG_URI: db:pg://postgres@/postgres
9496
LIVE_SNOWFLAKE_REQUIRED: true
95-
SQITCH_TEST_SNOWFLAKE_URI: db:snowflake://${{ secrets.SNOWFLAKE_USERNAME }}:${{ secrets.SNOWFLAKE_PASSWORD }}@sra81677.us-east-1/sqitchtest?Driver=Snowflake;warehouse=compute_wh
97+
SNOWSQL_PRIVATE_KEY_PASSPHRASE: ${{ secrets.SNOWFLAKE_KEY_PASSWORD }}
98+
SQITCH_TEST_SNOWFLAKE_URI: db:snowflake://${{ secrets.SNOWFLAKE_USERNAME }}@sra81677.us-east-1/sqitchtest?DSN=sqitch;warehouse=compute_wh
9699
LIVE_SQLITE_REQUIRED: true
97100
LIVE_VERTICA_REQUIRED: true
98101
SQITCH_TEST_VSQL_URI: db:vertica://dbadmin@localhost:${{ job.services.vertica.ports[5433] }}/VMart?Driver=Vertica

.github/workflows/snowflake.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@v4
1515
- name: Setup Clients
16+
env:
17+
SNOWFLAKE_KEY_PASSWORD: ${{ secrets.SNOWFLAKE_KEY_PASSWORD }}
18+
SNOWFLAKE_KEY_FILE: ${{ secrets.SNOWFLAKE_KEY_FILE }}
1619
run: .github/ubuntu/snowflake.sh
1720
- name: Setup Perl
1821
id: perl
@@ -29,5 +32,6 @@ jobs:
2932
env:
3033
PERL5LIB: "${{ github.workspace }}/local/lib/perl5"
3134
LIVE_SNOWFLAKE_REQUIRED: true
32-
SQITCH_TEST_SNOWFLAKE_URI: db:snowflake://${{ secrets.SNOWFLAKE_USERNAME }}:${{ secrets.SNOWFLAKE_PASSWORD }}@sra81677.us-east-1/sqitchtest?Driver=Snowflake;warehouse=compute_wh
35+
SNOWSQL_PRIVATE_KEY_PASSPHRASE: ${{ secrets.SNOWFLAKE_KEY_PASSWORD }}
36+
SQITCH_TEST_SNOWFLAKE_URI: db:snowflake://${{ secrets.SNOWFLAKE_USERNAME }}@sra81677.us-east-1/sqitchtest?DSN=sqitch;warehouse=compute_wh
3337
run: prove -lvr t/snowflake.t

Changes

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ Revision history for Perl extension App::Sqitch
1111
Alastair Douglas for the report and solution (#874)!
1212
- Added missing CockroachDB templates. Thanks to @Peterbyte for the
1313
report (#878)!
14+
- Removed support for the `SNOWSQL_PORT` environment variable, which has
15+
long been deprecated by Snowflake and likely never did anything.
16+
- Fixed the quoting of the role and schema names on connecting to
17+
Snowflake, which was silently failing and thus failing to properly use
18+
the registry schema, which lead to a failure to find the registry.
19+
Broken in v1.5.1.
20+
- Added redaction of passwords from Snowflake URL query parameters in the
21+
display URL. Any query parameter matching `pwd` will now be shown as
22+
"REDACTED".
23+
- Expanded the documentation of Snowflake key pair authentication in
24+
`sqitch-authentication.pod` to recommend setting sensitive ODBC
25+
parameters in an `odbc.ini` file rather than connection URL query
26+
parameters.
27+
- Switched to key pair authentication in the Snowflake CI workflows.
1428

1529
1.5.1 2025-03-16T26:55:17Z
1630
- Fixed a bug introduced in v1.5.0 where the MySQL engine connected to

lib/App/Sqitch/Engine/snowflake.pm

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ sub destination {
2424
# Just use the target name if it doesn't look like a URI.
2525
return $self->target->name if $self->target->name !~ /:/;
2626

27-
# Use the URI sans password.
27+
# Use the URI sans passwords.
2828
my $uri = $self->target->uri->clone;
2929
$uri->password(undef) if $uri->password;
30+
for my $key (grep { /pwd/ } $uri->query_params) {
31+
$uri->query_param($key => 'REDACTED');
32+
}
3033
return $uri->as_string;
3134
}
3235

@@ -98,10 +101,6 @@ has uri => (
98101

99102
# Set defaults in the URI.
100103
$uri->host($self->_host($uri));
101-
# Use _port instead of port so it's empty if no port is in the URI.
102-
# https://github.com/sqitchers/sqitch/issues/675
103-
# XXX SNOWSQL_PORT deprecated; remove once Snowflake removes it.
104-
$uri->port($ENV{SNOWSQL_PORT}) if !$uri->_port && $ENV{SNOWSQL_PORT};
105104
$uri->dbname(
106105
$ENV{SNOWSQL_DATABASE}
107106
|| $self->_snowcfg->{dbname}
@@ -204,21 +203,23 @@ has dbh => (
204203
connected => sub {
205204
my $dbh = shift;
206205
my $role = $self->role;
206+
# Use LITERAL(), but not for WAREHOUSE, which might be
207+
# database-qualified (db.wh). Details on IDENTIFIER():
208+
# https://docs.snowflake.com/en/sql-reference/identifier-literal
207209
$dbh->do($_) or return for (
208-
($role ? ("USE ROLE $role") : ()),
210+
($role ? ('USE ROLE IDENTIFIER(' . $dbh->quote($role) . ')') : ()),
209211
"ALTER WAREHOUSE $wh RESUME IF SUSPENDED",
210212
"USE WAREHOUSE $wh",
211213
'ALTER SESSION SET TIMESTAMP_TYPE_MAPPING=TIMESTAMP_LTZ',
212214
"ALTER SESSION SET TIMESTAMP_OUTPUT_FORMAT='YYYY-MM-DD HH24:MI:SS'",
213215
"ALTER SESSION SET TIMEZONE='UTC'",
214216
);
215-
$dbh->do('USE SCHEMA ' . $dbh->quote_identifier($self->registry))
217+
$dbh->do('USE SCHEMA IDENTIFIER(' . $dbh->quote($self->registry) . ')')
216218
or $self->_handle_no_registry($dbh);
217219
return;
218220
},
219221
disconnect => sub {
220222
my $dbh = shift;
221-
my $wh = $self->warehouse;
222223
$dbh->do("ALTER WAREHOUSE $wh SUSPEND");
223224
return;
224225
},

lib/sqitch-authentication.pod

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,28 @@ query string, e.g.,
159159

160160
Snowflake does not support password-less authentication, but does support
161161
key-pair authentication. Follow
162-
L<the instructions|https://docs.snowflake.com/en/user-guide/snowsql-start.html#using-key-pair-authentication>
163-
to create a key pair, then set the following variables in the F<~/.snowsql/config>
164-
file:
162+
L<the instructions|https://docs.snowflake.com/en/user-guide/key-pair-auth>
163+
to create a key pair, then set the C<private_key_path> in the F<~/.snowsql/config>
164+
to point to the private key file:
165165

166-
authenticator = SNOWFLAKE_JWT
167-
private_key_path = "path/to/privatekey.p8"
166+
private_key_path = "<path>/rsa_key.p8"
168167

169168
To connect, set the C<$SNOWSQL_PRIVATE_KEY_PASSPHRASE> environment variable to
170-
the passphrase for the private key, and add these parameters to the query part
171-
of your connection URI:
169+
the passphrase for the private key, and add these parameters under the
170+
configuration for your DSN in F</etc/odbc.ini> or F<~/.odbc.ini>:
171+
172+
[sqitch]
173+
AUTHENTICATOR = SNOWFLAKE_JWT
174+
UID = <username>
175+
PRIV_KEY_FILE = <path>/rsa_key.p8
176+
PRIV_KEY_FILE_PWD = <password>
177+
178+
Then connect using the named DSN via the C<DSN> query parameter:
179+
180+
db:snowflake://movera@example.snowflakecomputing.com/flipr?warehouse=compute_wh;DSN=sqitch
181+
182+
Or add the ODBC parameters directly to the query part of your connection URI
183+
(although it's safer to put C<priv_key_file_pwd> in F<odbc.ini>):
172184

173185
=over
174186

@@ -186,6 +198,10 @@ For example:
186198

187199
db:snowflake://movera@example.snowflakecomputing.com/flipr?Driver=Snowflake;warehouse=sqitch;authenticator=SNOWFLAKE_JWT;uid=movera;priv_key_file=path/to/privatekey.p8;priv_key_file_pwd=s0up3rs3cre7
188200

201+
Sadly, both the C<SNOWSQL_PRIVATE_KEY_PASSPHRASE> environment variable and
202+
the C<priv_key_file_pwd> ODBC parameter must be set, as Sqitch uses ODBC to
203+
maintain its registry and SnowSQL to execute change scripts.
204+
189205
=back
190206

191207
=head2 Use a Password File

lib/sqitch-environment.pod

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,6 @@ parameter.
316316
The Snowflake server host to connect to. Superseded by the target URI host
317317
name. Deprecated by Snowflake.
318318

319-
=item C<SNOWSQL_PORT>
320-
321-
The Snowflake server port to connect to. Superseded by the target URI port.
322-
Deprecated by Snowflake.
323-
324319
=item C<SNOWSQL_REGION>
325320

326321
The Snowflake region. Superseded by the target URI host name. Deprecated by

t/odbc/snowflake.ini

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[ODBC Data Sources]
2+
sqitch = Snowflake
3+
4+
[sqitch]
5+
Driver = /opt/snowflake/lib/libSnowflake.so
6+
ACCOUNT = YOHTWZV-ZSA24461
7+
AUTHENTICATOR = SNOWFLAKE_JWT
8+
PRIV_KEY_FILE = /opt/snowflake/rsa_key.p8
9+
PRIV_KEY_FILE_PWD = KEY_PASSWORD

t/snowflake.t

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use TestConfig;
3232

3333
my $CLASS;
3434

35-
delete $ENV{"SNOWSQL_$_"} for qw(USER PASSWORD DATABASE HOST PORT);
35+
delete $ENV{"SNOWSQL_$_"} for qw(USER PASSWORD DATABASE HOST);
3636

3737
BEGIN {
3838
$CLASS = 'App::Sqitch::Engine::snowflake';
@@ -87,6 +87,30 @@ is $snow->destination, $exp_uri, 'Destination should be URI string';
8787
is $snow->registry_destination, $snow->destination,
8888
'Registry destination should be the same as destination';
8989

90+
# Test destination URI redaction.
91+
PASSWORDS: {
92+
my $sensitive_uri = "db:snowflake://julie:s3cr3t@/?pwd=xyz;key_pwd=abc;pod=x";
93+
my $target = App::Sqitch::Target->new(
94+
sqitch => $sqitch,
95+
uri => URI::db->new($sensitive_uri),
96+
);
97+
isa_ok my $sensitive_snow = $CLASS->new(
98+
sqitch => $sqitch,
99+
target => $target,
100+
), $CLASS;
101+
102+
# Test URI.
103+
my $exp = URI::db->new($sensitive_uri);
104+
$exp->host("$ENV{SNOWSQL_ACCOUNT}.snowflakecomputing.com");
105+
$exp->dbname('julie');
106+
is $sensitive_snow->uri, $exp, 'Should have sensitive info in URI';
107+
108+
# Test redacted destination URI.
109+
$exp->query('pwd=REDACTED;key_pwd=REDACTED;pod=x');
110+
$exp->password(undef);
111+
is $sensitive_snow->destination, $exp, 'Should have sensitive info in URI';
112+
}
113+
90114
# Test environment variables.
91115
SNOWENV: {
92116
local $ENV{SNOWSQL_USER} = 'kamala';
@@ -95,12 +119,11 @@ SNOWENV: {
95119
local $ENV{SNOWSQL_WAREHOUSE} = 'madrigal';
96120
local $ENV{SNOWSQL_ACCOUNT} = 'egregious';
97121
local $ENV{SNOWSQL_HOST} = 'test.us-east-2.aws.snowflakecomputing.com';
98-
local $ENV{SNOWSQL_PORT} = 4242;
99122
local $ENV{SNOWSQL_DATABASE} = 'tryme';
100123

101124
my $target = App::Sqitch::Target->new(sqitch => $sqitch, uri => URI->new($uri));
102125
my $snow = $CLASS->new( sqitch => $sqitch, target => $target );
103-
is $snow->uri, 'db:snowflake://test.us-east-2.aws.snowflakecomputing.com:4242/tryme',
126+
is $snow->uri, 'db:snowflake://test.us-east-2.aws.snowflakecomputing.com/tryme',
104127
'Should build URI from environment';
105128
is $snow->username, 'kamala', 'Should read username from environment';
106129
is $snow->password, 'gimme', 'Should read password from environment';
@@ -112,7 +135,7 @@ SNOWENV: {
112135
delete $ENV{SNOWSQL_HOST};
113136
$snow = $CLASS->new( sqitch => $sqitch, target => $target );
114137
is $snow->uri,
115-
'db:snowflake://egregious.Australia.snowflakecomputing.com:4242/tryme',
138+
'db:snowflake://egregious.Australia.snowflakecomputing.com/tryme',
116139
'Should build URI host from account and region environment vars';
117140
is $snow->account, 'egregious.Australia',
118141
'Should read account and region from environment';

0 commit comments

Comments
 (0)