# HG changeset patch # User domruf # Date 1443984192 -7200 # Node ID ada6571a6d273504c406aed8adbb87ac2fac387e # Parent 9a4d4e623c856b6708b3639b8b7f1e09f1870f61 auth: let container authentication get email, first and last name from custom headers diff -r 9a4d4e623c85 -r ada6571a6d27 docs/setup.rst --- a/docs/setup.rst Fri Dec 25 13:50:18 2015 +0100 +++ b/docs/setup.rst Sun Oct 04 20:43:12 2015 +0200 @@ -427,6 +427,74 @@ RequestHeader set X-Forwarded-User %{RU}e +Setting metadata in container/reverse-proxy +''''''''''''''''''''''''''''''''''''''''''' + +When a new user account is created on the first login, Kallithea has no information about +the user's email and full name. So you can set some additional request headers like in the +example below. In this example the user is authenticated via Kerberos and an Apache +mod_python fixup handler is used to get the user information from a LDAP server. But you +could set the request headers however you want. + +.. code-block:: apache + + + ProxyPass http://127.0.0.1:5000/someprefix + ProxyPassReverse http://127.0.0.1:5000/someprefix + SetEnvIf X-Url-Scheme https HTTPS=1 + + AuthName "Kerberos Login" + AuthType Kerberos + Krb5Keytab /etc/apache2/http.keytab + KrbMethodK5Passwd off + KrbVerifyKDC on + Require valid-user + + PythonFixupHandler ldapmetadata + + RequestHeader set X_REMOTE_USER %{X_REMOTE_USER}e + RequestHeader set X_REMOTE_EMAIL %{X_REMOTE_EMAIL}e + RequestHeader set X_REMOTE_FIRSTNAME %{X_REMOTE_FIRSTNAME}e + RequestHeader set X_REMOTE_LASTNAME %{X_REMOTE_LASTNAME}e + + +.. code-block:: python + + from mod_python import apache + import ldap + + LDAP_SERVER = "ldap://server.mydomain.com:389" + LDAP_USER = "" + LDAP_PASS = "" + LDAP_ROOT = "dc=mydomain,dc=com" + LDAP_FILTER = "sAMAcountName=%s" + LDAP_ATTR_LIST = ['sAMAcountName','givenname','sn','mail'] + + def fixuphandler(req): + if req.user is None: + # no user to search for + return apache.OK + else: + try: + if('\\' in req.user): + username = req.user.split('\\')[1] + elif('@' in req.user): + username = req.user.split('@')[0] + else: + username = req.user + l = ldap.initialize(LDAP_SERVER) + l.simple_bind_s(LDAP_USER, LDAP_PASS) + r = l.search_s(LDAP_ROOT, ldap.SCOPE_SUBTREE, LDAP_FILTER % username, attrlist=LDAP_ATTR_LIST) + + req.subprocess_env['X_REMOTE_USER'] = username + req.subprocess_env['X_REMOTE_EMAIL'] = r[0][1]['mail'][0].lower() + req.subprocess_env['X_REMOTE_FIRSTNAME'] = "%s" % r[0][1]['givenname'][0] + req.subprocess_env['X_REMOTE_LASTNAME'] = "%s" % r[0][1]['sn'][0] + except Exception, e: + apache.log_error("error getting data from ldap %s" % str(e), apache.APLOG_ERR) + + return apache.OK + .. note:: If you enable proxy pass-through authentication, make sure your server is only accessible through the proxy. Otherwise, any client would be able to diff -r 9a4d4e623c85 -r ada6571a6d27 kallithea/lib/auth_modules/auth_container.py --- a/kallithea/lib/auth_modules/auth_container.py Fri Dec 25 13:50:18 2015 +0100 +++ b/kallithea/lib/auth_modules/auth_container.py Sun Oct 04 20:43:12 2015 +0200 @@ -29,7 +29,7 @@ from kallithea.lib import auth_modules from kallithea.lib.utils2 import str2bool, safe_unicode from kallithea.lib.compat import hybrid_property -from kallithea.model.db import User +from kallithea.model.db import User, Setting log = logging.getLogger(__name__) @@ -53,15 +53,39 @@ "name": "header", "validator": self.validators.UnicodeString(strip=True, not_empty=True), "type": "string", - "description": "Header to extract the user from", + "description": "Request header to extract the username from", "default": "REMOTE_USER", - "formname": "Header" + "formname": "Username header" + }, + { + "name": "email_header", + "validator": self.validators.UnicodeString(strip=True), + "type": "string", + "description": "Optional request header to extract the email from", + "default": "", + "formname": "Email header" + }, + { + "name": "firstname_header", + "validator": self.validators.UnicodeString(strip=True), + "type": "string", + "description": "Optional request header to extract the first name from", + "default": "", + "formname": "Firstname header" + }, + { + "name": "lastname_header", + "validator": self.validators.UnicodeString(strip=True), + "type": "string", + "description": "Optional request header to extract the last name from", + "default": "", + "formname": "Lastname header" }, { "name": "fallback_header", "validator": self.validators.UnicodeString(strip=True), "type": "string", - "description": "Header to extract the user from when main one fails", + "description": "Request header to extract the user from when main one fails", "default": "HTTP_X_FORWARDED_USER", "formname": "Fallback header" }, @@ -172,9 +196,9 @@ # old attrs fetched from Kallithea database admin = getattr(userobj, 'admin', False) active = getattr(userobj, 'active', True) - email = getattr(userobj, 'email', '') - firstname = getattr(userobj, 'firstname', '') - lastname = getattr(userobj, 'lastname', '') + email = environ.get(settings.get('email_header'), getattr(userobj, 'email', '')) + firstname = environ.get(settings.get('firstname_header'), getattr(userobj, 'firstname', '')) + lastname = environ.get(settings.get('lastname_header'), getattr(userobj, 'lastname', '')) user_data = { 'username': username, @@ -192,4 +216,11 @@ return user_data def get_managed_fields(self): - return ['username', 'password'] + fields = ['username', 'password'] + if(Setting.get_by_name('auth_container_email_header').app_settings_value): + fields.append('email') + if(Setting.get_by_name('auth_container_firstname_header').app_settings_value): + fields.append('firstname') + if(Setting.get_by_name('auth_container_lastname_header').app_settings_value): + fields.append('lastname') + return fields diff -r 9a4d4e623c85 -r ada6571a6d27 kallithea/tests/functional/test_admin_auth_settings.py --- a/kallithea/tests/functional/test_admin_auth_settings.py Fri Dec 25 13:50:18 2015 +0100 +++ b/kallithea/tests/functional/test_admin_auth_settings.py Sun Oct 04 20:43:12 2015 +0200 @@ -135,6 +135,9 @@ def test_container_auth_login_header(self): self._container_auth_setup( auth_container_header='THE_USER_NAME', + auth_container_email_header='', + auth_container_firstname_header='', + auth_container_lastname_header='', auth_container_fallback_header='', auth_container_clean_username='False', ) @@ -143,9 +146,34 @@ resulting_username='john@example.org', ) + def test_container_auth_login_header_attr(self): + self._container_auth_setup( + auth_container_header='THE_USER_NAME', + auth_container_email_header='THE_USER_EMAIL', + auth_container_firstname_header='THE_USER_FIRSTNAME', + auth_container_lastname_header='THE_USER_LASTNAME', + auth_container_fallback_header='', + auth_container_clean_username='False', + ) + response = self.app.get( + url=url(controller='admin/my_account', action='my_account'), + extra_environ={'THE_USER_NAME': 'johnd', + 'THE_USER_EMAIL': 'john@example.org', + 'THE_USER_FIRSTNAME': 'John', + 'THE_USER_LASTNAME': 'Doe', + } + ) + self.assertEqual(response.form['email'].value, 'john@example.org') + self.assertEqual(response.form['firstname'].value, 'John') + self.assertEqual(response.form['lastname'].value, 'Doe') + + def test_container_auth_login_fallback_header(self): self._container_auth_setup( auth_container_header='THE_USER_NAME', + auth_container_email_header='', + auth_container_firstname_header='', + auth_container_lastname_header='', auth_container_fallback_header='HTTP_X_YZZY', auth_container_clean_username='False', ) @@ -157,6 +185,9 @@ def test_container_auth_clean_username_at(self): self._container_auth_setup( auth_container_header='REMOTE_USER', + auth_container_email_header='', + auth_container_firstname_header='', + auth_container_lastname_header='', auth_container_fallback_header='', auth_container_clean_username='True', ) @@ -168,6 +199,9 @@ def test_container_auth_clean_username_backslash(self): self._container_auth_setup( auth_container_header='REMOTE_USER', + auth_container_email_header='', + auth_container_firstname_header='', + auth_container_lastname_header='', auth_container_fallback_header='', auth_container_clean_username='True', ) @@ -179,6 +213,9 @@ def test_container_auth_no_logout(self): self._container_auth_setup( auth_container_header='REMOTE_USER', + auth_container_email_header='', + auth_container_firstname_header='', + auth_container_lastname_header='', auth_container_fallback_header='', auth_container_clean_username='True', )