Skip to content

Commit 3e2319f

Browse files
feat: [SNOW-2002186] save password based connection if subsequent key… (#2149)
* feat: [SNOW-2002186] save password based connection if subsequent key pair setup fails * feat: [SNOW-2002186] display different messages when key pair cannot be set * feat: [SNOW-2002186] code review fixes
1 parent c7fd825 commit 3e2319f

File tree

4 files changed

+154
-42
lines changed

4 files changed

+154
-42
lines changed

src/snowflake/cli/_plugins/auth/keypair/manager.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from cryptography.hazmat.primitives import serialization
66
from cryptography.hazmat.primitives.asymmetric import rsa
77
from snowflake.cli._plugins.object.manager import ObjectManager
8+
from snowflake.cli.api import exceptions
89
from snowflake.cli.api.cli_global_context import (
910
_CliGlobalContextAccess,
1011
get_cli_context,
@@ -119,9 +120,7 @@ def _generate_key_pair_and_set_public_key(
119120
public_key_exists, public_key_2_exists = self._get_public_keys()
120121

121122
if public_key_exists or public_key_2_exists:
122-
raise ClickException(
123-
"The public key is set already. Use the rotate command instead."
124-
)
123+
raise exceptions.CouldNotSetKeyPairError()
125124

126125
if not output_path.exists():
127126
output_path.mkdir(parents=True)

src/snowflake/cli/_plugins/connection/commands.py

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
import logging
1818
import os.path
19+
from copy import deepcopy
1920
from pathlib import Path
20-
from typing import Dict, Optional
21+
from typing import Dict, Optional, Tuple
2122

2223
import typer
2324
from click import ( # type: ignore
@@ -35,6 +36,7 @@
3536
strip_if_value_present,
3637
)
3738
from snowflake.cli._plugins.object.manager import ObjectManager
39+
from snowflake.cli.api import exceptions
3840
from snowflake.cli.api.cli_global_context import get_cli_context
3941
from snowflake.cli.api.commands.flags import (
4042
PLAIN_PASSWORD_MSG,
@@ -288,7 +290,11 @@ def add(
288290
raise UsageError(f"Connection {connection_name} already exists")
289291

290292
if not no_interactive:
291-
_extend_add_with_key_pair(connection_name, connection_options)
293+
connection_options, keypair_error = _extend_add_with_key_pair(
294+
connection_name, connection_options
295+
)
296+
else:
297+
keypair_error = ""
292298

293299
connections_file = add_connection_to_proper_file(
294300
connection_name,
@@ -297,6 +303,12 @@ def add(
297303
if set_as_default:
298304
set_config_value(path=["default_connection_name"], value=connection_name)
299305

306+
if keypair_error:
307+
return MessageResult(
308+
f"Wrote new password-based connection {connection_name} to {connections_file}, "
309+
f"however there were some issues during key pair setup. Review the following error and check 'snow auth keypair' "
310+
f"commands to setup key pair authentication:\n * {keypair_error}"
311+
)
300312
return MessageResult(
301313
f"Wrote new connection {connection_name} to {connections_file}"
302314
)
@@ -412,43 +424,57 @@ def _decrypt(passphrase: str | None):
412424
raise ClickException(str(err))
413425

414426

415-
def _extend_add_with_key_pair(connection_name: str, connection_options: Dict):
416-
if (
427+
def _extend_add_with_key_pair(
428+
connection_name: str, connection_options: Dict
429+
) -> Tuple[Dict, str]:
430+
if not _should_extend_with_key_pair(connection_options):
431+
return connection_options, ""
432+
433+
configure_key_pair = typer.confirm(
434+
"Do you want to configure key pair authentication?",
435+
default=False,
436+
)
437+
if not configure_key_pair:
438+
return connection_options, ""
439+
440+
key_length = typer.prompt(
441+
"Key length",
442+
default=2048,
443+
show_default=True,
444+
)
445+
446+
output_path = typer.prompt(
447+
"Output path",
448+
default=KEY_PAIR_DEFAULT_PATH,
449+
show_default=True,
450+
value_proc=lambda value: SecurePath(value),
451+
)
452+
private_key_passphrase = typer.prompt(
453+
"Private key passphrase",
454+
default="",
455+
hide_input=True,
456+
show_default=False,
457+
value_proc=lambda value: SecretType(value),
458+
)
459+
connection = connect_to_snowflake(temporary_connection=True, **connection_options)
460+
try:
461+
connection_options = AuthManager(connection=connection).extend_connection_add(
462+
connection_name=connection_name,
463+
connection_options=deepcopy(connection_options),
464+
key_length=key_length,
465+
output_path=output_path,
466+
private_key_passphrase=private_key_passphrase,
467+
)
468+
except exceptions.CouldNotSetKeyPairError:
469+
return connection_options, "The public key is set already."
470+
except Exception as e:
471+
return connection_options, str(e)
472+
return connection_options, ""
473+
474+
475+
def _should_extend_with_key_pair(connection_options: Dict) -> bool:
476+
return (
417477
connection_options.get("password") is not None
418478
and connection_options.get("private_key_file") is None
419479
and connection_options.get("private_key_path") is None
420-
):
421-
configure_key_pair = typer.confirm(
422-
"Do you want to configure key pair authentication?",
423-
default=False,
424-
)
425-
if configure_key_pair:
426-
key_length = typer.prompt(
427-
"Key length",
428-
default=2048,
429-
show_default=True,
430-
)
431-
432-
output_path = typer.prompt(
433-
"Output path",
434-
default=KEY_PAIR_DEFAULT_PATH,
435-
show_default=True,
436-
value_proc=lambda value: SecurePath(value),
437-
)
438-
private_key_passphrase = typer.prompt(
439-
"Private key passphrase",
440-
default="",
441-
hide_input=True,
442-
show_default=False,
443-
value_proc=lambda value: SecretType(value),
444-
)
445-
connection = connect_to_snowflake(
446-
temporary_connection=True, **connection_options
447-
)
448-
AuthManager(connection=connection).extend_connection_add(
449-
connection_name=connection_name,
450-
connection_options=connection_options,
451-
key_length=key_length,
452-
output_path=output_path,
453-
private_key_passphrase=private_key_passphrase,
454-
)
480+
)

src/snowflake/cli/api/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,10 @@ def __init__(self, show_obj_query: str):
236236
super().__init__(
237237
f"Received multiple rows from result of SQL statement: {show_obj_query}. Usage of 'show_specific_object' may not be properly scoped."
238238
)
239+
240+
241+
class CouldNotSetKeyPairError(ClickException):
242+
def __init__(self):
243+
super().__init__(
244+
"The public key is set already. Use the rotate command instead."
245+
)

tests/test_connection.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,3 +1546,83 @@ def test_connection_add_no_key_pair_setup_if_no_interactive(
15461546
assert (
15471547
result.output.strip() == f"Wrote new connection conn to {test_snowcli_config}"
15481548
)
1549+
1550+
1551+
@mock.patch("snowflake.cli._plugins.auth.keypair.manager.AuthManager.execute_query")
1552+
@mock.patch("snowflake.cli._plugins.object.manager.ObjectManager.execute_query")
1553+
@mock.patch("snowflake.connector.connect")
1554+
def test_connection_add_with_key_pair_saves_password_if_keypair_is_set(
1555+
mock_connect,
1556+
mock_object_execute_query,
1557+
mock_auth_execute_query,
1558+
runner,
1559+
tmp_path,
1560+
mock_cursor,
1561+
test_snowcli_config,
1562+
):
1563+
mock_connect.return_value.user = "user"
1564+
mock_object_execute_query.return_value = mock_cursor(
1565+
rows=[
1566+
{"property": "RSA_PUBLIC_KEY", "value": None},
1567+
{"property": "RSA_PUBLIC_KEY_2", "value": "public key..."},
1568+
],
1569+
columns=[],
1570+
)
1571+
1572+
result = runner.invoke(
1573+
[
1574+
"connection",
1575+
"add",
1576+
],
1577+
input="conn\n" # connection name: zz
1578+
"test\n" # account:
1579+
"user\n" # user:
1580+
"123\n" # password:
1581+
"\n" # role:
1582+
"\n" # warehouse:
1583+
"\n" # database:
1584+
"\n" # schema:
1585+
"\n" # host:
1586+
"\n" # port:
1587+
"\n" # region:
1588+
"\n" # authenticator:
1589+
"\n" # private key file:
1590+
"\n" # token file path:
1591+
"y\n" #
1592+
"\n" # key_length
1593+
f"{tmp_path}\n" # output_path
1594+
"123\n", # passphrase
1595+
)
1596+
1597+
private_key_path = tmp_path / "conn.p8"
1598+
public_key_path = tmp_path / "conn.pub"
1599+
assert result.exit_code == 0, result.output
1600+
assert result.output == dedent(
1601+
f"""\
1602+
Enter connection name: conn
1603+
Enter account: test
1604+
Enter user: user
1605+
Enter password:
1606+
Enter role:
1607+
Enter warehouse:
1608+
Enter database:
1609+
Enter schema:
1610+
Enter host:
1611+
Enter port:
1612+
Enter region:
1613+
Enter authenticator:
1614+
Enter private key file:
1615+
Enter token file path:
1616+
Do you want to configure key pair authentication? [y/N]: y
1617+
Key length [2048]:
1618+
Output path [~/.ssh]: {tmp_path}
1619+
Private key passphrase:
1620+
Wrote new password-based connection conn to {test_snowcli_config}, however there were some issues during key pair setup. Review the following error and check 'snow auth keypair' commands to setup key pair authentication:
1621+
* The public key is set already.
1622+
"""
1623+
)
1624+
assert not private_key_path.exists()
1625+
assert not public_key_path.exists()
1626+
with open(test_snowcli_config, "r") as f:
1627+
connections = tomlkit.load(f)
1628+
assert connections["connections"]["conn"]["password"] == "123"

0 commit comments

Comments
 (0)