Skip to content

Commit e527472

Browse files
authored
SNOW-1940996 no-op auth for Stored Proc (#2182)
1 parent 4e167b8 commit e527472

File tree

8 files changed

+131
-5
lines changed

8 files changed

+131
-5
lines changed

src/snowflake/connector/auth/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .default import AuthByDefault
1010
from .idtoken import AuthByIdToken
1111
from .keypair import AuthByKeyPair
12+
from .no_auth import AuthNoAuth
1213
from .oauth import AuthByOAuth
1314
from .okta import AuthByOkta
1415
from .pat import AuthByPAT
@@ -25,6 +26,7 @@
2526
AuthByWebBrowser,
2627
AuthByIdToken,
2728
AuthByPAT,
29+
AuthNoAuth,
2830
)
2931
)
3032

@@ -37,6 +39,7 @@
3739
"AuthByOkta",
3840
"AuthByUsrPwdMfa",
3941
"AuthByWebBrowser",
42+
"AuthNoAuth",
4043
"Auth",
4144
"AuthType",
4245
"FIRST_PARTY_AUTHENTICATORS",

src/snowflake/connector/auth/_auth.py

+5
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
from ..options import installed_keyring, keyring
6464
from ..sqlstate import SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED
6565
from ..version import VERSION
66+
from .no_auth import AuthNoAuth
6667

6768
if TYPE_CHECKING:
6869
from . import AuthByPlugin
@@ -186,6 +187,10 @@ def authenticate(
186187
) -> dict[str, str | int | bool]:
187188
logger.debug("authenticate")
188189

190+
# For no-auth connection, authentication is no-op, and we can return early here.
191+
if isinstance(auth_instance, AuthNoAuth):
192+
return {}
193+
189194
if timeout is None:
190195
timeout = auth_instance.timeout
191196

src/snowflake/connector/auth/by_plugin.py

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class AuthType(Enum):
5555
USR_PWD_MFA = "USERNAME_PASSWORD_MFA"
5656
OKTA = "OKTA"
5757
PAT = "PROGRAMMATIC_ACCESS_TOKEN'"
58+
NO_AUTH = "NO_AUTH"
5859

5960

6061
class AuthByPlugin(ABC):
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
4+
#
5+
6+
from __future__ import annotations
7+
8+
from typing import Any
9+
10+
from .by_plugin import AuthByPlugin, AuthType
11+
12+
13+
class AuthNoAuth(AuthByPlugin):
14+
"""No-auth Authentication.
15+
16+
It is a dummy auth that requires no extra connection establishment.
17+
"""
18+
19+
@property
20+
def type_(self) -> AuthType:
21+
return AuthType.NO_AUTH
22+
23+
@property
24+
def assertion_content(self) -> str | None:
25+
return None
26+
27+
def __init__(self) -> None:
28+
super().__init__()
29+
30+
def reset_secrets(self) -> None:
31+
pass
32+
33+
def prepare(
34+
self,
35+
**kwargs: Any,
36+
) -> None:
37+
pass
38+
39+
def reauthenticate(self, **kwargs: Any) -> dict[str, bool]:
40+
return {"success": True}
41+
42+
def update_body(self, body: dict[Any, Any]) -> None:
43+
pass

src/snowflake/connector/connection.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
AuthByPlugin,
4545
AuthByUsrPwdMfa,
4646
AuthByWebBrowser,
47+
AuthNoAuth,
4748
)
4849
from .auth.idtoken import AuthByIdToken
4950
from .backoff_policies import exponential_backoff
@@ -98,6 +99,7 @@
9899
DEFAULT_AUTHENTICATOR,
99100
EXTERNAL_BROWSER_AUTHENTICATOR,
100101
KEY_PAIR_AUTHENTICATOR,
102+
NO_AUTH_AUTHENTICATOR,
101103
OAUTH_AUTHENTICATOR,
102104
PROGRAMMATIC_ACCESS_TOKEN,
103105
REQUEST_ID,
@@ -1248,9 +1250,15 @@ def __config(self, **kwargs):
12481250
with open(token_file_path) as f:
12491251
self._token = f.read()
12501252

1253+
# Set of authenticators allowing empty user.
1254+
empty_user_allowed_authenticators = {OAUTH_AUTHENTICATOR, NO_AUTH_AUTHENTICATOR}
1255+
12511256
if not (self._master_token and self._session_token):
1252-
if not self.user and self._authenticator != OAUTH_AUTHENTICATOR:
1253-
# OAuth Authentication does not require a username
1257+
if (
1258+
not self.user
1259+
and self._authenticator not in empty_user_allowed_authenticators
1260+
):
1261+
# OAuth and NoAuth Authentications does not require a username
12541262
Error.errorhandler_wrapper(
12551263
self,
12561264
None,
@@ -1279,14 +1287,15 @@ def __config(self, **kwargs):
12791287
{"msg": "Password is empty", "errno": ER_NO_PASSWORD},
12801288
)
12811289

1282-
if not self._account:
1290+
# Only AuthNoAuth allows account to be omitted.
1291+
if not self._account and not isinstance(self.auth_class, AuthNoAuth):
12831292
Error.errorhandler_wrapper(
12841293
self,
12851294
None,
12861295
ProgrammingError,
12871296
{"msg": "Account must be specified", "errno": ER_NO_ACCOUNT_NAME},
12881297
)
1289-
if "." in self._account:
1298+
if self._account and "." in self._account:
12901299
self._account = parse_account(self._account)
12911300

12921301
if not isinstance(self._backoff_policy, Callable) or not isinstance(

src/snowflake/connector/network.py

+1
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
ID_TOKEN_AUTHENTICATOR = "ID_TOKEN"
189189
USR_PWD_MFA_AUTHENTICATOR = "USERNAME_PASSWORD_MFA"
190190
PROGRAMMATIC_ACCESS_TOKEN = "PROGRAMMATIC_ACCESS_TOKEN"
191+
NO_AUTH_AUTHENTICATOR = "NO_AUTH"
191192

192193

193194
def is_retryable_http_code(code: int) -> bool:

test/integ/test_connection.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from snowflake.connector.telemetry import TelemetryField
4141

4242
from ..randomize import random_string
43-
from .conftest import RUNNING_ON_GH
43+
from .conftest import RUNNING_ON_GH, create_connection
4444

4545
try: # pragma: no cover
4646
from ..parameters import CONNECTION_PARAMETERS_ADMIN
@@ -1485,3 +1485,27 @@ def test_is_valid(conn_cnx):
14851485
assert conn
14861486
assert conn.is_valid() is True
14871487
assert conn.is_valid() is False
1488+
1489+
1490+
@pytest.mark.skipolddriver
1491+
def test_no_auth_connection_negative_case():
1492+
# AuthNoAuth does not exist in old drivers, so we import at test level to
1493+
# skip importing it for old driver tests.
1494+
from snowflake.connector.auth.no_auth import AuthNoAuth
1495+
1496+
no_auth = AuthNoAuth()
1497+
1498+
# Create a no-auth connection in an invalid way.
1499+
# We do not fail connection establishment because there is no validated way
1500+
# to tell whether the no-auth is a valid use case or not. But it is
1501+
# effectively protected because invalid no-auth will fail to run any query.
1502+
conn = create_connection("default", auth_class=no_auth)
1503+
1504+
# Make sure we are indeed passing the no-auth configuration to the
1505+
# connection.
1506+
assert isinstance(conn.auth_class, AuthNoAuth)
1507+
1508+
# We expect a failure here when executing queries, because invalid no-auth
1509+
# connection is not able to run any query
1510+
with pytest.raises(DatabaseError, match="Connection is closed"):
1511+
conn.execute_string("select 1")

test/unit/test_auth_no_auth.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#
2+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3+
#
4+
5+
from __future__ import annotations
6+
7+
import pytest
8+
9+
10+
@pytest.mark.skipolddriver
11+
def test_auth_no_auth():
12+
"""Simple test for AuthNoAuth."""
13+
14+
# AuthNoAuth does not exist in old drivers, so we import at test level to
15+
# skip importing it for old driver tests.
16+
from snowflake.connector.auth.no_auth import AuthNoAuth
17+
18+
auth = AuthNoAuth()
19+
20+
body = {"data": {}}
21+
old_body = body
22+
auth.update_body(body)
23+
# update_body should be no-op for SP auth, therefore the body content should remain the same.
24+
assert body == old_body, f"body is {body}, old_body is {old_body}"
25+
26+
# assertion_content should always return None in SP auth.
27+
assert auth.assertion_content is None, auth.assertion_content
28+
29+
# reauthenticate should always return success.
30+
expected_reauth_response = {"success": True}
31+
reauth_response = auth.reauthenticate()
32+
assert (
33+
reauth_response == expected_reauth_response
34+
), f"reauthenticate() is expected to return {expected_reauth_response}, but returns {reauth_response}"
35+
36+
# It also returns success response even if we pass extra keyword argument(s).
37+
reauth_response = auth.reauthenticate(foo="bar")
38+
assert (
39+
reauth_response == expected_reauth_response
40+
), f'reauthenticate(foo="bar") is expected to return {expected_reauth_response}, but returns {reauth_response}'

0 commit comments

Comments
 (0)