diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 609d0e39a9..45b86c1d24 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -26,6 +26,7 @@ * Fixed `SELECT *` output being corrupted when joined tables share column names. Duplicate column names are now disambiguated by appending a numeric suffix (e.g. `NAME`, `NAME_2`). * Fixed `snow connection generate-jwt` and `snow connection generate-workload-identity-token` failing with `Connection None is not configured` when used with `--temporary-connection`. * The internal connection cache now remembers failed connect attempts and re-raises the original exception on subsequent accesses within the same process, instead of re-dialing Snowflake every time a command accesses the shared connection. This fixes, among other cases, the customer-visible duplicate `LOGIN_HISTORY` events (and `OVERFLOW_FAILURE_EVENTS_ELIDED`) previously emitted when a `snow` invocation was rejected by an authentication policy. +* `snow connection list` (and other commands that enumerate connections) now raises a clear, actionable error when the `[connections]` section of `config.toml` contains an entry that is not a table (for example, a stray `default = "something"` line at the top of the section). Previously these configurations produced an unhandled `AttributeError: 'String' object has no attribute 'items'` with no indication of which entry was wrong. # v3.17.0 diff --git a/src/snowflake/cli/api/config_provider.py b/src/snowflake/cli/api/config_provider.py index ced3c7a681..dd056ce99b 100644 --- a/src/snowflake/cli/api/config_provider.py +++ b/src/snowflake/cli/api/config_provider.py @@ -136,14 +136,21 @@ def get_connection_dict(self, connection_name: str) -> dict: ) def get_all_connections(self, include_env_connections: bool = False) -> dict: + from click import ClickException from snowflake.cli.api.config import ConnectionConfig, get_config_section # Legacy provider ignores the flag since it never had env connections connections = get_config_section("connections") - return { - name: ConnectionConfig.from_dict(config) - for name, config in connections.items() - } + result = {} + for name, config in connections.items(): + if not isinstance(config, dict): + raise ClickException( + f"Connection '{name}' in the configuration file is malformed: " + f"expected a table of connection parameters but found a {type(config).__name__}. " + f"Each connection must be defined as [connections.{name}] with key = value entries." + ) + result[name] = ConnectionConfig.from_dict(config) + return result class AlternativeConfigProvider(ConfigProvider): diff --git a/tests/test_connection.py b/tests/test_connection.py index 47250a0bad..d3000b2087 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -415,6 +415,32 @@ def test_mask_sensitive_parameters_masks_all_known_sensitive_keys(): assert params["password"] == "hunter2" +@mock.patch.dict(os.environ, {}, clear=True) +def test_connection_list_reports_malformed_connection_entry(runner): + with NamedTemporaryFile("w+", suffix=".toml") as tmp_file: + tmp_file.write( + dedent( + """\ + [connections] + default = "conn" + + [connections.conn] + user = "foo" + """ + ) + ) + tmp_file.flush() + os.chmod(tmp_file.name, 0o600) + result = runner.invoke_with_config_file( + tmp_file.name, ["connection", "list", "--format", "json"] + ) + + assert result.exit_code != 0, result.output + assert "malformed" in result.output + assert "default" in result.output + assert "[connections.default]" in result.output + + @mock.patch.dict( os.environ, {