Skip to content

Commit c631eeb

Browse files
committed
WL14213: Added option for krb service principal
1 parent 8657e9a commit c631eeb

File tree

5 files changed

+73
-10
lines changed

5 files changed

+73
-10
lines changed

lib/mysql/connector/abstracts.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@
6262
TLS_VER_NO_SUPPORTED = ("No supported TLS protocol version found in the "
6363
"'tls-versions' list '{}'. ")
6464

65+
KRB_SERVICE_PINCIPAL_ERROR = (
66+
'Option "krb_service_principal" {error}, must be a string in the form '
67+
'"primary/instance@realm" e.g "ldap/[email protected]" where "@realm" '
68+
'is optional and if it is not given will be assumed to belong to the '
69+
'default realm, as configured in the krb5.conf file.')
70+
6571

6672
@make_abc(ABCMeta)
6773
class MySQLConnectionAbstract(object):
@@ -607,6 +613,18 @@ def config(self, **kwargs):
607613

608614
if self._client_flags & ClientFlag.CONNECT_ARGS:
609615
self._add_default_conn_attrs()
616+
if "krb_service_principal" in config and \
617+
config["krb_service_principal"] is not None:
618+
self._krb_service_principal = config["krb_service_principal"]
619+
if not isinstance(self._krb_service_principal, STRING_TYPES):
620+
raise errors.InterfaceError(KRB_SERVICE_PINCIPAL_ERROR.format(
621+
error="is not a string"))
622+
if self._krb_service_principal == "":
623+
raise errors.InterfaceError(KRB_SERVICE_PINCIPAL_ERROR.format(
624+
error="can not be an empty string"))
625+
if "/" not in self._krb_service_principal:
626+
raise errors.InterfaceError(KRB_SERVICE_PINCIPAL_ERROR.format(
627+
error="is incorrectly formatted"))
610628

611629
def _add_default_conn_attrs(self):
612630
"""Add the default connection attributes."""

lib/mysql/connector/authentication.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class BaseAuthPlugin(object):
6969
plugin_name = ''
7070

7171
def __init__(self, auth_data, username=None, password=None, database=None,
72-
ssl_enabled=False):
72+
ssl_enabled=False, instance=None):
7373
"""Initialization"""
7474
self._auth_data = auth_data
7575
self._username = username
@@ -295,6 +295,7 @@ class MySQLLdapSaslPasswordAuthPlugin(BaseAuthPlugin):
295295
client_nonce = None
296296
client_salt = None
297297
server_salt = None
298+
krb_service_principal = None
298299
iterations = 0
299300
server_auth_var = None
300301

@@ -404,7 +405,10 @@ def _first_message_krb(self):
404405
gssapi.RequirementFlag.delegate_to_peer
405406
)
406407

407-
service_principal = "ldap/ldapauth"
408+
if self.krb_service_principal:
409+
service_principal = self.krb_service_principal
410+
else:
411+
service_principal = "ldap/ldapauth"
408412
_LOGGER.debug("# service principal: %s", service_principal)
409413
servk = gssapi.Name(service_principal, name_type=gssapi.NameType.kerberos_principal)
410414
self.target_name = servk
@@ -496,12 +500,13 @@ def auth_accept_close_handshake(self, message):
496500

497501
return wraped.message
498502

499-
def auth_response(self):
503+
def auth_response(self, krb_service_principal=None):
500504
"""This method will prepare the fist message to the server.
501505
502506
Returns bytes to send to the server as the first message.
503507
"""
504508
auth_mechanism = self._auth_data.decode()
509+
self.krb_service_principal = krb_service_principal
505510
_LOGGER.debug("read_method_name_from_server: %s", auth_mechanism)
506511
if auth_mechanism not in self.sasl_mechanisms:
507512
raise errors.InterfaceError(

lib/mysql/connector/connection.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def __init__(self, *args, **kwargs):
9898

9999
self._ssl_active = False
100100
self._auth_plugin = None
101+
self._krb_service_principal = None
101102
self._pool_config_version = None
102103

103104
self._columns_desc = []
@@ -245,11 +246,15 @@ def _auth_switch_request(self, username=None, password=None):
245246
auth = get_auth_plugin(new_auth_plugin)(auth_data,
246247
username=self._user, password=password,
247248
ssl_enabled=self._ssl_active)
248-
response = auth.auth_response()
249-
_LOGGER.debug("# request size: %s", len(response))
249+
if new_auth_plugin == "authentication_ldap_sasl_client":
250+
_LOGGER.debug("# auth_data: %s", auth_data)
251+
response = auth.auth_response(self._krb_service_principal)
252+
else:
253+
response = auth.auth_response()
254+
_LOGGER.debug("# request: %s size: %s", response, len(response))
250255
self._socket.send(response)
251256
packet = self._socket.recv()
252-
_LOGGER.debug("server response packet: %s", packet)
257+
_LOGGER.debug("# server response packet: %s", packet)
253258
if new_auth_plugin == "authentication_ldap_sasl_client" \
254259
and len(packet) >= 6 and packet[5] == 114 and packet[6] == 61: # 'r' and '='
255260
# Continue with sasl authentication
@@ -261,7 +266,8 @@ def _auth_switch_request(self, username=None, password=None):
261266
if auth.auth_finalize(packet[5:]):
262267
# receive packed OK
263268
packet = self._socket.recv()
264-
elif new_auth_plugin == "authentication_ldap_sasl_client":
269+
elif new_auth_plugin == "authentication_ldap_sasl_client" and \
270+
auth_data == b'GSSAPI' and packet[4] != 255:
265271
rcode_size = 5 # header size for the response status code.
266272
_LOGGER.debug("# Continue with sasl GSSAPI authentication")
267273
_LOGGER.debug("# response header: %s", packet[:rcode_size+1])

lib/mysql/connector/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@
7979
'consume_results': False,
8080
'conn_attrs': None,
8181
'dns_srv': False,
82-
'use_pure': False
82+
'use_pure': False,
83+
'krb_service_principal': None
8384
}
8485

8586
CNX_POOL_ARGS = ('pool_name', 'pool_size', 'pool_reset_session')

tests/test_connection.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2548,7 +2548,7 @@ def test_connect_with_can_handle_expired_pw_flag(self):
25482548
@unittest.skipIf(tests.MYSQL_VERSION < (5, 7),
25492549
"Authentication with ldap_simple not supported")
25502550
#Skip if remote ldap server is not reachable.
2551-
@unittest.skipIf(not tests.is_host_reachable("100.103.18.98"),
2551+
@unittest.skipIf(not tests.is_host_reachable("10.172.166.126"),
25522552
"ldap server is not reachable")
25532553
@unittest.skipIf(not tests.is_plugin_available("authentication_ldap_sasl"),
25542554
"Plugin authentication_ldap_sasl not available")
@@ -2915,7 +2915,7 @@ def tearDown(self):
29152915

29162916
@tests.foreach_cnx()
29172917
def test_authentication_ldap_sasl_client(self):
2918-
"""test_authentication_ldap_sasl_client_with_SCRAM-SHA-1"""
2918+
"""test_authentication_ldap_sasl_client_with_SCRAM-SHA-256"""
29192919
# Not running with c-ext if plugin libraries are not setup
29202920
if self.cnx.__class__ == CMySQLConnection and \
29212921
os.getenv('TEST_AUTHENTICATION_LDAP_SASL_CLIENT_CEXT', None) is None:
@@ -3152,6 +3152,7 @@ def test_authentication_ldap_sasl_krb(self):
31523152
"host": self.config["host"],
31533153
"port": self.config["port"],
31543154
"password": "Testpw1",
3155+
"krb_service_principal": "ldap/[email protected]"
31553156
}
31563157

31573158
# Attempt connection with wrong password
@@ -3168,6 +3169,38 @@ def test_authentication_ldap_sasl_krb(self):
31683169
"password", context.exception.msg, "not the expected "
31693170
"error {}".format(context.exception.msg))
31703171

3172+
# Attempt connection with empty krb_service_principal
3173+
bad_pass_args = conn_args.copy()
3174+
bad_pass_args["krb_service_principal"] = ""
3175+
with self.assertRaises((ProgrammingError, InterfaceError)) as context:
3176+
_ = self.cnx.__class__(**bad_pass_args)
3177+
self.assertIn("can not be an empty string", context.exception.msg,
3178+
"not the expected error {}".format(context.exception.msg))
3179+
3180+
# Attempt connection with an incorrectly formatted krb_service_principal
3181+
bad_pass_args = conn_args.copy()
3182+
bad_pass_args["krb_service_principal"] = "service_principal123"
3183+
with self.assertRaises((ProgrammingError, InterfaceError)) as context:
3184+
_ = self.cnx.__class__(**bad_pass_args)
3185+
self.assertIn("incorrectly formatted", context.exception.msg,
3186+
"not the expected error {}".format(context.exception.msg))
3187+
3188+
# Attempt connection with an incorrectly formatted krb_service_principal
3189+
bad_pass_args = conn_args.copy()
3190+
bad_pass_args["krb_service_principal"] = 3308
3191+
with self.assertRaises((ProgrammingError, InterfaceError)) as context:
3192+
_ = self.cnx.__class__(**bad_pass_args)
3193+
self.assertIn("not a string", context.exception.msg,
3194+
"not the expected error {}".format(context.exception.msg))
3195+
3196+
# Attempt connection with a false krb_service_principal
3197+
bad_pass_args = conn_args.copy()
3198+
bad_pass_args["krb_service_principal"] = "principal/instance@realm"
3199+
with self.assertRaises((ProgrammingError, InterfaceError)) as context:
3200+
_ = self.cnx.__class__(**bad_pass_args)
3201+
self.assertIn("Unable to initiate security context", context.exception.msg,
3202+
"not the expected error {}".format(context.exception.msg))
3203+
31713204
# Attempt connection with correct password
31723205
cnx = self.cnx.__class__(**conn_args)
31733206
cnx.cmd_query('SELECT USER()')

0 commit comments

Comments
 (0)