PYTHON-3096 Finish implementation and tests for GSSAPI options (#1985)

This commit is contained in:
Steven Silvester 2024-12-31 16:29:48 -06:00 committed by GitHub
parent b3ce9320f0
commit 8d27699e75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 144 additions and 19 deletions

View File

@ -1,7 +1,8 @@
#!/bin/bash
set -eu
# Disable xtrace for security reasons (just in case it was accidentally set).
set +x
# Use the default python to bootstrap secrets.
PYTHON_BINARY="" bash "${DRIVERS_TOOLS}"/.evergreen/auth_aws/setup_secrets.sh drivers/enterprise_auth
bash "${DRIVERS_TOOLS}"/.evergreen/secrets_handling/setup-secrets.sh drivers/enterprise_auth
TEST_ENTERPRISE_AUTH=1 AUTH=auth bash "${PROJECT_DIRECTORY}"/.evergreen/hatch.sh test:test-eg

View File

@ -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":
return canonname.lower()
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

View File

@ -174,13 +174,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":
return canonname.lower()
try:
name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD)
except socket.gaierror:
@ -202,9 +209,8 @@ def _authenticate_gssapi(credentials: MongoCredential, conn: Connection) -> None
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

View File

@ -35,7 +35,7 @@ from test.utils import AllowListEventListener, delay, ignore_deprecations
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 @@ class TestGSSAPI(AsyncPyMongoTestCase):
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 @@ class TestGSSAPI(AsyncPyMongoTestCase):
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,58 @@ class TestGSSAPI(AsyncPyMongoTestCase):
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()
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

View File

@ -40,7 +40,7 @@ from pymongo.errors import OperationFailure
from pymongo.hello import HelloCompat
from pymongo.read_preferences import ReadPreference
from pymongo.saslprep import HAVE_STRINGPREP
from pymongo.synchronous.auth import HAVE_KERBEROS
from pymongo.synchronous.auth import HAVE_KERBEROS, _canonicalize_hostname
_IS_SYNC = True
@ -96,10 +96,11 @@ class TestGSSAPI(PyMongoTestCase):
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
def test_credentials_hashing(self):
@ -167,7 +168,10 @@ class TestGSSAPI(PyMongoTestCase):
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)
client[GSSAPI_DB].collection.find_one()
@ -268,6 +272,58 @@ class TestGSSAPI(PyMongoTestCase):
thread.join()
self.assertTrue(thread.success)
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,
)
client.server_info()
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):
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,
)
client.server_info()
class TestSASLPlain(PyMongoTestCase):
@classmethod