Skip to content

Commit 995f91a

Browse files
committed
SNOW-2306184: config refactor - ensure file permissions checks
1 parent acdfb84 commit 995f91a

File tree

3 files changed

+94
-6
lines changed

3 files changed

+94
-6
lines changed

src/snowflake/cli/api/config_ng/sources.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,28 @@
3737

3838
from snowflake.cli.api.config_ng.constants import SNOWFLAKE_HOME_ENV
3939
from snowflake.cli.api.config_ng.core import SourceType, ValueSource
40+
from snowflake.cli.api.exceptions import ConfigFileTooWidePermissionsError
41+
from snowflake.cli.api.secure_utils import file_permissions_are_strict
42+
from snowflake.connector.compat import IS_WINDOWS
4043

4144
log = logging.getLogger(__name__)
4245

4346

47+
def _ensure_strict_file_permissions(config_file: Path) -> None:
48+
"""
49+
Validate that configuration files have strict permissions before reading.
50+
51+
Raises:
52+
ConfigFileTooWidePermissionsError: If permissions are too wide on non-Windows.
53+
"""
54+
55+
if IS_WINDOWS or not config_file.exists():
56+
return
57+
58+
if not file_permissions_are_strict(config_file):
59+
raise ConfigFileTooWidePermissionsError(config_file)
60+
61+
4462
class SnowSQLSection(Enum):
4563
"""
4664
SnowSQL configuration file section names.
@@ -153,6 +171,7 @@ def _read_and_merge_files(self) -> str:
153171

154172
for config_file in self._config_paths:
155173
if config_file.exists():
174+
_ensure_strict_file_permissions(config_file)
156175
try:
157176
merged_config.read(config_file)
158177
except Exception as e:
@@ -273,6 +292,7 @@ def _read_first_file(self) -> str:
273292
"""
274293
for config_file in self._search_paths:
275294
if config_file.exists():
295+
_ensure_strict_file_permissions(config_file)
276296
try:
277297
return config_file.read_text()
278298
except Exception as e:
@@ -387,6 +407,7 @@ def discover(self, key: Optional[str] = None) -> Dict[str, Any]:
387407
else:
388408
if not self._file_path.exists():
389409
return {}
410+
_ensure_strict_file_permissions(self._file_path)
390411
try:
391412
content = self._file_path.read_text()
392413
except Exception as e:

tests/config_ng/conftest.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,17 @@
2424
from contextlib import contextmanager
2525
from pathlib import Path
2626
from textwrap import dedent
27-
from typing import Dict, Optional
27+
from typing import Dict, Literal, Optional
2828

2929
import pytest
30+
from snowflake.connector.compat import IS_WINDOWS
31+
32+
STRICT_FILE_PERMISSIONS: Literal[0o600] = 0o600
33+
34+
35+
def _restrict_permissions(path: Path) -> None:
36+
if not IS_WINDOWS:
37+
path.chmod(STRICT_FILE_PERMISSIONS)
3038

3139

3240
@contextmanager
@@ -96,13 +104,17 @@ def _setup(
96104

97105
# Write config files if provided
98106
if snowsql_config:
99-
(snowflake_home / "config").write_text(dedent(snowsql_config))
107+
snowsql_path = snowflake_home / "config"
108+
snowsql_path.write_text(dedent(snowsql_config))
109+
_restrict_permissions(snowsql_path)
100110
if cli_config:
101-
(snowflake_home / "config.toml").write_text(dedent(cli_config))
111+
cli_config_path = snowflake_home / "config.toml"
112+
cli_config_path.write_text(dedent(cli_config))
113+
_restrict_permissions(cli_config_path)
102114
if connections_toml:
103-
(snowflake_home / "connections.toml").write_text(
104-
dedent(connections_toml)
105-
)
115+
connections_path = snowflake_home / "connections.toml"
116+
connections_path.write_text(dedent(connections_toml))
117+
_restrict_permissions(connections_path)
106118

107119
# Prepare environment variables
108120
env_to_set = {

tests/config_ng/test_sources.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@
1414

1515
"""Tests for configuration sources with string-based testing."""
1616

17+
from typing import Literal
18+
19+
import pytest
1720
from snowflake.cli.api.config_ng.sources import (
1821
CliConfigFile,
1922
ConnectionsConfigFile,
2023
SnowSQLConfigFile,
2124
)
25+
from snowflake.cli.api.exceptions import ConfigFileTooWidePermissionsError
26+
from snowflake.connector.compat import IS_WINDOWS
27+
28+
INSECURE_FILE_PERMISSIONS: Literal[0o644] = 0o644
2229

2330

2431
class TestSnowSQLConfigFileFromString:
@@ -378,3 +385,51 @@ def test_non_connections_file_marker(self):
378385
not hasattr(snowsql_source, "is_connections_file")
379386
or not snowsql_source.is_connections_file
380387
)
388+
389+
390+
@pytest.mark.skipif(IS_WINDOWS, reason="Permission checks disabled on Windows")
391+
class TestFilePermissionValidation:
392+
def test_snowsql_config_raises_on_insecure_file(self, tmp_path):
393+
config_path = tmp_path / "snowsql.cnf"
394+
config_path.write_text(
395+
"""
396+
[connections.test]
397+
accountname = test_account
398+
"""
399+
)
400+
config_path.chmod(INSECURE_FILE_PERMISSIONS)
401+
402+
source = SnowSQLConfigFile(config_paths=[config_path])
403+
404+
with pytest.raises(ConfigFileTooWidePermissionsError):
405+
source.discover()
406+
407+
def test_cli_config_raises_on_insecure_file(self, tmp_path):
408+
config_path = tmp_path / "config.toml"
409+
config_path.write_text(
410+
"""
411+
[connections.test]
412+
account = "cli-account"
413+
"""
414+
)
415+
config_path.chmod(INSECURE_FILE_PERMISSIONS)
416+
417+
source = CliConfigFile(search_paths=[config_path])
418+
419+
with pytest.raises(ConfigFileTooWidePermissionsError):
420+
source.discover()
421+
422+
def test_connections_config_raises_on_insecure_file(self, tmp_path):
423+
config_path = tmp_path / "connections.toml"
424+
config_path.write_text(
425+
"""
426+
[connections.test]
427+
account = "connections-account"
428+
"""
429+
)
430+
config_path.chmod(INSECURE_FILE_PERMISSIONS)
431+
432+
source = ConnectionsConfigFile(file_path=config_path)
433+
434+
with pytest.raises(ConfigFileTooWidePermissionsError):
435+
source.discover()

0 commit comments

Comments
 (0)