|
50 | 50 |
|
51 | 51 | try:
|
52 | 52 | import gssapi
|
53 |
| -except: |
| 53 | +except ImportError: |
54 | 54 | gssapi = None
|
55 | 55 |
|
| 56 | +try: |
| 57 | + import sspi |
| 58 | + import sspicon |
| 59 | + import win32api |
| 60 | +except ImportError: |
| 61 | + sspi = None |
| 62 | + sspicon = None |
| 63 | + win32api = None |
| 64 | + |
56 | 65 | from . import errors
|
57 | 66 | from .utils import (normalize_unicode_string as norm_ustr,
|
58 | 67 | validate_normalized_unicode_string as valid_norm)
|
@@ -1017,6 +1026,125 @@ def auth_response(self, oci_path=None):
|
1017 | 1026 | return auth_response.encode()
|
1018 | 1027 |
|
1019 | 1028 |
|
| 1029 | +class MySQLSSPIKerberosAuthPlugin(BaseAuthPlugin): |
| 1030 | + """Implement the MySQL Kerberos authentication plugin with Windows SSPI""" |
| 1031 | + |
| 1032 | + plugin_name = "authentication_kerberos_client" |
| 1033 | + requires_ssl = False |
| 1034 | + context = None |
| 1035 | + |
| 1036 | + def _parse_auth_data(self, packet): |
| 1037 | + """Parse authentication data. |
| 1038 | +
|
| 1039 | + Get the SPN and REALM from the authentication data packet. |
| 1040 | +
|
| 1041 | + Format: |
| 1042 | + SPN string length two bytes <B1> <B2> + |
| 1043 | + SPN string + |
| 1044 | + UPN realm string length two bytes <B1> <B2> + |
| 1045 | + UPN realm string |
| 1046 | +
|
| 1047 | + Returns: |
| 1048 | + tuple: With 'spn' and 'realm'. |
| 1049 | + """ |
| 1050 | + spn_len = struct.unpack("<H", packet[:2])[0] |
| 1051 | + packet = packet[2:] |
| 1052 | + |
| 1053 | + spn = struct.unpack(f"<{spn_len}s", packet[:spn_len])[0] |
| 1054 | + packet = packet[spn_len:] |
| 1055 | + |
| 1056 | + realm_len = struct.unpack("<H", packet[:2])[0] |
| 1057 | + realm = struct.unpack(f"<{realm_len}s", packet[2:])[0] |
| 1058 | + |
| 1059 | + return spn.decode(), realm.decode() |
| 1060 | + |
| 1061 | + def auth_response(self, auth_data=None): |
| 1062 | + """Prepare the first message to the server.""" |
| 1063 | + _LOGGER.debug("auth_response for sspi") |
| 1064 | + spn = None |
| 1065 | + realm = None |
| 1066 | + |
| 1067 | + if auth_data: |
| 1068 | + try: |
| 1069 | + spn, realm = self._parse_auth_data(auth_data) |
| 1070 | + except struct.error as err: |
| 1071 | + raise InterruptedError(f"Invalid authentication data: {err}") |
| 1072 | + |
| 1073 | + _LOGGER.debug("Service Principal: %s", spn) |
| 1074 | + _LOGGER.debug("Realm: %s", realm) |
| 1075 | + _LOGGER.debug("Username: %s", self._username) |
| 1076 | + |
| 1077 | + if sspicon is None or sspi is None: |
| 1078 | + raise errors.ProgrammingError( |
| 1079 | + 'Package "pywin32" (Python for Win32 (pywin32) extensions)' |
| 1080 | + ' is not installed.') |
| 1081 | + |
| 1082 | + flags = ( |
| 1083 | + sspicon.ISC_REQ_MUTUAL_AUTH, |
| 1084 | + sspicon.ISC_REQ_DELEGATE |
| 1085 | + ) |
| 1086 | + |
| 1087 | + if self._username and self._password: |
| 1088 | + _auth_info = (self._username, realm, self._password) |
| 1089 | + else: |
| 1090 | + _auth_info = None |
| 1091 | + |
| 1092 | + targetspn = spn |
| 1093 | + _LOGGER.debug("targetspn: %s", targetspn) |
| 1094 | + _LOGGER.debug("_auth_info is None: %s", _auth_info is None) |
| 1095 | + |
| 1096 | + self.clientauth = sspi.ClientAuth( |
| 1097 | + 'Kerberos', targetspn=targetspn, auth_info=_auth_info, |
| 1098 | + scflags=sum(flags), datarep=sspicon.SECURITY_NETWORK_DREP) |
| 1099 | + |
| 1100 | + try: |
| 1101 | + data = None |
| 1102 | + err, out_buf = self.clientauth.authorize(data) |
| 1103 | + _LOGGER.debug("Context step err: %s", err) |
| 1104 | + _LOGGER.debug("Context step out_buf: %s", out_buf) |
| 1105 | + _LOGGER.debug("Context completed?: %s", self.clientauth.authenticated) |
| 1106 | + initial_client_token = out_buf[0].Buffer |
| 1107 | + _LOGGER.debug("pkg_info: %s", self.clientauth.pkg_info) |
| 1108 | + except Exception as err: |
| 1109 | + raise errors.InterfaceError( |
| 1110 | + f"Unable to initiate security context: {err}" |
| 1111 | + ) |
| 1112 | + |
| 1113 | + _LOGGER.debug("Initial client token: %s", initial_client_token) |
| 1114 | + return initial_client_token |
| 1115 | + |
| 1116 | + def auth_continue(self, tgt_auth_challenge): |
| 1117 | + """Continue with the Kerberos TGT service request. |
| 1118 | +
|
| 1119 | + With the TGT authentication service given response generate a TGT |
| 1120 | + service request. This method must be invoked sequentially (in a loop) |
| 1121 | + until the security context is completed and an empty response needs to |
| 1122 | + be send to acknowledge the server. |
| 1123 | +
|
| 1124 | + Args: |
| 1125 | + tgt_auth_challenge: the challenge for the negotiation. |
| 1126 | +
|
| 1127 | + Returns: |
| 1128 | + tuple (bytearray TGS service request, |
| 1129 | + bool True if context is completed otherwise False). |
| 1130 | + """ |
| 1131 | + _LOGGER.debug("tgt_auth challenge: %s", tgt_auth_challenge) |
| 1132 | + |
| 1133 | + err, out_buf = self.clientauth.authorize(tgt_auth_challenge) |
| 1134 | + |
| 1135 | + _LOGGER.debug("Context step err: %s", err) |
| 1136 | + _LOGGER.debug("Context step out_buf: %s", out_buf) |
| 1137 | + resp = out_buf[0].Buffer |
| 1138 | + _LOGGER.debug("Context step resp: %s", resp) |
| 1139 | + _LOGGER.debug("Context completed?: %s", self.clientauth.authenticated) |
| 1140 | + |
| 1141 | + return resp, self.clientauth.authenticated |
| 1142 | + |
| 1143 | + |
| 1144 | +if os.name == 'nt': |
| 1145 | + MySQLKerberosAuthPlugin = MySQLSSPIKerberosAuthPlugin |
| 1146 | + |
| 1147 | + |
1020 | 1148 | def get_auth_plugin(plugin_name):
|
1021 | 1149 | """Return authentication class based on plugin name
|
1022 | 1150 |
|
|
0 commit comments