From 51b7dab2b240907d4d9e7886cdc526643615651a Mon Sep 17 00:00:00 2001 From: root Date: Tue, 30 Sep 2025 05:05:50 -0400 Subject: [PATCH 1/7] Refactor error handling to show clean authentication messages and hide technical details --- vertica_python/vertica/connection.py | 114 ++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/vertica_python/vertica/connection.py b/vertica_python/vertica/connection.py index 94159502..0d0e6a54 100644 --- a/vertica_python/vertica/connection.py +++ b/vertica_python/vertica/connection.py @@ -44,6 +44,11 @@ import ssl import uuid import warnings +import re +import time +import signal +import select +import sys from collections import deque from struct import unpack @@ -303,6 +308,13 @@ def __init__(self, options: Optional[Dict[str, Any]] = None) -> None: self.address_list = _AddressList(self.options['host'], self.options['port'], self.options['backup_server_node'], self._logger) + # TOTP support + self.totp = self.options.get('totp') + if self.totp is not None: + if not isinstance(self.totp, str): + raise TypeError('The value of connection option "totp" should be a string') + self._logger.info('TOTP received in connection options') + # OAuth authentication setup self.options.setdefault('oauth_access_token', DEFAULT_OAUTH_ACCESS_TOKEN) if not isinstance(self.options['oauth_access_token'], str): @@ -918,16 +930,112 @@ def startup_connection(self) -> None: else: auth_category = '' - self.write(messages.Startup(user, database, session_label, os_user_name, autocommit, binary_transfer, - request_complex_types, oauth_access_token, workload, auth_category)) + # Check if user has provided TOTP in options + totp = self.options.get("totp", None) + retried_totp = False + + def send_startup(totp_value=None): + self.write(messages.Startup( + user, database, session_label, os_user_name, + autocommit, binary_transfer, request_complex_types, + oauth_access_token, workload, auth_category, + totp_value + )) + + send_startup(totp_value=totp) # ✅ First attempt while True: message = self.read_message() - + self._logger.debug(f"Received message: {type(message).__name__}") + self._logger.debug(f"Message code: {getattr(message, 'code', None)}") if isinstance(message, messages.Authentication): if message.code == messages.Authentication.OK: self._logger.info("User {} successfully authenticated" .format(self.options['user'])) + # 🔁 Continue reading messages after successful authentication + while True: + message = self.read_message() + self._logger.debug(f"Post-auth message: {type(message).__name__}") + if isinstance(message, messages.ReadyForQuery): + self.transaction_status = message.transaction_status + # self.session_id = message.session_id + self._logger.info("Connection is ready") + break + elif isinstance(message, messages.ParameterStatus): + self.parameters[message.key] = message.value + elif isinstance(message, messages.BackendKeyData): + self.backend_pid = message.pid + self.backend_key = message.key + elif isinstance(message, messages.ErrorResponse): + error_msg = message.error_message() + + # Extract only the "Message: ..." part + match = re.search(r'Message: (.+?)(?:, Sqlstate|$)', error_msg, re.DOTALL) + short_msg = match.group(1).strip() if match else error_msg.strip() + + if "Invalid TOTP" in short_msg: + print("Authentication failed: Invalid TOTP token.") + self._logger.error("Authentication failed: Invalid TOTP token.") + self.close_socket() + raise errors.ConnectionError("Authentication failed: Invalid TOTP token.") + + # Generic error fallback + print(f"Authentication failed: {short_msg}") + self._logger.error(short_msg) + raise errors.ConnectionError(f"Authentication failed: {short_msg}") + else: + self._logger.warning(f"Unexpected message type: {type(message).__name__}") + + break + elif message.code == messages.Authentication.TOTP: + if retried_totp: + raise errors.ConnectionError("TOTP authentication failed.") + + # ✅ If TOTP not provided initially, prompt only once + if not totp: + timeout_seconds = 30 # 5 minutes timeout + try: + print("Enter TOTP: ", end="", flush=True) + ready, _, _ = select.select([sys.stdin], [], [], timeout_seconds) + if ready: + totp_input = sys.stdin.readline().strip() + + # ❌ Blank TOTP entered + if not totp_input: + self._logger.error("Invalid TOTP: Cannot be empty.") + raise errors.ConnectionError("Invalid TOTP: Cannot be empty.") + + # ❌ Validate TOTP format (must be 6 digits) + if not totp_input.isdigit() or len(totp_input) != 6: + print("Invalid TOTP format. Please enter a 6-digit code.") + self._logger.error("Invalid TOTP format entered.") + raise errors.ConnectionError("Invalid TOTP format: Must be a 6-digit number.") + # ✅ Valid TOTP — retry connection + totp = totp_input + self.close_socket() + self.socket = self.establish_socket_connection(self.address_list) + self._logger.info(f"Retrying with TOTP: '{totp}'") + + # ✅ Re-init required attributes + self.backend_pid = 0 + self.backend_key = 0 + self.transaction_status = None + self.session_id = None + + self._logger.debug("Startup message sent with TOTP.") + send_startup(totp_value=totp) + + else: + self._logger.error("Session timeout: No TOTP entered within time limit.") + self.close_socket() + raise errors.ConnectionError("Session timeout: No TOTP entered within time limit.") + except (KeyboardInterrupt, EOFError): + raise errors.ConnectionError("TOTP input cancelled.") + else: + raise errors.ConnectionError("TOTP was requested but not provided.") + retried_totp = True + continue + elif message.code == messages.Authentication.CHANGE_PASSWORD: msg = "The password for user {} has expired".format(self.options['user']) self._logger.error(msg) From c637e99e8bc0feb0b317d5fd4d54f80d6cca8faf Mon Sep 17 00:00:00 2001 From: root Date: Tue, 14 Oct 2025 02:37:31 -0400 Subject: [PATCH 2/7] Added a 5-minute timeout for TOTP input during authentication --- vertica_python/vertica/connection.py | 96 +++++++++++++--------------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/vertica_python/vertica/connection.py b/vertica_python/vertica/connection.py index 0d0e6a54..2e89955c 100644 --- a/vertica_python/vertica/connection.py +++ b/vertica_python/vertica/connection.py @@ -939,15 +939,15 @@ def send_startup(totp_value=None): user, database, session_label, os_user_name, autocommit, binary_transfer, request_complex_types, oauth_access_token, workload, auth_category, - totp_value + totp_value if totp_value is not None else None )) send_startup(totp_value=totp) # ✅ First attempt while True: message = self.read_message() - self._logger.debug(f"Received message: {type(message).__name__}") - self._logger.debug(f"Message code: {getattr(message, 'code', None)}") + self._logger.debug(f"📨 Received message: {type(message).__name__}") + self._logger.debug(f"🔁 Message code: {getattr(message, 'code', None)}") if isinstance(message, messages.Authentication): if message.code == messages.Authentication.OK: self._logger.info("User {} successfully authenticated" @@ -955,11 +955,11 @@ def send_startup(totp_value=None): # 🔁 Continue reading messages after successful authentication while True: message = self.read_message() - self._logger.debug(f"Post-auth message: {type(message).__name__}") + self._logger.debug(f"📨 Post-auth message: {type(message).__name__}") if isinstance(message, messages.ReadyForQuery): self.transaction_status = message.transaction_status # self.session_id = message.session_id - self._logger.info("Connection is ready") + self._logger.info("✅ Connection is ready") break elif isinstance(message, messages.ParameterStatus): self.parameters[message.key] = message.value @@ -974,68 +974,64 @@ def send_startup(totp_value=None): short_msg = match.group(1).strip() if match else error_msg.strip() if "Invalid TOTP" in short_msg: - print("Authentication failed: Invalid TOTP token.") - self._logger.error("Authentication failed: Invalid TOTP token.") + print("❌ Authentication failed: Invalid TOTP token.") + self._logger.error("❌ Authentication failed: Invalid TOTP token.") self.close_socket() raise errors.ConnectionError("Authentication failed: Invalid TOTP token.") # Generic error fallback - print(f"Authentication failed: {short_msg}") + print(f"❌ Authentication failed: {short_msg}") self._logger.error(short_msg) - raise errors.ConnectionError(f"Authentication failed: {short_msg}") + raise errors.ConnectionError(f"❌ Authentication failed: {short_msg}") else: - self._logger.warning(f"Unexpected message type: {type(message).__name__}") + self._logger.warning(f"⚠️ Unexpected message type: {type(message).__name__}") break elif message.code == messages.Authentication.TOTP: if retried_totp: raise errors.ConnectionError("TOTP authentication failed.") - # ✅ If TOTP not provided initially, prompt only once + # ✅ If TOTP not provided initially, allow 3 retry attempts if not totp: - timeout_seconds = 30 # 5 minutes timeout - try: - print("Enter TOTP: ", end="", flush=True) - ready, _, _ = select.select([sys.stdin], [], [], timeout_seconds) - if ready: - totp_input = sys.stdin.readline().strip() - - # ❌ Blank TOTP entered - if not totp_input: - self._logger.error("Invalid TOTP: Cannot be empty.") - raise errors.ConnectionError("Invalid TOTP: Cannot be empty.") - - # ❌ Validate TOTP format (must be 6 digits) - if not totp_input.isdigit() or len(totp_input) != 6: - print("Invalid TOTP format. Please enter a 6-digit code.") - self._logger.error("Invalid TOTP format entered.") - raise errors.ConnectionError("Invalid TOTP format: Must be a 6-digit number.") - # ✅ Valid TOTP — retry connection - totp = totp_input - self.close_socket() - self.socket = self.establish_socket_connection(self.address_list) - self._logger.info(f"Retrying with TOTP: '{totp}'") - - # ✅ Re-init required attributes - self.backend_pid = 0 - self.backend_key = 0 - self.transaction_status = None - self.session_id = None - - self._logger.debug("Startup message sent with TOTP.") - send_startup(totp_value=totp) - - else: - self._logger.error("Session timeout: No TOTP entered within time limit.") - self.close_socket() - raise errors.ConnectionError("Session timeout: No TOTP entered within time limit.") - except (KeyboardInterrupt, EOFError): - raise errors.ConnectionError("TOTP input cancelled.") + max_attempts = 3 + timeout_seconds = 300 # 5 minutes timeout + for attempt in range(max_attempts): + try: + print(f"🔐 Enter TOTP (attempt {attempt+1}/{max_attempts}): ", end="", flush=True) + ready, _, _ = select.select([sys.stdin], [], [], timeout_seconds) + if ready: + totp_input = sys.stdin.readline().strip() + if not totp_input: + print("⚠️ TOTP cannot be empty.") + continue + totp = totp_input + self.close_socket() + self.socket = self.establish_socket_connection(self.address_list) + self._logger.info(f"🚀 Retrying with TOTP: '{totp}'") + # ✅ Re-init required attributes + self.backend_pid = 0 + self.backend_key = 0 + self.transaction_status = None + self.session_id = None + self._logger.debug("✅ Startup message sent with TOTP.") + # Send new startup message with updated TOTP + send_startup(totp_value=totp) + break + else: + print("⏰ Session timed out. No TOTP entered within time limit.") + self._logger.error("Session timeout: No TOTP entered within time limit.") + self.close_socket() + raise errors.ConnectionError("Session timeout: No TOTP entered within time limit.") + except (KeyboardInterrupt, EOFError): + raise errors.ConnectionError("TOTP input cancelled.") + else: + print("❌ Maximum TOTP attempts exceeded without input.") + self.close_socket() + raise errors.ConnectionError("Authentication failed: No valid TOTP provided.") else: raise errors.ConnectionError("TOTP was requested but not provided.") retried_totp = True continue - elif message.code == messages.Authentication.CHANGE_PASSWORD: msg = "The password for user {} has expired".format(self.options['user']) self._logger.error(msg) From 2e5be52dc09eed649e1562b1b9970d040c0d3131 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 Oct 2025 03:24:47 -0400 Subject: [PATCH 3/7] Fix TOTP prompt and validation to handle blank and invalid inputs correctly --- vertica_python/vertica/connection.py | 76 +++++++++++++++------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/vertica_python/vertica/connection.py b/vertica_python/vertica/connection.py index 2e89955c..f423a4ab 100644 --- a/vertica_python/vertica/connection.py +++ b/vertica_python/vertica/connection.py @@ -991,47 +991,51 @@ def send_startup(totp_value=None): if retried_totp: raise errors.ConnectionError("TOTP authentication failed.") - # ✅ If TOTP not provided initially, allow 3 retry attempts + # ✅ If TOTP not provided initially, prompt only once if not totp: - max_attempts = 3 - timeout_seconds = 300 # 5 minutes timeout - for attempt in range(max_attempts): - try: - print(f"🔐 Enter TOTP (attempt {attempt+1}/{max_attempts}): ", end="", flush=True) - ready, _, _ = select.select([sys.stdin], [], [], timeout_seconds) - if ready: - totp_input = sys.stdin.readline().strip() - if not totp_input: - print("⚠️ TOTP cannot be empty.") - continue - totp = totp_input - self.close_socket() - self.socket = self.establish_socket_connection(self.address_list) - self._logger.info(f"🚀 Retrying with TOTP: '{totp}'") - # ✅ Re-init required attributes - self.backend_pid = 0 - self.backend_key = 0 - self.transaction_status = None - self.session_id = None - self._logger.debug("✅ Startup message sent with TOTP.") - # Send new startup message with updated TOTP - send_startup(totp_value=totp) - break - else: - print("⏰ Session timed out. No TOTP entered within time limit.") - self._logger.error("Session timeout: No TOTP entered within time limit.") - self.close_socket() - raise errors.ConnectionError("Session timeout: No TOTP entered within time limit.") - except (KeyboardInterrupt, EOFError): - raise errors.ConnectionError("TOTP input cancelled.") - else: - print("❌ Maximum TOTP attempts exceeded without input.") - self.close_socket() - raise errors.ConnectionError("Authentication failed: No valid TOTP provided.") + timeout_seconds = 30 # 5 minutes timeout + try: + print("🔐 Enter TOTP: ", end="", flush=True) + ready, _, _ = select.select([sys.stdin], [], [], timeout_seconds) + if ready: + totp_input = sys.stdin.readline().strip() + + # ❌ Blank TOTP entered + if not totp_input: + self._logger.error("Invalid TOTP: Cannot be empty.") + raise errors.ConnectionError("Invalid TOTP: Cannot be empty.") + + # ❌ Validate TOTP format (must be 6 digits) + if not totp_input.isdigit() or len(totp_input) != 6: + print("❌ Invalid TOTP format. Please enter a 6-digit code.") + self._logger.error("Invalid TOTP format entered.") + raise errors.ConnectionError("Invalid TOTP format: Must be a 6-digit number.") + # ✅ Valid TOTP — retry connection + totp = totp_input + self.close_socket() + self.socket = self.establish_socket_connection(self.address_list) + self._logger.info(f"🚀 Retrying with TOTP: '{totp}'") + + # ✅ Re-init required attributes + self.backend_pid = 0 + self.backend_key = 0 + self.transaction_status = None + self.session_id = None + + self._logger.debug("✅ Startup message sent with TOTP.") + send_startup(totp_value=totp) + + else: + self._logger.error("Session timeout: No TOTP entered within time limit.") + self.close_socket() + raise errors.ConnectionError("Session timeout: No TOTP entered within time limit.") + except (KeyboardInterrupt, EOFError): + raise errors.ConnectionError("TOTP input cancelled.") else: raise errors.ConnectionError("TOTP was requested but not provided.") retried_totp = True continue + elif message.code == messages.Authentication.CHANGE_PASSWORD: msg = "The password for user {} has expired".format(self.options['user']) self._logger.error(msg) From fcd385532e13ca8a27696ea70f57991aaa9ebc49 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 11 Nov 2025 04:33:31 -0500 Subject: [PATCH 4/7] Removed the emoji symbols. --- vertica_python/vertica/connection.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/vertica_python/vertica/connection.py b/vertica_python/vertica/connection.py index f423a4ab..0d0e6a54 100644 --- a/vertica_python/vertica/connection.py +++ b/vertica_python/vertica/connection.py @@ -939,15 +939,15 @@ def send_startup(totp_value=None): user, database, session_label, os_user_name, autocommit, binary_transfer, request_complex_types, oauth_access_token, workload, auth_category, - totp_value if totp_value is not None else None + totp_value )) send_startup(totp_value=totp) # ✅ First attempt while True: message = self.read_message() - self._logger.debug(f"📨 Received message: {type(message).__name__}") - self._logger.debug(f"🔁 Message code: {getattr(message, 'code', None)}") + self._logger.debug(f"Received message: {type(message).__name__}") + self._logger.debug(f"Message code: {getattr(message, 'code', None)}") if isinstance(message, messages.Authentication): if message.code == messages.Authentication.OK: self._logger.info("User {} successfully authenticated" @@ -955,11 +955,11 @@ def send_startup(totp_value=None): # 🔁 Continue reading messages after successful authentication while True: message = self.read_message() - self._logger.debug(f"📨 Post-auth message: {type(message).__name__}") + self._logger.debug(f"Post-auth message: {type(message).__name__}") if isinstance(message, messages.ReadyForQuery): self.transaction_status = message.transaction_status # self.session_id = message.session_id - self._logger.info("✅ Connection is ready") + self._logger.info("Connection is ready") break elif isinstance(message, messages.ParameterStatus): self.parameters[message.key] = message.value @@ -974,17 +974,17 @@ def send_startup(totp_value=None): short_msg = match.group(1).strip() if match else error_msg.strip() if "Invalid TOTP" in short_msg: - print("❌ Authentication failed: Invalid TOTP token.") - self._logger.error("❌ Authentication failed: Invalid TOTP token.") + print("Authentication failed: Invalid TOTP token.") + self._logger.error("Authentication failed: Invalid TOTP token.") self.close_socket() raise errors.ConnectionError("Authentication failed: Invalid TOTP token.") # Generic error fallback - print(f"❌ Authentication failed: {short_msg}") + print(f"Authentication failed: {short_msg}") self._logger.error(short_msg) - raise errors.ConnectionError(f"❌ Authentication failed: {short_msg}") + raise errors.ConnectionError(f"Authentication failed: {short_msg}") else: - self._logger.warning(f"⚠️ Unexpected message type: {type(message).__name__}") + self._logger.warning(f"Unexpected message type: {type(message).__name__}") break elif message.code == messages.Authentication.TOTP: @@ -995,7 +995,7 @@ def send_startup(totp_value=None): if not totp: timeout_seconds = 30 # 5 minutes timeout try: - print("🔐 Enter TOTP: ", end="", flush=True) + print("Enter TOTP: ", end="", flush=True) ready, _, _ = select.select([sys.stdin], [], [], timeout_seconds) if ready: totp_input = sys.stdin.readline().strip() @@ -1007,14 +1007,14 @@ def send_startup(totp_value=None): # ❌ Validate TOTP format (must be 6 digits) if not totp_input.isdigit() or len(totp_input) != 6: - print("❌ Invalid TOTP format. Please enter a 6-digit code.") + print("Invalid TOTP format. Please enter a 6-digit code.") self._logger.error("Invalid TOTP format entered.") raise errors.ConnectionError("Invalid TOTP format: Must be a 6-digit number.") # ✅ Valid TOTP — retry connection totp = totp_input self.close_socket() self.socket = self.establish_socket_connection(self.address_list) - self._logger.info(f"🚀 Retrying with TOTP: '{totp}'") + self._logger.info(f"Retrying with TOTP: '{totp}'") # ✅ Re-init required attributes self.backend_pid = 0 @@ -1022,7 +1022,7 @@ def send_startup(totp_value=None): self.transaction_status = None self.session_id = None - self._logger.debug("✅ Startup message sent with TOTP.") + self._logger.debug("Startup message sent with TOTP.") send_startup(totp_value=totp) else: From b44526cbf578f456f810d0568c32a7eeb2bb20a9 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 25 Nov 2025 05:40:59 -0500 Subject: [PATCH 5/7] tests: cover valid and invalid TOTP authentication flows --- .../integration_tests/test_authentication.py | 117 +++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/vertica_python/tests/integration_tests/test_authentication.py b/vertica_python/tests/integration_tests/test_authentication.py index 6080480c..85503b54 100644 --- a/vertica_python/tests/integration_tests/test_authentication.py +++ b/vertica_python/tests/integration_tests/test_authentication.py @@ -123,6 +123,121 @@ def test_oauth_access_token(self): cur.execute("SELECT authentication_method FROM sessions WHERE session_id=(SELECT current_session())") res = cur.fetchone() self.assertEqual(res[0], 'OAuth') + # ------------------------------- + # TOTP Authentication Test for Vertica-Python Driver + # ------------------------------- + import os + import pyotp + from io import StringIO + import sys -exec(AuthenticationTestCase.createPrepStmtClass()) + # Positive TOTP Test (Like SHA512 format) + def totp_positive_scenario(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("DROP USER IF EXISTS totp_user") + cur.execute("DROP AUTHENTICATION IF EXISTS totp_auth CASCADE") + + try: + # Create user with MFA + cur.execute("CREATE USER totp_user IDENTIFIED BY 'password' ENFORCEMFA") + + # Grant authentication + # Note: METHOD is 'trusted' or 'password' depending on how MFA is enforced in Vertica + cur.execute("CREATE AUTHENTICATION totp_auth METHOD 'password' HOST '0.0.0.0/0'") + cur.execute("GRANT AUTHENTICATION totp_auth TO totp_user") + + # Generate TOTP + TOTP_SECRET = "O5D7DQICJTM34AZROWHSAO4O53ELRJN3" + totp_code = pyotp.TOTP(TOTP_SECRET).now() + + # Set connection info + self._conn_info['user'] = 'totp_user' + self._conn_info['password'] = 'password' + self._conn_info['totp'] = totp_code + + # Try connection + with self._connect() as totp_conn: + c = totp_conn.cursor() + c.execute("SELECT 1") + res = c.fetchone() + self.assertEqual(res[0], 1) + + finally: + cur.execute("DROP USER IF EXISTS totp_user") + cur.execute("DROP AUTHENTICATION IF EXISTS totp_auth CASCADE") + + # Negative Test: Missing TOTP + def totp_missing_code_scenario(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("DROP USER IF EXISTS totp_user") + cur.execute("DROP AUTHENTICATION IF EXISTS totp_auth CASCADE") + + try: + cur.execute("CREATE USER totp_user IDENTIFIED BY 'password' ENFORCEMFA") + cur.execute("CREATE AUTHENTICATION totp_auth METHOD 'password' HOST '0.0.0.0/0'") + cur.execute("GRANT AUTHENTICATION totp_auth TO totp_user") + + self._conn_info['user'] = 'totp_user' + self._conn_info['password'] = 'password' + self._conn_info.pop('totp', None) # No TOTP + + err_msg = "TOTP was requested but not provided" + self.assertConnectionFail(err_msg=err_msg) + + finally: + cur.execute("DROP USER IF EXISTS totp_user") + cur.execute("DROP AUTHENTICATION IF EXISTS totp_auth CASCADE") + + # Negative Test: Invalid TOTP Format + def totp_invalid_format_scenario(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("DROP USER IF EXISTS totp_user") + cur.execute("DROP AUTHENTICATION IF EXISTS totp_auth CASCADE") + + try: + cur.execute("CREATE USER totp_user IDENTIFIED BY 'password' ENFORCEMFA") + cur.execute("CREATE AUTHENTICATION totp_auth METHOD 'password' HOST '0.0.0.0/0'") + cur.execute("GRANT AUTHENTICATION totp_auth TO totp_user") + + self._conn_info['user'] = 'totp_user' + self._conn_info['password'] = 'password' + self._conn_info['totp'] = "123" # Invalid + + err_msg = "Invalid TOTP format" + self.assertConnectionFail(err_msg=err_msg) + + finally: + cur.execute("DROP USER IF EXISTS totp_user") + cur.execute("DROP AUTHENTICATION IF EXISTS totp_auth CASCADE") + + # Negative Test: Wrong TOTP (Valid format, wrong value) + def totp_wrong_code_scenario(self): + with self._connect() as conn: + cur = conn.cursor() + + cur.execute("DROP USER IF EXISTS totp_user") + cur.execute("DROP AUTHENTICATION IF EXISTS totp_auth CASCADE") + + try: + cur.execute("CREATE USER totp_user IDENTIFIED BY 'password' ENFORCEMFA") + cur.execute("CREATE AUTHENTICATION totp_auth METHOD 'password' HOST '0.0.0.0/0'") + cur.execute("GRANT AUTHENTICATION totp_auth TO totp_user") + + self._conn_info['user'] = 'totp_user' + self._conn_info['password'] = 'password' + self._conn_info['totp'] = "999999" # Wrong OTP + + err_msg = "Invalid TOTP" + self.assertConnectionFail(err_msg=err_msg) + + finally: + cur.execute("DROP USER IF EXISTS totp_user") + cur.execute("DROP AUTHENTICATION IF EXISTS totp_auth CASCADE") + From 02823330b70da3fa3d9f218cb4616ab818576562 Mon Sep 17 00:00:00 2001 From: mkottakota1 <149763406+mkottakota1@users.noreply.github.com> Date: Wed, 26 Nov 2025 06:32:39 +0000 Subject: [PATCH 6/7] Fixing intermidate toke fetch issue in pipeline --- .github/workflows/ci.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 464cc13f..d5517af5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -408,15 +408,15 @@ jobs: RAW=$(printf "%s" "$RAW" | sed -n '$p') # Validate RAW is JSON + # Validate JSON; do NOT exit — allow retry if ! printf '%s' "$RAW" | python3 -c 'import sys,json; json.load(sys.stdin)' >/dev/null 2>&1; then - echo "Token endpoint did not return valid JSON:" - printf '%s\n' "$RAW" - exit 1 + echo "Token endpoint did not return valid JSON, retrying..." + TOKEN="" + else + # Extract token only if JSON is valid + TOKEN=$(printf '%s' "$RAW" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("access_token", ""))') fi - # Extract token (without printing it) - TOKEN=$(printf '%s' "$RAW" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("access_token", ""))') - if [ -n "$TOKEN" ] && [ "$TOKEN" != "null" ]; then echo "Access token retrieved successfully." break From 02718311e985e081cfa31403712113aeae3f14c7 Mon Sep 17 00:00:00 2001 From: sharmagot Date: Wed, 26 Nov 2025 08:49:55 -0500 Subject: [PATCH 7/7] Bump version to 1.5.0 and add pyotp dependency --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7b5a7fe9..9bfe1e8f 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ # version should use the format 'x.x.x' (instead of 'vx.x.x') setup( name='vertica-python', - version='1.4.0', + version='1.5.0', description='Official native Python client for the Vertica database.', long_description="vertica-python is the official Vertica database client for the Python programming language. Please check the [project homepage](https://github.com/vertica/vertica-python) for the details.", long_description_content_type='text/markdown', @@ -59,6 +59,7 @@ python_requires=">=3.8", install_requires=[ 'python-dateutil>=1.5', + 'pyotp>=2.9.0', ], classifiers=[ "Development Status :: 5 - Production/Stable",