From bab46a93e2bcded1686418675bcd5506a7265867 Mon Sep 17 00:00:00 2001
From: Robbie Harwood <rharwood@redhat.com>
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 <rharwood@redhat.com>
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.

