Skip to content

Commit 9b7954b

Browse files
sfc-gh-zyaosfc-gh-pczajka
authored andcommitted
SNOW-1940996 no-op auth for Stored Proc (#2182)
1 parent 0a83f11 commit 9b7954b

File tree

8 files changed

+130
-5
lines changed

8 files changed

+130
-5
lines changed

src/snowflake/connector/auth/__init__.py

Lines changed: 3 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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):
Lines changed: 43 additions & 0 deletions
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

Lines changed: 13 additions & 4 deletions
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,
@@ -1251,9 +1253,15 @@ def __config(self, **kwargs):
12511253
with open(token_file_path) as f:
12521254
self._token = f.read()
12531255

1256+
# Set of authenticators allowing empty user.
1257+
empty_user_allowed_authenticators = {OAUTH_AUTHENTICATOR, NO_AUTH_AUTHENTICATOR}
1258+
12541259
if not (self._master_token and self._session_token):
1255-
if not self.user and self._authenticator != OAUTH_AUTHENTICATOR:
1256-
# OAuth Authentication does not require a username
1260+
if (
1261+
not self.user
1262+
and self._authenticator not in empty_user_allowed_authenticators
1263+
):
1264+
# OAuth and NoAuth Authentications does not require a username
12571265
Error.errorhandler_wrapper(
12581266
self,
12591267
None,
@@ -1282,14 +1290,15 @@ def __config(self, **kwargs):
12821290
{"msg": "Password is empty", "errno": ER_NO_PASSWORD},
12831291
)
12841292

1285-
if not self._account:
1293+
# Only AuthNoAuth allows account to be omitted.
1294+
if not self._account and not isinstance(self.auth_class, AuthNoAuth):
12861295
Error.errorhandler_wrapper(
12871296
self,
12881297
None,
12891298
ProgrammingError,
12901299
{"msg": "Account must be specified", "errno": ER_NO_ACCOUNT_NAME},
12911300
)
1292-
if "." in self._account:
1301+
if self._account and "." in self._account:
12931302
self._account = parse_account(self._account)
12941303

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

src/snowflake/connector/network.py

Lines changed: 1 addition & 0 deletions
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

Lines changed: 24 additions & 1 deletion
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
@@ -1568,3 +1568,26 @@ def test_is_valid(conn_cnx):
15681568
assert conn
15691569
assert conn.is_valid() is True
15701570
assert conn.is_valid() is False
1571+
1572+
1573+
def test_no_auth_connection_negative_case():
1574+
# AuthNoAuth does not exist in old drivers, so we import at test level to
1575+
# skip importing it for old driver tests.
1576+
from snowflake.connector.auth.no_auth import AuthNoAuth
1577+
1578+
no_auth = AuthNoAuth()
1579+
1580+
# Create a no-auth connection in an invalid way.
1581+
# We do not fail connection establishment because there is no validated way
1582+
# to tell whether the no-auth is a valid use case or not. But it is
1583+
# effectively protected because invalid no-auth will fail to run any query.
1584+
conn = create_connection("default", auth_class=no_auth)
1585+
1586+
# Make sure we are indeed passing the no-auth configuration to the
1587+
# connection.
1588+
assert isinstance(conn.auth_class, AuthNoAuth)
1589+
1590+
# We expect a failure here when executing queries, because invalid no-auth
1591+
# connection is not able to run any query
1592+
with pytest.raises(DatabaseError, match="Connection is closed"):
1593+
conn.execute_string("select 1")

test/unit/test_auth_no_auth.py

Lines changed: 40 additions & 0 deletions
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)