From 7d8d1bab56ec24bc7fa9ca06cd2e29e35b779e28 Mon Sep 17 00:00:00 2001
From: Tanner Prestegard <tanner.prestegard@ligo.org>
Date: Thu, 14 Feb 2019 14:19:08 -0600
Subject: [PATCH] Adding cert info based authentication backend for API

Traefik can pass the subject and issuer in a 'cert-infos'
header, so I am adding an authentication backend to use that.
---
 config/settings/base.py |  1 +
 gracedb/api/backends.py | 41 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 42 insertions(+)

diff --git a/config/settings/base.py b/config/settings/base.py
index ed24bd04e..affca13cf 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -272,6 +272,7 @@ SHIB_ATTRIBUTE_MAP = {
 X509_SUBJECT_DN_HEADER = 'HTTP_SSL_CLIENT_S_DN'
 X509_ISSUER_DN_HEADER = 'HTTP_SSL_CLIENT_I_DN'
 X509_CERT_HEADER = 'X_FORWARDED_TLS_CLIENT_CERT'
+X509_INFOS_HEADER = 'X_FORWARDED_TLS_CLIENT_CERT_INFOS'
 
 # List of authentication backends to use when attempting to authenticate
 # a user.  Will be used in this order.  Authentication for the API is
diff --git a/gracedb/api/backends.py b/gracedb/api/backends.py
index e8c067d78..ba273c6ff 100644
--- a/gracedb/api/backends.py
+++ b/gracedb/api/backends.py
@@ -161,6 +161,47 @@ class GraceDbX509Authentication(authentication.BaseAuthentication):
         return (user, None)
 
 
+class GraceDbX509CertInfosAuthentication(GraceDbX509Authentication):
+    """
+    Authentication based on X509 "infos" header.
+    Certificate should be verified by Traefik already.
+    """
+    api_only = True
+    infos_header = getattr(settings, 'X509_INFOS_HEADER',
+        'X_FORWARDED_TLS_CLIENT_CERT_INFOS')
+    infos_pattern = re.compile(r'Subject="(.*?)".*Issuer="(.*?)"')
+
+    @classmethod
+    def get_cert_dn_from_request(cls, request):
+        """Get SSL headers and return subject for user"""
+
+        # Get infos from request headers
+        infos = request.META.get(cls.infos_header, None)
+
+        # Unquote (handle pluses -> spaces)
+        infos_unquoted = unquote_plus(infos)
+
+        # Extract subject and issuer
+        subject, issuer = cls.infos_pattern.search(infos_unquoted).groups()
+
+        # Convert formats
+        subject = cls.convert_format(subject)
+        issuer = cls.convert_format(issuer)
+
+        # Handled proxied certificates
+        subject = cls.extract_subject_from_proxied_cert(subject, issuer)
+
+        return subject
+
+    @staticmethod
+    def convert_format(s):
+        # Convert subject or issuer strings from comma to slash format
+        s = s.replace(',', '/')
+        if not s.startswith('/'):
+            s = '/' + s
+        return s
+
+
 class GraceDbX509FullCertAuthentication(GraceDbX509Authentication):
     """
     Authentication based on a full X509 certificate. We verify the
-- 
GitLab