snowflake-cli 3.5.0__py3-none-any.whl → 3.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snowflake/cli/__about__.py +13 -1
- snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -0
- snowflake/cli/_app/snow_connector.py +5 -4
- snowflake/cli/_app/telemetry.py +3 -15
- snowflake/cli/_app/version_check.py +4 -4
- snowflake/cli/_plugins/auth/__init__.py +11 -0
- snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
- snowflake/cli/_plugins/auth/keypair/commands.py +151 -0
- snowflake/cli/_plugins/auth/keypair/manager.py +331 -0
- snowflake/cli/_plugins/auth/keypair/plugin_spec.py +30 -0
- snowflake/cli/_plugins/connection/commands.py +77 -1
- snowflake/cli/_plugins/nativeapp/entities/application.py +4 -1
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +33 -6
- snowflake/cli/_plugins/object/command_aliases.py +3 -1
- snowflake/cli/_plugins/object/manager.py +4 -2
- snowflake/cli/_plugins/project/commands.py +16 -0
- snowflake/cli/_plugins/spcs/compute_pool/commands.py +17 -5
- snowflake/cli/_plugins/sql/manager.py +42 -51
- snowflake/cli/_plugins/sql/source_reader.py +230 -0
- snowflake/cli/_plugins/stage/manager.py +8 -2
- snowflake/cli/api/commands/flags.py +12 -2
- snowflake/cli/api/constants.py +2 -0
- snowflake/cli/api/errno.py +1 -0
- snowflake/cli/api/exceptions.py +7 -0
- snowflake/cli/api/feature_flags.py +1 -0
- snowflake/cli/api/rest_api.py +2 -3
- snowflake/cli/{_app → api}/secret.py +4 -1
- snowflake/cli/api/secure_path.py +16 -4
- snowflake/cli/api/sql_execution.py +7 -3
- {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/METADATA +7 -7
- {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/RECORD +34 -28
- {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
|
@@ -14,4 +14,16 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
from enum import Enum, unique
|
|
18
|
+
|
|
19
|
+
VERSION = "3.6.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@unique
|
|
23
|
+
class CLIInstallationSource(Enum):
|
|
24
|
+
BINARY = "binary"
|
|
25
|
+
PYPI = "pypi"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# This variable is changed in binary release script
|
|
29
|
+
INSTALLATION_SOURCE = CLIInstallationSource.PYPI
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
from snowflake.cli._plugins.auth.keypair import plugin_spec as auth_plugin_spec
|
|
15
16
|
from snowflake.cli._plugins.connection import plugin_spec as connection_plugin_spec
|
|
16
17
|
from snowflake.cli._plugins.cortex import plugin_spec as cortex_plugin_spec
|
|
17
18
|
from snowflake.cli._plugins.git import plugin_spec as git_plugin_spec
|
|
@@ -33,6 +34,7 @@ from snowflake.cli._plugins.workspace import plugin_spec as workspace_plugin_spe
|
|
|
33
34
|
# plugin name to plugin spec
|
|
34
35
|
def get_builtin_plugin_name_to_plugin_spec():
|
|
35
36
|
plugin_specs = {
|
|
37
|
+
"auth": auth_plugin_spec,
|
|
36
38
|
"connection": connection_plugin_spec,
|
|
37
39
|
"helpers": migrate_plugin_spec,
|
|
38
40
|
"spcs": spcs_plugin_spec,
|
|
@@ -21,12 +21,11 @@ from typing import Dict, Optional
|
|
|
21
21
|
|
|
22
22
|
import snowflake.connector
|
|
23
23
|
from click.exceptions import ClickException
|
|
24
|
-
from snowflake.cli
|
|
24
|
+
from snowflake.cli import __about__
|
|
25
25
|
from snowflake.cli._app.constants import (
|
|
26
26
|
INTERNAL_APPLICATION_NAME,
|
|
27
27
|
PARAM_APPLICATION_NAME,
|
|
28
28
|
)
|
|
29
|
-
from snowflake.cli._app.secret import SecretType
|
|
30
29
|
from snowflake.cli._app.telemetry import command_info
|
|
31
30
|
from snowflake.cli.api.config import (
|
|
32
31
|
get_connection_dict,
|
|
@@ -38,6 +37,7 @@ from snowflake.cli.api.exceptions import (
|
|
|
38
37
|
SnowflakeConnectionError,
|
|
39
38
|
)
|
|
40
39
|
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
40
|
+
from snowflake.cli.api.secret import SecretType
|
|
41
41
|
from snowflake.cli.api.secure_path import SecurePath
|
|
42
42
|
from snowflake.connector import SnowflakeConnection
|
|
43
43
|
from snowflake.connector.errors import DatabaseError, ForbiddenError
|
|
@@ -56,6 +56,7 @@ SUPPORTED_ENV_OVERRIDES = [
|
|
|
56
56
|
"authenticator",
|
|
57
57
|
"private_key_file",
|
|
58
58
|
"private_key_path",
|
|
59
|
+
"private_key_raw",
|
|
59
60
|
"database",
|
|
60
61
|
"schema",
|
|
61
62
|
"role",
|
|
@@ -247,7 +248,7 @@ def _update_internal_application_info(connection_parameters: Dict):
|
|
|
247
248
|
"""Update internal application data if ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID is enabled."""
|
|
248
249
|
if FeatureFlag.ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID.is_enabled():
|
|
249
250
|
connection_parameters["internal_application_name"] = INTERNAL_APPLICATION_NAME
|
|
250
|
-
connection_parameters["internal_application_version"] = VERSION
|
|
251
|
+
connection_parameters["internal_application_version"] = __about__.VERSION
|
|
251
252
|
|
|
252
253
|
|
|
253
254
|
def _load_pem_from_file(private_key_file: str) -> SecretType:
|
|
@@ -273,7 +274,7 @@ def _load_pem_to_der(private_key_pem: SecretType) -> SecretType:
|
|
|
273
274
|
and private_key_passphrase.value is None
|
|
274
275
|
):
|
|
275
276
|
raise ClickException(
|
|
276
|
-
"Encrypted private key, you must provide the"
|
|
277
|
+
"Encrypted private key, you must provide the "
|
|
277
278
|
"passphrase in the environment variable PRIVATE_KEY_PASSPHRASE"
|
|
278
279
|
)
|
|
279
280
|
|
snowflake/cli/_app/telemetry.py
CHANGED
|
@@ -22,7 +22,7 @@ from typing import Any, Dict, Union
|
|
|
22
22
|
|
|
23
23
|
import click
|
|
24
24
|
import typer
|
|
25
|
-
from snowflake.cli
|
|
25
|
+
from snowflake.cli import __about__
|
|
26
26
|
from snowflake.cli._app.constants import PARAM_APPLICATION_NAME
|
|
27
27
|
from snowflake.cli.api.cli_global_context import (
|
|
28
28
|
_CliGlobalContextAccess,
|
|
@@ -40,12 +40,6 @@ from snowflake.connector.telemetry import (
|
|
|
40
40
|
from snowflake.connector.time_util import get_time_millis
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
@unique
|
|
44
|
-
class CLIInstallationSource(Enum):
|
|
45
|
-
BINARY = "binary"
|
|
46
|
-
PYPI = "pypi"
|
|
47
|
-
|
|
48
|
-
|
|
49
43
|
@unique
|
|
50
44
|
class CLITelemetryField(Enum):
|
|
51
45
|
# Basic information
|
|
@@ -172,12 +166,6 @@ def _get_definition_version() -> str | None:
|
|
|
172
166
|
return None
|
|
173
167
|
|
|
174
168
|
|
|
175
|
-
def _get_installation_source() -> CLIInstallationSource:
|
|
176
|
-
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
|
177
|
-
return CLIInstallationSource.BINARY
|
|
178
|
-
return CLIInstallationSource.PYPI
|
|
179
|
-
|
|
180
|
-
|
|
181
169
|
def _get_ci_environment_type() -> str:
|
|
182
170
|
if "GITHUB_ACTIONS" in os.environ:
|
|
183
171
|
return "GITHUB_ACTIONS"
|
|
@@ -214,8 +202,8 @@ class CLITelemetryClient:
|
|
|
214
202
|
) -> Dict[str, Any]:
|
|
215
203
|
data = {
|
|
216
204
|
CLITelemetryField.SOURCE: PARAM_APPLICATION_NAME,
|
|
217
|
-
CLITelemetryField.INSTALLATION_SOURCE:
|
|
218
|
-
CLITelemetryField.VERSION_CLI: VERSION,
|
|
205
|
+
CLITelemetryField.INSTALLATION_SOURCE: __about__.INSTALLATION_SOURCE.value,
|
|
206
|
+
CLITelemetryField.VERSION_CLI: __about__.VERSION,
|
|
219
207
|
CLITelemetryField.VERSION_OS: platform.platform(),
|
|
220
208
|
CLITelemetryField.VERSION_PYTHON: python_version(),
|
|
221
209
|
CLITelemetryField.COMMAND_CI_ENVIRONMENT: _get_ci_environment_type(),
|
|
@@ -8,6 +8,8 @@ from snowflake.cli.api.console import cli_console
|
|
|
8
8
|
from snowflake.cli.api.secure_path import SecurePath
|
|
9
9
|
from snowflake.connector.config_manager import CONFIG_MANAGER
|
|
10
10
|
|
|
11
|
+
REPOSITORY_URL = "https://pypi.org/pypi/snowflake-cli/json"
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
def get_new_version_msg() -> str | None:
|
|
13
15
|
last = _VersionCache().get_last_version()
|
|
@@ -45,9 +47,7 @@ class _VersionCache:
|
|
|
45
47
|
@staticmethod
|
|
46
48
|
def _get_version_from_pypi() -> str | None:
|
|
47
49
|
headers = {"Content-Type": "application/vnd.pypi.simple.v1+json"}
|
|
48
|
-
response = requests.get(
|
|
49
|
-
"https://pypi.org/pypi/snowflake-cli-labs/json", headers=headers, timeout=3
|
|
50
|
-
)
|
|
50
|
+
response = requests.get(REPOSITORY_URL, headers=headers, timeout=3)
|
|
51
51
|
response.raise_for_status()
|
|
52
52
|
return response.json()["info"]["version"]
|
|
53
53
|
|
|
@@ -60,7 +60,7 @@ class _VersionCache:
|
|
|
60
60
|
|
|
61
61
|
def _read_latest_version(self) -> Version | None:
|
|
62
62
|
if self._cache_file.exists():
|
|
63
|
-
data = json.loads(self._cache_file.read_text())
|
|
63
|
+
data = json.loads(self._cache_file.read_text(file_size_limit_mb=1))
|
|
64
64
|
now = time.time()
|
|
65
65
|
if data[_VersionCache._last_time] > now - 60 * 60:
|
|
66
66
|
return Version(data[_VersionCache._version])
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from snowflake.cli._plugins.auth.keypair.commands import app as keypair_app
|
|
2
|
+
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
3
|
+
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
4
|
+
|
|
5
|
+
app = SnowTyperFactory(
|
|
6
|
+
name="auth",
|
|
7
|
+
help="Manages authentication methods.",
|
|
8
|
+
is_hidden=lambda: FeatureFlag.ENABLE_AUTH_KEYPAIR.is_disabled(),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
app.add_typer(keypair_app)
|
|
File without changes
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from snowflake.cli._plugins.auth.keypair.manager import AuthManager, PublicKeyProperty
|
|
5
|
+
from snowflake.cli.api.commands.flags import SecretTypeParser
|
|
6
|
+
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
7
|
+
from snowflake.cli.api.output.types import (
|
|
8
|
+
CollectionResult,
|
|
9
|
+
CommandResult,
|
|
10
|
+
MessageResult,
|
|
11
|
+
SingleQueryResult,
|
|
12
|
+
)
|
|
13
|
+
from snowflake.cli.api.secret import SecretType
|
|
14
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
15
|
+
|
|
16
|
+
app = SnowTyperFactory(
|
|
17
|
+
name="keypair",
|
|
18
|
+
help="Manages authentication.",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
KEY_PAIR_DEFAULT_PATH = "~/.ssh"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _show_connection_name_prompt(ctx: typer.Context, value: str):
|
|
26
|
+
for param in ctx.command.params:
|
|
27
|
+
if param.name == "connection_name":
|
|
28
|
+
if value:
|
|
29
|
+
param.prompt = "Enter connection name"
|
|
30
|
+
break
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_new_connection_option = typer.Option(
|
|
35
|
+
True,
|
|
36
|
+
help="Create a new connection.",
|
|
37
|
+
prompt="Create a new connection?",
|
|
38
|
+
callback=_show_connection_name_prompt,
|
|
39
|
+
show_default=False,
|
|
40
|
+
hidden=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_connection_name_option = typer.Option(
|
|
45
|
+
None,
|
|
46
|
+
help="The new connection name.",
|
|
47
|
+
prompt=False,
|
|
48
|
+
show_default=False,
|
|
49
|
+
hidden=True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_key_length_option = typer.Option(
|
|
54
|
+
2048,
|
|
55
|
+
"--key-length",
|
|
56
|
+
help="The RSA key length.",
|
|
57
|
+
prompt="Enter key length",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_output_path_option = typer.Option(
|
|
62
|
+
KEY_PAIR_DEFAULT_PATH,
|
|
63
|
+
"--output-path",
|
|
64
|
+
help="The output path for private and public keys",
|
|
65
|
+
prompt="Enter output path",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_private_key_passphrase_option = typer.Option(
|
|
70
|
+
"",
|
|
71
|
+
"--private-key-passphrase",
|
|
72
|
+
help="The private key passphrase.",
|
|
73
|
+
click_type=SecretTypeParser(),
|
|
74
|
+
prompt="Enter private key passphrase",
|
|
75
|
+
hide_input=True,
|
|
76
|
+
show_default=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.command("setup", requires_connection=True)
|
|
81
|
+
def setup(
|
|
82
|
+
new_connection: bool = _new_connection_option,
|
|
83
|
+
connection_name: str = _connection_name_option,
|
|
84
|
+
key_length: int = _key_length_option,
|
|
85
|
+
output_path: Path = _output_path_option,
|
|
86
|
+
private_key_passphrase: SecretType = _private_key_passphrase_option,
|
|
87
|
+
**options,
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Generates the key pair, sets the public key for the user in Snowflake, and creates or updates the connection.
|
|
91
|
+
"""
|
|
92
|
+
AuthManager().setup(
|
|
93
|
+
connection_name=connection_name,
|
|
94
|
+
key_length=key_length,
|
|
95
|
+
output_path=SecurePath(output_path),
|
|
96
|
+
private_key_passphrase=private_key_passphrase,
|
|
97
|
+
)
|
|
98
|
+
return MessageResult(f"Setup completed.")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("rotate", requires_connection=True)
|
|
102
|
+
def rotate(
|
|
103
|
+
key_length: int = _key_length_option,
|
|
104
|
+
output_path: Path = _output_path_option,
|
|
105
|
+
private_key_passphrase: SecretType = _private_key_passphrase_option,
|
|
106
|
+
**options,
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Rotates the key for the connection. Generates the key pair, sets the public key for the user in Snowflake, and creates or updates the connection.
|
|
110
|
+
"""
|
|
111
|
+
AuthManager().rotate(
|
|
112
|
+
key_length=key_length,
|
|
113
|
+
output_path=SecurePath(output_path),
|
|
114
|
+
private_key_passphrase=private_key_passphrase,
|
|
115
|
+
)
|
|
116
|
+
return MessageResult(f"Rotate completed.")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command("list", requires_connection=True)
|
|
120
|
+
def list_keys(**options) -> CommandResult:
|
|
121
|
+
"""
|
|
122
|
+
Lists the public keys set for the user.
|
|
123
|
+
"""
|
|
124
|
+
return CollectionResult(AuthManager().list_keys())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command("remove", requires_connection=True)
|
|
128
|
+
def remove(
|
|
129
|
+
public_key_property: PublicKeyProperty = typer.Option(
|
|
130
|
+
...,
|
|
131
|
+
"--key-id",
|
|
132
|
+
help=f"Local path to the template directory or a URL to Git repository with templates.",
|
|
133
|
+
show_default=False,
|
|
134
|
+
),
|
|
135
|
+
**options,
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
Removes the public key for the user.
|
|
139
|
+
"""
|
|
140
|
+
return SingleQueryResult(
|
|
141
|
+
AuthManager().remove_public_key(public_key_property=public_key_property)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command("status", requires_connection=True)
|
|
146
|
+
def status(**options):
|
|
147
|
+
"""
|
|
148
|
+
Verifies the key pair configuration and tests the connection.
|
|
149
|
+
"""
|
|
150
|
+
AuthManager().status()
|
|
151
|
+
return MessageResult("Status check completed.")
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Dict, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from click import ClickException
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
7
|
+
from snowflake.cli._plugins.object.manager import ObjectManager
|
|
8
|
+
from snowflake.cli.api import exceptions
|
|
9
|
+
from snowflake.cli.api.cli_global_context import (
|
|
10
|
+
_CliGlobalContextAccess,
|
|
11
|
+
get_cli_context,
|
|
12
|
+
)
|
|
13
|
+
from snowflake.cli.api.config import (
|
|
14
|
+
connection_exists,
|
|
15
|
+
get_connection_dict,
|
|
16
|
+
set_config_value,
|
|
17
|
+
)
|
|
18
|
+
from snowflake.cli.api.console import cli_console
|
|
19
|
+
from snowflake.cli.api.constants import ObjectType
|
|
20
|
+
from snowflake.cli.api.identifiers import FQN
|
|
21
|
+
from snowflake.cli.api.secret import SecretType
|
|
22
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
23
|
+
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
24
|
+
from snowflake.connector import DictCursor
|
|
25
|
+
from snowflake.connector.cursor import SnowflakeCursor
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PublicKeyProperty(Enum):
|
|
29
|
+
RSA_PUBLIC_KEY = "RSA_PUBLIC_KEY"
|
|
30
|
+
RSA_PUBLIC_KEY_2 = "RSA_PUBLIC_KEY_2"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthManager(SqlExecutionMixin):
|
|
34
|
+
def setup(
|
|
35
|
+
self,
|
|
36
|
+
connection_name: Optional[str],
|
|
37
|
+
key_length: int,
|
|
38
|
+
output_path: SecurePath,
|
|
39
|
+
private_key_passphrase: SecretType,
|
|
40
|
+
):
|
|
41
|
+
# When the user provide new connection name
|
|
42
|
+
if connection_name and connection_exists(connection_name):
|
|
43
|
+
raise ClickException(
|
|
44
|
+
f"Connection with name {connection_name} already exists."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
cli_context = get_cli_context()
|
|
48
|
+
# When the use not provide connection name, so we overwrite the current connection
|
|
49
|
+
if not connection_name:
|
|
50
|
+
connection_name = cli_context.connection_context.connection_name
|
|
51
|
+
|
|
52
|
+
key_name = AuthManager._get_free_key_name(output_path, connection_name) # type: ignore[arg-type]
|
|
53
|
+
self._generate_key_pair_and_set_public_key(
|
|
54
|
+
user=cli_context.connection.user,
|
|
55
|
+
key_length=key_length,
|
|
56
|
+
output_path=output_path,
|
|
57
|
+
key_name=key_name, # type: ignore[arg-type]
|
|
58
|
+
private_key_passphrase=private_key_passphrase,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self._create_or_update_connection(
|
|
62
|
+
current_connection=cli_context.connection_context.connection_name,
|
|
63
|
+
connection_name=connection_name, # type: ignore[arg-type]
|
|
64
|
+
private_key_path=self._get_private_key_path(
|
|
65
|
+
output_path=output_path, key_name=key_name # type: ignore[arg-type]
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def rotate(
|
|
70
|
+
self,
|
|
71
|
+
key_length: int,
|
|
72
|
+
output_path: SecurePath,
|
|
73
|
+
private_key_passphrase: SecretType,
|
|
74
|
+
):
|
|
75
|
+
cli_context = get_cli_context()
|
|
76
|
+
connection_name = cli_context.connection_context.connection_name
|
|
77
|
+
|
|
78
|
+
self._ensure_connection_has_private_key(
|
|
79
|
+
cli_context.connection_context.connection_name
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
public_key, public_key_2 = self._get_public_keys()
|
|
83
|
+
|
|
84
|
+
if not public_key and not public_key_2:
|
|
85
|
+
raise ClickException("No public key found. Use the setup command first.")
|
|
86
|
+
|
|
87
|
+
if public_key_2:
|
|
88
|
+
self.set_public_key(
|
|
89
|
+
cli_context.connection.user,
|
|
90
|
+
PublicKeyProperty.RSA_PUBLIC_KEY,
|
|
91
|
+
public_key_2,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
key_name = AuthManager._get_free_key_name(output_path, connection_name)
|
|
95
|
+
public_key = self._generate_keys_and_return_public_key(
|
|
96
|
+
key_length=key_length,
|
|
97
|
+
output_path=output_path,
|
|
98
|
+
key_name=key_name,
|
|
99
|
+
private_key_passphrase=private_key_passphrase,
|
|
100
|
+
)
|
|
101
|
+
self.set_public_key(
|
|
102
|
+
cli_context.connection.user, PublicKeyProperty.RSA_PUBLIC_KEY_2, public_key
|
|
103
|
+
)
|
|
104
|
+
self._create_or_update_connection(
|
|
105
|
+
current_connection=cli_context.connection_context.connection_name,
|
|
106
|
+
connection_name=connection_name,
|
|
107
|
+
private_key_path=self._get_private_key_path(
|
|
108
|
+
output_path=output_path, key_name=key_name
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _generate_key_pair_and_set_public_key(
|
|
113
|
+
self,
|
|
114
|
+
user: str,
|
|
115
|
+
key_length: int,
|
|
116
|
+
output_path: SecurePath,
|
|
117
|
+
key_name: str,
|
|
118
|
+
private_key_passphrase: SecretType,
|
|
119
|
+
):
|
|
120
|
+
public_key_exists, public_key_2_exists = self._get_public_keys()
|
|
121
|
+
|
|
122
|
+
if public_key_exists or public_key_2_exists:
|
|
123
|
+
raise exceptions.CouldNotSetKeyPairError()
|
|
124
|
+
|
|
125
|
+
if not output_path.exists():
|
|
126
|
+
output_path.mkdir(parents=True)
|
|
127
|
+
|
|
128
|
+
public_key = self._generate_keys_and_return_public_key(
|
|
129
|
+
key_length=key_length,
|
|
130
|
+
output_path=output_path,
|
|
131
|
+
key_name=key_name, # type: ignore[arg-type]
|
|
132
|
+
private_key_passphrase=private_key_passphrase,
|
|
133
|
+
)
|
|
134
|
+
self.set_public_key(user, PublicKeyProperty.RSA_PUBLIC_KEY, public_key)
|
|
135
|
+
|
|
136
|
+
def list_keys(self) -> List[Dict]:
|
|
137
|
+
key_properties = [
|
|
138
|
+
"RSA_PUBLIC_KEY",
|
|
139
|
+
"RSA_PUBLIC_KEY_FP",
|
|
140
|
+
"RSA_PUBLIC_KEY_LAST_SET_TIME",
|
|
141
|
+
"RSA_PUBLIC_KEY_2",
|
|
142
|
+
"RSA_PUBLIC_KEY_2_FP",
|
|
143
|
+
"RSA_PUBLIC_KEY_2_LAST_SET_TIME",
|
|
144
|
+
]
|
|
145
|
+
cursor = ObjectManager(connection=self._conn).describe(
|
|
146
|
+
object_type=ObjectType.USER.value.sf_name,
|
|
147
|
+
fqn=FQN.from_string(self._conn.user),
|
|
148
|
+
cursor_class=DictCursor,
|
|
149
|
+
)
|
|
150
|
+
only_public_key_properties = [
|
|
151
|
+
p for p in cursor.fetchall() if p.get("property") in key_properties
|
|
152
|
+
]
|
|
153
|
+
return only_public_key_properties
|
|
154
|
+
|
|
155
|
+
def set_public_key(
|
|
156
|
+
self, user: str, public_key_property: PublicKeyProperty, public_key: str
|
|
157
|
+
) -> SnowflakeCursor:
|
|
158
|
+
return self.execute_query(
|
|
159
|
+
f"ALTER USER {user} SET {public_key_property.value}='{public_key}'"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def remove_public_key(
|
|
163
|
+
self, public_key_property: PublicKeyProperty
|
|
164
|
+
) -> SnowflakeCursor:
|
|
165
|
+
cli_context = get_cli_context()
|
|
166
|
+
return self.execute_query(
|
|
167
|
+
f"ALTER USER {cli_context.connection.user} UNSET {public_key_property.value}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def status(self):
|
|
171
|
+
cli_context = get_cli_context()
|
|
172
|
+
self._ensure_connection_has_private_key(
|
|
173
|
+
cli_context.connection_context.connection_name
|
|
174
|
+
)
|
|
175
|
+
cli_console.step("Private key set for connection - OK")
|
|
176
|
+
self._check_connection(cli_context)
|
|
177
|
+
cli_console.step("Test connection - OK")
|
|
178
|
+
|
|
179
|
+
def extend_connection_add(
|
|
180
|
+
self,
|
|
181
|
+
connection_name: str,
|
|
182
|
+
connection_options: Dict,
|
|
183
|
+
key_length: int,
|
|
184
|
+
output_path: SecurePath,
|
|
185
|
+
private_key_passphrase: SecretType,
|
|
186
|
+
) -> Dict:
|
|
187
|
+
key_name = AuthManager._get_free_key_name(output_path, connection_name)
|
|
188
|
+
|
|
189
|
+
self._generate_key_pair_and_set_public_key(
|
|
190
|
+
user=connection_options["user"],
|
|
191
|
+
key_length=key_length,
|
|
192
|
+
output_path=output_path,
|
|
193
|
+
key_name=key_name,
|
|
194
|
+
private_key_passphrase=private_key_passphrase,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
connection_options["authenticator"] = "SNOWFLAKE_JWT"
|
|
198
|
+
connection_options["private_key_file"] = str(
|
|
199
|
+
self._get_private_key_path(output_path=output_path, key_name=key_name).path
|
|
200
|
+
)
|
|
201
|
+
if connection_options.get("password"):
|
|
202
|
+
del connection_options["password"]
|
|
203
|
+
|
|
204
|
+
return connection_options
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _ensure_connection_has_private_key(connection_name: str) -> None:
|
|
208
|
+
connection = get_connection_dict(connection_name)
|
|
209
|
+
if not connection.get("private_key_file") and not connection.get(
|
|
210
|
+
"private_key_path"
|
|
211
|
+
):
|
|
212
|
+
raise ClickException(
|
|
213
|
+
f"The private key is not set in {connection_name} connection."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _check_connection(cli_context: _CliGlobalContextAccess) -> None:
|
|
218
|
+
cli_context.connection
|
|
219
|
+
|
|
220
|
+
def _get_public_keys(self) -> Tuple[str, str]:
|
|
221
|
+
keys = self.list_keys()
|
|
222
|
+
public_key = ""
|
|
223
|
+
public_key_2 = ""
|
|
224
|
+
for p in keys:
|
|
225
|
+
if (
|
|
226
|
+
p.get("property") == PublicKeyProperty.RSA_PUBLIC_KEY.value
|
|
227
|
+
and p.get("value") != "null"
|
|
228
|
+
):
|
|
229
|
+
public_key = p.get("value") # type: ignore
|
|
230
|
+
if (
|
|
231
|
+
p.get("property") == PublicKeyProperty.RSA_PUBLIC_KEY_2.value
|
|
232
|
+
and p.get("value") != "null"
|
|
233
|
+
):
|
|
234
|
+
public_key_2 = p.get("value") # type: ignore
|
|
235
|
+
return public_key, public_key_2
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def _generate_keys_and_return_public_key(
|
|
239
|
+
key_length: int,
|
|
240
|
+
output_path: SecurePath,
|
|
241
|
+
key_name: str,
|
|
242
|
+
private_key_passphrase: SecretType,
|
|
243
|
+
) -> str:
|
|
244
|
+
private_key = rsa.generate_private_key(
|
|
245
|
+
public_exponent=65537,
|
|
246
|
+
key_size=key_length,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if private_key_passphrase:
|
|
250
|
+
pem = SecretType(
|
|
251
|
+
private_key.private_bytes(
|
|
252
|
+
encoding=serialization.Encoding.PEM,
|
|
253
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
254
|
+
encryption_algorithm=serialization.BestAvailableEncryption(
|
|
255
|
+
private_key_passphrase.value.encode("utf-8")
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
cli_console.message(
|
|
260
|
+
"Set the `PRIVATE_KEY_PASSPHRASE` environment variable before using the connection."
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
pem = SecretType(
|
|
264
|
+
private_key.private_bytes(
|
|
265
|
+
encoding=serialization.Encoding.PEM,
|
|
266
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
267
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
with AuthManager._get_private_key_path(output_path, key_name).open(
|
|
272
|
+
mode="wb"
|
|
273
|
+
) as file:
|
|
274
|
+
file.write(pem.value)
|
|
275
|
+
|
|
276
|
+
public_key = private_key.public_key()
|
|
277
|
+
public_pem = public_key.public_bytes(
|
|
278
|
+
encoding=serialization.Encoding.PEM,
|
|
279
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
280
|
+
)
|
|
281
|
+
with AuthManager._get_public_key_path(output_path, key_name).open(
|
|
282
|
+
mode="wb"
|
|
283
|
+
) as file:
|
|
284
|
+
file.write(public_pem)
|
|
285
|
+
|
|
286
|
+
return public_pem.decode("utf-8")
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def _get_free_key_name(output_path: SecurePath, key_name: str) -> str:
|
|
290
|
+
new_private_key = f"{key_name}.p8"
|
|
291
|
+
new_public_key = f"{key_name}.pub"
|
|
292
|
+
new_key_name = key_name
|
|
293
|
+
counter = 1
|
|
294
|
+
|
|
295
|
+
while (
|
|
296
|
+
(output_path / new_private_key).exists()
|
|
297
|
+
and (output_path / new_public_key).exists()
|
|
298
|
+
and counter <= 100
|
|
299
|
+
):
|
|
300
|
+
new_key_name = f"{key_name}_{counter}"
|
|
301
|
+
new_private_key = f"{new_key_name}.p8"
|
|
302
|
+
new_public_key = f"{new_key_name}.pub"
|
|
303
|
+
counter += 1
|
|
304
|
+
|
|
305
|
+
if counter == 100:
|
|
306
|
+
raise ClickException(
|
|
307
|
+
"Too many key pairs with the same name in the output directory."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return new_key_name
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _get_private_key_path(output_path: SecurePath, key_name: str) -> SecurePath:
|
|
314
|
+
return (output_path / f"{key_name}.p8").resolve()
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def _get_public_key_path(output_path: SecurePath, key_name: str) -> SecurePath:
|
|
318
|
+
return (output_path / f"{key_name}.pub").resolve()
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def _create_or_update_connection(
|
|
322
|
+
current_connection: Optional[str],
|
|
323
|
+
connection_name: str,
|
|
324
|
+
private_key_path: SecurePath,
|
|
325
|
+
):
|
|
326
|
+
connection = get_connection_dict(current_connection)
|
|
327
|
+
connection.pop("password", None)
|
|
328
|
+
connection["authenticator"] = "SNOWFLAKE_JWT"
|
|
329
|
+
connection["private_key_file"] = str(private_key_path.path)
|
|
330
|
+
|
|
331
|
+
set_config_value(["connections", connection_name], value=connection)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from snowflake.cli._plugins.auth import app
|
|
16
|
+
from snowflake.cli.api.plugins.command import (
|
|
17
|
+
SNOWCLI_ROOT_COMMAND_PATH,
|
|
18
|
+
CommandSpec,
|
|
19
|
+
CommandType,
|
|
20
|
+
plugin_hook_impl,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@plugin_hook_impl
|
|
25
|
+
def command_spec():
|
|
26
|
+
return CommandSpec(
|
|
27
|
+
parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
|
|
28
|
+
command_type=CommandType.COMMAND_GROUP,
|
|
29
|
+
typer_instance=app.create_instance(),
|
|
30
|
+
)
|