Merge "Add optional kerberos support"
This commit is contained in:
commit
6728d36463
@ -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
|
.. _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.
|
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
|
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.
|
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
|
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.
|
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.
|
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.
|
This is an example showing how to add, configure, enable and delete Jenkins nodes.
|
||||||
@ -120,7 +148,7 @@ This is an example showing how to add, configure, enable and delete Jenkins node
|
|||||||
launcher=jenkins.LAUNCHER_SSH,
|
launcher=jenkins.LAUNCHER_SSH,
|
||||||
launcher_params=params)
|
launcher_params=params)
|
||||||
|
|
||||||
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.
|
This is an example showing how to retrieve information on the Jenkins queue.
|
||||||
@ -133,7 +161,7 @@ This is an example showing how to retrieve information on the Jenkins queue.
|
|||||||
server.cancel_queue(id)
|
server.cancel_queue(id)
|
||||||
|
|
||||||
|
|
||||||
Example 7: Working with Jenkins Cloudbees Folders
|
Example 8: Working with Jenkins Cloudbees Folders
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
Requires the `Cloudbees Folders Plugin
|
Requires the `Cloudbees Folders Plugin
|
||||||
@ -151,7 +179,7 @@ This is an example showing how to create, configure and delete Jenkins folders.
|
|||||||
server.delete_job('folder')
|
server.delete_job('folder')
|
||||||
|
|
||||||
|
|
||||||
Example 8: Updating Next Build Number
|
Example 9: Updating Next Build Number
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
Requires the `Next Build Number Plugin
|
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 HTTPError
|
||||||
from six.moves.urllib.error import URLError
|
from six.moves.urllib.error import URLError
|
||||||
from six.moves.urllib.parse import quote, urlencode, urljoin, urlparse
|
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
|
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)
|
warnings.simplefilter("default", DeprecationWarning)
|
||||||
|
|
||||||
if sys.version_info < (2, 7, 0):
|
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
|
coverage>=3.6
|
||||||
discover
|
discover
|
||||||
hacking>=0.5.6,<0.11
|
hacking>=0.5.6,<0.11
|
||||||
|
kerberos>=1.2.4
|
||||||
mock<1.1
|
mock<1.1
|
||||||
unittest2
|
unittest2
|
||||||
python-subunit
|
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