Skip to content

Commit

Permalink
Change underlying lib to pyspnego
Browse files Browse the repository at this point in the history
Changes the Kerberos backend library from pykerberos and winkerberos to pyspnego. This moves away from those old library as they are getting harder to build on modern systems. It also opens up the possibility of explicit username/password authentication as more of the GSSAPI/SSPI libraries are exposed due to the more modern and stable API used underneath.
  • Loading branch information
jborean93 authored Oct 30, 2021
2 parents a31a6e2 + 5dfe4b0 commit f211fe8
Show file tree
Hide file tree
Showing 11 changed files with 949 additions and 1,048 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ jobs:
image: python:3.8-slim
- python-version: '3.9'
image: python:3.9-slim
#- python-version: '3.10'
# image: python:3.10-slim
- python-version: '3.10'
image: python:3.10-slim

steps:
- uses: actions/checkout@v2

- name: Start test HTTP container
- name: Run tests
shell: bash
run: >-
docker run
Expand All @@ -54,7 +54,7 @@ jobs:
-e PYTEST_ADDOPTS="--color=yes"
-e KERBEROS_USERNAME=administrator
-e KERBEROS_PASSWORD=Password01
-e KERBEROS_REALM=example.com
-h host.example.com
${{ matrix.image }}
/bin/bash ci/setup-kerb.sh
Expand Down
12 changes: 12 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
History
=======

0.13.0: TBD
------------------

- Change Kerberos dependencies to pyspnego_ to modernise the underlying
Kerberos library that is used.
- Removed the ``wrap_winrm`` and ``unwrap_winrm`` functions
- Dropped support for Python 2 and raised minimum Python version to 3.6.
- Renamed the ``context`` attribute to ``_context`` to indicate it's meant for
internal use only.

.. _pyspnego: https://github.com/jborean93/pyspnego

0.12.0: 2017-12-20
------------------------

Expand Down
24 changes: 6 additions & 18 deletions ci/setup-kerb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@

set -e

IP_ADDRESS=$(hostname -I)
HOSTNAME=$(cat /etc/hostname)
PY_MAJOR=${PYENV:0:1}

export KERBEROS_HOSTNAME=$HOSTNAME.$KERBEROS_REALM
export KERBEROS_HOSTNAME=$(cat /etc/hostname)
export KERBEROS_REALM=$(echo $KERBEROS_HOSTNAME | cut -d'.' -f2,3)
export DEBIAN_FRONTEND=noninteractive

echo "Configure the hosts file for Kerberos to work in a container"
cp /etc/hosts ~/hosts.new
sed -i "/.*$HOSTNAME/c\\$IP_ADDRESS\t$KERBEROS_HOSTNAME" ~/hosts.new
cp -f ~/hosts.new /etc/hosts

echo "Setting up Kerberos config file at /etc/krb5.conf"
cat > /etc/krb5.conf << EOL
[libdefaults]
Expand All @@ -23,8 +17,8 @@ cat > /etc/krb5.conf << EOL
[realms]
${KERBEROS_REALM^^} = {
kdc = $KERBEROS_HOSTNAME
admin_server = $KERBEROS_HOSTNAME
kdc = localhost
admin_server = localhost
}
[domain_realm]
Expand All @@ -43,21 +37,15 @@ echo -e "*/*@${KERBEROS_REALM^^}\t*" > /etc/krb5kdc/kadm5.acl
echo "Installing all the packages required in this test"
apt-get update
apt-get \
-y \
-yq \
-qq \
install \
krb5-{user,kdc,admin-server,multidev} \
libkrb5-dev \
wget \
curl \
apache2 \
libapache2-mod-auth-gssapi \
python-dev \
libffi-dev \
build-essential \
libssl-dev \
zlib1g-dev \
libbz2-dev
build-essential

echo "Creating KDC database"
# krb5_newrealm returns non-0 return code as it is running in a container, ignore it for this command only
Expand Down
3 changes: 1 addition & 2 deletions requests_kerberos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@

from .kerberos_ import HTTPKerberosAuth, REQUIRED, OPTIONAL, DISABLED
from .exceptions import MutualAuthenticationError
from .compat import NullHandler

logging.getLogger(__name__).addHandler(NullHandler())
logging.getLogger(__name__).addHandler(logging.NullHandler())

__all__ = ('HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED',
'OPTIONAL', 'DISABLED')
Expand Down
14 changes: 0 additions & 14 deletions requests_kerberos/compat.py

This file was deleted.

119 changes: 36 additions & 83 deletions requests_kerberos/kerberos_.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
try:
import kerberos
except ImportError:
import winkerberos as kerberos
import base64
import logging
import re
import sys
import warnings

import spnego
import spnego.channel_bindings
import spnego.exceptions

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import UnsupportedAlgorithm

from requests.auth import AuthBase
from requests.models import Response
from requests.compat import urlparse, StringIO
from requests.structures import CaseInsensitiveDict
from requests.cookies import cookiejar_from_dict
from requests.packages.urllib3 import HTTPResponse

from urllib.parse import urlparse

from .exceptions import MutualAuthenticationError, KerberosExchangeError

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -90,7 +91,7 @@ def _negotiate_value(response):
if authreq:
match_obj = regex.search(authreq)
if match_obj:
return match_obj.group(1)
return base64.b64decode(match_obj.group(1))

return None

Expand Down Expand Up @@ -139,10 +140,7 @@ def _get_channel_bindings_application_data(response):

if isinstance(raw_response, HTTPResponse):
try:
if sys.version_info > (3, 0):
socket = raw_response._fp.fp.raw._sock
else:
socket = raw_response._fp.fp._sock
socket = raw_response._fp.fp.raw._sock
except AttributeError:
warnings.warn("Failed to get raw socket for CBT; has urllib3 impl changed",
NoCertificateRetrievedWarning)
Expand All @@ -169,7 +167,7 @@ def __init__(
service="HTTP", delegate=False, force_preemptive=False,
principal=None, hostname_override=None,
sanitize_mutual_error_response=True, send_cbt=True):
self.context = {}
self._context = {}
self.mutual_authentication = mutual_authentication
self.delegate = delegate
self.pos = None
Expand All @@ -179,7 +177,6 @@ def __init__(
self.hostname_override = hostname_override
self.sanitize_mutual_error_response = sanitize_mutual_error_response
self.auth_done = False
self.winrm_encryption_available = hasattr(kerberos, 'authGSSWinRMEncryptMessage')

# Set the CBT values populated after the first response
self.send_cbt = send_cbt
Expand All @@ -196,61 +193,43 @@ def generate_request_header(self, response, host, is_preemptive=False):
"""

# Flags used by kerberos module.
gssflags = kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_SEQUENCE_FLAG
gssflags = spnego.ContextReq.sequence_detect
if self.delegate:
gssflags |= kerberos.GSS_C_DELEG_FLAG
gssflags |= spnego.ContextReq.delegate
if self.mutual_authentication != DISABLED:
gssflags |= spnego.ContextReq.mutual_auth

try:
kerb_stage = "authGSSClientInit()"
kerb_stage = "ctx init"
# contexts still need to be stored by host, but hostname_override
# allows use of an arbitrary hostname for the kerberos exchange
# (eg, in cases of aliased hosts, internal vs external, CNAMEs
# w/ name-based HTTP hosting)
kerb_host = self.hostname_override if self.hostname_override is not None else host
kerb_spn = "{0}@{1}".format(self.service, kerb_host)

result, self.context[host] = kerberos.authGSSClientInit(kerb_spn,
gssflags=gssflags, principal=self.principal)

if result < 1:
raise EnvironmentError(result, kerb_stage)
self._context[host] = ctx = spnego.client(
username=self.principal,
hostname=kerb_host,
service=self.service,
channel_bindings=self.cbt_struct,
context_req=gssflags,
protocol="kerberos",
)

# if we have a previous response from the server, use it to continue
# the auth process, otherwise use an empty value
negotiate_resp_value = '' if is_preemptive else _negotiate_value(response)

kerb_stage = "authGSSClientStep()"
# If this is set pass along the struct to Kerberos
if self.cbt_struct:
result = kerberos.authGSSClientStep(self.context[host],
negotiate_resp_value,
channel_bindings=self.cbt_struct)
else:
result = kerberos.authGSSClientStep(self.context[host],
negotiate_resp_value)

if result < 0:
raise EnvironmentError(result, kerb_stage)
negotiate_resp_value = None if is_preemptive else _negotiate_value(response)

kerb_stage = "authGSSClientResponse()"
gss_response = kerberos.authGSSClientResponse(self.context[host])
kerb_stage = "ctx step"
gss_response = ctx.step(in_token=negotiate_resp_value)

return "Negotiate {0}".format(gss_response)
return "Negotiate {0}".format(base64.b64encode(gss_response).decode())

except kerberos.GSSError as error:
except spnego.exceptions.SpnegoError as error:
log.exception(
"generate_request_header(): {0} failed:".format(kerb_stage))
log.exception(error)
raise KerberosExchangeError("%s failed: %s" % (kerb_stage, str(error.args)))

except EnvironmentError as error:
# ensure we raised this for translation to KerberosExchangeError
# by comparing errno to result, re-raise if not
if error.errno != result:
raise
message = "{0} failed, result: {1}".format(kerb_stage, result)
log.error("generate_request_header(): {0}".format(message))
raise KerberosExchangeError(message)
raise KerberosExchangeError("%s failed: %s" % (kerb_stage, str(error))) from error

def authenticate_user(self, response, **kwargs):
"""Handles user authentication with gssapi/kerberos"""
Expand Down Expand Up @@ -351,21 +330,9 @@ def authenticate_server(self, response):
host = urlparse(response.url).hostname

try:
# If this is set pass along the struct to Kerberos
if self.cbt_struct:
result = kerberos.authGSSClientStep(self.context[host],
_negotiate_value(response),
channel_bindings=self.cbt_struct)
else:
result = kerberos.authGSSClientStep(self.context[host],
_negotiate_value(response))
except kerberos.GSSError:
log.exception("authenticate_server(): authGSSClientStep() failed:")
return False

if result < 1:
log.error("authenticate_server(): authGSSClientStep() failed: "
"{0}".format(result))
self._context[host].step(in_token=_negotiate_value(response))
except spnego.exceptions.SpnegoError:
log.exception("authenticate_server(): ctx step() failed:")
return False

log.debug("authenticate_server(): returning {0}".format(response))
Expand All @@ -380,12 +347,10 @@ def handle_response(self, response, **kwargs):
# If we haven't tried, try getting it now
cbt_application_data = _get_channel_bindings_application_data(response)
if cbt_application_data:
# Only the latest version of pykerberos has this method available
try:
self.cbt_struct = kerberos.channelBindings(application_data=cbt_application_data)
except AttributeError:
# Using older version set to None
self.cbt_struct = None
self.cbt_struct = spnego.channel_bindings.GssChannelBindings(
application_data=cbt_application_data,
)

# Regardless of the result, set tried to True so we don't waste time next time
self.cbt_binding_tried = True

Expand Down Expand Up @@ -416,18 +381,6 @@ def deregister(self, response):
"""Deregisters the response handler"""
response.request.deregister_hook('response', self.handle_response)

def wrap_winrm(self, host, message):
if not self.winrm_encryption_available:
raise NotImplementedError("WinRM encryption is not available on the installed version of pykerberos")

return kerberos.authGSSWinRMEncryptMessage(self.context[host], message)

def unwrap_winrm(self, host, message, header):
if not self.winrm_encryption_available:
raise NotImplementedError("WinRM encryption is not available on the installed version of pykerberos")

return kerberos.authGSSWinRMDecryptMessage(self.context[host], message, header)

def __call__(self, request):
if self.force_preemptive and not self.auth_done:
# add Authorization header before we receive a 401
Expand Down
2 changes: 1 addition & 1 deletion requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
mock
pytest>=4.0.0
pytest-cov
pytest-mock
5 changes: 1 addition & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
requests>=1.1.0
winkerberos >= 0.5.0; sys.platform == 'win32'
pykerberos >= 1.1.8, < 2.0.0; sys.platform != 'win32'
pyspnego
cryptography>=1.3
cryptography>=1.3; python_version!="3.3"
cryptography>=1.3, <2; python_version=="3.3"
11 changes: 3 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,11 @@ def get_version():
version=get_version(),
install_requires=[
'requests>=1.1.0',
'cryptography>=1.3;python_version!="3.3"',
'cryptography>=1.3,<2;python_version=="3.3"'
'cryptography>=1.3',
'pyspnego[kerberos]',
],
extras_require={
':sys_platform=="win32"': ['winkerberos>=0.5.0'],
':sys_platform!="win32"': ['pykerberos>=1.1.8,<2.0.0'],
},
extras_require={},
python_requires='>=3.6',
test_suite='test_requests_kerberos',
tests_require=['mock'],
classifiers=[
"License :: OSI Approved :: ISC License (ISCL)"
],
Expand Down
Loading

0 comments on commit f211fe8

Please sign in to comment.