cbs-web-antivirus-scanner/venv/lib/python3.12/site-packages/mysql/connector/authentication.py
2024-11-19 15:19:23 -05:00

376 lines
14 KiB
Python

# Copyright (c) 2014, 2024, Oracle and/or its affiliates.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0, as
# published by the Free Software Foundation.
#
# This program is designed to work with certain software (including
# but not limited to OpenSSL) that is licensed under separate terms,
# as designated in a particular file or component or in included license
# documentation. The authors of MySQL hereby grant you an
# additional permission to link the program and your derivative works
# with the separately licensed software that they have either included with
# the program or referenced in the documentation.
#
# Without limiting anything contained in the foregoing, this file,
# which is part of MySQL Connector/Python, is also subject to the
# Universal FOSS Exception, version 1.0, a copy of which can be found at
# http://oss.oracle.com/licenses/universal-foss-exception.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License, version 2.0, for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
"""Implementing support for MySQL Authentication Plugins"""
from __future__ import annotations
import copy
from typing import TYPE_CHECKING, Any, Dict, Optional
from .errors import InterfaceError, NotSupportedError, get_exception
from .logger import logger
from .plugins import MySQLAuthPlugin, get_auth_plugin
from .protocol import (
AUTH_SWITCH_STATUS,
DEFAULT_CHARSET_ID,
DEFAULT_MAX_ALLOWED_PACKET,
ERR_STATUS,
EXCHANGE_FURTHER_STATUS,
MFA_STATUS,
OK_STATUS,
MySQLProtocol,
)
from .types import HandShakeType
if TYPE_CHECKING:
from .network import MySQLSocket
class MySQLAuthenticator:
"""Implements the authentication phase."""
def __init__(self) -> None:
"""Constructor."""
self._username: str = ""
self._passwords: Dict[int, str] = {}
self._plugin_config: Dict[str, Any] = {}
self._ssl_enabled: bool = False
self._auth_strategy: Optional[MySQLAuthPlugin] = None
self._auth_plugin_class: Optional[str] = None
@property
def ssl_enabled(self) -> bool:
"""Signals whether or not SSL is enabled."""
return self._ssl_enabled
@property
def plugin_config(self) -> Dict[str, Any]:
"""Custom arguments that are being provided to the authentication plugin when called.
The parameters defined here will override the ones defined in the
auth plugin itself.
The plugin config is a read-only property - the plugin configuration
provided when invoking `authenticate()` is recorded and can be queried
by accessing this property.
Returns:
dict: The latest plugin configuration provided when invoking
`authenticate()`.
"""
return self._plugin_config
def setup_ssl(
self,
sock: MySQLSocket,
host: str,
ssl_options: Optional[Dict[str, Any]],
charset: int = DEFAULT_CHARSET_ID,
client_flags: int = 0,
max_allowed_packet: int = DEFAULT_MAX_ALLOWED_PACKET,
) -> bytes:
"""Sets up an SSL communication channel.
Args:
sock: Pointer to the socket connection.
host: Server host name.
ssl_options: SSL and TLS connection options (see
`network.MySQLSocket.build_ssl_context`).
charset: Client charset (see [1]), only the lower 8-bits.
client_flags: Integer representing client capabilities flags.
max_allowed_packet: Maximum packet size.
Returns:
ssl_request_payload: Payload used to carry out SSL authentication.
References:
[1]: https://dev.mysql.com/doc/dev/mysql-server/latest/\
page_protocol_basic_character_set.html#a_protocol_character_set
"""
if ssl_options is None:
ssl_options = {}
# SSL connection request packet
ssl_request_payload = MySQLProtocol.make_auth_ssl(
charset=charset,
client_flags=client_flags,
max_allowed_packet=max_allowed_packet,
)
sock.send(ssl_request_payload)
logger.debug("Building SSL context")
ssl_context = sock.build_ssl_context(
ssl_ca=ssl_options.get("ca"),
ssl_cert=ssl_options.get("cert"),
ssl_key=ssl_options.get("key"),
ssl_verify_cert=ssl_options.get("verify_cert", False),
ssl_verify_identity=ssl_options.get("verify_identity", False),
tls_versions=ssl_options.get("tls_versions"),
tls_cipher_suites=ssl_options.get("tls_ciphersuites"),
)
logger.debug("Switching to SSL")
sock.switch_to_ssl(ssl_context, host)
logger.debug("SSL has been enabled")
self._ssl_enabled = True
return ssl_request_payload
def _switch_auth_strategy(
self,
new_strategy_name: str,
strategy_class: Optional[str] = None,
username: Optional[str] = None,
password_factor: int = 1,
) -> None:
"""Switches the authorization plugin.
Args:
new_strategy_name: New authorization plugin name to switch to.
strategy_class: New authorization plugin class to switch to
(has higher precedence than the authorization plugin name).
username: Username to be used - if not defined, the username
provided when `authentication()` was invoked is used.
password_factor: Up to three levels of authentication (MFA) are allowed,
hence you can choose the password corresponding to the 1st,
2nd, or 3rd factor - 1st is the default.
"""
if username is None:
username = self._username
if strategy_class is None:
strategy_class = self._auth_plugin_class
logger.debug("Switching to strategy %s", new_strategy_name)
self._auth_strategy = get_auth_plugin(
plugin_name=new_strategy_name, auth_plugin_class=strategy_class
)(
username,
self._passwords.get(password_factor, ""),
ssl_enabled=self.ssl_enabled,
)
def _mfa_n_factor(
self,
sock: MySQLSocket,
pkt: bytes,
) -> Optional[bytes]:
"""Handles MFA (Multi-Factor Authentication) response.
Up to three levels of authentication (MFA) are allowed.
Args:
sock: Pointer to the socket connection.
pkt: MFA response.
Returns:
ok_packet: If last server's response is an OK packet.
None: If last server's response isn't an OK packet and no ERROR was raised.
Raises:
InterfaceError: If got an invalid N factor.
errors.ErrorTypes: If got an ERROR response.
"""
n_factor = 2
while pkt[4] == MFA_STATUS:
if n_factor not in self._passwords:
raise InterfaceError(
"Failed Multi Factor Authentication (invalid N factor)"
)
new_strategy_name, auth_data = MySQLProtocol.parse_auth_next_factor(pkt)
self._switch_auth_strategy(new_strategy_name, password_factor=n_factor)
logger.debug("MFA %i factor %s", n_factor, self._auth_strategy.name)
pkt = self._auth_strategy.auth_switch_response(
sock, auth_data, **self._plugin_config
)
if pkt[4] == EXCHANGE_FURTHER_STATUS:
auth_data = MySQLProtocol.parse_auth_more_data(pkt)
pkt = self._auth_strategy.auth_more_response(
sock, auth_data, **self._plugin_config
)
if pkt[4] == OK_STATUS:
logger.debug("MFA completed succesfully")
return pkt
if pkt[4] == ERR_STATUS:
raise get_exception(pkt)
n_factor += 1
logger.warning("MFA terminated with a no ok packet")
return None
def _handle_server_response(
self,
sock: MySQLSocket,
pkt: bytes,
) -> Optional[bytes]:
"""Handles server's response.
Args:
sock: Pointer to the socket connection.
pkt: Server's response after completing the `HandShakeResponse`.
Returns:
ok_packet: If last server's response is an OK packet.
None: If last server's response isn't an OK packet and no ERROR was raised.
Raises:
errors.ErrorTypes: If got an ERROR response.
NotSupportedError: If got Authentication with old (insecure) passwords.
"""
if pkt[4] == AUTH_SWITCH_STATUS and len(pkt) == 5:
raise NotSupportedError(
"Authentication with old (insecure) passwords "
"is not supported. For more information, lookup "
"Password Hashing in the latest MySQL manual"
)
if pkt[4] == AUTH_SWITCH_STATUS:
logger.debug("Server's response is an auth switch request")
new_strategy_name, auth_data = MySQLProtocol.parse_auth_switch_request(pkt)
self._switch_auth_strategy(new_strategy_name)
pkt = self._auth_strategy.auth_switch_response(
sock, auth_data, **self._plugin_config
)
if pkt[4] == EXCHANGE_FURTHER_STATUS:
logger.debug("Exchanging further packets")
auth_data = MySQLProtocol.parse_auth_more_data(pkt)
pkt = self._auth_strategy.auth_more_response(
sock, auth_data, **self._plugin_config
)
if pkt[4] == OK_STATUS:
logger.debug("%s completed succesfully", self._auth_strategy.name)
return pkt
if pkt[4] == MFA_STATUS:
logger.debug("Starting multi-factor authentication")
logger.debug("MFA 1 factor %s", self._auth_strategy.name)
return self._mfa_n_factor(sock, pkt)
if pkt[4] == ERR_STATUS:
raise get_exception(pkt)
return None
def authenticate(
self,
sock: MySQLSocket,
handshake: HandShakeType,
username: str = "",
password1: str = "",
password2: str = "",
password3: str = "",
database: Optional[str] = None,
charset: int = DEFAULT_CHARSET_ID,
client_flags: int = 0,
max_allowed_packet: int = DEFAULT_MAX_ALLOWED_PACKET,
auth_plugin: Optional[str] = None,
auth_plugin_class: Optional[str] = None,
conn_attrs: Optional[Dict[str, str]] = None,
is_change_user_request: bool = False,
**plugin_config: Any,
) -> bytes:
"""Performs the authentication phase.
During re-authentication you must set `is_change_user_request` to True.
Args:
sock: Pointer to the socket connection.
handshake: Initial handshake.
username: Account's username.
password1: Account's password factor 1.
password2: Account's password factor 2.
password3: Account's password factor 3.
database: Initial database name for the connection.
charset: Client charset (see [1]), only the lower 8-bits.
client_flags: Integer representing client capabilities flags.
max_allowed_packet: Maximum packet size.
auth_plugin: Authorization plugin name.
auth_plugin_class: Authorization plugin class (has higher precedence
than the authorization plugin name).
conn_attrs: Connection attributes.
is_change_user_request: Whether is a `change user request` operation or not.
plugin_config: Custom configuration to be passed to the auth plugin
when invoked. The parameters defined here will override the
ones defined in the auth plugin itself.
Returns:
ok_packet: OK packet.
Raises:
InterfaceError: If OK packet is NULL.
References:
[1]: https://dev.mysql.com/doc/dev/mysql-server/latest/\
page_protocol_basic_character_set.html#a_protocol_character_set
"""
# update credentials, plugin config and plugin class
self._username = username
self._passwords = {1: password1, 2: password2, 3: password3}
self._plugin_config = copy.deepcopy(plugin_config)
self._auth_plugin_class = auth_plugin_class
# client's handshake response
response_payload, self._auth_strategy = MySQLProtocol.make_auth(
handshake=handshake,
username=username,
password=password1,
database=database,
charset=charset,
client_flags=client_flags,
max_allowed_packet=max_allowed_packet,
auth_plugin=auth_plugin,
auth_plugin_class=auth_plugin_class,
conn_attrs=conn_attrs,
is_change_user_request=is_change_user_request,
ssl_enabled=self.ssl_enabled,
plugin_config=self.plugin_config,
)
# client sends transaction response
send_args = (0, 0) if is_change_user_request else (None, None)
sock.send(response_payload, *send_args)
# server replies back
pkt = bytes(sock.recv())
ok_pkt = self._handle_server_response(sock, pkt)
if ok_pkt is None:
raise InterfaceError("Got a NULL ok_pkt") from None
return ok_pkt