-
Notifications
You must be signed in to change notification settings - Fork 1.1k
PYTHON-3096 Finish implementation and tests for GSSAPI options #1985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
5518090
2c0242a
5b69e09
5e04104
954ffce
2a16ba1
c73a87a
cb852dd
ec3c1f0
8c2e48f
55720fb
9b7f9ae
20407bc
e180518
b9a54f6
46d3fb9
53a23b8
b74725e
4b371de
714f717
397f1a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -177,13 +177,20 @@ def _auth_key(nonce: str, username: str, password: str) -> str: | |
return md5hash.hexdigest() | ||
|
||
|
||
def _canonicalize_hostname(hostname: str) -> str: | ||
def _canonicalize_hostname(hostname: str, option: str | bool) -> str: | ||
"""Canonicalize hostname following MIT-krb5 behavior.""" | ||
# https://github.com/krb5/krb5/blob/d406afa363554097ac48646a29249c04f498c88e/src/util/k5test.py#L505-L520 | ||
if option in [False, "none"]: | ||
return hostname | ||
|
||
af, socktype, proto, canonname, sockaddr = socket.getaddrinfo( | ||
hostname, None, 0, 0, socket.IPPROTO_TCP, socket.AI_CANONNAME | ||
)[0] | ||
|
||
# For forward just to resolve the cname as dns.lookup() will not return it. | ||
if option == "forward": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the string
Should that have the same behavior as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed it to convert "none" to false, so the three validated options are True, False, and "forward". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually no, the spec test is expecting it to validate as a string, so I updated the business logic instead. |
||
return canonname.lower() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still confused by this method. DRIVERS-1803 says:
But here we are introducing new behavior for "forward". Is that because our behavior for "true" is actually "forwardAndReverse"? If so could you add a comment mentioning that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I misinterpreted the Node implementation and got confused. I'll redo the logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it was the description in the ticket that was incorrect. I verified by reading the kerberos spec, the drivers spec, and the node implementation that this is the correct implementation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also updated the ticket description |
||
|
||
try: | ||
name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD) | ||
except socket.gaierror: | ||
|
@@ -205,9 +212,8 @@ async def _authenticate_gssapi(credentials: MongoCredential, conn: AsyncConnecti | |
props = credentials.mechanism_properties | ||
# Starting here and continuing through the while loop below - establish | ||
# the security context. See RFC 4752, Section 3.1, first paragraph. | ||
host = conn.address[0] | ||
if props.canonicalize_host_name: | ||
host = _canonicalize_hostname(host) | ||
host = props.service_host or conn.address[0] | ||
host = _canonicalize_hostname(host, props.canonicalize_host_name) | ||
service = props.service_name + "@" + host | ||
if props.service_realm is not None: | ||
service = service + "@" + props.service_realm | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,7 +35,7 @@ | |
import pytest | ||
|
||
from pymongo import AsyncMongoClient, monitoring | ||
from pymongo.asynchronous.auth import HAVE_KERBEROS | ||
from pymongo.asynchronous.auth import HAVE_KERBEROS, _canonicalize_hostname | ||
from pymongo.auth_shared import _build_credentials_tuple | ||
from pymongo.errors import OperationFailure | ||
from pymongo.hello import HelloCompat | ||
|
@@ -96,10 +96,11 @@ def setUpClass(cls): | |
cls.service_realm_required = ( | ||
GSSAPI_SERVICE_REALM is not None and GSSAPI_SERVICE_REALM not in GSSAPI_PRINCIPAL | ||
) | ||
mech_properties = f"SERVICE_NAME:{GSSAPI_SERVICE_NAME}" | ||
mech_properties += f",CANONICALIZE_HOST_NAME:{GSSAPI_CANONICALIZE}" | ||
mech_properties = dict( | ||
SERVICE_NAME=GSSAPI_SERVICE_NAME, CANONICALIZE_HOST_NAME=GSSAPI_CANONICALIZE | ||
) | ||
if GSSAPI_SERVICE_REALM is not None: | ||
mech_properties += f",SERVICE_REALM:{GSSAPI_SERVICE_REALM}" | ||
mech_properties["SERVICE_REALM"] = GSSAPI_SERVICE_REALM | ||
cls.mech_properties = mech_properties | ||
|
||
async def test_credentials_hashing(self): | ||
|
@@ -167,7 +168,10 @@ async def test_gssapi_simple(self): | |
await client[GSSAPI_DB].collection.find_one() | ||
|
||
# Log in using URI, with authMechanismProperties. | ||
mech_uri = uri + f"&authMechanismProperties={self.mech_properties}" | ||
mech_properties_str = "" | ||
for key, value in self.mech_properties.items(): | ||
mech_properties_str += f"{key}:{value}," | ||
mech_uri = uri + f"&authMechanismProperties={mech_properties_str[:-1]}" | ||
client = self.simple_client(mech_uri) | ||
await client[GSSAPI_DB].collection.find_one() | ||
|
||
|
@@ -268,6 +272,61 @@ async def test_gssapi_threaded(self): | |
thread.join() | ||
self.assertTrue(thread.success) | ||
|
||
async def test_gssapi_canonicalize_host_name(self): | ||
# Test the low level method. | ||
assert GSSAPI_HOST is not None | ||
result = _canonicalize_hostname(GSSAPI_HOST, "forward") | ||
if "compute-1.amazonaws.com" not in result: | ||
self.assertEqual(result, GSSAPI_HOST) | ||
result = _canonicalize_hostname(GSSAPI_HOST, "forwardAndReverse") | ||
self.assertEqual(result, GSSAPI_HOST) | ||
|
||
# Use the equivalent named CANONICALIZE_HOST_NAME. | ||
props = self.mech_properties.copy() | ||
if props["CANONICALIZE_HOST_NAME"] == "true": | ||
props["CANONICALIZE_HOST_NAME"] = "forwardAndReverse" | ||
else: | ||
props["CANONICALIZE_HOST_NAME"] = "none" | ||
client = self.simple_client( | ||
GSSAPI_HOST, | ||
GSSAPI_PORT, | ||
username=GSSAPI_PRINCIPAL, | ||
password=GSSAPI_PASS, | ||
authMechanism="GSSAPI", | ||
authMechanismProperties=props, | ||
) | ||
await client.server_info() | ||
|
||
if props["CANONICALIZE_HOST_NAME"] == "none": | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This if-statement should be removed since it's redundant, or is there something missing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed |
||
|
||
async def test_gssapi_host_name(self): | ||
props = self.mech_properties | ||
props["SERVICE_HOST"] = "example.com" | ||
|
||
# Authenticate with authMechanismProperties. | ||
client = self.simple_client( | ||
GSSAPI_HOST, | ||
GSSAPI_PORT, | ||
username=GSSAPI_PRINCIPAL, | ||
password=GSSAPI_PASS, | ||
authMechanism="GSSAPI", | ||
authMechanismProperties=self.mech_properties, | ||
) | ||
with self.assertRaises(OperationFailure): | ||
await client.server_info() | ||
|
||
props["SERVICE_HOST"] = GSSAPI_HOST | ||
client = self.simple_client( | ||
GSSAPI_HOST, | ||
GSSAPI_PORT, | ||
username=GSSAPI_PRINCIPAL, | ||
password=GSSAPI_PASS, | ||
authMechanism="GSSAPI", | ||
authMechanismProperties=self.mech_properties, | ||
) | ||
await client.server_info() | ||
|
||
|
||
class TestSASLPlain(AsyncPyMongoTestCase): | ||
@classmethod | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be using asyncio's loop.getaddrinfo. Can you open a new ticket to fix that? We might have the same problem in other places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://jira.mongodb.org/browse/PYTHON-5021