PYTHON-3096 Finish implementation and tests for GSSAPI options (#1985)
This commit is contained in:
parent
b3ce9320f0
commit
8d27699e75
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user