##// END OF EJS Templates
auth-ldap: added connection pinning and timeout option to ldap plugin
marcink -
r2654:c156e0f4 default
parent child Browse files
Show More
@@ -1,201 +1,202 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.tests import assert_session_flash
23 from rhodecode.tests import assert_session_flash
24 from rhodecode.tests.utils import AssertResponse
24 from rhodecode.tests.utils import AssertResponse
25 from rhodecode.model.db import Session
25 from rhodecode.model.db import Session
26 from rhodecode.model.settings import SettingsModel
26 from rhodecode.model.settings import SettingsModel
27
27
28
28
29 def assert_auth_settings_updated(response):
29 def assert_auth_settings_updated(response):
30 assert response.status_int == 302, 'Expected response HTTP Found 302'
30 assert response.status_int == 302, 'Expected response HTTP Found 302'
31 assert_session_flash(response, 'Auth settings updated successfully')
31 assert_session_flash(response, 'Auth settings updated successfully')
32
32
33
33
34 @pytest.mark.usefixtures("autologin_user", "app")
34 @pytest.mark.usefixtures("autologin_user", "app")
35 class TestAuthSettingsView(object):
35 class TestAuthSettingsView(object):
36
36
37 def _enable_plugins(self, plugins_list, csrf_token, override=None,
37 def _enable_plugins(self, plugins_list, csrf_token, override=None,
38 verify_response=False):
38 verify_response=False):
39 test_url = '/_admin/auth'
39 test_url = '/_admin/auth'
40 params = {
40 params = {
41 'auth_plugins': plugins_list,
41 'auth_plugins': plugins_list,
42 'csrf_token': csrf_token,
42 'csrf_token': csrf_token,
43 }
43 }
44 if override:
44 if override:
45 params.update(override)
45 params.update(override)
46 _enabled_plugins = []
46 _enabled_plugins = []
47 for plugin in plugins_list.split(','):
47 for plugin in plugins_list.split(','):
48 plugin_name = plugin.partition('#')[-1]
48 plugin_name = plugin.partition('#')[-1]
49 enabled_plugin = '%s_enabled' % plugin_name
49 enabled_plugin = '%s_enabled' % plugin_name
50 cache_ttl = '%s_cache_ttl' % plugin_name
50 cache_ttl = '%s_cache_ttl' % plugin_name
51
51
52 # default params that are needed for each plugin,
52 # default params that are needed for each plugin,
53 # `enabled` and `cache_ttl`
53 # `enabled` and `cache_ttl`
54 params.update({
54 params.update({
55 enabled_plugin: True,
55 enabled_plugin: True,
56 cache_ttl: 0
56 cache_ttl: 0
57 })
57 })
58 _enabled_plugins.append(enabled_plugin)
58 _enabled_plugins.append(enabled_plugin)
59
59
60 # we need to clean any enabled plugin before, since they require
60 # we need to clean any enabled plugin before, since they require
61 # form params to be present
61 # form params to be present
62 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
62 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
63 db_plugin.app_settings_value = \
63 db_plugin.app_settings_value = \
64 'egg:rhodecode-enterprise-ce#rhodecode'
64 'egg:rhodecode-enterprise-ce#rhodecode'
65 Session().add(db_plugin)
65 Session().add(db_plugin)
66 Session().commit()
66 Session().commit()
67 for _plugin in _enabled_plugins:
67 for _plugin in _enabled_plugins:
68 db_plugin = SettingsModel().get_setting_by_name(_plugin)
68 db_plugin = SettingsModel().get_setting_by_name(_plugin)
69 if db_plugin:
69 if db_plugin:
70 Session().delete(db_plugin)
70 Session().delete(db_plugin)
71 Session().commit()
71 Session().commit()
72
72
73 response = self.app.post(url=test_url, params=params)
73 response = self.app.post(url=test_url, params=params)
74
74
75 if verify_response:
75 if verify_response:
76 assert_auth_settings_updated(response)
76 assert_auth_settings_updated(response)
77 return params
77 return params
78
78
79 def _post_ldap_settings(self, params, override=None, force=False):
79 def _post_ldap_settings(self, params, override=None, force=False):
80
80
81 params.update({
81 params.update({
82 'filter': 'user',
82 'filter': 'user',
83 'user_member_of': '',
83 'user_member_of': '',
84 'user_search_base': '',
84 'user_search_base': '',
85 'user_search_filter': 'test_filter',
85 'user_search_filter': 'test_filter',
86
86
87 'host': 'dc.example.com',
87 'host': 'dc.example.com',
88 'port': '999',
88 'port': '999',
89 'timeout': 3600,
89 'tls_kind': 'PLAIN',
90 'tls_kind': 'PLAIN',
90 'tls_reqcert': 'NEVER',
91 'tls_reqcert': 'NEVER',
91
92
92 'dn_user': 'test_user',
93 'dn_user': 'test_user',
93 'dn_pass': 'test_pass',
94 'dn_pass': 'test_pass',
94 'base_dn': 'test_base_dn',
95 'base_dn': 'test_base_dn',
95 'search_scope': 'BASE',
96 'search_scope': 'BASE',
96 'attr_login': 'test_attr_login',
97 'attr_login': 'test_attr_login',
97 'attr_firstname': 'ima',
98 'attr_firstname': 'ima',
98 'attr_lastname': 'tester',
99 'attr_lastname': 'tester',
99 'attr_email': 'test@example.com',
100 'attr_email': 'test@example.com',
100 'cache_ttl': '0',
101 'cache_ttl': '0',
101 })
102 })
102 if force:
103 if force:
103 params = {}
104 params = {}
104 params.update(override or {})
105 params.update(override or {})
105
106
106 test_url = '/_admin/auth/ldap/'
107 test_url = '/_admin/auth/ldap/'
107
108
108 response = self.app.post(url=test_url, params=params)
109 response = self.app.post(url=test_url, params=params)
109 return response
110 return response
110
111
111 def test_index(self):
112 def test_index(self):
112 response = self.app.get('/_admin/auth')
113 response = self.app.get('/_admin/auth')
113 response.mustcontain('Authentication Plugins')
114 response.mustcontain('Authentication Plugins')
114
115
115 @pytest.mark.parametrize("disable_plugin, needs_import", [
116 @pytest.mark.parametrize("disable_plugin, needs_import", [
116 ('egg:rhodecode-enterprise-ce#headers', None),
117 ('egg:rhodecode-enterprise-ce#headers', None),
117 ('egg:rhodecode-enterprise-ce#crowd', None),
118 ('egg:rhodecode-enterprise-ce#crowd', None),
118 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
119 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
119 ('egg:rhodecode-enterprise-ce#ldap', None),
120 ('egg:rhodecode-enterprise-ce#ldap', None),
120 ('egg:rhodecode-enterprise-ce#pam', "pam"),
121 ('egg:rhodecode-enterprise-ce#pam', "pam"),
121 ])
122 ])
122 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
123 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
123 # TODO: johbo: "pam" is currently not available on darwin,
124 # TODO: johbo: "pam" is currently not available on darwin,
124 # although the docs state that it should work on darwin.
125 # although the docs state that it should work on darwin.
125 if needs_import:
126 if needs_import:
126 pytest.importorskip(needs_import)
127 pytest.importorskip(needs_import)
127
128
128 self._enable_plugins(
129 self._enable_plugins(
129 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
130 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
130 csrf_token, verify_response=True)
131 csrf_token, verify_response=True)
131
132
132 self._enable_plugins(
133 self._enable_plugins(
133 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
134 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
134 verify_response=True)
135 verify_response=True)
135
136
136 def test_ldap_save_settings(self, csrf_token):
137 def test_ldap_save_settings(self, csrf_token):
137 params = self._enable_plugins(
138 params = self._enable_plugins(
138 'egg:rhodecode-enterprise-ce#rhodecode,'
139 'egg:rhodecode-enterprise-ce#rhodecode,'
139 'egg:rhodecode-enterprise-ce#ldap',
140 'egg:rhodecode-enterprise-ce#ldap',
140 csrf_token)
141 csrf_token)
141 response = self._post_ldap_settings(params)
142 response = self._post_ldap_settings(params)
142 assert_auth_settings_updated(response)
143 assert_auth_settings_updated(response)
143
144
144 new_settings = SettingsModel().get_auth_settings()
145 new_settings = SettingsModel().get_auth_settings()
145 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
146 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
146 'fail db write compare'
147 'fail db write compare'
147
148
148 def test_ldap_error_form_wrong_port_number(self, csrf_token):
149 def test_ldap_error_form_wrong_port_number(self, csrf_token):
149 params = self._enable_plugins(
150 params = self._enable_plugins(
150 'egg:rhodecode-enterprise-ce#rhodecode,'
151 'egg:rhodecode-enterprise-ce#rhodecode,'
151 'egg:rhodecode-enterprise-ce#ldap',
152 'egg:rhodecode-enterprise-ce#ldap',
152 csrf_token)
153 csrf_token)
153 invalid_port_value = 'invalid-port-number'
154 invalid_port_value = 'invalid-port-number'
154 response = self._post_ldap_settings(params, override={
155 response = self._post_ldap_settings(params, override={
155 'port': invalid_port_value,
156 'port': invalid_port_value,
156 })
157 })
157 assertr = AssertResponse(response)
158 assertr = AssertResponse(response)
158 assertr.element_contains(
159 assertr.element_contains(
159 '.form .field #port ~ .error-message',
160 '.form .field #port ~ .error-message',
160 invalid_port_value)
161 invalid_port_value)
161
162
162 def test_ldap_error_form(self, csrf_token):
163 def test_ldap_error_form(self, csrf_token):
163 params = self._enable_plugins(
164 params = self._enable_plugins(
164 'egg:rhodecode-enterprise-ce#rhodecode,'
165 'egg:rhodecode-enterprise-ce#rhodecode,'
165 'egg:rhodecode-enterprise-ce#ldap',
166 'egg:rhodecode-enterprise-ce#ldap',
166 csrf_token)
167 csrf_token)
167 response = self._post_ldap_settings(params, override={
168 response = self._post_ldap_settings(params, override={
168 'attr_login': '',
169 'attr_login': '',
169 })
170 })
170 response.mustcontain("""<span class="error-message">The LDAP Login"""
171 response.mustcontain("""<span class="error-message">The LDAP Login"""
171 """ attribute of the CN must be specified""")
172 """ attribute of the CN must be specified""")
172
173
173 def test_post_ldap_group_settings(self, csrf_token):
174 def test_post_ldap_group_settings(self, csrf_token):
174 params = self._enable_plugins(
175 params = self._enable_plugins(
175 'egg:rhodecode-enterprise-ce#rhodecode,'
176 'egg:rhodecode-enterprise-ce#rhodecode,'
176 'egg:rhodecode-enterprise-ce#ldap',
177 'egg:rhodecode-enterprise-ce#ldap',
177 csrf_token)
178 csrf_token)
178
179
179 response = self._post_ldap_settings(params, override={
180 response = self._post_ldap_settings(params, override={
180 'host': 'dc-legacy.example.com',
181 'host': 'dc-legacy.example.com',
181 'port': '999',
182 'port': '999',
182 'tls_kind': 'PLAIN',
183 'tls_kind': 'PLAIN',
183 'tls_reqcert': 'NEVER',
184 'tls_reqcert': 'NEVER',
184 'dn_user': 'test_user',
185 'dn_user': 'test_user',
185 'dn_pass': 'test_pass',
186 'dn_pass': 'test_pass',
186 'base_dn': 'test_base_dn',
187 'base_dn': 'test_base_dn',
187 'filter': 'test_filter',
188 'filter': 'test_filter',
188 'search_scope': 'BASE',
189 'search_scope': 'BASE',
189 'attr_login': 'test_attr_login',
190 'attr_login': 'test_attr_login',
190 'attr_firstname': 'ima',
191 'attr_firstname': 'ima',
191 'attr_lastname': 'tester',
192 'attr_lastname': 'tester',
192 'attr_email': 'test@example.com',
193 'attr_email': 'test@example.com',
193 'cache_ttl': '60',
194 'cache_ttl': '60',
194 'csrf_token': csrf_token,
195 'csrf_token': csrf_token,
195 }
196 }
196 )
197 )
197 assert_auth_settings_updated(response)
198 assert_auth_settings_updated(response)
198
199
199 new_settings = SettingsModel().get_auth_settings()
200 new_settings = SettingsModel().get_auth_settings()
200 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
201 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
201 'fail db write compare'
202 'fail db write compare'
@@ -1,498 +1,542 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for LDAP
22 RhodeCode authentication plugin for LDAP
23 """
23 """
24
24
25 import socket
25 import re
26 import re
26 import colander
27 import colander
27 import logging
28 import logging
28 import traceback
29 import traceback
29 import string
30 import string
30
31
31 from rhodecode.translation import _
32 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
33 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
34 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.exceptions import (
38 from rhodecode.lib.exceptions import (
38 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
39 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
39 )
40 )
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 from rhodecode.model.db import User
42 from rhodecode.model.db import User
42 from rhodecode.model.validators import Missing
43 from rhodecode.model.validators import Missing
43
44
44 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
45
46
46 try:
47 try:
47 import ldap
48 import ldap
48 except ImportError:
49 except ImportError:
49 # means that python-ldap is not installed, we use Missing object to mark
50 # means that python-ldap is not installed, we use Missing object to mark
50 # ldap lib is Missing
51 # ldap lib is Missing
51 ldap = Missing
52 ldap = Missing
52
53
53
54
54 class LdapError(Exception):
55 class LdapError(Exception):
55 pass
56 pass
56
57
57 def plugin_factory(plugin_id, *args, **kwds):
58 def plugin_factory(plugin_id, *args, **kwds):
58 """
59 """
59 Factory function that is called during plugin discovery.
60 Factory function that is called during plugin discovery.
60 It returns the plugin instance.
61 It returns the plugin instance.
61 """
62 """
62 plugin = RhodeCodeAuthPlugin(plugin_id)
63 plugin = RhodeCodeAuthPlugin(plugin_id)
63 return plugin
64 return plugin
64
65
65
66
66 class LdapAuthnResource(AuthnPluginResourceBase):
67 class LdapAuthnResource(AuthnPluginResourceBase):
67 pass
68 pass
68
69
69
70
70 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
71 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
71 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
72 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
72 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
73 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
73 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
74 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
74
75
75 host = colander.SchemaNode(
76 host = colander.SchemaNode(
76 colander.String(),
77 colander.String(),
77 default='',
78 default='',
78 description=_('Host[s] of the LDAP Server \n'
79 description=_('Host[s] of the LDAP Server \n'
79 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
80 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
80 'Multiple servers can be specified using commas'),
81 'Multiple servers can be specified using commas'),
81 preparer=strip_whitespace,
82 preparer=strip_whitespace,
82 title=_('LDAP Host'),
83 title=_('LDAP Host'),
83 widget='string')
84 widget='string')
84 port = colander.SchemaNode(
85 port = colander.SchemaNode(
85 colander.Int(),
86 colander.Int(),
86 default=389,
87 default=389,
87 description=_('Custom port that the LDAP server is listening on. '
88 description=_('Custom port that the LDAP server is listening on. '
88 'Default value is: 389'),
89 'Default value is: 389'),
89 preparer=strip_whitespace,
90 preparer=strip_whitespace,
90 title=_('Port'),
91 title=_('Port'),
91 validator=colander.Range(min=0, max=65536),
92 validator=colander.Range(min=0, max=65536),
92 widget='int')
93 widget='int')
94
95 timeout = colander.SchemaNode(
96 colander.Int(),
97 default=60 * 5,
98 description=_('Timeout for LDAP connection'),
99 preparer=strip_whitespace,
100 title=_('Connection timeout'),
101 validator=colander.Range(min=1),
102 widget='int')
103
93 dn_user = colander.SchemaNode(
104 dn_user = colander.SchemaNode(
94 colander.String(),
105 colander.String(),
95 default='',
106 default='',
96 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
107 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
97 'e.g., cn=admin,dc=mydomain,dc=com, or '
108 'e.g., cn=admin,dc=mydomain,dc=com, or '
98 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
109 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
99 missing='',
110 missing='',
100 preparer=strip_whitespace,
111 preparer=strip_whitespace,
101 title=_('Account'),
112 title=_('Account'),
102 widget='string')
113 widget='string')
103 dn_pass = colander.SchemaNode(
114 dn_pass = colander.SchemaNode(
104 colander.String(),
115 colander.String(),
105 default='',
116 default='',
106 description=_('Password to authenticate for given user DN.'),
117 description=_('Password to authenticate for given user DN.'),
107 missing='',
118 missing='',
108 preparer=strip_whitespace,
119 preparer=strip_whitespace,
109 title=_('Password'),
120 title=_('Password'),
110 widget='password')
121 widget='password')
111 tls_kind = colander.SchemaNode(
122 tls_kind = colander.SchemaNode(
112 colander.String(),
123 colander.String(),
113 default=tls_kind_choices[0],
124 default=tls_kind_choices[0],
114 description=_('TLS Type'),
125 description=_('TLS Type'),
115 title=_('Connection Security'),
126 title=_('Connection Security'),
116 validator=colander.OneOf(tls_kind_choices),
127 validator=colander.OneOf(tls_kind_choices),
117 widget='select')
128 widget='select')
118 tls_reqcert = colander.SchemaNode(
129 tls_reqcert = colander.SchemaNode(
119 colander.String(),
130 colander.String(),
120 default=tls_reqcert_choices[0],
131 default=tls_reqcert_choices[0],
121 description=_('Require Cert over TLS?. Self-signed and custom '
132 description=_('Require Cert over TLS?. Self-signed and custom '
122 'certificates can be used when\n `RhodeCode Certificate` '
133 'certificates can be used when\n `RhodeCode Certificate` '
123 'found in admin > settings > system info page is extended.'),
134 'found in admin > settings > system info page is extended.'),
124 title=_('Certificate Checks'),
135 title=_('Certificate Checks'),
125 validator=colander.OneOf(tls_reqcert_choices),
136 validator=colander.OneOf(tls_reqcert_choices),
126 widget='select')
137 widget='select')
127 base_dn = colander.SchemaNode(
138 base_dn = colander.SchemaNode(
128 colander.String(),
139 colander.String(),
129 default='',
140 default='',
130 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
141 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
131 'in it to be replaced with current user credentials \n'
142 'in it to be replaced with current user credentials \n'
132 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
143 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
133 missing='',
144 missing='',
134 preparer=strip_whitespace,
145 preparer=strip_whitespace,
135 title=_('Base DN'),
146 title=_('Base DN'),
136 widget='string')
147 widget='string')
137 filter = colander.SchemaNode(
148 filter = colander.SchemaNode(
138 colander.String(),
149 colander.String(),
139 default='',
150 default='',
140 description=_('Filter to narrow results \n'
151 description=_('Filter to narrow results \n'
141 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
152 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
142 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
153 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
143 missing='',
154 missing='',
144 preparer=strip_whitespace,
155 preparer=strip_whitespace,
145 title=_('LDAP Search Filter'),
156 title=_('LDAP Search Filter'),
146 widget='string')
157 widget='string')
147
158
148 search_scope = colander.SchemaNode(
159 search_scope = colander.SchemaNode(
149 colander.String(),
160 colander.String(),
150 default=search_scope_choices[2],
161 default=search_scope_choices[2],
151 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
162 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
152 title=_('LDAP Search Scope'),
163 title=_('LDAP Search Scope'),
153 validator=colander.OneOf(search_scope_choices),
164 validator=colander.OneOf(search_scope_choices),
154 widget='select')
165 widget='select')
155 attr_login = colander.SchemaNode(
166 attr_login = colander.SchemaNode(
156 colander.String(),
167 colander.String(),
157 default='uid',
168 default='uid',
158 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
169 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
159 preparer=strip_whitespace,
170 preparer=strip_whitespace,
160 title=_('Login Attribute'),
171 title=_('Login Attribute'),
161 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
172 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
162 widget='string')
173 widget='string')
163 attr_firstname = colander.SchemaNode(
174 attr_firstname = colander.SchemaNode(
164 colander.String(),
175 colander.String(),
165 default='',
176 default='',
166 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
177 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
167 missing='',
178 missing='',
168 preparer=strip_whitespace,
179 preparer=strip_whitespace,
169 title=_('First Name Attribute'),
180 title=_('First Name Attribute'),
170 widget='string')
181 widget='string')
171 attr_lastname = colander.SchemaNode(
182 attr_lastname = colander.SchemaNode(
172 colander.String(),
183 colander.String(),
173 default='',
184 default='',
174 description=_('LDAP Attribute to map to last name (e.g., sn)'),
185 description=_('LDAP Attribute to map to last name (e.g., sn)'),
175 missing='',
186 missing='',
176 preparer=strip_whitespace,
187 preparer=strip_whitespace,
177 title=_('Last Name Attribute'),
188 title=_('Last Name Attribute'),
178 widget='string')
189 widget='string')
179 attr_email = colander.SchemaNode(
190 attr_email = colander.SchemaNode(
180 colander.String(),
191 colander.String(),
181 default='',
192 default='',
182 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
193 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
183 'Emails are a crucial part of RhodeCode. \n'
194 'Emails are a crucial part of RhodeCode. \n'
184 'If possible add a valid email attribute to ldap users.'),
195 'If possible add a valid email attribute to ldap users.'),
185 missing='',
196 missing='',
186 preparer=strip_whitespace,
197 preparer=strip_whitespace,
187 title=_('Email Attribute'),
198 title=_('Email Attribute'),
188 widget='string')
199 widget='string')
189
200
190
201
191 class AuthLdap(object):
202 class AuthLdap(object):
192
203
193 def _build_servers(self):
204 def _build_servers(self):
205 def host_resolver(host, port):
206 """
207 Main work for this function is to prevent ldap connection issues,
208 and detect them early using a "greenified" sockets
209 """
210 host = host.strip()
211
212 log.info('Resolving LDAP host %s', host)
213 try:
214 ip = socket.gethostbyname(host)
215 log.info('Got LDAP server %s ip %s', host, ip)
216 except Exception:
217 raise LdapConnectionError(
218 'Failed to resolve host: `{}`'.format(host))
219
220 log.info('Checking LDAP IP access %s', ip)
221 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
222 try:
223 s.connect((ip, int(port)))
224 s.shutdown(socket.SHUT_RD)
225 except Exception:
226 raise LdapConnectionError(
227 'Failed to connect to host: `{}:{}`'.format(host, port))
228
229 return '{}:{}'.format(host, port)
230
231 port = self.LDAP_SERVER_PORT
194 return ', '.join(
232 return ', '.join(
195 ["{}://{}:{}".format(
233 ["{}://{}".format(
196 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
234 self.ldap_server_type, host_resolver(host, port))
197 for host in self.SERVER_ADDRESSES])
235 for host in self.SERVER_ADDRESSES])
198
236
199 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
237 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
200 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
238 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
201 search_scope='SUBTREE', attr_login='uid',
239 search_scope='SUBTREE', attr_login='uid',
202 ldap_filter=''):
240 ldap_filter='', timeout=None):
203 if ldap == Missing:
241 if ldap == Missing:
204 raise LdapImportError("Missing or incompatible ldap library")
242 raise LdapImportError("Missing or incompatible ldap library")
205
243
206 self.debug = False
244 self.debug = False
245 self.timeout = timeout or 60 * 5
207 self.ldap_version = ldap_version
246 self.ldap_version = ldap_version
208 self.ldap_server_type = 'ldap'
247 self.ldap_server_type = 'ldap'
209
248
210 self.TLS_KIND = tls_kind
249 self.TLS_KIND = tls_kind
211
250
212 if self.TLS_KIND == 'LDAPS':
251 if self.TLS_KIND == 'LDAPS':
213 port = port or 689
252 port = port or 689
214 self.ldap_server_type += 's'
253 self.ldap_server_type += 's'
215
254
216 OPT_X_TLS_DEMAND = 2
255 OPT_X_TLS_DEMAND = 2
217 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
256 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
218 OPT_X_TLS_DEMAND)
257 OPT_X_TLS_DEMAND)
219 # split server into list
258 # split server into list
220 self.SERVER_ADDRESSES = server.split(',')
259 self.SERVER_ADDRESSES = server.split(',')
221 self.LDAP_SERVER_PORT = port
260 self.LDAP_SERVER_PORT = port
222
261
223 # USE FOR READ ONLY BIND TO LDAP SERVER
262 # USE FOR READ ONLY BIND TO LDAP SERVER
224 self.attr_login = attr_login
263 self.attr_login = attr_login
225
264
226 self.LDAP_BIND_DN = safe_str(bind_dn)
265 self.LDAP_BIND_DN = safe_str(bind_dn)
227 self.LDAP_BIND_PASS = safe_str(bind_pass)
266 self.LDAP_BIND_PASS = safe_str(bind_pass)
228 self.LDAP_SERVER = self._build_servers()
267 self.LDAP_SERVER = self._build_servers()
229 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
268 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
230 self.BASE_DN = safe_str(base_dn)
269 self.BASE_DN = safe_str(base_dn)
231 self.LDAP_FILTER = safe_str(ldap_filter)
270 self.LDAP_FILTER = safe_str(ldap_filter)
232
271
233 def _get_ldap_conn(self):
272 def _get_ldap_conn(self):
273 log.debug('initializing LDAP connection to:%s', self.LDAP_SERVER)
274
234 if self.debug:
275 if self.debug:
235 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
276 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
236
277
237 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
278 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
238 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
279 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, '/etc/openldap/cacerts')
239 '/etc/openldap/cacerts')
240 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
241 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
242 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 60 * 10)
243 ldap.set_option(ldap.OPT_TIMEOUT, 60 * 10)
244
245 if self.TLS_KIND != 'PLAIN':
280 if self.TLS_KIND != 'PLAIN':
246 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
281 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
247
282
248 log.debug('initializing LDAP connection to:%s', self.LDAP_SERVER)
283 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
284 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
285
286 # init connection now
249 ldap_conn = ldap.initialize(self.LDAP_SERVER)
287 ldap_conn = ldap.initialize(self.LDAP_SERVER)
288 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
289 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
290 ldap_conn.timeout = self.timeout
291
250 if self.ldap_version == 2:
292 if self.ldap_version == 2:
251 ldap_conn.protocol = ldap.VERSION2
293 ldap_conn.protocol = ldap.VERSION2
252 else:
294 else:
253 ldap_conn.protocol = ldap.VERSION3
295 ldap_conn.protocol = ldap.VERSION3
254
296
255 if self.TLS_KIND == 'START_TLS':
297 if self.TLS_KIND == 'START_TLS':
256 ldap_conn.start_tls_s()
298 ldap_conn.start_tls_s()
257
299
258 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
300 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
259 log.debug('Trying simple_bind with password and given login DN: %s',
301 log.debug('Trying simple_bind with password and given login DN: %s',
260 self.LDAP_BIND_DN)
302 self.LDAP_BIND_DN)
261 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
303 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
262
304
263 return ldap_conn
305 return ldap_conn
264
306
265 def get_uid(self, username):
307 def get_uid(self, username):
266 uid = username
308 uid = username
267 for server_addr in self.SERVER_ADDRESSES:
309 for server_addr in self.SERVER_ADDRESSES:
268 uid = chop_at(username, "@%s" % server_addr)
310 uid = chop_at(username, "@%s" % server_addr)
269 return uid
311 return uid
270
312
271 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
313 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
272 try:
314 try:
273 log.debug('Trying simple bind with %s', dn)
315 log.debug('Trying simple bind with %s', dn)
274 server.simple_bind_s(dn, safe_str(password))
316 server.simple_bind_s(dn, safe_str(password))
275 user = server.search_ext_s(
317 user = server.search_ext_s(
276 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
318 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
277 _, attrs = user
319 _, attrs = user
278 return attrs
320 return attrs
279
321
280 except ldap.INVALID_CREDENTIALS:
322 except ldap.INVALID_CREDENTIALS:
281 log.debug(
323 log.debug(
282 "LDAP rejected password for user '%s': %s, org_exc:",
324 "LDAP rejected password for user '%s': %s, org_exc:",
283 username, dn, exc_info=True)
325 username, dn, exc_info=True)
284
326
285 def authenticate_ldap(self, username, password):
327 def authenticate_ldap(self, username, password):
286 """
328 """
287 Authenticate a user via LDAP and return his/her LDAP properties.
329 Authenticate a user via LDAP and return his/her LDAP properties.
288
330
289 Raises AuthenticationError if the credentials are rejected, or
331 Raises AuthenticationError if the credentials are rejected, or
290 EnvironmentError if the LDAP server can't be reached.
332 EnvironmentError if the LDAP server can't be reached.
291
333
292 :param username: username
334 :param username: username
293 :param password: password
335 :param password: password
294 """
336 """
295
337
296 uid = self.get_uid(username)
338 uid = self.get_uid(username)
297
339
298 if not password:
340 if not password:
299 msg = "Authenticating user %s with blank password not allowed"
341 msg = "Authenticating user %s with blank password not allowed"
300 log.warning(msg, username)
342 log.warning(msg, username)
301 raise LdapPasswordError(msg)
343 raise LdapPasswordError(msg)
302 if "," in username:
344 if "," in username:
303 raise LdapUsernameError(
345 raise LdapUsernameError(
304 "invalid character `,` in username: `{}`".format(username))
346 "invalid character `,` in username: `{}`".format(username))
305 ldap_conn = None
347 ldap_conn = None
306 try:
348 try:
307 ldap_conn = self._get_ldap_conn()
349 ldap_conn = self._get_ldap_conn()
308 filter_ = '(&%s(%s=%s))' % (
350 filter_ = '(&%s(%s=%s))' % (
309 self.LDAP_FILTER, self.attr_login, username)
351 self.LDAP_FILTER, self.attr_login, username)
310 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
352 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
311 filter_, self.LDAP_SERVER)
353 filter_, self.LDAP_SERVER)
312 lobjects = ldap_conn.search_ext_s(
354 lobjects = ldap_conn.search_ext_s(
313 self.BASE_DN, self.SEARCH_SCOPE, filter_)
355 self.BASE_DN, self.SEARCH_SCOPE, filter_)
314
356
315 if not lobjects:
357 if not lobjects:
316 log.debug("No matching LDAP objects for authentication "
358 log.debug("No matching LDAP objects for authentication "
317 "of UID:'%s' username:(%s)", uid, username)
359 "of UID:'%s' username:(%s)", uid, username)
318 raise ldap.NO_SUCH_OBJECT()
360 raise ldap.NO_SUCH_OBJECT()
319
361
320 log.debug('Found matching ldap object, trying to authenticate')
362 log.debug('Found matching ldap object, trying to authenticate')
321 for (dn, _attrs) in lobjects:
363 for (dn, _attrs) in lobjects:
322 if dn is None:
364 if dn is None:
323 continue
365 continue
324
366
325 user_attrs = self.fetch_attrs_from_simple_bind(
367 user_attrs = self.fetch_attrs_from_simple_bind(
326 ldap_conn, dn, username, password)
368 ldap_conn, dn, username, password)
327 if user_attrs:
369 if user_attrs:
328 break
370 break
329
371
330 else:
372 else:
331 raise LdapPasswordError(
373 raise LdapPasswordError(
332 'Failed to authenticate user `{}`'
374 'Failed to authenticate user `{}`'
333 'with given password'.format(username))
375 'with given password'.format(username))
334
376
335 except ldap.NO_SUCH_OBJECT:
377 except ldap.NO_SUCH_OBJECT:
336 log.debug("LDAP says no such user '%s' (%s), org_exc:",
378 log.debug("LDAP says no such user '%s' (%s), org_exc:",
337 uid, username, exc_info=True)
379 uid, username, exc_info=True)
338 raise LdapUsernameError('Unable to find user')
380 raise LdapUsernameError('Unable to find user')
339 except ldap.SERVER_DOWN:
381 except ldap.SERVER_DOWN:
340 org_exc = traceback.format_exc()
382 org_exc = traceback.format_exc()
341 raise LdapConnectionError(
383 raise LdapConnectionError(
342 "LDAP can't access authentication "
384 "LDAP can't access authentication "
343 "server, org_exc:%s" % org_exc)
385 "server, org_exc:%s" % org_exc)
344 finally:
386 finally:
345 if ldap_conn:
387 if ldap_conn:
346 log.debug('ldap: connection release')
388 log.debug('ldap: connection release')
347 try:
389 try:
348 ldap_conn.unbind_s()
390 ldap_conn.unbind_s()
349 except Exception:
391 except Exception:
350 # for any reason this can raise exception we must catch it
392 # for any reason this can raise exception we must catch it
351 # to not crush the server
393 # to not crush the server
352 pass
394 pass
353
395
354 return dn, user_attrs
396 return dn, user_attrs
355
397
356
398
357 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
399 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
358 # used to define dynamic binding in the
400 # used to define dynamic binding in the
359 DYNAMIC_BIND_VAR = '$login'
401 DYNAMIC_BIND_VAR = '$login'
360 _settings_unsafe_keys = ['dn_pass']
402 _settings_unsafe_keys = ['dn_pass']
361
403
362 def includeme(self, config):
404 def includeme(self, config):
363 config.add_authn_plugin(self)
405 config.add_authn_plugin(self)
364 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
406 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
365 config.add_view(
407 config.add_view(
366 'rhodecode.authentication.views.AuthnPluginViewBase',
408 'rhodecode.authentication.views.AuthnPluginViewBase',
367 attr='settings_get',
409 attr='settings_get',
368 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
410 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
369 request_method='GET',
411 request_method='GET',
370 route_name='auth_home',
412 route_name='auth_home',
371 context=LdapAuthnResource)
413 context=LdapAuthnResource)
372 config.add_view(
414 config.add_view(
373 'rhodecode.authentication.views.AuthnPluginViewBase',
415 'rhodecode.authentication.views.AuthnPluginViewBase',
374 attr='settings_post',
416 attr='settings_post',
375 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
417 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
376 request_method='POST',
418 request_method='POST',
377 route_name='auth_home',
419 route_name='auth_home',
378 context=LdapAuthnResource)
420 context=LdapAuthnResource)
379
421
380 def get_settings_schema(self):
422 def get_settings_schema(self):
381 return LdapSettingsSchema()
423 return LdapSettingsSchema()
382
424
383 def get_display_name(self):
425 def get_display_name(self):
384 return _('LDAP')
426 return _('LDAP')
385
427
386 @hybrid_property
428 @hybrid_property
387 def name(self):
429 def name(self):
388 return "ldap"
430 return "ldap"
389
431
390 def use_fake_password(self):
432 def use_fake_password(self):
391 return True
433 return True
392
434
393 def user_activation_state(self):
435 def user_activation_state(self):
394 def_user_perms = User.get_default_user().AuthUser().permissions['global']
436 def_user_perms = User.get_default_user().AuthUser().permissions['global']
395 return 'hg.extern_activate.auto' in def_user_perms
437 return 'hg.extern_activate.auto' in def_user_perms
396
438
397 def try_dynamic_binding(self, username, password, current_args):
439 def try_dynamic_binding(self, username, password, current_args):
398 """
440 """
399 Detects marker inside our original bind, and uses dynamic auth if
441 Detects marker inside our original bind, and uses dynamic auth if
400 present
442 present
401 """
443 """
402
444
403 org_bind = current_args['bind_dn']
445 org_bind = current_args['bind_dn']
404 passwd = current_args['bind_pass']
446 passwd = current_args['bind_pass']
405
447
406 def has_bind_marker(username):
448 def has_bind_marker(username):
407 if self.DYNAMIC_BIND_VAR in username:
449 if self.DYNAMIC_BIND_VAR in username:
408 return True
450 return True
409
451
410 # we only passed in user with "special" variable
452 # we only passed in user with "special" variable
411 if org_bind and has_bind_marker(org_bind) and not passwd:
453 if org_bind and has_bind_marker(org_bind) and not passwd:
412 log.debug('Using dynamic user/password binding for ldap '
454 log.debug('Using dynamic user/password binding for ldap '
413 'authentication. Replacing `%s` with username',
455 'authentication. Replacing `%s` with username',
414 self.DYNAMIC_BIND_VAR)
456 self.DYNAMIC_BIND_VAR)
415 current_args['bind_dn'] = org_bind.replace(
457 current_args['bind_dn'] = org_bind.replace(
416 self.DYNAMIC_BIND_VAR, username)
458 self.DYNAMIC_BIND_VAR, username)
417 current_args['bind_pass'] = password
459 current_args['bind_pass'] = password
418
460
419 return current_args
461 return current_args
420
462
421 def auth(self, userobj, username, password, settings, **kwargs):
463 def auth(self, userobj, username, password, settings, **kwargs):
422 """
464 """
423 Given a user object (which may be null), username, a plaintext password,
465 Given a user object (which may be null), username, a plaintext password,
424 and a settings object (containing all the keys needed as listed in
466 and a settings object (containing all the keys needed as listed in
425 settings()), authenticate this user's login attempt.
467 settings()), authenticate this user's login attempt.
426
468
427 Return None on failure. On success, return a dictionary of the form:
469 Return None on failure. On success, return a dictionary of the form:
428
470
429 see: RhodeCodeAuthPluginBase.auth_func_attrs
471 see: RhodeCodeAuthPluginBase.auth_func_attrs
430 This is later validated for correctness
472 This is later validated for correctness
431 """
473 """
432
474
433 if not username or not password:
475 if not username or not password:
434 log.debug('Empty username or password skipping...')
476 log.debug('Empty username or password skipping...')
435 return None
477 return None
436
478
437 ldap_args = {
479 ldap_args = {
438 'server': settings.get('host', ''),
480 'server': settings.get('host', ''),
439 'base_dn': settings.get('base_dn', ''),
481 'base_dn': settings.get('base_dn', ''),
440 'port': settings.get('port'),
482 'port': settings.get('port'),
441 'bind_dn': settings.get('dn_user'),
483 'bind_dn': settings.get('dn_user'),
442 'bind_pass': settings.get('dn_pass'),
484 'bind_pass': settings.get('dn_pass'),
443 'tls_kind': settings.get('tls_kind'),
485 'tls_kind': settings.get('tls_kind'),
444 'tls_reqcert': settings.get('tls_reqcert'),
486 'tls_reqcert': settings.get('tls_reqcert'),
445 'search_scope': settings.get('search_scope'),
487 'search_scope': settings.get('search_scope'),
446 'attr_login': settings.get('attr_login'),
488 'attr_login': settings.get('attr_login'),
447 'ldap_version': 3,
489 'ldap_version': 3,
448 'ldap_filter': settings.get('filter'),
490 'ldap_filter': settings.get('filter'),
491 'timeout': settings.get('timeout')
449 }
492 }
450
493
451 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
494 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
452
495
453 log.debug('Checking for ldap authentication.')
496 log.debug('Checking for ldap authentication.')
454
497
455 try:
498 try:
456 aldap = AuthLdap(**ldap_args)
499 aldap = AuthLdap(**ldap_args)
457 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
500 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
458 log.debug('Got ldap DN response %s', user_dn)
501 log.debug('Got ldap DN response %s', user_dn)
459
502
460 def get_ldap_attr(k):
503 def get_ldap_attr(k):
461 return ldap_attrs.get(settings.get(k), [''])[0]
504 return ldap_attrs.get(settings.get(k), [''])[0]
462
505
463 # old attrs fetched from RhodeCode database
506 # old attrs fetched from RhodeCode database
464 admin = getattr(userobj, 'admin', False)
507 admin = getattr(userobj, 'admin', False)
465 active = getattr(userobj, 'active', True)
508 active = getattr(userobj, 'active', True)
466 email = getattr(userobj, 'email', '')
509 email = getattr(userobj, 'email', '')
467 username = getattr(userobj, 'username', username)
510 username = getattr(userobj, 'username', username)
468 firstname = getattr(userobj, 'firstname', '')
511 firstname = getattr(userobj, 'firstname', '')
469 lastname = getattr(userobj, 'lastname', '')
512 lastname = getattr(userobj, 'lastname', '')
470 extern_type = getattr(userobj, 'extern_type', '')
513 extern_type = getattr(userobj, 'extern_type', '')
471
514
472 groups = []
515 groups = []
473 user_attrs = {
516 user_attrs = {
474 'username': username,
517 'username': username,
475 'firstname': safe_unicode(
518 'firstname': safe_unicode(
476 get_ldap_attr('attr_firstname') or firstname),
519 get_ldap_attr('attr_firstname') or firstname),
477 'lastname': safe_unicode(
520 'lastname': safe_unicode(
478 get_ldap_attr('attr_lastname') or lastname),
521 get_ldap_attr('attr_lastname') or lastname),
479 'groups': groups,
522 'groups': groups,
480 'user_group_sync': False,
523 'user_group_sync': False,
481 'email': get_ldap_attr('attr_email') or email,
524 'email': get_ldap_attr('attr_email') or email,
482 'admin': admin,
525 'admin': admin,
483 'active': active,
526 'active': active,
484 'active_from_extern': None,
527 'active_from_extern': None,
485 'extern_name': user_dn,
528 'extern_name': user_dn,
486 'extern_type': extern_type,
529 'extern_type': extern_type,
487 }
530 }
488 log.debug('ldap user: %s', user_attrs)
531 log.debug('ldap user: %s', user_attrs)
489 log.info('user `%s` authenticated correctly', user_attrs['username'])
532 log.info('user `%s` authenticated correctly', user_attrs['username'])
490
533
491 return user_attrs
534 return user_attrs
492
535
493 except (LdapUsernameError, LdapPasswordError, LdapImportError):
536 except (LdapUsernameError, LdapPasswordError, LdapImportError):
494 log.exception("LDAP related exception")
537 log.exception("LDAP related exception")
495 return None
538 return None
496 except (Exception,):
539 except (Exception,):
497 log.exception("Other exception")
540 log.exception("Other exception")
498 return None
541 return None
542
General Comments 0
You need to be logged in to leave comments. Login now