diff --git a/rhodecode/authentication/base.py b/rhodecode/authentication/base.py --- a/rhodecode/authentication/base.py +++ b/rhodecode/authentication/base.py @@ -21,7 +21,8 @@ """ Authentication modules """ - +import socket +import string import colander import copy import logging @@ -31,14 +32,13 @@ import warnings import functools from pyramid.threadlocal import get_current_registry -from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode.authentication.interface import IAuthnPluginRegistry from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase from rhodecode.lib import caches from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt -from rhodecode.lib.utils2 import safe_int -from rhodecode.lib.utils2 import safe_str +from rhodecode.lib.utils2 import safe_int, safe_str +from rhodecode.lib.exceptions import LdapConnectionError from rhodecode.model.db import User from rhodecode.model.meta import Session from rhodecode.model.settings import SettingsModel @@ -556,6 +556,63 @@ class RhodeCodeExternalAuthPlugin(RhodeC return auth +class AuthLdapBase(object): + + @classmethod + def _build_servers(cls, ldap_server_type, ldap_server, port): + def host_resolver(host, port, full_resolve=True): + """ + Main work for this function is to prevent ldap connection issues, + and detect them early using a "greenified" sockets + """ + host = host.strip() + if not full_resolve: + return '{}:{}'.format(host, port) + + log.debug('LDAP: Resolving IP for LDAP host %s', host) + try: + ip = socket.gethostbyname(host) + log.debug('Got LDAP server %s ip %s', host, ip) + except Exception: + raise LdapConnectionError( + 'Failed to resolve host: `{}`'.format(host)) + + log.debug('LDAP: Checking if IP %s is accessible', ip) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect((ip, int(port))) + s.shutdown(socket.SHUT_RD) + except Exception: + raise LdapConnectionError( + 'Failed to connect to host: `{}:{}`'.format(host, port)) + + return '{}:{}'.format(host, port) + + if len(ldap_server) == 1: + # in case of single server use resolver to detect potential + # connection issues + full_resolve = True + else: + full_resolve = False + + return ', '.join( + ["{}://{}".format( + ldap_server_type, + host_resolver(host, port, full_resolve=full_resolve)) + for host in ldap_server]) + + @classmethod + def _get_server_list(cls, servers): + return map(string.strip, servers.split(',')) + + @classmethod + def get_uid(cls, username, server_addresses): + uid = username + for server_addr in server_addresses: + uid = chop_at(username, "@%s" % server_addr) + return uid + + def loadplugin(plugin_id): """ Loads and returns an instantiated authentication plugin. @@ -613,7 +670,7 @@ def authenticate(username, password, env authn_registry = get_authn_registry(registry) plugins_to_check = authn_registry.get_plugins_for_authentication() log.debug('Starting ordered authentication chain using %s plugins', - plugins_to_check) + [x.name for x in plugins_to_check]) for plugin in plugins_to_check: plugin.set_auth_type(auth_type) plugin.set_calling_scope_repo(acl_repo_name) diff --git a/rhodecode/authentication/plugins/__init__.py b/rhodecode/authentication/plugins/__init__.py --- a/rhodecode/authentication/plugins/__init__.py +++ b/rhodecode/authentication/plugins/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2018 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# 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 Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ diff --git a/rhodecode/authentication/plugins/auth_ldap.py b/rhodecode/authentication/plugins/auth_ldap.py --- a/rhodecode/authentication/plugins/auth_ldap.py +++ b/rhodecode/authentication/plugins/auth_ldap.py @@ -22,16 +22,13 @@ RhodeCode authentication plugin for LDAP """ -import socket -import re -import colander import logging import traceback -import string +import colander from rhodecode.translation import _ from rhodecode.authentication.base import ( - RhodeCodeExternalAuthPlugin, chop_at, hybrid_property) + RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property) from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase from rhodecode.authentication.routes import AuthnPluginResourceBase from rhodecode.lib.colander_utils import strip_whitespace @@ -199,40 +196,7 @@ class LdapSettingsSchema(AuthnPluginSett widget='string') -class AuthLdap(object): - - def _build_servers(self): - def host_resolver(host, port): - """ - Main work for this function is to prevent ldap connection issues, - and detect them early using a "greenified" sockets - """ - host = host.strip() - - log.info('Resolving LDAP host %s', host) - try: - ip = socket.gethostbyname(host) - log.info('Got LDAP server %s ip %s', host, ip) - except Exception: - raise LdapConnectionError( - 'Failed to resolve host: `{}`'.format(host)) - - log.info('Checking LDAP IP access %s', ip) - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s.connect((ip, int(port))) - s.shutdown(socket.SHUT_RD) - except Exception: - raise LdapConnectionError( - 'Failed to connect to host: `{}:{}`'.format(host, port)) - - return '{}:{}'.format(host, port) - - port = self.LDAP_SERVER_PORT - return ', '.join( - ["{}://{}".format( - self.ldap_server_type, host_resolver(host, port)) - for host in self.SERVER_ADDRESSES]) +class AuthLdap(AuthLdapBase): def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='', tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3, @@ -255,8 +219,9 @@ class AuthLdap(object): OPT_X_TLS_DEMAND = 2 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND) + self.LDAP_SERVER = server # split server into list - self.SERVER_ADDRESSES = server.split(',') + self.SERVER_ADDRESSES = self._get_server_list(server) self.LDAP_SERVER_PORT = port # USE FOR READ ONLY BIND TO LDAP SERVER @@ -264,13 +229,12 @@ class AuthLdap(object): self.LDAP_BIND_DN = safe_str(bind_dn) self.LDAP_BIND_PASS = safe_str(bind_pass) - self.LDAP_SERVER = self._build_servers() + self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope) self.BASE_DN = safe_str(base_dn) self.LDAP_FILTER = safe_str(ldap_filter) def _get_ldap_conn(self): - log.debug('initializing LDAP connection to:%s', self.LDAP_SERVER) if self.debug: ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255) @@ -284,7 +248,10 @@ class AuthLdap(object): ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON) # init connection now - ldap_conn = ldap.initialize(self.LDAP_SERVER) + ldap_servers = self._build_servers( + self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT) + log.debug('initializing LDAP connection to:%s', ldap_servers) + ldap_conn = ldap.initialize(ldap_servers) ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout) ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout) ldap_conn.timeout = self.timeout @@ -304,12 +271,6 @@ class AuthLdap(object): return ldap_conn - def get_uid(self, username): - uid = username - for server_addr in self.SERVER_ADDRESSES: - uid = chop_at(username, "@%s" % server_addr) - return uid - def fetch_attrs_from_simple_bind(self, server, dn, username, password): try: log.debug('Trying simple bind with %s', dn) @@ -335,7 +296,9 @@ class AuthLdap(object): :param password: password """ - uid = self.get_uid(username) + uid = self.get_uid(username, self.SERVER_ADDRESSES) + user_attrs = {} + dn = '' if not password: msg = "Authenticating user %s with blank password not allowed" @@ -349,8 +312,8 @@ class AuthLdap(object): ldap_conn = self._get_ldap_conn() filter_ = '(&%s(%s=%s))' % ( self.LDAP_FILTER, self.attr_login, username) - log.debug("Authenticating %r filter %s at %s", self.BASE_DN, - filter_, self.LDAP_SERVER) + log.debug( + "Authenticating %r filter %s", self.BASE_DN, filter_) lobjects = ldap_conn.search_ext_s( self.BASE_DN, self.SEARCH_SCOPE, filter_)