diff --git a/src/snowflake/connector/connection.py b/src/snowflake/connector/connection.py index 6b46249451..a89a8e2d5c 100644 --- a/src/snowflake/connector/connection.py +++ b/src/snowflake/connector/connection.py @@ -134,7 +134,12 @@ from .telemetry import TelemetryClient, TelemetryData, TelemetryField from .time_util import HeartBeatTimer, get_time_millis from .url_util import extract_top_level_domain_from_hostname -from .util_text import construct_hostname, parse_account, split_statements +from .util_text import ( + construct_hostname, + is_valid_account_identifier, + parse_account, + split_statements, +) from .wif_util import AttestationProvider if sys.version_info >= (3, 13) or typing.TYPE_CHECKING: @@ -1743,8 +1748,20 @@ def __config(self, **kwargs): ProgrammingError, {"msg": "Account must be specified", "errno": ER_NO_ACCOUNT_NAME}, ) - if self._account and "." in self._account: - self._account = parse_account(self._account) + if self._account: + # Allow legacy formats like "acc.region" to continue parsing into simple account id + if "." in self._account: + self._account = parse_account(self._account) + if not is_valid_account_identifier(self._account): + Error.errorhandler_wrapper( + self, + None, + ProgrammingError, + { + "msg": "Invalid account identifier: only letters, digits, '_' and '-' allowed; no dots or slashes", + "errno": ER_INVALID_VALUE, + }, + ) if not isinstance(self._backoff_policy, Callable) or not isinstance( self._backoff_policy(), Iterator diff --git a/src/snowflake/connector/util_text.py b/src/snowflake/connector/util_text.py index 39762c2111..156f930bef 100644 --- a/src/snowflake/connector/util_text.py +++ b/src/snowflake/connector/util_text.py @@ -254,6 +254,22 @@ def _is_china_region(r: str) -> bool: return host +ACCOUNT_ID_VALIDATOR_RE = re.compile(r"^[A-Za-z0-9_-]+$") + + +def is_valid_account_identifier(account: str) -> bool: + """Validate the Snowflake account identifier format. + + The account identifier must be a single label (no dots or slashes) composed + only of ASCII letters, digits, underscores, or hyphens. + """ + if not isinstance(account, str) or not account: + return False + if "." in account or "/" in account or "\\" in account: + return False + return bool(ACCOUNT_ID_VALIDATOR_RE.fullmatch(account)) + + def parse_account(account): url_parts = account.split(".") # if this condition is true, then we have some extra diff --git a/test/unit/test_parse_account.py b/test/unit/test_parse_account.py index c07dd46c05..e3ef04f4f1 100644 --- a/test/unit/test_parse_account.py +++ b/test/unit/test_parse_account.py @@ -1,7 +1,9 @@ #!/usr/bin/env python from __future__ import annotations -from snowflake.connector.util_text import parse_account +import pytest + +from snowflake.connector.util_text import is_valid_account_identifier, parse_account def test_parse_account_basic(): @@ -12,3 +14,20 @@ def test_parse_account_basic(): assert ( parse_account("account1-jkabfvdjisoa778wqfgeruishafeuw89q.global") == "account1" ) + + +@pytest.mark.parametrize( + "value,expected", + [ + ("abc", True), + ("ABC", True), + ("a_b-c1", True), + ("a.b", False), + ("a/b", False), + ("a\\b", False), + ("", False), + ("snowflakecomputing.com", False), + ], +) +def test_is_valid_account_identifier(value, expected): + assert is_valid_account_identifier(value) is expected