From bab46a93e2bcded1686418675bcd5506a7265867 Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Mar 28 2018 17:14:00 +0000 Subject: [PATCH 1/2] Port CLI from requests-kerberos to requests-gssapi --- diff --git a/requirements.txt b/requirements.txt index b9d5985..1fba304 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ sphinxcontrib-httpdomain click configparser openidc-client -requests-kerberos +requests-gssapi # Database psycopg2-binary diff --git a/tests/test_cli.py b/tests/test_cli.py index cacda99..c8f598b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -149,8 +149,8 @@ resultsdb_api_url=http://localhost:5001/api/v2.0 def test_oidc_auth_is_enabled(tmpdir): - # Skip if waiverdb is rebuilt for an environment where Kerberos authentication - # is used and python-openidc-client is not available. + # Skip if waiverdb is rebuilt for an environment where GSSAPI + # authentication is used and python-openidc-client is not available. pytest.importorskip('openidc_client') with patch('openidc_client.OpenIDCClient.send_request') as mock_oidc_req: mock_rv = Mock() @@ -197,10 +197,10 @@ resultsdb_api_url=http://localhost:5001/api/v2.0 assert result.output == 'Created waiver 15 for result with subject {"subject.test": "test", "s": "t"} and testcase test.testcase\n' # noqa -def test_kerberos_is_enabled(tmpdir): +def test_gssapi_is_enabled(tmpdir): # Skip if waiverdb is rebuilt for an environment where OIDC authentication - # is used and python-requests-kerberos is not available. - pytest.importorskip('requests_kerberos') + # is used and python-requests-gssapi is not available. + pytest.importorskip('requests_gssapi') with patch('requests.request') as mock_request: mock_rv = Mock() mock_rv.json.return_value = { diff --git a/waiverdb/cli.py b/waiverdb/cli.py index d0398c6..b1b35cc 100644 --- a/waiverdb/cli.py +++ b/waiverdb/cli.py @@ -151,17 +151,19 @@ def cli(comment, waived, product_version, testcase, subject, result_id, config_f # Try to import this now so the user gets immediate feedback if # it isn't installed try: - import requests_kerberos # noqa: F401 + import requests_gssapi # noqa: F401 except ImportError: - raise click.ClickException('python-requests-kerberos needs to be installed') - auth = requests_kerberos.HTTPKerberosAuth(mutual_authentication=requests_kerberos.OPTIONAL) + raise click.ClickException( + 'python-requests-gssapi needs to be installed') + auth = requests_gssapi.HTTPKerberosAuth( + mutual_authentication=requests_gssapi.OPTIONAL) for data in data_list: resp = requests.request('POST', '{0}/waivers/'.format(api_url.rstrip('/')), data=json.dumps(data), auth=auth, headers={'Content-Type': 'application/json'}, timeout=60) if resp.status_code == 401: - raise click.ClickException('WaiverDB authentication using Kerberos failed. ' + raise click.ClickException('WaiverDB authentication using GSSAPI failed. ' 'Make sure you have a valid Kerberos ticket.') check_response(resp, data, data.get('result_id', None)) elif auth_method == 'dummy': From 7288d464a2de48c49a7ca290aa4a5fa453baa4e1 Mon Sep 17 00:00:00 2001 From: Robbie Harwood Date: Mar 28 2018 17:22:03 +0000 Subject: [PATCH 2/2] Port from pykerberos to python-gssapi Please note that, as a side effect of this change, setting a keytab file is no longer required: if one is not explicitly set, /etc/krb5.keytab will be used. Removed test from test suite accordingly. --- diff --git a/requirements.txt b/requirements.txt index 1fba304..4419aa1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ Flask Flask-RESTful Flask-SQLAlchemy SQLAlchemy -kerberos >= 1.1.1 +gssapi flask-oidc systemd # packages for the unit tests diff --git a/tests/test_auth.py b/tests/test_auth.py index 3091db5..918be59 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,7 +1,8 @@ # SPDX-License-Identifier: GPL-2.0+ +from base64 import b64encode import pytest -import kerberos +import gssapi # noqa import mock import json from werkzeug.exceptions import Unauthorized @@ -10,32 +11,21 @@ import flask_oidc @pytest.mark.usefixtures('enable_kerberos') -class TestKerberosAuthentication(object): - - def test_keytab_file_is_not_set_should_raise_error(self): - with pytest.raises(Unauthorized): - request = mock.MagicMock() - headers = {'Authorization': "babablaba"} - request.headers.return_value = mock.MagicMock(spec_set=dict) - request.headers.__getitem__.side_effect = headers.__getitem__ - request.headers.__setitem__.side_effect = headers.__setitem__ - request.headers.__contains__.side_effect = headers.__contains__ - waiverdb.auth.get_user(request) - +class TestGSSAPIAuthentication(object): def test_unauthorized(self, client, monkeypatch): monkeypatch.setenv('KRB5_KTNAME', '/etc/foo.keytab') r = client.post('/api/v1.0/waivers/', content_type='application/json') assert r.status_code == 401 assert r.headers.get('www-authenticate') == 'Negotiate' - @mock.patch('kerberos.authGSSServerInit', return_value=(kerberos.AUTH_GSS_COMPLETE, object())) - @mock.patch('kerberos.authGSSServerStep', return_value=kerberos.AUTH_GSS_COMPLETE) - @mock.patch('kerberos.authGSSServerResponse', return_value='STOKEN') - @mock.patch('kerberos.authGSSServerUserName', return_value='foo@EXAMPLE.ORG') - @mock.patch('kerberos.authGSSServerClean') - @mock.patch('kerberos.getServerPrincipalDetails') - def test_authorized(self, principal, clean, name, response, step, init, - client, monkeypatch, session): + @mock.patch.multiple("gssapi.SecurityContext", complete=True, + __init__=mock.Mock(return_value=None), + step=mock.Mock(return_value=b"STOKEN"), + initiator_name="foo@EXAMPLE.ORG") + @mock.patch.multiple("gssapi.Credentials", + __init__=mock.Mock(return_value=None), + __new__=mock.Mock(return_value=None)) + def test_authorized(self, client, monkeypatch): monkeypatch.setenv('KRB5_KTNAME', '/etc/foo.keytab') data = { 'subject': {'subject.test': 'subject'}, @@ -44,11 +34,13 @@ class TestKerberosAuthentication(object): 'waived': True, 'comment': 'it broke', } + headers = {'Authorization': + 'Negotiate %s' % b64encode(b"CTOKEN").decode()} r = client.post('/api/v1.0/waivers/', data=json.dumps(data), - content_type='application/json', - headers={'Authorization': 'Negotiate CTOKEN'}) + content_type='application/json', headers=headers) assert r.status_code == 201 - assert r.headers.get('WWW-Authenticate') == 'negotiate STOKEN' + assert r.headers.get('WWW-Authenticate') == \ + 'negotiate %s' % b64encode(b"STOKEN").decode() res_data = json.loads(r.data.decode('utf-8')) assert res_data['username'] == 'foo' diff --git a/waiverdb.spec b/waiverdb.spec index 4be891e..07cb403 100644 --- a/waiverdb.spec +++ b/waiverdb.spec @@ -20,7 +20,7 @@ BuildRequires: python3-sqlalchemy BuildRequires: python3-flask-restful BuildRequires: python3-flask-sqlalchemy BuildRequires: python3-psycopg2 -BuildRequires: python3-kerberos +BuildRequires: python3-gssapi BuildRequires: python3-systemd BuildRequires: python3-pytest BuildRequires: python3-mock @@ -37,7 +37,7 @@ BuildRequires: python-sqlalchemy BuildRequires: python-flask-restful BuildRequires: python-flask-sqlalchemy BuildRequires: python-psycopg2 -BuildRequires: python-kerberos +BuildRequires: python-gssapi BuildRequires: systemd-python BuildRequires: pytest BuildRequires: python-mock @@ -57,7 +57,7 @@ Requires: python3-sqlalchemy Requires: python3-flask-restful Requires: python3-flask-sqlalchemy Requires: python3-psycopg2 -Requires: python3-kerberos +Requires: python3-gssapi Requires: python3-systemd Requires: python3-mock Requires: python3-flask-oidc @@ -71,7 +71,7 @@ Requires: python-sqlalchemy Requires: python-flask-restful Requires: python-flask-sqlalchemy Requires: python-psycopg2 -Requires: python-kerberos +Requires: python-gssapi Requires: systemd-python Requires: python-mock Requires: python-flask-oidc diff --git a/waiverdb/auth.py b/waiverdb/auth.py index 4e6bb01..1c1d08d 100644 --- a/waiverdb/auth.py +++ b/waiverdb/auth.py @@ -1,85 +1,50 @@ # SPDX-License-Identifier: GPL-2.0+ -import os -import kerberos +import base64 +import gssapi from flask import current_app, Response, g -# Starting with Flask 0.9, the _app_ctx_stack is the correct one, -# before that we need to use the _request_ctx_stack. -try: - from flask import _app_ctx_stack as stack -except ImportError: - from flask import _request_ctx_stack as stack from socket import gethostname from werkzeug.exceptions import Unauthorized, Forbidden # Inspired by https://github.com/mkomitee/flask-kerberos/blob/master/flask_kerberos.py -class KerberosAuthenticate(object): +# Later cleaned and ported to python-gssapi +def process_gssapi_request(token): + if current_app.config['KERBEROS_HTTP_HOST']: + hostname = current_app.config['KERBEROS_HTTP_HOST'] + else: + hostname = gethostname() + + service_name = gssapi.Name("HTTP@%s" % hostname, + gssapi.NameType.hostbased_service) + + try: + stage = "initialize server context" + creds = gssapi.Credentials(name=service_name, usage="accept") + sc = gssapi.SecurityContext(usage="accept", creds=creds) + + stage = "step context" + token = sc.step(token if token != "" else None) + token = token if token is not None else "" - def __init__(self): - if current_app.config['KERBEROS_HTTP_HOST']: - hostname = current_app.config['KERBEROS_HTTP_HOST'] - else: - hostname = gethostname() - self.service_name = "HTTP@%s" % (hostname) - if 'KRB5_KTNAME' in os.environ: - try: - principal = kerberos.getServerPrincipalDetails('HTTP', hostname) - except kerberos.KrbError as exc: - raise Unauthorized("Authentication Kerberos Failure: %s" % exc.message[0]) - else: - current_app.logger.debug("Kerberos: server is identifying as %s" % principal) - else: - raise Unauthorized("Kerberos: set KRB5_KTNAME to your keytab file") + # The current architecture cannot support continuation here + stage = "checking completion" + if not sc.complete: + current_app.logger.error( + 'Multiple GSSAPI round trips not supported') + raise Forbidden("Attempted multiple GSSAPI round trips") - def _gssapi_authenticate(self, token): - ''' - Performs GSSAPI Negotiate Authentication - On success also stashes the server response token for mutual authentication - at the top of request context with the name kerberos_token, along with the - authenticated user principal with the name kerberos_user. - ''' - state = None - ctx = stack.top - try: - rc, state = kerberos.authGSSServerInit(self.service_name) - if rc != kerberos.AUTH_GSS_COMPLETE: - current_app.logger.error('Unable to initialize server context') - return None - rc = kerberos.authGSSServerStep(state, token) - if rc == kerberos.AUTH_GSS_COMPLETE: - current_app.logger.debug('Completed GSSAPI negotiation') - ctx.kerberos_token = kerberos.authGSSServerResponse(state) - ctx.kerberos_user = kerberos.authGSSServerUserName(state) - return rc - elif rc == kerberos.AUTH_GSS_CONTINUE: - current_app.logger.debug('Continuing GSSAPI negotiation') - return kerberos.AUTH_GSS_CONTINUE - else: - current_app.logger.debug('Unable to step server context') - return None - except kerberos.GSSError as e: - current_app.logger.error('Unable to authenticate: %s', e) - return None - finally: - if state: - kerberos.authGSSServerClean(state) + current_app.logger.debug('Completed GSSAPI negotiation') - def process_request(self, token): - """ - Authenticates the current request using Kerberos. - """ - kerberos_user = None - kerberos_token = None - ctx = stack.top - rc = self._gssapi_authenticate(token) - if rc == kerberos.AUTH_GSS_COMPLETE: - kerberos_user = ctx.kerberos_user - kerberos_token = ctx.kerberos_token - elif rc != kerberos.AUTH_GSS_CONTINUE: - raise Forbidden("Invalid Kerberos ticket") - return kerberos_user, kerberos_token + stage = "getting remote user" + user = str(sc.initiator_name) + return user, token + except gssapi.exceptions.GSSError as e: + current_app.logger.error( + 'Unable to authenticate: failed to %s: %s' % + (stage, e.gen_message())) + raise Forbidden("Authentication failed") def get_user(request): @@ -107,11 +72,11 @@ def get_user(request): raise Unauthorized(response=response) header = request.headers.get("Authorization") token = ''.join(header.strip().split()[1:]) - user, kerberos_token = KerberosAuthenticate().process_request(token) + user, token = process_gssapi_request(base64.b64decode(token)) # remove realm user = user.split("@")[0] - if kerberos_token is not None: - headers = {'WWW-Authenticate': ' '.join(['negotiate', kerberos_token])} + headers = {'WWW-Authenticate': ' '.join( + ['negotiate', base64.b64encode(token).decode()])} elif current_app.config['AUTH_METHOD'] == 'SSL': # Nginx sets SSL_CLIENT_VERIFY and SSL_CLIENT_S_DN in request.environ # when doing SSL authentication.