snowflake-cli-labs 3.0.0rc4__py3-none-any.whl → 3.0.1__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.
- README.md +21 -0
- {snowflake_cli_labs-3.0.0rc4.dist-info → snowflake_cli_labs-3.0.1.dist-info}/METADATA +6 -96
- snowflake_cli_labs-3.0.1.dist-info/RECORD +5 -0
- snowflake/cli/__about__.py +0 -17
- snowflake/cli/__init__.py +0 -13
- snowflake/cli/_app/__init__.py +0 -22
- snowflake/cli/_app/__main__.py +0 -31
- snowflake/cli/_app/api_impl/__init__.py +0 -13
- snowflake/cli/_app/api_impl/plugin/__init__.py +0 -13
- snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
- snowflake/cli/_app/cli_app.py +0 -252
- snowflake/cli/_app/commands_registration/__init__.py +0 -33
- snowflake/cli/_app/commands_registration/builtin_plugins.py +0 -50
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +0 -169
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +0 -105
- snowflake/cli/_app/commands_registration/exception_logging.py +0 -26
- snowflake/cli/_app/commands_registration/threadsafe.py +0 -48
- snowflake/cli/_app/commands_registration/typer_registration.py +0 -153
- snowflake/cli/_app/constants.py +0 -19
- snowflake/cli/_app/dev/__init__.py +0 -13
- snowflake/cli/_app/dev/commands_structure.py +0 -48
- snowflake/cli/_app/dev/docs/__init__.py +0 -13
- snowflake/cli/_app/dev/docs/commands_docs_generator.py +0 -118
- snowflake/cli/_app/dev/docs/generator.py +0 -35
- snowflake/cli/_app/dev/docs/project_definition_docs_generator.py +0 -58
- snowflake/cli/_app/dev/docs/project_definition_generate_json_schema.py +0 -227
- snowflake/cli/_app/dev/docs/template_utils.py +0 -23
- snowflake/cli/_app/dev/docs/templates/definition_description.rst.jinja2 +0 -38
- snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +0 -9
- snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +0 -67
- snowflake/cli/_app/dev/pycharm_remote_debug.py +0 -46
- snowflake/cli/_app/loggers.py +0 -199
- snowflake/cli/_app/main_typer.py +0 -62
- snowflake/cli/_app/printing.py +0 -181
- snowflake/cli/_app/secret.py +0 -9
- snowflake/cli/_app/snow_connector.py +0 -309
- snowflake/cli/_app/telemetry.py +0 -220
- snowflake/cli/_app/version_check.py +0 -74
- snowflake/cli/_plugins/__init__.py +0 -13
- snowflake/cli/_plugins/connection/__init__.py +0 -13
- snowflake/cli/_plugins/connection/commands.py +0 -353
- snowflake/cli/_plugins/connection/plugin_spec.py +0 -30
- snowflake/cli/_plugins/connection/util.py +0 -195
- snowflake/cli/_plugins/cortex/__init__.py +0 -13
- snowflake/cli/_plugins/cortex/commands.py +0 -332
- snowflake/cli/_plugins/cortex/constants.py +0 -17
- snowflake/cli/_plugins/cortex/manager.py +0 -189
- snowflake/cli/_plugins/cortex/plugin_spec.py +0 -30
- snowflake/cli/_plugins/cortex/types.py +0 -22
- snowflake/cli/_plugins/git/__init__.py +0 -13
- snowflake/cli/_plugins/git/commands.py +0 -358
- snowflake/cli/_plugins/git/manager.py +0 -151
- snowflake/cli/_plugins/git/plugin_spec.py +0 -30
- snowflake/cli/_plugins/helpers/__init__.py +0 -13
- snowflake/cli/_plugins/helpers/commands.py +0 -61
- snowflake/cli/_plugins/helpers/plugin_spec.py +0 -30
- snowflake/cli/_plugins/init/__init__.py +0 -13
- snowflake/cli/_plugins/init/commands.py +0 -248
- snowflake/cli/_plugins/init/plugin_spec.py +0 -30
- snowflake/cli/_plugins/nativeapp/__init__.py +0 -13
- snowflake/cli/_plugins/nativeapp/artifacts.py +0 -757
- snowflake/cli/_plugins/nativeapp/bundle_context.py +0 -31
- snowflake/cli/_plugins/nativeapp/codegen/__init__.py +0 -13
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +0 -91
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +0 -149
- snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +0 -306
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +0 -249
- snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +0 -59
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/callback_source.py.jinja +0 -181
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +0 -217
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/models.py +0 -61
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +0 -523
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +0 -114
- snowflake/cli/_plugins/nativeapp/commands.py +0 -559
- snowflake/cli/_plugins/nativeapp/common_flags.py +0 -44
- snowflake/cli/_plugins/nativeapp/constants.py +0 -27
- snowflake/cli/_plugins/nativeapp/entities/__init__.py +0 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +0 -878
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +0 -1392
- snowflake/cli/_plugins/nativeapp/exceptions.py +0 -113
- snowflake/cli/_plugins/nativeapp/feature_flags.py +0 -24
- snowflake/cli/_plugins/nativeapp/manager.py +0 -415
- snowflake/cli/_plugins/nativeapp/plugin_spec.py +0 -30
- snowflake/cli/_plugins/nativeapp/policy.py +0 -53
- snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
- snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -70
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
- snowflake/cli/_plugins/nativeapp/utils.py +0 -98
- snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +0 -262
- snowflake/cli/_plugins/nativeapp/version/__init__.py +0 -13
- snowflake/cli/_plugins/nativeapp/version/commands.py +0 -141
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
- snowflake/cli/_plugins/notebook/__init__.py +0 -13
- snowflake/cli/_plugins/notebook/commands.py +0 -86
- snowflake/cli/_plugins/notebook/exceptions.py +0 -20
- snowflake/cli/_plugins/notebook/manager.py +0 -71
- snowflake/cli/_plugins/notebook/plugin_spec.py +0 -30
- snowflake/cli/_plugins/notebook/types.py +0 -15
- snowflake/cli/_plugins/object/__init__.py +0 -13
- snowflake/cli/_plugins/object/command_aliases.py +0 -95
- snowflake/cli/_plugins/object/commands.py +0 -180
- snowflake/cli/_plugins/object/common.py +0 -85
- snowflake/cli/_plugins/object/manager.py +0 -118
- snowflake/cli/_plugins/object/plugin_spec.py +0 -30
- snowflake/cli/_plugins/snowpark/__init__.py +0 -13
- snowflake/cli/_plugins/snowpark/commands.py +0 -450
- snowflake/cli/_plugins/snowpark/common.py +0 -268
- snowflake/cli/_plugins/snowpark/models.py +0 -150
- snowflake/cli/_plugins/snowpark/package/__init__.py +0 -13
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +0 -199
- snowflake/cli/_plugins/snowpark/package/commands.py +0 -195
- snowflake/cli/_plugins/snowpark/package/manager.py +0 -44
- snowflake/cli/_plugins/snowpark/package/utils.py +0 -26
- snowflake/cli/_plugins/snowpark/package_utils.py +0 -354
- snowflake/cli/_plugins/snowpark/plugin_spec.py +0 -30
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +0 -29
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +0 -173
- snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +0 -109
- snowflake/cli/_plugins/snowpark/snowpark_shared.py +0 -59
- snowflake/cli/_plugins/snowpark/zipper.py +0 -89
- snowflake/cli/_plugins/spcs/__init__.py +0 -33
- snowflake/cli/_plugins/spcs/common.py +0 -99
- snowflake/cli/_plugins/spcs/compute_pool/__init__.py +0 -13
- snowflake/cli/_plugins/spcs/compute_pool/commands.py +0 -241
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +0 -121
- snowflake/cli/_plugins/spcs/image_registry/__init__.py +0 -13
- snowflake/cli/_plugins/spcs/image_registry/commands.py +0 -65
- snowflake/cli/_plugins/spcs/image_registry/manager.py +0 -105
- snowflake/cli/_plugins/spcs/image_repository/__init__.py +0 -13
- snowflake/cli/_plugins/spcs/image_repository/commands.py +0 -202
- snowflake/cli/_plugins/spcs/image_repository/manager.py +0 -84
- snowflake/cli/_plugins/spcs/plugin_spec.py +0 -30
- snowflake/cli/_plugins/spcs/services/__init__.py +0 -13
- snowflake/cli/_plugins/spcs/services/commands.py +0 -345
- snowflake/cli/_plugins/spcs/services/manager.py +0 -208
- snowflake/cli/_plugins/sql/__init__.py +0 -13
- snowflake/cli/_plugins/sql/commands.py +0 -86
- snowflake/cli/_plugins/sql/manager.py +0 -92
- snowflake/cli/_plugins/sql/plugin_spec.py +0 -30
- snowflake/cli/_plugins/sql/snowsql_templating.py +0 -28
- snowflake/cli/_plugins/stage/__init__.py +0 -13
- snowflake/cli/_plugins/stage/commands.py +0 -264
- snowflake/cli/_plugins/stage/diff.py +0 -280
- snowflake/cli/_plugins/stage/manager.py +0 -582
- snowflake/cli/_plugins/stage/md5.py +0 -160
- snowflake/cli/_plugins/stage/plugin_spec.py +0 -30
- snowflake/cli/_plugins/stage/utils.py +0 -54
- snowflake/cli/_plugins/streamlit/__init__.py +0 -13
- snowflake/cli/_plugins/streamlit/commands.py +0 -195
- snowflake/cli/_plugins/streamlit/manager.py +0 -220
- snowflake/cli/_plugins/streamlit/plugin_spec.py +0 -30
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +0 -12
- snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +0 -66
- snowflake/cli/_plugins/workspace/__init__.py +0 -13
- snowflake/cli/_plugins/workspace/action_context.py +0 -18
- snowflake/cli/_plugins/workspace/commands.py +0 -306
- snowflake/cli/_plugins/workspace/manager.py +0 -74
- snowflake/cli/_plugins/workspace/plugin_spec.py +0 -30
- snowflake/cli/api/__init__.py +0 -48
- snowflake/cli/api/cli_global_context.py +0 -247
- snowflake/cli/api/commands/__init__.py +0 -13
- snowflake/cli/api/commands/alias.py +0 -23
- snowflake/cli/api/commands/common.py +0 -25
- snowflake/cli/api/commands/decorators.py +0 -369
- snowflake/cli/api/commands/execution_metadata.py +0 -40
- snowflake/cli/api/commands/experimental_behaviour.py +0 -18
- snowflake/cli/api/commands/flags.py +0 -561
- snowflake/cli/api/commands/overrideable_parameter.py +0 -143
- snowflake/cli/api/commands/snow_typer.py +0 -247
- snowflake/cli/api/commands/utils.py +0 -18
- snowflake/cli/api/config.py +0 -380
- snowflake/cli/api/connections.py +0 -216
- snowflake/cli/api/console/__init__.py +0 -17
- snowflake/cli/api/console/abc.py +0 -94
- snowflake/cli/api/console/console.py +0 -134
- snowflake/cli/api/console/enum.py +0 -17
- snowflake/cli/api/constants.py +0 -90
- snowflake/cli/api/entities/common.py +0 -56
- snowflake/cli/api/entities/utils.py +0 -370
- snowflake/cli/api/errno.py +0 -28
- snowflake/cli/api/exceptions.py +0 -190
- snowflake/cli/api/feature_flags.py +0 -54
- snowflake/cli/api/identifiers.py +0 -190
- snowflake/cli/api/metrics.py +0 -92
- snowflake/cli/api/output/__init__.py +0 -13
- snowflake/cli/api/output/formats.py +0 -20
- snowflake/cli/api/output/types.py +0 -118
- snowflake/cli/api/plugins/__init__.py +0 -13
- snowflake/cli/api/plugins/command/__init__.py +0 -72
- snowflake/cli/api/plugins/command/plugin_hook_specs.py +0 -21
- snowflake/cli/api/plugins/plugin_config.py +0 -32
- snowflake/cli/api/project/__init__.py +0 -13
- snowflake/cli/api/project/definition.py +0 -126
- snowflake/cli/api/project/definition_conversion.py +0 -395
- snowflake/cli/api/project/definition_manager.py +0 -145
- snowflake/cli/api/project/errors.py +0 -56
- snowflake/cli/api/project/project_verification.py +0 -23
- snowflake/cli/api/project/schemas/__init__.py +0 -13
- snowflake/cli/api/project/schemas/entities/__init__.py +0 -13
- snowflake/cli/api/project/schemas/entities/common.py +0 -153
- snowflake/cli/api/project/schemas/entities/entities.py +0 -61
- snowflake/cli/api/project/schemas/project_definition.py +0 -330
- snowflake/cli/api/project/schemas/template.py +0 -77
- snowflake/cli/api/project/schemas/updatable_model.py +0 -202
- snowflake/cli/api/project/schemas/v1/__init__.py +0 -0
- snowflake/cli/api/project/schemas/v1/identifier_model.py +0 -51
- snowflake/cli/api/project/schemas/v1/native_app/__init__.py +0 -0
- snowflake/cli/api/project/schemas/v1/native_app/application.py +0 -61
- snowflake/cli/api/project/schemas/v1/native_app/native_app.py +0 -93
- snowflake/cli/api/project/schemas/v1/native_app/package.py +0 -84
- snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
- snowflake/cli/api/project/schemas/v1/snowpark/__init__.py +0 -0
- snowflake/cli/api/project/schemas/v1/snowpark/argument.py +0 -28
- snowflake/cli/api/project/schemas/v1/snowpark/callable.py +0 -69
- snowflake/cli/api/project/schemas/v1/snowpark/snowpark.py +0 -36
- snowflake/cli/api/project/schemas/v1/streamlit/__init__.py +0 -0
- snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +0 -47
- snowflake/cli/api/project/util.py +0 -278
- snowflake/cli/api/rendering/__init__.py +0 -13
- snowflake/cli/api/rendering/jinja.py +0 -118
- snowflake/cli/api/rendering/project_definition_templates.py +0 -43
- snowflake/cli/api/rendering/project_templates.py +0 -98
- snowflake/cli/api/rendering/sql_templates.py +0 -105
- snowflake/cli/api/rest_api.py +0 -178
- snowflake/cli/api/sanitizers.py +0 -43
- snowflake/cli/api/secure_path.py +0 -360
- snowflake/cli/api/secure_utils.py +0 -118
- snowflake/cli/api/sql_execution.py +0 -280
- snowflake/cli/api/utils/__init__.py +0 -13
- snowflake/cli/api/utils/cursor.py +0 -34
- snowflake/cli/api/utils/definition_rendering.py +0 -415
- snowflake/cli/api/utils/dict_utils.py +0 -73
- snowflake/cli/api/utils/error_handling.py +0 -23
- snowflake/cli/api/utils/graph.py +0 -97
- snowflake/cli/api/utils/models.py +0 -63
- snowflake/cli/api/utils/naming_utils.py +0 -13
- snowflake/cli/api/utils/path_utils.py +0 -36
- snowflake/cli/api/utils/templating_functions.py +0 -144
- snowflake/cli/api/utils/types.py +0 -35
- snowflake_cli_labs-3.0.0rc4.dist-info/RECORD +0 -242
- snowflake_cli_labs-3.0.0rc4.dist-info/entry_points.txt +0 -2
- {snowflake_cli_labs-3.0.0rc4.dist-info → snowflake_cli_labs-3.0.1.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc4.dist-info → snowflake_cli_labs-3.0.1.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/api/rest_api.py
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
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 __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import logging
|
|
19
|
-
from typing import Any, Dict, Optional
|
|
20
|
-
|
|
21
|
-
from click import ClickException
|
|
22
|
-
from snowflake.cli.api.constants import SF_REST_API_URL_PREFIX
|
|
23
|
-
from snowflake.connector.connection import SnowflakeConnection
|
|
24
|
-
from snowflake.connector.errors import BadRequest
|
|
25
|
-
from snowflake.connector.network import SnowflakeRestful
|
|
26
|
-
from snowflake.connector.vendored.requests.exceptions import HTTPError
|
|
27
|
-
|
|
28
|
-
log = logging.getLogger(__name__)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _pluralize_object_type(object_type: str) -> str:
|
|
32
|
-
"""
|
|
33
|
-
Pluralize object type without depending on OBJECT_TO_NAMES.
|
|
34
|
-
"""
|
|
35
|
-
if object_type.endswith("y"):
|
|
36
|
-
return object_type[:-1].lower() + "ies"
|
|
37
|
-
else:
|
|
38
|
-
return object_type.lower() + "s"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class RestApi:
|
|
42
|
-
def __init__(self, connection: SnowflakeConnection):
|
|
43
|
-
self.conn = connection
|
|
44
|
-
self.rest: SnowflakeRestful = connection.rest
|
|
45
|
-
|
|
46
|
-
def get_endpoint_exists(self, url: str) -> bool:
|
|
47
|
-
"""
|
|
48
|
-
Check whether [get] endpoint exists under given URL.
|
|
49
|
-
"""
|
|
50
|
-
try:
|
|
51
|
-
self.send_rest_request(url, method="get")
|
|
52
|
-
return True
|
|
53
|
-
except HTTPError as err:
|
|
54
|
-
if err.response.status_code == 404:
|
|
55
|
-
return False
|
|
56
|
-
raise err
|
|
57
|
-
|
|
58
|
-
def _fetch_endpoint_exists(self, url: str) -> bool:
|
|
59
|
-
try:
|
|
60
|
-
result = self.send_rest_request(url, method="get")
|
|
61
|
-
return bool(result)
|
|
62
|
-
except BadRequest:
|
|
63
|
-
return False
|
|
64
|
-
except HTTPError as err:
|
|
65
|
-
if err.response.status_code == 404:
|
|
66
|
-
return False
|
|
67
|
-
raise err
|
|
68
|
-
|
|
69
|
-
def send_rest_request(
|
|
70
|
-
self, url: str, method: str, data: Optional[Dict[str, Any]] = None
|
|
71
|
-
):
|
|
72
|
-
"""
|
|
73
|
-
Executes rest request via snowflake.connector.network.SnowflakeRestful
|
|
74
|
-
"""
|
|
75
|
-
# SnowflakeRestful.request assumes that API response is always a dict,
|
|
76
|
-
# which is not true in case of this API, so we need to do this workaround:
|
|
77
|
-
from snowflake.connector.network import (
|
|
78
|
-
CONTENT_TYPE_APPLICATION_JSON,
|
|
79
|
-
HTTP_HEADER_ACCEPT,
|
|
80
|
-
HTTP_HEADER_CONTENT_TYPE,
|
|
81
|
-
HTTP_HEADER_USER_AGENT,
|
|
82
|
-
PYTHON_CONNECTOR_USER_AGENT,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
log.debug("Sending %s request to %s", method, url)
|
|
86
|
-
full_url = f"{self.rest.server_url}{url}"
|
|
87
|
-
headers = {
|
|
88
|
-
HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_APPLICATION_JSON,
|
|
89
|
-
HTTP_HEADER_ACCEPT: CONTENT_TYPE_APPLICATION_JSON,
|
|
90
|
-
HTTP_HEADER_USER_AGENT: PYTHON_CONNECTOR_USER_AGENT,
|
|
91
|
-
}
|
|
92
|
-
return self.rest.fetch(
|
|
93
|
-
method=method,
|
|
94
|
-
full_url=full_url,
|
|
95
|
-
headers=headers,
|
|
96
|
-
token=self.rest.token,
|
|
97
|
-
data=json.dumps(data if data else {}),
|
|
98
|
-
no_retry=True,
|
|
99
|
-
raise_raw_http_failure=True,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
def _database_exists(self, db_name: str) -> bool:
|
|
103
|
-
url = f"{SF_REST_API_URL_PREFIX}/databases/{db_name}"
|
|
104
|
-
return self._fetch_endpoint_exists(url)
|
|
105
|
-
|
|
106
|
-
def _schema_exists(self, db_name: str, schema_name: str) -> bool:
|
|
107
|
-
url = f"{SF_REST_API_URL_PREFIX}/databases/{db_name}/schemas/{schema_name}"
|
|
108
|
-
return self._fetch_endpoint_exists(url)
|
|
109
|
-
|
|
110
|
-
def determine_url_for_create_query(self, object_type: str) -> str:
|
|
111
|
-
"""
|
|
112
|
-
Determine an url for creating an object of given type via REST API.
|
|
113
|
-
If URL cannot be determined, the function throws CannotDetermineCreateURLException exception.
|
|
114
|
-
|
|
115
|
-
URLs we check:
|
|
116
|
-
* /api/v2/<type>/
|
|
117
|
-
* /api/v2/databases/<database>/<type>/
|
|
118
|
-
* /api/v2/databases/<database>/schemas/<schema>/<type>/
|
|
119
|
-
|
|
120
|
-
We assume that the URLs for CREATE and LIST are the same for every type of object
|
|
121
|
-
(endpoints differ by method: POST vs GET, accordingly).
|
|
122
|
-
To check whether an URL exists, we send read-only GET request (LIST endpoint,
|
|
123
|
-
which should imply CREATE endpoint).
|
|
124
|
-
"""
|
|
125
|
-
plural_object_type = _pluralize_object_type(object_type)
|
|
126
|
-
|
|
127
|
-
if self.get_endpoint_exists(
|
|
128
|
-
url := f"{SF_REST_API_URL_PREFIX}/{plural_object_type}/"
|
|
129
|
-
):
|
|
130
|
-
return url
|
|
131
|
-
|
|
132
|
-
db = self.conn.database
|
|
133
|
-
if not db:
|
|
134
|
-
raise DatabaseNotDefinedException(
|
|
135
|
-
"Database not defined in connection. Please try again with `--database` flag."
|
|
136
|
-
)
|
|
137
|
-
if not self._database_exists(db):
|
|
138
|
-
raise DatabaseNotExistsException(f"Database '{db}' does not exist.")
|
|
139
|
-
if self.get_endpoint_exists(
|
|
140
|
-
url := f"{SF_REST_API_URL_PREFIX}/databases/{db}/{plural_object_type}/"
|
|
141
|
-
):
|
|
142
|
-
return url
|
|
143
|
-
|
|
144
|
-
schema = self.conn.schema
|
|
145
|
-
if not schema:
|
|
146
|
-
raise SchemaNotDefinedException(
|
|
147
|
-
"Schema not defined in connection. Please try again with `--schema` flag."
|
|
148
|
-
)
|
|
149
|
-
if not self._schema_exists(db_name=db, schema_name=schema):
|
|
150
|
-
raise SchemaNotExistsException(f"Schema '{schema}' does not exist.")
|
|
151
|
-
if self.get_endpoint_exists(
|
|
152
|
-
url := f"{SF_REST_API_URL_PREFIX}/databases/{self.conn.database}/schemas/{self.conn.schema}/{plural_object_type}/"
|
|
153
|
-
):
|
|
154
|
-
return url
|
|
155
|
-
|
|
156
|
-
raise CannotDetermineCreateURLException(
|
|
157
|
-
f"Create operation for type {object_type} is not supported. Try using `sql -q 'CREATE ...'` command."
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class DatabaseNotDefinedException(ClickException):
|
|
162
|
-
pass
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
class SchemaNotDefinedException(ClickException):
|
|
166
|
-
pass
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class DatabaseNotExistsException(ClickException):
|
|
170
|
-
pass
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class SchemaNotExistsException(ClickException):
|
|
174
|
-
pass
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
class CannotDetermineCreateURLException(ClickException):
|
|
178
|
-
pass
|
snowflake/cli/api/sanitizers.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
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 __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import re
|
|
18
|
-
|
|
19
|
-
# 7-bit C1 ANSI sequences
|
|
20
|
-
_ANSI_ESCAPE = re.compile(
|
|
21
|
-
r"""
|
|
22
|
-
\x1B # ESC
|
|
23
|
-
(?: # 7-bit C1 Fe (except CSI)
|
|
24
|
-
[@-Z\\-_]
|
|
25
|
-
| # or [ for CSI, followed by a control sequence
|
|
26
|
-
\[
|
|
27
|
-
[0-?]* # Parameter bytes
|
|
28
|
-
[ -/]* # Intermediate bytes
|
|
29
|
-
[@-~] # Final byte
|
|
30
|
-
)
|
|
31
|
-
""",
|
|
32
|
-
re.VERBOSE,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def sanitize_for_terminal(text: str) -> str | None:
|
|
37
|
-
"""
|
|
38
|
-
Escape ASCII escape codes in string. This should be always used
|
|
39
|
-
when printing output to terminal.
|
|
40
|
-
"""
|
|
41
|
-
if text is None:
|
|
42
|
-
return None
|
|
43
|
-
return _ANSI_ESCAPE.sub("", text)
|
snowflake/cli/api/secure_path.py
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
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 __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import errno
|
|
18
|
-
import logging
|
|
19
|
-
import os
|
|
20
|
-
import shutil
|
|
21
|
-
import tempfile
|
|
22
|
-
from contextlib import contextmanager
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
from typing import Optional, Union
|
|
25
|
-
|
|
26
|
-
from snowflake.cli.api.exceptions import DirectoryIsNotEmptyError, FileTooLargeError
|
|
27
|
-
from snowflake.cli.api.secure_utils import (
|
|
28
|
-
chmod as secure_chmod,
|
|
29
|
-
)
|
|
30
|
-
from snowflake.cli.api.secure_utils import (
|
|
31
|
-
restrict_file_permissions,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
log = logging.getLogger(__name__)
|
|
35
|
-
|
|
36
|
-
UNLIMITED = -1
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class SecurePath:
|
|
40
|
-
def __init__(self, path: Union[Path, str]):
|
|
41
|
-
self._path = Path(path)
|
|
42
|
-
|
|
43
|
-
def __repr__(self):
|
|
44
|
-
return f'SecurePath("{self._path}")'
|
|
45
|
-
|
|
46
|
-
def __truediv__(self, key):
|
|
47
|
-
return SecurePath(self._path / key)
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def path(self) -> Path:
|
|
51
|
-
"""
|
|
52
|
-
Returns itself in pathlib.Path format
|
|
53
|
-
"""
|
|
54
|
-
return self._path
|
|
55
|
-
|
|
56
|
-
def chmod(self, permissions_mask: int) -> None:
|
|
57
|
-
"""
|
|
58
|
-
Change the file mode and permissions, like os.chmod().
|
|
59
|
-
"""
|
|
60
|
-
secure_chmod(self._path, permissions_mask)
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
def parent(self):
|
|
64
|
-
"""
|
|
65
|
-
The logical parent of the path. For details, check pathlib.Path.parent
|
|
66
|
-
"""
|
|
67
|
-
return SecurePath(self._path.parent)
|
|
68
|
-
|
|
69
|
-
def absolute(self):
|
|
70
|
-
"""
|
|
71
|
-
Make the path absolute, without normalization or resolving symlinks.
|
|
72
|
-
"""
|
|
73
|
-
return SecurePath(self._path.absolute())
|
|
74
|
-
|
|
75
|
-
def iterdir(self):
|
|
76
|
-
"""
|
|
77
|
-
When the path points to a directory, yield path objects of the directory contents.
|
|
78
|
-
Otherwise, NotADirectoryError is raised.
|
|
79
|
-
If the location does not exist, FileNotFoundError is raised.
|
|
80
|
-
|
|
81
|
-
For details, check pathlib.Path.iterdir()
|
|
82
|
-
"""
|
|
83
|
-
self.assert_exists()
|
|
84
|
-
self.assert_is_directory()
|
|
85
|
-
return (SecurePath(p) for p in self._path.iterdir())
|
|
86
|
-
|
|
87
|
-
def exists(self) -> bool:
|
|
88
|
-
"""
|
|
89
|
-
Return True if the path points to an existing file or directory.
|
|
90
|
-
"""
|
|
91
|
-
return self._path.exists()
|
|
92
|
-
|
|
93
|
-
def is_dir(self) -> bool:
|
|
94
|
-
"""
|
|
95
|
-
Return True if the path points to a directory (or a symbolic link pointing to a directory),
|
|
96
|
-
False if it points to another kind of file.
|
|
97
|
-
"""
|
|
98
|
-
return self._path.is_dir()
|
|
99
|
-
|
|
100
|
-
def is_file(self) -> bool:
|
|
101
|
-
"""
|
|
102
|
-
Return True if the path points to a regular file (or a symbolic link pointing to a regular file),
|
|
103
|
-
False if it points to another kind of file.
|
|
104
|
-
"""
|
|
105
|
-
return self._path.is_file()
|
|
106
|
-
|
|
107
|
-
@property
|
|
108
|
-
def name(self) -> str:
|
|
109
|
-
"""A string representing the final path component."""
|
|
110
|
-
return self._path.name
|
|
111
|
-
|
|
112
|
-
def restrict_permissions(self) -> None:
|
|
113
|
-
"""
|
|
114
|
-
Restrict file/directory permissions to owner-only.
|
|
115
|
-
"""
|
|
116
|
-
restrict_file_permissions(self._path)
|
|
117
|
-
|
|
118
|
-
def touch(self, permissions_mask: int = 0o600, exist_ok: bool = True) -> None:
|
|
119
|
-
"""
|
|
120
|
-
Create a file at this given path. For details, check pathlib.Path.touch()
|
|
121
|
-
"""
|
|
122
|
-
if not self.exists():
|
|
123
|
-
log.info("Creating file %s", str(self._path))
|
|
124
|
-
self._path.touch(mode=permissions_mask, exist_ok=exist_ok)
|
|
125
|
-
|
|
126
|
-
def mkdir(
|
|
127
|
-
self,
|
|
128
|
-
permissions_mask: int = 0o700,
|
|
129
|
-
parents: bool = False,
|
|
130
|
-
exist_ok: bool = False,
|
|
131
|
-
) -> None:
|
|
132
|
-
"""
|
|
133
|
-
Create a directory at this given path. For details, check pathlib.Path.mkdir()
|
|
134
|
-
"""
|
|
135
|
-
if parents and not self.parent.exists():
|
|
136
|
-
self.parent.mkdir(
|
|
137
|
-
permissions_mask=permissions_mask, exist_ok=exist_ok, parents=True
|
|
138
|
-
)
|
|
139
|
-
if not self.exists():
|
|
140
|
-
log.info("Creating directory %s", str(self._path))
|
|
141
|
-
self._path.mkdir(mode=permissions_mask, exist_ok=exist_ok)
|
|
142
|
-
|
|
143
|
-
def read_text(self, file_size_limit_mb: int, *args, **kwargs) -> str:
|
|
144
|
-
"""
|
|
145
|
-
Return the decoded contents of the file as a string.
|
|
146
|
-
Raises an error of the file exceeds the specified size limit.
|
|
147
|
-
For details, check pathlib.Path.read_text()
|
|
148
|
-
"""
|
|
149
|
-
self._assert_exists_and_is_file()
|
|
150
|
-
self._assert_file_size_limit(file_size_limit_mb)
|
|
151
|
-
log.info("Reading file %s", self._path)
|
|
152
|
-
return self._path.read_text(*args, **kwargs)
|
|
153
|
-
|
|
154
|
-
def write_text(self, *args, **kwargs):
|
|
155
|
-
"""
|
|
156
|
-
Open the file pointed to in text mode, write data to it, and close the file.
|
|
157
|
-
"""
|
|
158
|
-
if not self.exists():
|
|
159
|
-
self.touch()
|
|
160
|
-
log.info("Writing to file %s", self._path)
|
|
161
|
-
self.path.write_text(*args, **kwargs)
|
|
162
|
-
|
|
163
|
-
@contextmanager
|
|
164
|
-
def open( # noqa: A003
|
|
165
|
-
self,
|
|
166
|
-
mode="r",
|
|
167
|
-
read_file_limit_mb: Optional[int] = None,
|
|
168
|
-
**open_kwargs,
|
|
169
|
-
):
|
|
170
|
-
"""
|
|
171
|
-
Open the file pointed by this path and return a file object, as
|
|
172
|
-
the built-in open() function does.
|
|
173
|
-
If the file is opened for reading, [read_file_limit_kb] parameter must be provided.
|
|
174
|
-
Raises error if the read file exceeds the specified size limit.
|
|
175
|
-
"""
|
|
176
|
-
opened_for_reading = "r" in mode
|
|
177
|
-
if opened_for_reading:
|
|
178
|
-
assert (
|
|
179
|
-
read_file_limit_mb is not None
|
|
180
|
-
), "For reading mode ('r') read_file_limit_mb must be provided"
|
|
181
|
-
self._assert_exists_and_is_file()
|
|
182
|
-
self._assert_file_size_limit(read_file_limit_mb)
|
|
183
|
-
|
|
184
|
-
if self.exists():
|
|
185
|
-
self.assert_is_file()
|
|
186
|
-
else:
|
|
187
|
-
self.touch() # makes sure permissions of freshly-created file are strict
|
|
188
|
-
|
|
189
|
-
log.info("Opening file %s in mode '%s'", self._path, mode)
|
|
190
|
-
with self._path.open(mode=mode, **open_kwargs) as fd:
|
|
191
|
-
yield fd
|
|
192
|
-
log.info("Closing file %s", self._path)
|
|
193
|
-
|
|
194
|
-
def move(self, destination: Union[Path, str]) -> "SecurePath":
|
|
195
|
-
"""Recursively move a file or directory (src) to another location and return the destination.
|
|
196
|
-
|
|
197
|
-
If dst is an existing directory or a symlink to a directory, then src is moved inside that directory.
|
|
198
|
-
The destination path in that directory must not already exist.
|
|
199
|
-
"""
|
|
200
|
-
destination = Path(destination)
|
|
201
|
-
if destination.is_dir():
|
|
202
|
-
destination = destination / self._path.name
|
|
203
|
-
if destination.exists():
|
|
204
|
-
_raise_file_exists_error(destination)
|
|
205
|
-
log.info("Moving %s to %s", str(self._path), destination.resolve())
|
|
206
|
-
return SecurePath(shutil.move(str(self._path), destination))
|
|
207
|
-
|
|
208
|
-
def copy(
|
|
209
|
-
self, destination: Union[Path, str], dirs_exist_ok: bool = False
|
|
210
|
-
) -> "SecurePath":
|
|
211
|
-
"""
|
|
212
|
-
Copy the file/directory into the destination.
|
|
213
|
-
If source is a directory, its whole content is copied recursively.
|
|
214
|
-
Permissions of the copy are limited only to the owner.
|
|
215
|
-
|
|
216
|
-
If destination is an existing directory, the copy will be created inside it,
|
|
217
|
-
unless dirs_exist_ok is true and the destination has the same name as this path.
|
|
218
|
-
|
|
219
|
-
Otherwise, the copied file/base directory will be renamed to match destination.
|
|
220
|
-
If dirs_exist_ok is false (the default) and dst already exists,
|
|
221
|
-
a FileExistsError is raised. If dirs_exist_ok is true,
|
|
222
|
-
the copying operation will continue if it encounters existing directories,
|
|
223
|
-
and files within the destination tree will be overwritten by corresponding
|
|
224
|
-
files from the src tree.
|
|
225
|
-
"""
|
|
226
|
-
self.assert_exists()
|
|
227
|
-
|
|
228
|
-
destination = Path(destination)
|
|
229
|
-
if destination.exists():
|
|
230
|
-
if destination.is_dir() and (
|
|
231
|
-
destination.name != self._path.name or self.path.is_file()
|
|
232
|
-
):
|
|
233
|
-
destination = destination / self._path.name
|
|
234
|
-
|
|
235
|
-
if destination.exists():
|
|
236
|
-
if not all([destination.is_dir(), self._path.is_dir(), dirs_exist_ok]):
|
|
237
|
-
raise FileExistsError(
|
|
238
|
-
errno.EEXIST, os.strerror(errno.EEXIST), self._path.resolve()
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
def _recursive_check_for_conflicts(src: Path, dst: Path):
|
|
242
|
-
if dst.exists() and not dirs_exist_ok:
|
|
243
|
-
_raise_file_exists_error(dst)
|
|
244
|
-
if dst.is_file() and not src.is_file():
|
|
245
|
-
_raise_not_a_directory_error(dst)
|
|
246
|
-
if dst.is_dir() and not src.is_dir():
|
|
247
|
-
_raise_is_a_directory_error(dst)
|
|
248
|
-
if src.is_dir():
|
|
249
|
-
for child in src.iterdir():
|
|
250
|
-
_recursive_check_for_conflicts(child, dst / child.name)
|
|
251
|
-
|
|
252
|
-
def _recursive_copy(src: SecurePath, dst: SecurePath):
|
|
253
|
-
if src.is_file():
|
|
254
|
-
log.info("Copying file %s into %s", src.path, dst.path)
|
|
255
|
-
if dst.exists():
|
|
256
|
-
dst.unlink()
|
|
257
|
-
shutil.copyfile(src.path, dst.path)
|
|
258
|
-
dst.restrict_permissions()
|
|
259
|
-
if src.is_dir():
|
|
260
|
-
dst.mkdir(exist_ok=True)
|
|
261
|
-
for child in src.iterdir():
|
|
262
|
-
_recursive_copy(child, dst / child.name)
|
|
263
|
-
|
|
264
|
-
_recursive_check_for_conflicts(self._path, destination)
|
|
265
|
-
_recursive_copy(self, self.__class__(destination))
|
|
266
|
-
|
|
267
|
-
return SecurePath(destination)
|
|
268
|
-
|
|
269
|
-
def unlink(self, missing_ok=False):
|
|
270
|
-
"""
|
|
271
|
-
Remove this file or symbolic link.
|
|
272
|
-
If the path points to a directory, use SecurePath.rmdir() instead.
|
|
273
|
-
|
|
274
|
-
Check pathlib.Path.unlink() for details.
|
|
275
|
-
"""
|
|
276
|
-
if not self.exists():
|
|
277
|
-
if not missing_ok:
|
|
278
|
-
self.assert_exists()
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
self.assert_is_file()
|
|
282
|
-
log.info("Removing file %s", self._path)
|
|
283
|
-
self._path.unlink()
|
|
284
|
-
|
|
285
|
-
def rmdir(self, recursive=False, missing_ok=False):
|
|
286
|
-
"""
|
|
287
|
-
Remove this directory.
|
|
288
|
-
If the path points to a file, use SecurePath.unlink() instead.
|
|
289
|
-
|
|
290
|
-
If path points to a file, NotADirectoryError will be raised.
|
|
291
|
-
If directory does not exist, FileNotFoundError will be raised unless [missing_ok] is True.
|
|
292
|
-
If the directory is not empty, DirectoryNotEmpty will be raised unless [recursive] is True.
|
|
293
|
-
"""
|
|
294
|
-
if not self.exists():
|
|
295
|
-
if not missing_ok:
|
|
296
|
-
self.assert_exists()
|
|
297
|
-
return
|
|
298
|
-
|
|
299
|
-
self.assert_is_directory()
|
|
300
|
-
|
|
301
|
-
if not recursive and any(self._path.iterdir()):
|
|
302
|
-
raise DirectoryIsNotEmptyError(self._path.resolve())
|
|
303
|
-
|
|
304
|
-
log.info("Removing directory %s", self._path)
|
|
305
|
-
shutil.rmtree(str(self._path))
|
|
306
|
-
|
|
307
|
-
@classmethod
|
|
308
|
-
@contextmanager
|
|
309
|
-
def temporary_directory(cls):
|
|
310
|
-
"""
|
|
311
|
-
Creates a temporary directory in the most secure manner possible.
|
|
312
|
-
The directory is readable, writable, and searchable only by the creating user ID.
|
|
313
|
-
Yields SecurePath pointing to the absolute location of created directory.
|
|
314
|
-
|
|
315
|
-
Works similarly to tempfile.TemporaryDirectory
|
|
316
|
-
"""
|
|
317
|
-
with tempfile.TemporaryDirectory(prefix="snowflake-cli") as tmpdir:
|
|
318
|
-
log.info("Created temporary directory %s", tmpdir)
|
|
319
|
-
yield SecurePath(tmpdir)
|
|
320
|
-
log.info("Removing temporary directory %s", tmpdir)
|
|
321
|
-
|
|
322
|
-
def _assert_exists_and_is_file(self) -> None:
|
|
323
|
-
self.assert_exists()
|
|
324
|
-
self.assert_is_file()
|
|
325
|
-
|
|
326
|
-
def assert_exists(self) -> None:
|
|
327
|
-
if not self.exists():
|
|
328
|
-
raise FileNotFoundError(
|
|
329
|
-
errno.ENOENT, os.strerror(errno.ENOENT), self._path.resolve()
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
def assert_is_file(self) -> None:
|
|
333
|
-
if not self._path.is_file():
|
|
334
|
-
_raise_is_a_directory_error(self._path.resolve())
|
|
335
|
-
|
|
336
|
-
def assert_is_directory(self) -> None:
|
|
337
|
-
if not self._path.is_dir():
|
|
338
|
-
_raise_not_a_directory_error(self._path.resolve())
|
|
339
|
-
|
|
340
|
-
def _assert_file_size_limit(self, size_limit_in_mb):
|
|
341
|
-
if (
|
|
342
|
-
size_limit_in_mb != UNLIMITED
|
|
343
|
-
and self._path.stat().st_size > size_limit_in_mb * 1024 * 1024
|
|
344
|
-
):
|
|
345
|
-
raise FileTooLargeError(self._path.resolve(), size_limit_in_mb)
|
|
346
|
-
|
|
347
|
-
def rename(self, new_name: Union[str | Path]):
|
|
348
|
-
self._path.rename(new_name)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def _raise_file_exists_error(path: Path):
|
|
352
|
-
raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), path)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
def _raise_is_a_directory_error(path: Path):
|
|
356
|
-
raise IsADirectoryError(errno.EISDIR, os.strerror(errno.EISDIR), path)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def _raise_not_a_directory_error(path: Path):
|
|
360
|
-
raise NotADirectoryError(errno.ENOTDIR, os.strerror(errno.ENOTDIR), path)
|
|
@@ -1,118 +0,0 @@
|
|
|
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
|
-
import logging
|
|
16
|
-
import stat
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import List
|
|
19
|
-
|
|
20
|
-
from snowflake.connector.compat import IS_WINDOWS
|
|
21
|
-
|
|
22
|
-
log = logging.getLogger(__name__)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _get_windows_whitelisted_users():
|
|
26
|
-
# whitelisted users list obtained in consultation with prodsec: CASEC-9627
|
|
27
|
-
import os
|
|
28
|
-
|
|
29
|
-
return [
|
|
30
|
-
"SYSTEM",
|
|
31
|
-
"Administrators",
|
|
32
|
-
"Network",
|
|
33
|
-
"Domain Admins",
|
|
34
|
-
"Domain Users",
|
|
35
|
-
os.getlogin(),
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _run_icacls(file_path: Path) -> str:
|
|
40
|
-
import subprocess
|
|
41
|
-
|
|
42
|
-
return subprocess.check_output(["icacls", str(file_path)], text=True)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _windows_permissions_are_denied(permission_codes: str) -> bool:
|
|
46
|
-
# according to https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls
|
|
47
|
-
return "(DENY)" in permission_codes or "(N)" in permission_codes
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def windows_get_not_whitelisted_users_with_access(file_path: Path) -> List[str]:
|
|
51
|
-
import re
|
|
52
|
-
|
|
53
|
-
# according to https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls
|
|
54
|
-
icacls_output_regex = (
|
|
55
|
-
rf"({re.escape(str(file_path))})?.*\\(?P<user>.*):(?P<permissions>[(A-Z),]+)"
|
|
56
|
-
)
|
|
57
|
-
whitelisted_users = _get_windows_whitelisted_users()
|
|
58
|
-
|
|
59
|
-
users_with_access = []
|
|
60
|
-
for permission in re.finditer(icacls_output_regex, _run_icacls(file_path)):
|
|
61
|
-
if (permission.group("user") not in whitelisted_users) and (
|
|
62
|
-
not _windows_permissions_are_denied(permission.group("permissions"))
|
|
63
|
-
):
|
|
64
|
-
users_with_access.append(permission.group("user"))
|
|
65
|
-
return list(set(users_with_access))
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _windows_file_permissions_are_strict(file_path: Path) -> bool:
|
|
69
|
-
return windows_get_not_whitelisted_users_with_access(file_path) == []
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _unix_file_permissions_are_strict(file_path: Path) -> bool:
|
|
73
|
-
accessible_by_others = (
|
|
74
|
-
# https://docs.python.org/3/library/stat.html
|
|
75
|
-
stat.S_IRGRP # readable by group
|
|
76
|
-
| stat.S_IROTH # readable by others
|
|
77
|
-
| stat.S_IWGRP # writeable by group
|
|
78
|
-
| stat.S_IWOTH # writeable by others
|
|
79
|
-
| stat.S_IXGRP # executable by group
|
|
80
|
-
| stat.S_IXOTH # executable by others
|
|
81
|
-
)
|
|
82
|
-
return (file_path.stat().st_mode & accessible_by_others) == 0
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def file_permissions_are_strict(file_path: Path) -> bool:
|
|
86
|
-
if IS_WINDOWS:
|
|
87
|
-
return _windows_file_permissions_are_strict(file_path)
|
|
88
|
-
return _unix_file_permissions_are_strict(file_path)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def chmod(path: Path, permissions_mask: int) -> None:
|
|
92
|
-
log.info("Update permissions of file %s to %s", path, oct(permissions_mask))
|
|
93
|
-
path.chmod(permissions_mask)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _unix_restrict_file_permissions(path: Path) -> None:
|
|
97
|
-
owner_permissions = (
|
|
98
|
-
# https://docs.python.org/3/library/stat.html
|
|
99
|
-
stat.S_IRUSR # readable by owner
|
|
100
|
-
| stat.S_IWUSR # writeable by owner
|
|
101
|
-
| stat.S_IXUSR # executable by owner
|
|
102
|
-
)
|
|
103
|
-
chmod(path, path.stat().st_mode & owner_permissions)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _windows_restrict_file_permissions(path: Path) -> None:
|
|
107
|
-
import subprocess
|
|
108
|
-
|
|
109
|
-
for user in windows_get_not_whitelisted_users_with_access(path):
|
|
110
|
-
log.info("Removing permissions of user %s from file %s", user, path)
|
|
111
|
-
subprocess.run(["icacls", str(path), "/DENY", f"{user}:F"])
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def restrict_file_permissions(file_path: Path) -> None:
|
|
115
|
-
if IS_WINDOWS:
|
|
116
|
-
_windows_restrict_file_permissions(file_path)
|
|
117
|
-
else:
|
|
118
|
-
_unix_restrict_file_permissions(file_path)
|