Skip to content

Commit 5518090

Browse files
committed
PYTHON-3096 Finish implementation and tests for GSSAPI options
1 parent 351196b commit 5518090

File tree

4 files changed

+144
-16
lines changed

4 files changed

+144
-16
lines changed

pymongo/asynchronous/auth.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,20 @@ def _auth_key(nonce: str, username: str, password: str) -> str:
177177
return md5hash.hexdigest()
178178

179179

180-
def _canonicalize_hostname(hostname: str) -> str:
180+
def _canonicalize_hostname(hostname: str, option: str | bool) -> str:
181181
"""Canonicalize hostname following MIT-krb5 behavior."""
182182
# https://github.com/krb5/krb5/blob/d406afa363554097ac48646a29249c04f498c88e/src/util/k5test.py#L505-L520
183+
if option in [False, None]:
184+
return hostname
185+
183186
af, socktype, proto, canonname, sockaddr = socket.getaddrinfo(
184187
hostname, None, 0, 0, socket.IPPROTO_TCP, socket.AI_CANONNAME
185188
)[0]
186189

190+
# For forward just to resolve the cname as dns.lookup() will not return it.
191+
if option == "forward":
192+
return canonname.lower()
193+
187194
try:
188195
name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD)
189196
except socket.gaierror:
@@ -205,9 +212,8 @@ async def _authenticate_gssapi(credentials: MongoCredential, conn: AsyncConnecti
205212
props = credentials.mechanism_properties
206213
# Starting here and continuing through the while loop below - establish
207214
# the security context. See RFC 4752, Section 3.1, first paragraph.
208-
host = conn.address[0]
209-
if props.canonicalize_host_name:
210-
host = _canonicalize_hostname(host)
215+
host = props.host_name or conn.address[0]
216+
host = _canonicalize_hostname(host, props.canonicalize_host_name)
211217
service = props.service_name + "@" + host
212218
if props.service_realm is not None:
213219
service = service + "@" + props.service_realm

pymongo/synchronous/auth.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,20 @@ def _auth_key(nonce: str, username: str, password: str) -> str:
174174
return md5hash.hexdigest()
175175

176176

177-
def _canonicalize_hostname(hostname: str) -> str:
177+
def _canonicalize_hostname(hostname: str, option: str | bool) -> str:
178178
"""Canonicalize hostname following MIT-krb5 behavior."""
179179
# https://github.com/krb5/krb5/blob/d406afa363554097ac48646a29249c04f498c88e/src/util/k5test.py#L505-L520
180+
if option in [False, None]:
181+
return hostname
182+
180183
af, socktype, proto, canonname, sockaddr = socket.getaddrinfo(
181184
hostname, None, 0, 0, socket.IPPROTO_TCP, socket.AI_CANONNAME
182185
)[0]
183186

187+
# For forward just to resolve the cname as dns.lookup() will not return it.
188+
if option == "forward":
189+
return canonname.lower()
190+
184191
try:
185192
name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD)
186193
except socket.gaierror:
@@ -202,9 +209,8 @@ def _authenticate_gssapi(credentials: MongoCredential, conn: Connection) -> None
202209
props = credentials.mechanism_properties
203210
# Starting here and continuing through the while loop below - establish
204211
# the security context. See RFC 4752, Section 3.1, first paragraph.
205-
host = conn.address[0]
206-
if props.canonicalize_host_name:
207-
host = _canonicalize_hostname(host)
212+
host = props.host_name or conn.address[0]
213+
host = _canonicalize_hostname(host, props.canonicalize_host_name)
208214
service = props.service_name + "@" + host
209215
if props.service_realm is not None:
210216
service = service + "@" + props.service_realm

test/asynchronous/test_auth.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ def setUpClass(cls):
9696
cls.service_realm_required = (
9797
GSSAPI_SERVICE_REALM is not None and GSSAPI_SERVICE_REALM not in GSSAPI_PRINCIPAL
9898
)
99-
mech_properties = f"SERVICE_NAME:{GSSAPI_SERVICE_NAME}"
100-
mech_properties += f",CANONICALIZE_HOST_NAME:{GSSAPI_CANONICALIZE}"
101-
if GSSAPI_SERVICE_REALM is not None:
102-
mech_properties += f",SERVICE_REALM:{GSSAPI_SERVICE_REALM}"
99+
mech_properties = dict(
100+
SERVICE_NAME=GSSAPI_SERVICE_NAME, CANONICALIZE_HOST_NAME=GSSAPI_CANONICALIZE
101+
)
102+
mech_properties["SERVICE_REALM"] = GSSAPI_SERVICE_REALM
103103
cls.mech_properties = mech_properties
104104

105105
async def test_credentials_hashing(self):
@@ -268,6 +268,64 @@ async def test_gssapi_threaded(self):
268268
thread.join()
269269
self.assertTrue(thread.success)
270270

271+
async def test_gssapi_canonicalize_host_name(self):
272+
# Use the equivalent named CANONICALIZE_HOST_NAME.
273+
if self.mech_properties["CANONICALIZE_HOST_NAME"] == "true":
274+
self.mech_properties["CANONICALIZE_HOST_NAME"] = "forwardAndReverse"
275+
else:
276+
self.mech_properties["CANONICALIZE_HOST_NAME"] = "none"
277+
client = self.simple_client(
278+
GSSAPI_HOST,
279+
GSSAPI_PORT,
280+
username=GSSAPI_PRINCIPAL,
281+
password=GSSAPI_PASS,
282+
authMechanism="GSSAPI",
283+
authMechanismProperties=self.mech_properties,
284+
)
285+
await client.server_info()
286+
287+
if self.mech_properties["CANONICALIZE_HOST_NAME"] == "none":
288+
return
289+
290+
# Test with "forward".
291+
self.mech_properties["CANONICALIZE_HOST_NAME"] = "forward"
292+
client = self.simple_client(
293+
GSSAPI_HOST,
294+
GSSAPI_PORT,
295+
username=GSSAPI_PRINCIPAL,
296+
password=GSSAPI_PASS,
297+
authMechanism="GSSAPI",
298+
authMechanismProperties=self.mech_properties,
299+
)
300+
await client.server_info()
301+
302+
async def test_gssapi_host_name(self):
303+
props = self.mech_properties
304+
props["SERVICE_HOST"] = "example.com"
305+
306+
# Authenticate with authMechanismProperties.
307+
client = self.simple_client(
308+
GSSAPI_HOST,
309+
GSSAPI_PORT,
310+
username=GSSAPI_PRINCIPAL,
311+
password=GSSAPI_PASS,
312+
authMechanism="GSSAPI",
313+
authMechanismProperties=self.mech_properties,
314+
)
315+
with self.assertRaises(OperationFailure):
316+
await client.server_info()
317+
318+
props["SERVICE_HOST"] = GSSAPI_HOST
319+
client = self.simple_client(
320+
GSSAPI_HOST,
321+
GSSAPI_PORT,
322+
username=GSSAPI_PRINCIPAL,
323+
password=GSSAPI_PASS,
324+
authMechanism="GSSAPI",
325+
authMechanismProperties=self.mech_properties,
326+
)
327+
await client.server_info()
328+
271329

272330
class TestSASLPlain(AsyncPyMongoTestCase):
273331
@classmethod

test/test_auth.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ def setUpClass(cls):
9696
cls.service_realm_required = (
9797
GSSAPI_SERVICE_REALM is not None and GSSAPI_SERVICE_REALM not in GSSAPI_PRINCIPAL
9898
)
99-
mech_properties = f"SERVICE_NAME:{GSSAPI_SERVICE_NAME}"
100-
mech_properties += f",CANONICALIZE_HOST_NAME:{GSSAPI_CANONICALIZE}"
101-
if GSSAPI_SERVICE_REALM is not None:
102-
mech_properties += f",SERVICE_REALM:{GSSAPI_SERVICE_REALM}"
99+
mech_properties = dict(
100+
SERVICE_NAME=GSSAPI_SERVICE_NAME, CANONICALIZE_HOST_NAME=GSSAPI_CANONICALIZE
101+
)
102+
mech_properties["SERVICE_REALM"] = GSSAPI_SERVICE_REALM
103103
cls.mech_properties = mech_properties
104104

105105
def test_credentials_hashing(self):
@@ -268,6 +268,64 @@ def test_gssapi_threaded(self):
268268
thread.join()
269269
self.assertTrue(thread.success)
270270

271+
def test_gssapi_canonicalize_host_name(self):
272+
# Use the equivalent named CANONICALIZE_HOST_NAME.
273+
if self.mech_properties["CANONICALIZE_HOST_NAME"] == "true":
274+
self.mech_properties["CANONICALIZE_HOST_NAME"] = "forwardAndReverse"
275+
else:
276+
self.mech_properties["CANONICALIZE_HOST_NAME"] = "none"
277+
client = self.simple_client(
278+
GSSAPI_HOST,
279+
GSSAPI_PORT,
280+
username=GSSAPI_PRINCIPAL,
281+
password=GSSAPI_PASS,
282+
authMechanism="GSSAPI",
283+
authMechanismProperties=self.mech_properties,
284+
)
285+
client.server_info()
286+
287+
if self.mech_properties["CANONICALIZE_HOST_NAME"] == "none":
288+
return
289+
290+
# Test with "forward".
291+
self.mech_properties["CANONICALIZE_HOST_NAME"] = "forward"
292+
client = self.simple_client(
293+
GSSAPI_HOST,
294+
GSSAPI_PORT,
295+
username=GSSAPI_PRINCIPAL,
296+
password=GSSAPI_PASS,
297+
authMechanism="GSSAPI",
298+
authMechanismProperties=self.mech_properties,
299+
)
300+
client.server_info()
301+
302+
def test_gssapi_host_name(self):
303+
props = self.mech_properties
304+
props["SERVICE_HOST"] = "example.com"
305+
306+
# Authenticate with authMechanismProperties.
307+
client = self.simple_client(
308+
GSSAPI_HOST,
309+
GSSAPI_PORT,
310+
username=GSSAPI_PRINCIPAL,
311+
password=GSSAPI_PASS,
312+
authMechanism="GSSAPI",
313+
authMechanismProperties=self.mech_properties,
314+
)
315+
with self.assertRaises(OperationFailure):
316+
client.server_info()
317+
318+
props["SERVICE_HOST"] = GSSAPI_HOST
319+
client = self.simple_client(
320+
GSSAPI_HOST,
321+
GSSAPI_PORT,
322+
username=GSSAPI_PRINCIPAL,
323+
password=GSSAPI_PASS,
324+
authMechanism="GSSAPI",
325+
authMechanismProperties=self.mech_properties,
326+
)
327+
client.server_info()
328+
271329

272330
class TestSASLPlain(PyMongoTestCase):
273331
@classmethod

0 commit comments

Comments
 (0)