Skip to content

Commit 1cdbd3b

Browse files
SNOW-761004 Added URL Validator and URL escaping of strings (#1480)
1 parent 4b1d474 commit 1cdbd3b

File tree

8 files changed

+131
-9
lines changed

8 files changed

+131
-9
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ repos:
9393
| ssd_internal_keys
9494
| test_util
9595
| util_text
96+
| url_util
9697
| version
9798
).py$
9899
additional_dependencies:

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1212

1313
- Fixed a memory leak in the logging module of the Cython extension.
1414
- Fixed a bug where the `put` command on AWS raised `AttributeError` when the file size was larger than 200M.
15+
- Validate SSO URL before opening it in the browser for External browser authenticator.
1516

1617
- v3.0.1(February 28, 2023)
1718

setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@
4949

5050
_ABLE_TO_COMPILE_EXTENSIONS = True
5151
except ImportError:
52-
warnings.warn("Cannot compile native C code, because of a missing build dependency")
52+
warnings.warn(
53+
"Cannot compile native C code, because of a missing build dependency",
54+
stacklevel=1,
55+
)
5356
_ABLE_TO_COMPILE_EXTENSIONS = False
5457

5558
if _ABLE_TO_COMPILE_EXTENSIONS:
56-
5759
pyarrow_version = tuple(int(x) for x in pyarrow.__version__.split("."))
5860
extensions = cythonize(
5961
[
@@ -66,7 +68,6 @@
6668
)
6769

6870
class MyBuildExt(build_ext):
69-
7071
# list of libraries that will be bundled with python connector,
7172
# this list should be carefully examined when pyarrow lib is
7273
# upgraded

src/snowflake/connector/auth/webbrowser.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424
from ..errorcode import (
2525
ER_IDP_CONNECTION_ERROR,
26+
ER_INVALID_VALUE,
2627
ER_NO_HOSTNAME_FOUND,
2728
ER_UNABLE_TO_OPEN_BROWSER,
2829
)
@@ -32,6 +33,7 @@
3233
EXTERNAL_BROWSER_AUTHENTICATOR,
3334
PYTHON_CONNECTOR_USER_AGENT,
3435
)
36+
from ..url_util import is_valid_url
3537
from . import Auth
3638
from .by_plugin import AuthByPlugin, AuthType
3739

@@ -131,18 +133,29 @@ def prepare(
131133
socket_connection.listen(0) # no backlog
132134
callback_port = socket_connection.getsockname()[1]
133135

136+
logger.debug("step 1: query GS to obtain SSO url")
137+
sso_url = self._get_sso_url(
138+
conn, authenticator, service_name, account, callback_port, user
139+
)
140+
141+
logger.debug("Validate SSO URL")
142+
if not is_valid_url(sso_url):
143+
self._handle_failure(
144+
conn=conn,
145+
ret={
146+
"code": ER_INVALID_VALUE,
147+
"message": (f"The SSO URL provided {sso_url} is invalid"),
148+
},
149+
)
150+
return
151+
134152
print(
135153
"Initiating login request with your identity provider. A "
136154
"browser window should have opened for you to complete the "
137155
"login. If you can't see it, check existing browser windows, "
138156
"or your OS settings. Press CTRL+C to abort and try again..."
139157
)
140158

141-
logger.debug("step 1: query GS to obtain SSO url")
142-
sso_url = self._get_sso_url(
143-
conn, authenticator, service_name, account, callback_port, user
144-
)
145-
146159
logger.debug("step 2: open a browser")
147160
print(f"Going to open: {sso_url} to authenticate...")
148161
if not self._webbrowser.open_new(sso_url):

src/snowflake/connector/snow_logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def warn( # type: ignore[override]
6767
warnings.warn(
6868
"The 'warn' method is deprecated, " "use 'warning' instead",
6969
DeprecationWarning,
70-
2,
70+
stacklevel=2,
7171
)
7272
self.warning(msg, path_name, func_name, *args, **kwargs)
7373

src/snowflake/connector/url_util.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#
2+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3+
#
4+
5+
from __future__ import annotations
6+
7+
import re
8+
import urllib.parse
9+
from logging import getLogger
10+
11+
logger = getLogger(__name__)
12+
13+
14+
URL_VALIDATOR = re.compile(
15+
"^http(s?)\\:\\/\\/[0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z@:])*(:(0-9)*)*(\\/?)([a-zA-Z0-9\\-\\.\\?\\,\\&\\(\\)\\/\\\\\\+&%\\$#_=@]*)?$"
16+
)
17+
18+
19+
def is_valid_url(url: str) -> bool:
20+
"""Confirms if the provided URL is a valid HTTP/ HTTPs URL
21+
22+
Args:
23+
url: the URL that needs to be validated
24+
25+
Returns:
26+
true/ false depending on whether the URL is valid or not
27+
"""
28+
return bool(URL_VALIDATOR.match(url))
29+
30+
31+
def url_encode_str(target: str | None) -> str:
32+
"""Converts a target string into escaped URL safe string
33+
34+
Args:
35+
target: string to be URL encoded
36+
37+
Returns:
38+
URL encoded string
39+
"""
40+
if target is None:
41+
logger.debug("The string to be URL encoded is None")
42+
return ""
43+
return urllib.parse.quote_plus(target, safe="")

test/unit/test_auth_webbrowser.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
SERVICE_NAME = ""
3333
REF_PROOF_KEY = "MOCK_PROOF_KEY"
3434
REF_SSO_URL = "https://testsso.snowflake.net/sso"
35+
INVALID_SSO_URL = "this is an invalid URL"
3536

3637

3738
def mock_webserver(target_instance, application, port):
@@ -318,3 +319,39 @@ class StopExecuting(Exception):
318319
account="account",
319320
auth_class=auth_inst,
320321
)
322+
323+
324+
def test_auth_webbrowser_invalid_sso(monkeypatch):
325+
"""Authentication by WebBrowser with failed to start web browser case."""
326+
rest = _init_rest(INVALID_SSO_URL, REF_PROOF_KEY)
327+
328+
# mock webbrowser
329+
mock_webbrowser = MagicMock()
330+
mock_webbrowser.open_new.return_value = False
331+
332+
# mock socket
333+
mock_socket_instance = MagicMock()
334+
mock_socket_instance.getsockname.return_value = [None, 12345]
335+
336+
mock_socket_client = MagicMock()
337+
mock_socket_client.recv.return_value = (
338+
"\r\n".join(["GET /?token=MOCK_TOKEN HTTP/1.1", "User-Agent: snowflake-agent"])
339+
).encode("utf-8")
340+
mock_socket_instance.accept.return_value = (mock_socket_client, None)
341+
mock_socket = Mock(return_value=mock_socket_instance)
342+
343+
auth = AuthByWebBrowser(
344+
application=APPLICATION,
345+
webbrowser_pkg=mock_webbrowser,
346+
socket_pkg=mock_socket,
347+
)
348+
auth.prepare(
349+
conn=rest._connection,
350+
authenticator=AUTHENTICATOR,
351+
service_name=SERVICE_NAME,
352+
account=ACCOUNT,
353+
user=USER,
354+
password=PASSWORD,
355+
)
356+
assert rest._connection.errorhandler.called # an error
357+
assert auth.assertion_content is None

test/unit/test_url_util.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#
2+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3+
#
4+
5+
try:
6+
from snowflake.connector.url_util import is_valid_url, url_encode_str
7+
except ImportError:
8+
9+
def is_valid_url(s):
10+
return False
11+
12+
13+
def test_url_validator():
14+
assert is_valid_url("https://ssoTestURL.okta.com")
15+
assert is_valid_url("https://ssoTestURL.okta.com:8080")
16+
assert is_valid_url("https://ssoTestURL.okta.com/testpathvalue")
17+
18+
assert not is_valid_url("-a Calculator")
19+
assert not is_valid_url("This is a random text")
20+
assert not is_valid_url("file://TestForFile")
21+
22+
23+
def test_encoder():
24+
assert url_encode_str("Hello @World") == "Hello+%40World"
25+
assert url_encode_str("Test//String") == "Test%2F%2FString"
26+
assert url_encode_str(None) == ""

0 commit comments

Comments
 (0)