Changeset - ada6571a6d27
[Not reviewed]
default
0 3 0
domruf - 10 years ago 2015-10-04 20:43:12
dominikruf@gmail.com
auth: let container authentication get email, first and last name from custom headers
3 files changed with 144 insertions and 8 deletions:
0 comments (0 inline, 0 general)
docs/setup.rst
Show inline comments
 
@@ -406,48 +406,116 @@ configure the authentication and add the
 

	
 
For example, the following config section for Apache sets a subdirectory in a
 
reverse-proxy setup with basic auth:
 

	
 
.. code-block:: apache
 

	
 
    <Location /someprefix>
 
      ProxyPass http://127.0.0.1:5000/someprefix
 
      ProxyPassReverse http://127.0.0.1:5000/someprefix
 
      SetEnvIf X-Url-Scheme https HTTPS=1
 

	
 
      AuthType Basic
 
      AuthName "Kallithea authentication"
 
      AuthUserFile /srv/kallithea/.htpasswd
 
      Require valid-user
 

	
 
      RequestHeader unset X-Forwarded-User
 

	
 
      RewriteEngine On
 
      RewriteCond %{LA-U:REMOTE_USER} (.+)
 
      RewriteRule .* - [E=RU:%1]
 
      RequestHeader set X-Forwarded-User %{RU}e
 
    </Location>
 

	
 
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
 

	
 
    <Location /someprefix>
 
      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
 
    </Location>
 

	
 
.. 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
 
   forge the authentication header and could effectively become authenticated
 
   using any account of their liking.
 

	
 

	
 
Integration with issue trackers
 
-------------------------------
 

	
 
Kallithea provides a simple integration with issue trackers. It's possible
 
to define a regular expression that will match an issue ID in commit messages,
 
and have that replaced with a URL to the issue. To enable this simply
 
uncomment the following variables in the ini file::
 

	
 
    issue_pat = (?:^#|\s#)(\w+)
 
    issue_server_link = https://issues.example.com/{repo}/issue/{id}
 
    issue_prefix = #
 

	
 
``issue_pat`` is the regular expression describing which strings in
 
commit messages will be treated as issue references. A match group in
 
parentheses should be used to specify the actual issue id.
 

	
 
The default expression matches issues in the format ``#<number>``, e.g., ``#300``.
kallithea/lib/auth_modules/auth_container.py
Show inline comments
 
@@ -8,81 +8,105 @@
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.lib.auth_modules.auth_container
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
Kallithea container based authentication plugin
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Created on Nov 17, 2012
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import logging
 
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__)
 

	
 

	
 
class KallitheaAuthPlugin(auth_modules.KallitheaExternalAuthPlugin):
 
    def __init__(self):
 
        pass
 

	
 
    @hybrid_property
 
    def name(self):
 
        return "container"
 

	
 
    @hybrid_property
 
    def is_container_auth(self):
 
        return True
 

	
 
    def settings(self):
 

	
 
        settings = [
 
            {
 
                "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"
 
            },
 
            {
 
                "name": "clean_username",
 
                "validator": self.validators.StringBoolean(if_missing=False),
 
                "type": "bool",
 
                "description": "Perform cleaning of user, if passed user has @ in username "
 
                               "then first part before @ is taken. "
 
                               "If there's \\ in the username only the part after \\ is taken",
 
                "default": "True",
 
                "formname": "Clean username"
 
            },
 
        ]
 
        return settings
 

	
 
    def use_fake_password(self):
 
        return True
 

	
 
    def user_activation_state(self):
 
        def_user_perms = User.get_default_user().AuthUser.permissions['global']
 
        return 'hg.extern_activate.auto' in def_user_perms
 

	
 
    def _clean_username(self, username):
 
@@ -151,45 +175,52 @@ class KallitheaAuthPlugin(auth_modules.K
 
            log.debug('Empty environ data skipping...')
 
            return None
 

	
 
        if not userobj:
 
            userobj = self.get_user('', environ=environ, settings=settings)
 

	
 
        # we don't care passed username/password for container auth plugins.
 
        # only way to log in is using environ
 
        username = None
 
        if userobj:
 
            username = getattr(userobj, 'username')
 

	
 
        if not username:
 
            # we don't have any objects in DB, user doesn't exist, extract
 
            # username from environ based on the settings
 
            username = self._get_username(environ, settings)
 

	
 
        # if cannot fetch username, it's a no-go for this plugin to proceed
 
        if not username:
 
            return None
 

	
 
        # 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,
 
            'firstname': safe_unicode(firstname or username),
 
            'lastname': safe_unicode(lastname or ''),
 
            'groups': [],
 
            'email': email or '',
 
            'admin': admin or False,
 
            'active': active,
 
            'active_from_extern': True,
 
            'extern_name': username,
 
        }
 

	
 
        log.info('user `%s` authenticated correctly', user_data['username'])
 
        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
kallithea/tests/functional/test_admin_auth_settings.py
Show inline comments
 
@@ -114,92 +114,129 @@ class TestAuthSettingsController(TestCon
 

	
 
    def _container_auth_setup(self, **settings):
 
        self.log_user()
 

	
 
        params = self._enable_plugins('kallithea.lib.auth_modules.auth_internal,kallithea.lib.auth_modules.auth_container')
 
        params.update(settings)
 

	
 
        test_url = url(controller='admin/auth_settings',
 
                       action='auth_settings')
 

	
 
        response = self.app.post(url=test_url, params=params)
 
        response = response.follow()
 
        response.click('Log Out') # end admin login session
 

	
 
    def _container_auth_verify_login(self, resulting_username, **get_kwargs):
 
        response = self.app.get(
 
            url=url(controller='admin/my_account', action='my_account'),
 
            **get_kwargs
 
        )
 
        response.mustcontain('My Account %s' % resulting_username)
 

	
 
    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',
 
        )
 
        self._container_auth_verify_login(
 
            extra_environ={'THE_USER_NAME': 'john@example.org'},
 
            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',
 
        )
 
        self._container_auth_verify_login(
 
            headers={'X-Yzzy': r'foo\bar'},
 
            resulting_username=r'foo\bar',
 
        )
 

	
 
    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',
 
        )
 
        self._container_auth_verify_login(
 
            extra_environ={'REMOTE_USER': 'john@example.org'},
 
            resulting_username='john',
 
        )
 

	
 
    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',
 
        )
 
        self._container_auth_verify_login(
 
            extra_environ={'REMOTE_USER': r'example\jane'},
 
            resulting_username=r'jane',
 
        )
 

	
 
    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',
 
        )
 
        response = self.app.get(
 
            url=url(controller='admin/my_account', action='my_account'),
 
            extra_environ={'REMOTE_USER': 'john'},
 
        )
 
        self.assertNotIn('Log Out', response.normal_body)
 

	
 
    def test_crowd_save_settings(self):
 
        self.log_user()
 

	
 
        params = self._enable_plugins('kallithea.lib.auth_modules.auth_internal,kallithea.lib.auth_modules.auth_crowd')
 
        params.update({'auth_crowd_host': ' hostname ',
 
                       'auth_crowd_app_password': 'secret',
 
                       'auth_crowd_admin_groups': 'mygroup',
 
                       'auth_crowd_port': '123',
 
                       'auth_crowd_app_name': 'xyzzy'})
 

	
 
        test_url = url(controller='admin/auth_settings',
 
                       action='auth_settings')
 

	
 
        response = self.app.post(url=test_url, params=params)
 
        self.checkSessionFlash(response, 'Auth settings updated successfully')
0 comments (0 inline, 0 general)