From 46298d6635534b44e0f0135ef997e75a3b04cf0e Mon Sep 17 00:00:00 2001
From: Tanner Prestegard <tanner.prestegard@ligo.org>
Date: Tue, 18 Dec 2018 13:24:21 -0600
Subject: [PATCH] Fix up proxied X509 cert authentication

Clean up the logic and add some examples for X509 cert auth with
impersonation proxies.  Fully implement the corresponding unit tests.
---
 gracedb/api/backends.py            | 43 +++++++++++++++++++++++++-----
 gracedb/api/tests/test_backends.py | 28 ++++++++++++++-----
 2 files changed, 59 insertions(+), 12 deletions(-)

diff --git a/gracedb/api/backends.py b/gracedb/api/backends.py
index b0321d846..f6926520d 100644
--- a/gracedb/api/backends.py
+++ b/gracedb/api/backends.py
@@ -71,7 +71,6 @@ class GraceDbX509Authentication(authentication.BaseAuthentication):
     proxy_pattern = re.compile(r'^(.*?)(/CN=\d+)*$')
 
     def authenticate(self, request):
-
         # Make sure this request is directed to the API
         if self.api_only and not is_api_request(request.path):
             return None
@@ -97,14 +96,46 @@ class GraceDbX509Authentication(authentication.BaseAuthentication):
         certdn = request.META.get(cls.subject_dn_header, None)
         issuer = request.META.get(cls.issuer_dn_header, '')
 
-        # Proxies can be signed by proxies; each level adds '/CN=[0-9]+' to the
-        # signers' subject, so we remove those to get the original identity's
-        # certificate DN
-        if certdn and issuer and certdn.startswith(issuer):
-            certdn = cls.proxy_pattern.match(issuer).group(1)
+        # Handled proxied certificates
+        certdn = cls.extract_subject_from_proxied_cert(certdn, issuer)
 
         return certdn
 
+    @classmethod
+    def extract_subject_from_proxied_cert(cls, subject, issuer):
+        """
+        Handles the case of "impersonation proxies", where /CN=[0-9]+ is
+        appended to the end of the certificate subject. This occurs when you
+        generate a certificate and it "follows" you to another machine - you
+        effectively self-sign a copy of the certificate to use on the other
+        machine.
+
+        Example:
+
+        Albert generates a certificate with ligo-proxy-init on his laptop.
+
+        Subject and issuer when he pings the GraceDB server from his laptop:
+        /DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org
+        /DC=org/DC=cilogon/C=US/O=CILogon/CN=CILogon Basic CA 1
+
+        Subject and issuer when he gsisshs to an LDG cluster and then pings the
+        GraceDB server from there:
+        /DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org/CN=1492637212
+        /DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org
+
+        If he then gsisshs to *another* machine from there and repeats this,
+        he would get:
+        /DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org/CN=1492637212/CN=28732493
+        /DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org/CN=1492637212
+        """
+        if subject and issuer and subject.startswith(issuer):
+            # If we get here, we have an impersonation proxy, so we extract
+            # the proxy /CN=12345... part from the subject.  Could also
+            # do it from the issuer (see above examples)
+            subject = cls.proxy_pattern.match(subject).group(1)
+
+        return subject
+
     def authenticate_credentials(self, user_cert_dn):
         certs = X509Cert.objects.filter(subject=user_cert_dn)
         if not certs.exists():
diff --git a/gracedb/api/tests/test_backends.py b/gracedb/api/tests/test_backends.py
index 9e3909a34..6cbb390e3 100644
--- a/gracedb/api/tests/test_backends.py
+++ b/gracedb/api/tests/test_backends.py
@@ -192,17 +192,33 @@ class TestGraceDbX509Authentication(GraceDbApiTestBase):
         """User can authenticate to API with proxied X509 certificate"""
         # Set up request
         request = self.factory.get(api_reverse('api:root'))
-        #request.META[GraceDbX509Authentication.subject_dn_header] = \
-        #    '/CN=123' + self.x509_subject
-        #request.META[GraceDbX509Authentication.issuer_dn_header] = \
-        #    '/CN=123'
+        request.META[GraceDbX509Authentication.subject_dn_header] = \
+            self.x509_subject + '/CN=123456789'
+        request.META[GraceDbX509Authentication.issuer_dn_header] = \
+            self.x509_subject
 
         # Authentication attempt
-        #user, other = self.backend_instance.authenticate(request)
+        user, other = self.backend_instance.authenticate(request)
 
         # Check authenticated user
-        #self.assertEqual(user, self.internal_user)
+        self.assertEqual(user, self.internal_user)
+
+    def test_authenticate_cert_with_double_proxy(self):
+        """User can authenticate to API with double-proxied X509 certificate"""
+        proxied_x509_subject = self.x509_subject + '/CN=123456789'
+
+        # Set up request
+        request = self.factory.get(api_reverse('api:root'))
+        request.META[GraceDbX509Authentication.subject_dn_header] = \
+            proxied_x509_subject + '/CN=987654321'
+        request.META[GraceDbX509Authentication.issuer_dn_header] = \
+            proxied_x509_subject
 
+        # Authentication attempt
+        user, other = self.backend_instance.authenticate(request)
+
+        # Check authenticated user
+        self.assertEqual(user, self.internal_user)
 
 class TestGraceDbAuthenticatedAuthentication(GraceDbApiTestBase):
     """Test shibboleth auth backend for API"""
-- 
GitLab