Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SNOW-1176541] Config file permissions on Windows #869

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/test_e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ concurrency:

jobs:
tests:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
steps:
- uses: actions/checkout@v4
with:
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ dependencies = [
"typer==0.9.0",
"urllib3>=1.21.1,<2.3",
"GitPython==3.1.42",
"pydantic==2.6.3"
"pydantic==2.6.3",
"oschmod"
]
classifiers = [
"Development Status :: 3 - Alpha",
Expand Down
17 changes: 17 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
coverage==7.4.3
jinja2==3.1.3
pluggy==1.4.0
PyYAML==6.0.1
rich==13.7.1
requests==2.31.0
requirements-parser==0.5.0
setuptools==69.1.1
snowflake-connector-python[secure-local-storage]==3.7.1
strictyaml==1.7.3
tomlkit==0.12.3
typer==0.9.0
urllib3>=1.21.1,<2.3
GitPython==3.1.42
pydantic==2.6.3
oschmod
pywin32
7 changes: 3 additions & 4 deletions src/snowflake/cli/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
)
from snowflake.cli.api.secure_path import SecurePath
from snowflake.cli.api.secure_utils import file_permissions_are_strict
from snowflake.connector.compat import IS_WINDOWS
from snowflake.connector.config_manager import CONFIG_MANAGER
from snowflake.connector.constants import CONFIG_FILE, CONNECTIONS_FILE
from snowflake.connector.errors import MissingConfigOptionError
Expand Down Expand Up @@ -224,13 +223,13 @@ def _get_envs_for_path(*path) -> dict:


def _dump_config(conf_file_cache: Dict):
with SecurePath(CONFIG_MANAGER.file_path).open("w+") as fh:
secure_config_path = SecurePath(CONFIG_MANAGER.file_path)
with secure_config_path.open("w+") as fh:
dump(conf_file_cache, fh)
secure_config_path.chmod(0o600)


def _check_default_config_files_permissions() -> None:
if IS_WINDOWS:
return
if CONNECTIONS_FILE.exists() and not file_permissions_are_strict(CONNECTIONS_FILE):
raise ConfigFileTooWidePermissionsError(CONNECTIONS_FILE)
if CONFIG_FILE.exists() and not file_permissions_are_strict(CONFIG_FILE):
Expand Down
9 changes: 8 additions & 1 deletion src/snowflake/cli/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from click.exceptions import ClickException
from snowflake.cli.api.constants import ObjectType
from snowflake.connector.compat import IS_WINDOWS


class EnvironmentVariableNotFoundError(ClickException):
Expand Down Expand Up @@ -111,8 +112,14 @@ def __init__(self, path: Path):

class ConfigFileTooWidePermissionsError(ClickException):
def __init__(self, path: Path):
change_permission_message = (
"set read and write access only to the current user."
if IS_WINDOWS
else f'run `chmod 0600 "{path}"`.'
)

super().__init__(
f'Configuration file {path} has too wide permissions, run `chmod 0600 "{path}"`'
f"Configuration file {path} has too wide permissions, {change_permission_message}"
)


Expand Down
3 changes: 2 additions & 1 deletion src/snowflake/cli/api/secure_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pathlib import Path
from typing import Optional, Union

import oschmod
from snowflake.cli.api.exceptions import DirectoryIsNotEmptyError, FileTooLargeError

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -69,7 +70,7 @@ def chmod(self, permissions_mask: int) -> None:
log.info(
"Update permissions of file %s to %s", self._path, oct(permissions_mask)
)
self._path.chmod(permissions_mask)
oschmod.set_mode(str(self._path), permissions_mask)

def touch(self, permissions_mask: int = 0o600, exist_ok: bool = True) -> None:
"""
Expand Down
4 changes: 3 additions & 1 deletion src/snowflake/cli/api/secure_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import stat
from pathlib import Path

import oschmod


def file_permissions_are_strict(file_path: Path) -> bool:
accessible_by_others = (
Expand All @@ -12,4 +14,4 @@ def file_permissions_are_strict(file_path: Path) -> bool:
| stat.S_IXGRP # executable by group
| stat.S_IXOTH # executable by others
)
return (file_path.stat().st_mode & accessible_by_others) == 0
return (oschmod.get_mode(str(file_path)) & accessible_by_others) == 0
15 changes: 15 additions & 0 deletions tests_e2e/__snapshots__/test_config.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# serializer version: 1
# name: test_config_file_creation
'''
No data


'''
# ---
# name: test_config_file_creation.1
'''
No data


'''
# ---
34 changes: 30 additions & 4 deletions tests_e2e/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import platform
import subprocess
import tempfile
from pathlib import Path
Expand All @@ -10,6 +11,18 @@
TEST_DIR = Path(__file__).parent


@pytest.fixture
def snowflake_home(monkeypatch):
"""
Set up the default location of config files to [temp_dir]/.snowflake
"""
with tempfile.TemporaryDirectory() as tmp_dir:
snowflake_home = Path(tmp_dir) / ".snowflake"
snowflake_home.mkdir()
monkeypatch.setenv("SNOWFLAKE_HOME", str(snowflake_home))
yield snowflake_home


@pytest.fixture(scope="session")
def test_root_path():
return TEST_DIR
Expand Down Expand Up @@ -41,7 +54,7 @@ def snowcli(test_root_path):
_create_venv(tmp_dir_path)
_build_snowcli(tmp_dir_path, test_root_path)
_install_snowcli_with_external_plugin(tmp_dir_path, test_root_path)
yield tmp_dir_path / "bin" / "snow"
yield _snow_path(tmp_dir_path)


@pytest.fixture(autouse=True)
Expand All @@ -58,7 +71,7 @@ def _build_snowcli(venv_path: Path, test_root_path: Path) -> None:
[_python_path(venv_path), "-m", "pip", "install", "--upgrade", "build"]
)
subprocess.check_call(
[_python_path(venv_path), "-m", "build", test_root_path / ".."]
[_python_path(venv_path), "-m", "build", test_root_path.parent]
)


Expand All @@ -73,7 +86,9 @@ def _install_snowcli_with_external_plugin(
python = _python_path(venv_path)
_pip_install(
python,
test_root_path / f"../dist/snowflake_cli_labs-{version}-py3-none-any.whl",
test_root_path.parent
/ "dist"
/ f"snowflake_cli_labs-{version}-py3-none-any.whl",
)
_pip_install(
python,
Expand All @@ -87,4 +102,15 @@ def _install_snowcli_with_external_plugin(


def _python_path(venv_path: Path) -> Path:
return venv_path / "bin" / "python"
return _bin_in_venv_path(venv_path, "python")


def _snow_path(venv_path: Path) -> Path:
return _bin_in_venv_path(venv_path, "snow")


def _bin_in_venv_path(venv_path: Path, bin_name: str) -> Path:
if platform.system() == "Windows":
return venv_path / "Scripts" / bin_name
else:
return venv_path / "bin" / bin_name
12 changes: 12 additions & 0 deletions tests_e2e/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import subprocess

import pytest


@pytest.mark.e2e
def test_config_file_creation(snowcli, test_root_path, snowflake_home, snapshot):
output1 = subprocess.check_output([snowcli, "connection", "list"], encoding="utf-8")
snapshot.assert_match(output1)

output2 = subprocess.check_output([snowcli, "connection", "list"], encoding="utf-8")
snapshot.assert_match(output2)
2 changes: 2 additions & 0 deletions tests_e2e/test_error_handling.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import os
import platform
import subprocess

import pytest


@pytest.mark.e2e
@pytest.mark.skipif(platform.system() == "Windows", reason="Chmod issues on Windows")
def test_error_traceback_disabled_without_debug(snowcli, test_root_path):
traceback_msg = "Traceback (most recent call last)"
config_path = test_root_path / "config" / "malformatted_config.toml"
Expand Down
4 changes: 4 additions & 0 deletions tests_e2e/test_installation.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import platform
import subprocess
from pathlib import Path

import pytest


@pytest.mark.e2e
@pytest.mark.skipif(
platform.system() == "Windows", reason="Snapshot comparison issues on Windows"
)
def test_snow_help(snowcli, snapshot):
output = subprocess.check_output([snowcli, "--help"], encoding="utf-8")
snapshot.assert_match(output)
Expand Down
Loading