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