Add optional kerberos support
Check if "kerberos" package can be imported at runtime (in __init__.py). If not, everything works as before. If "kerberos" package can be imported register a new urllib handler (defined in urllib_kerb.py) which adds support for kerberos auth. This urllib handler (urllib_kerb.py) is only triggered on 401 (Unauthorized) response, and we try to auth with kerberos. If unsuccessful, next 401 handler (if any) is triggered. Change-Id: I1a47e455aa14535a124df950994718a11d7e4f57
This commit is contained in:
parent
3b2dc286c9
commit
4152a0138b
@ -31,8 +31,36 @@ instead of a real password while creating a Jenkins instance.
|
||||
|
||||
.. _Jenkins Authentication: https://wiki.jenkins-ci.org/display/JENKINS/Authenticating+scripted+clients
|
||||
|
||||
Example 2: Logging into Jenkins using kerberos
|
||||
----------------------------------------------
|
||||
|
||||
Example 2: Working with Jenkins Jobs
|
||||
Kerberos support is only enabled if you have "kerberos" python package installed.
|
||||
You can install the "kerberos" package from PyPI using the obvious pip command.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install kerberos
|
||||
|
||||
.. note:: This might require python header files as well
|
||||
as kerberos header files.
|
||||
|
||||
If you have "kerberos" python package installed, python-jenkins tries to authenticate
|
||||
using kerberos automatically when the Jenkins server replies "401 Unauthorized"
|
||||
and indicates it supports kerberos. That is, kerberos authentication should
|
||||
work automagically. For a quick test, just try the following.
|
||||
|
||||
::
|
||||
|
||||
import jenkins
|
||||
|
||||
server = jenkins.Jenkins('http://localhost:8080')
|
||||
print server.jobs_count()
|
||||
|
||||
.. note:: Jenkins as such does not support kerberos, it needs to be supported by
|
||||
the Servlet container or a reverse proxy sitting in front of Jenkins.
|
||||
|
||||
|
||||
Example 3: Working with Jenkins Jobs
|
||||
------------------------------------
|
||||
|
||||
This is an example showing how to create, configure and delete Jenkins jobs.
|
||||
@ -59,7 +87,7 @@ This is an example showing how to create, configure and delete Jenkins jobs.
|
||||
print build_info
|
||||
|
||||
|
||||
Example 3: Working with Jenkins Views
|
||||
Example 4: Working with Jenkins Views
|
||||
-------------------------------------
|
||||
|
||||
This is an example showing how to create, configure and delete Jenkins views.
|
||||
@ -73,7 +101,7 @@ This is an example showing how to create, configure and delete Jenkins views.
|
||||
print views
|
||||
|
||||
|
||||
Example 4: Working with Jenkins Plugins
|
||||
Example 5: Working with Jenkins Plugins
|
||||
---------------------------------------
|
||||
|
||||
This is an example showing how to retrieve Jenkins plugins information.
|
||||
@ -89,7 +117,7 @@ from the :func:`get_plugins_info` method is documented in the :doc:`api`
|
||||
doc.
|
||||
|
||||
|
||||
Example 5: Working with Jenkins Nodes
|
||||
Example 6: Working with Jenkins Nodes
|
||||
-------------------------------------
|
||||
|
||||
This is an example showing how to add, configure, enable and delete Jenkins nodes.
|
||||
@ -105,7 +133,7 @@ This is an example showing how to add, configure, enable and delete Jenkins node
|
||||
server.enable_node('slave1')
|
||||
|
||||
|
||||
Example 6: Working with Jenkins Build Queue
|
||||
Example 7: Working with Jenkins Build Queue
|
||||
-------------------------------------------
|
||||
|
||||
This is an example showing how to retrieve information on the Jenkins queue.
|
||||
@ -118,7 +146,7 @@ This is an example showing how to retrieve information on the Jenkins queue.
|
||||
server.cancel_queue(id)
|
||||
|
||||
|
||||
Example 7: Working with Jenkins Cloudbees Folders
|
||||
Example 8: Working with Jenkins Cloudbees Folders
|
||||
-------------------------------------------------
|
||||
|
||||
Requires the `Cloudbees Folders Plugin
|
||||
@ -136,7 +164,7 @@ This is an example showing how to create, configure and delete Jenkins folders.
|
||||
server.delete_job('folder')
|
||||
|
||||
|
||||
Example 8: Updating Next Build Number
|
||||
Example 9: Updating Next Build Number
|
||||
-------------------------------------
|
||||
|
||||
Requires the `Next Build Number Plugin
|
||||
|
@ -60,10 +60,20 @@ from six.moves.http_client import BadStatusLine
|
||||
from six.moves.urllib.error import HTTPError
|
||||
from six.moves.urllib.error import URLError
|
||||
from six.moves.urllib.parse import quote, urlencode, urljoin, urlparse
|
||||
from six.moves.urllib.request import Request, urlopen
|
||||
from six.moves.urllib.request import Request, install_opener, build_opener, urlopen
|
||||
|
||||
from jenkins import plugins
|
||||
|
||||
try:
|
||||
import kerberos
|
||||
assert kerberos # pyflakes
|
||||
import urllib_kerb
|
||||
opener = build_opener()
|
||||
opener.add_handler(urllib_kerb.HTTPNegotiateHandler())
|
||||
install_opener(opener)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
warnings.simplefilter("default", DeprecationWarning)
|
||||
|
||||
if sys.version_info < (2, 7, 0):
|
||||
|
121
jenkins/urllib_kerb.py
Normal file
121
jenkins/urllib_kerb.py
Normal file
@ -0,0 +1,121 @@
|
||||
# Copyright (C) 2015 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import kerberos
|
||||
from six.moves.urllib import error, request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTTPNegotiateHandler(request.BaseHandler):
|
||||
handler_order = 490 # before Digest auth
|
||||
|
||||
def __init__(self, max_tries=5):
|
||||
self.krb_context = None
|
||||
self.tries = 0
|
||||
self.max_tries = max_tries
|
||||
self.re_extract_auth = re.compile('.*?Negotiate\s*([^,]*)', re.I)
|
||||
|
||||
def http_error_401(self, req, fp, code, msg, headers):
|
||||
logger.debug("INSIDE http_error_401")
|
||||
try:
|
||||
try:
|
||||
krb_req = self._extract_krb_value(headers)
|
||||
except ValueError:
|
||||
# Negotiate header not found or a similar error
|
||||
# we can't handle this, let the next handler have a go
|
||||
return None
|
||||
|
||||
if not krb_req:
|
||||
# First reply from server (no neg value)
|
||||
self.tries = 0
|
||||
krb_req = ""
|
||||
else:
|
||||
if self.tries > self.max_tries:
|
||||
raise error.HTTPError(
|
||||
req.get_full_url(), 401, "Negotiate auth failed",
|
||||
headers, None)
|
||||
|
||||
self.tries += 1
|
||||
try:
|
||||
krb_resp = self._krb_response(req.get_host(), krb_req)
|
||||
|
||||
req.add_unredirected_header('Authorization',
|
||||
"Negotiate %s" % krb_resp)
|
||||
|
||||
resp = self.parent.open(req, timeout=req.timeout)
|
||||
self._authenticate_server(resp.headers)
|
||||
return resp
|
||||
|
||||
except kerberos.GSSError as err:
|
||||
try:
|
||||
msg = err.args[1][0]
|
||||
except Exception:
|
||||
msg = "Negotiate auth failed"
|
||||
logger.debug(msg)
|
||||
return None # let the next handler (if any) have a go
|
||||
|
||||
finally:
|
||||
if self.krb_context is not None:
|
||||
kerberos.authGSSClientClean(self.krb_context)
|
||||
self.krb_context = None
|
||||
|
||||
def _krb_response(self, host, krb_val):
|
||||
logger.debug("INSIDE _krb_response")
|
||||
|
||||
_dummy, self.krb_context = kerberos.authGSSClientInit("HTTP@%s" % host)
|
||||
kerberos.authGSSClientStep(self.krb_context, krb_val)
|
||||
response = kerberos.authGSSClientResponse(self.krb_context)
|
||||
|
||||
logger.debug("kerb auth successful")
|
||||
|
||||
return response
|
||||
|
||||
def _authenticate_server(self, headers):
|
||||
logger.debug("INSIDE _authenticate_server")
|
||||
try:
|
||||
val = self._extract_krb_value(headers)
|
||||
except ValueError:
|
||||
logger.critical("Server authentication failed."
|
||||
"Auth value couldn't be extracted from headers.")
|
||||
return None
|
||||
if not val:
|
||||
logger.critical("Server authentication failed."
|
||||
"Empty 'Negotiate' value.")
|
||||
return None
|
||||
|
||||
kerberos.authGSSClientStep(self.krb_context, val)
|
||||
|
||||
def _extract_krb_value(self, headers):
|
||||
logger.debug("INSIDE _extract_krb_value")
|
||||
header = headers.get('www-authenticate', None)
|
||||
|
||||
if header is None:
|
||||
msg = "www-authenticate header not found"
|
||||
logger.debug(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
if "negotiate" in header.lower():
|
||||
matches = self.re_extract_auth.search(header)
|
||||
if matches:
|
||||
return matches.group(1)
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
msg = "Negotiate not in www-authenticate header (%s)" % header
|
||||
logger.debug(msg)
|
||||
raise ValueError(msg)
|
@ -1,6 +1,7 @@
|
||||
coverage>=3.6
|
||||
discover
|
||||
hacking>=0.5.6,<0.11
|
||||
kerberos>=1.2.4
|
||||
mock<1.1
|
||||
unittest2
|
||||
python-subunit
|
||||
|
116
tests/test_kerberos.py
Normal file
116
tests/test_kerberos.py
Normal file
@ -0,0 +1,116 @@
|
||||
import kerberos
|
||||
assert kerberos # pyflakes
|
||||
from mock import patch, Mock
|
||||
import testtools
|
||||
|
||||
from jenkins import urllib_kerb
|
||||
|
||||
|
||||
class KerberosTests(testtools.TestCase):
|
||||
|
||||
@patch('kerberos.authGSSClientResponse')
|
||||
@patch('kerberos.authGSSClientStep')
|
||||
@patch('kerberos.authGSSClientInit')
|
||||
@patch('kerberos.authGSSClientClean')
|
||||
def test_http_error_401_simple(self, clean_mock, init_mock, step_mock, response_mock):
|
||||
headers_from_server = {'www-authenticate': 'Negotiate xxx'}
|
||||
|
||||
init_mock.side_effect = lambda x: (x, "context")
|
||||
response_mock.return_value = "foo"
|
||||
|
||||
parent_mock = Mock()
|
||||
parent_return_mock = Mock()
|
||||
parent_return_mock.headers = {'www-authenticate': "Negotiate bar"}
|
||||
parent_mock.open.return_value = parent_return_mock
|
||||
|
||||
request_mock = Mock()
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
h.add_parent(parent_mock)
|
||||
rv = h.http_error_401(request_mock, "", "", "", headers_from_server)
|
||||
|
||||
init_mock.assert_called()
|
||||
step_mock.assert_any_call("context", "xxx")
|
||||
# verify authGSSClientStep was called for response as well
|
||||
step_mock.assert_any_call("context", "bar")
|
||||
response_mock.assert_called_with("context")
|
||||
request_mock.add_unredirected_header.assert_called_with(
|
||||
'Authorization', 'Negotiate %s' % "foo")
|
||||
self.assertEqual(rv, parent_return_mock)
|
||||
clean_mock.assert_called_with("context")
|
||||
|
||||
@patch('kerberos.authGSSClientResponse')
|
||||
@patch('kerberos.authGSSClientStep')
|
||||
@patch('kerberos.authGSSClientInit')
|
||||
@patch('kerberos.authGSSClientClean')
|
||||
def test_http_error_401_gsserror(self, clean_mock, init_mock, step_mock, response_mock):
|
||||
headers_from_server = {'www-authenticate': 'Negotiate xxx'}
|
||||
|
||||
init_mock.side_effect = kerberos.GSSError
|
||||
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
rv = h.http_error_401(Mock(), "", "", "", headers_from_server)
|
||||
self.assertEqual(rv, None)
|
||||
|
||||
@patch('kerberos.authGSSClientResponse')
|
||||
@patch('kerberos.authGSSClientStep')
|
||||
@patch('kerberos.authGSSClientInit')
|
||||
@patch('kerberos.authGSSClientClean')
|
||||
def test_http_error_401_empty(self, clean_mock, init_mock, step_mock, response_mock):
|
||||
headers_from_server = {}
|
||||
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
rv = h.http_error_401(Mock(), "", "", "", headers_from_server)
|
||||
self.assertEqual(rv, None)
|
||||
|
||||
@patch('kerberos.authGSSClientResponse')
|
||||
@patch('kerberos.authGSSClientStep')
|
||||
@patch('kerberos.authGSSClientInit')
|
||||
def test_krb_response_simple(self, init_mock, step_mock, response_mock):
|
||||
response_mock.return_value = "foo"
|
||||
init_mock.return_value = ("bar", "context")
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
rv = h._krb_response("host", "xxx")
|
||||
self.assertEqual(rv, "foo")
|
||||
|
||||
@patch('kerberos.authGSSClientResponse')
|
||||
@patch('kerberos.authGSSClientStep')
|
||||
@patch('kerberos.authGSSClientInit')
|
||||
def test_krb_response_gsserror(self, init_mock, step_mock, response_mock):
|
||||
response_mock.side_effect = kerberos.GSSError
|
||||
init_mock.return_value = ("bar", "context")
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
with testtools.ExpectedException(kerberos.GSSError):
|
||||
h._krb_response("host", "xxx")
|
||||
|
||||
@patch('kerberos.authGSSClientStep')
|
||||
def test_authenticate_server_simple(self, step_mock):
|
||||
headers_from_server = {'www-authenticate': 'Negotiate xxx'}
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
h.krb_context = "foo"
|
||||
h._authenticate_server(headers_from_server)
|
||||
step_mock.assert_called_with("foo", "xxx")
|
||||
|
||||
@patch('kerberos.authGSSClientStep')
|
||||
def test_authenticate_server_empty(self, step_mock):
|
||||
headers_from_server = {'www-authenticate': 'Negotiate'}
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
rv = h._authenticate_server(headers_from_server)
|
||||
self.assertEqual(rv, None)
|
||||
|
||||
def test_extract_krb_value_simple(self):
|
||||
headers_from_server = {'www-authenticate': 'Negotiate xxx'}
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
rv = h._extract_krb_value(headers_from_server)
|
||||
self.assertEqual(rv, "xxx")
|
||||
|
||||
def test_extract_krb_value_empty(self):
|
||||
headers_from_server = {}
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
with testtools.ExpectedException(ValueError):
|
||||
h._extract_krb_value(headers_from_server)
|
||||
|
||||
def test_extract_krb_value_invalid(self):
|
||||
headers_from_server = {'www-authenticate': 'Foo-&#@^%:; bar'}
|
||||
h = urllib_kerb.HTTPNegotiateHandler()
|
||||
with testtools.ExpectedException(ValueError):
|
||||
h._extract_krb_value(headers_from_server)
|
Loading…
x
Reference in New Issue
Block a user