##// END OF EJS Templates
ldap: adde common ldap-ce na ldap-ee structure, and extend options...
marcink -
r3235:db5132ef default
parent child Browse files
Show More
@@ -1,202 +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 'timeout': 3600,
90 'tls_kind': 'PLAIN',
90 'tls_kind': 'PLAIN',
91 'tls_reqcert': 'NEVER',
91 'tls_reqcert': 'NEVER',
92
92 'tls_cert_dir':'/etc/openldap/cacerts',
93 'dn_user': 'test_user',
93 'dn_user': 'test_user',
94 'dn_pass': 'test_pass',
94 'dn_pass': 'test_pass',
95 'base_dn': 'test_base_dn',
95 'base_dn': 'test_base_dn',
96 'search_scope': 'BASE',
96 'search_scope': 'BASE',
97 'attr_login': 'test_attr_login',
97 'attr_login': 'test_attr_login',
98 'attr_firstname': 'ima',
98 'attr_firstname': 'ima',
99 'attr_lastname': 'tester',
99 'attr_lastname': 'tester',
100 'attr_email': 'test@example.com',
100 'attr_email': 'test@example.com',
101 'cache_ttl': '0',
101 'cache_ttl': '0',
102 })
102 })
103 if force:
103 if force:
104 params = {}
104 params = {}
105 params.update(override or {})
105 params.update(override or {})
106
106
107 test_url = '/_admin/auth/ldap/'
107 test_url = '/_admin/auth/ldap/'
108
108
109 response = self.app.post(url=test_url, params=params)
109 response = self.app.post(url=test_url, params=params)
110 return response
110 return response
111
111
112 def test_index(self):
112 def test_index(self):
113 response = self.app.get('/_admin/auth')
113 response = self.app.get('/_admin/auth')
114 response.mustcontain('Authentication Plugins')
114 response.mustcontain('Authentication Plugins')
115
115
116 @pytest.mark.parametrize("disable_plugin, needs_import", [
116 @pytest.mark.parametrize("disable_plugin, needs_import", [
117 ('egg:rhodecode-enterprise-ce#headers', None),
117 ('egg:rhodecode-enterprise-ce#headers', None),
118 ('egg:rhodecode-enterprise-ce#crowd', None),
118 ('egg:rhodecode-enterprise-ce#crowd', None),
119 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
119 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
120 ('egg:rhodecode-enterprise-ce#ldap', None),
120 ('egg:rhodecode-enterprise-ce#ldap', None),
121 ('egg:rhodecode-enterprise-ce#pam', "pam"),
121 ('egg:rhodecode-enterprise-ce#pam', "pam"),
122 ])
122 ])
123 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
123 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
124 # TODO: johbo: "pam" is currently not available on darwin,
124 # TODO: johbo: "pam" is currently not available on darwin,
125 # although the docs state that it should work on darwin.
125 # although the docs state that it should work on darwin.
126 if needs_import:
126 if needs_import:
127 pytest.importorskip(needs_import)
127 pytest.importorskip(needs_import)
128
128
129 self._enable_plugins(
129 self._enable_plugins(
130 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
130 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
131 csrf_token, verify_response=True)
131 csrf_token, verify_response=True)
132
132
133 self._enable_plugins(
133 self._enable_plugins(
134 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
134 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
135 verify_response=True)
135 verify_response=True)
136
136
137 def test_ldap_save_settings(self, csrf_token):
137 def test_ldap_save_settings(self, csrf_token):
138 params = self._enable_plugins(
138 params = self._enable_plugins(
139 'egg:rhodecode-enterprise-ce#rhodecode,'
139 'egg:rhodecode-enterprise-ce#rhodecode,'
140 'egg:rhodecode-enterprise-ce#ldap',
140 'egg:rhodecode-enterprise-ce#ldap',
141 csrf_token)
141 csrf_token)
142 response = self._post_ldap_settings(params)
142 response = self._post_ldap_settings(params)
143 assert_auth_settings_updated(response)
143 assert_auth_settings_updated(response)
144
144
145 new_settings = SettingsModel().get_auth_settings()
145 new_settings = SettingsModel().get_auth_settings()
146 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
146 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
147 'fail db write compare'
147 'fail db write compare'
148
148
149 def test_ldap_error_form_wrong_port_number(self, csrf_token):
149 def test_ldap_error_form_wrong_port_number(self, csrf_token):
150 params = self._enable_plugins(
150 params = self._enable_plugins(
151 'egg:rhodecode-enterprise-ce#rhodecode,'
151 'egg:rhodecode-enterprise-ce#rhodecode,'
152 'egg:rhodecode-enterprise-ce#ldap',
152 'egg:rhodecode-enterprise-ce#ldap',
153 csrf_token)
153 csrf_token)
154 invalid_port_value = 'invalid-port-number'
154 invalid_port_value = 'invalid-port-number'
155 response = self._post_ldap_settings(params, override={
155 response = self._post_ldap_settings(params, override={
156 'port': invalid_port_value,
156 'port': invalid_port_value,
157 })
157 })
158 assertr = AssertResponse(response)
158 assertr = AssertResponse(response)
159 assertr.element_contains(
159 assertr.element_contains(
160 '.form .field #port ~ .error-message',
160 '.form .field #port ~ .error-message',
161 invalid_port_value)
161 invalid_port_value)
162
162
163 def test_ldap_error_form(self, csrf_token):
163 def test_ldap_error_form(self, csrf_token):
164 params = self._enable_plugins(
164 params = self._enable_plugins(
165 'egg:rhodecode-enterprise-ce#rhodecode,'
165 'egg:rhodecode-enterprise-ce#rhodecode,'
166 'egg:rhodecode-enterprise-ce#ldap',
166 'egg:rhodecode-enterprise-ce#ldap',
167 csrf_token)
167 csrf_token)
168 response = self._post_ldap_settings(params, override={
168 response = self._post_ldap_settings(params, override={
169 'attr_login': '',
169 'attr_login': '',
170 })
170 })
171 response.mustcontain("""<span class="error-message">The LDAP Login"""
171 response.mustcontain("""<span class="error-message">The LDAP Login"""
172 """ attribute of the CN must be specified""")
172 """ attribute of the CN must be specified""")
173
173
174 def test_post_ldap_group_settings(self, csrf_token):
174 def test_post_ldap_group_settings(self, csrf_token):
175 params = self._enable_plugins(
175 params = self._enable_plugins(
176 'egg:rhodecode-enterprise-ce#rhodecode,'
176 'egg:rhodecode-enterprise-ce#rhodecode,'
177 'egg:rhodecode-enterprise-ce#ldap',
177 'egg:rhodecode-enterprise-ce#ldap',
178 csrf_token)
178 csrf_token)
179
179
180 response = self._post_ldap_settings(params, override={
180 response = self._post_ldap_settings(params, override={
181 'host': 'dc-legacy.example.com',
181 'host': 'dc-legacy.example.com',
182 'port': '999',
182 'port': '999',
183 'tls_kind': 'PLAIN',
183 'tls_kind': 'PLAIN',
184 'tls_reqcert': 'NEVER',
184 'tls_reqcert': 'NEVER',
185 'dn_user': 'test_user',
185 'dn_user': 'test_user',
186 'dn_pass': 'test_pass',
186 'dn_pass': 'test_pass',
187 'base_dn': 'test_base_dn',
187 'base_dn': 'test_base_dn',
188 'filter': 'test_filter',
188 'filter': 'test_filter',
189 'search_scope': 'BASE',
189 'search_scope': 'BASE',
190 'attr_login': 'test_attr_login',
190 'attr_login': 'test_attr_login',
191 'attr_firstname': 'ima',
191 'attr_firstname': 'ima',
192 'attr_lastname': 'tester',
192 'attr_lastname': 'tester',
193 'attr_email': 'test@example.com',
193 'attr_email': 'test@example.com',
194 'cache_ttl': '60',
194 'cache_ttl': '60',
195 'csrf_token': csrf_token,
195 'csrf_token': csrf_token,
196 }
196 }
197 )
197 )
198 assert_auth_settings_updated(response)
198 assert_auth_settings_updated(response)
199
199
200 new_settings = SettingsModel().get_auth_settings()
200 new_settings = SettingsModel().get_auth_settings()
201 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
201 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
202 'fail db write compare'
202 'fail db write compare'
@@ -1,777 +1,792 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 Authentication modules
22 Authentication modules
23 """
23 """
24 import socket
24 import socket
25 import string
25 import string
26 import colander
26 import colander
27 import copy
27 import copy
28 import logging
28 import logging
29 import time
29 import time
30 import traceback
30 import traceback
31 import warnings
31 import warnings
32 import functools
32 import functools
33
33
34 from pyramid.threadlocal import get_current_registry
34 from pyramid.threadlocal import get_current_registry
35
35
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.lib import rc_cache
38 from rhodecode.lib import rc_cache
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.utils2 import safe_int, safe_str
40 from rhodecode.lib.utils2 import safe_int, safe_str
41 from rhodecode.lib.exceptions import LdapConnectionError
41 from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, \
42 LdapPasswordError
42 from rhodecode.model.db import User
43 from rhodecode.model.db import User
43 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.user import UserModel
46 from rhodecode.model.user import UserModel
46 from rhodecode.model.user_group import UserGroupModel
47 from rhodecode.model.user_group import UserGroupModel
47
48
48
49
49 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
50
51
51 # auth types that authenticate() function can receive
52 # auth types that authenticate() function can receive
52 VCS_TYPE = 'vcs'
53 VCS_TYPE = 'vcs'
53 HTTP_TYPE = 'http'
54 HTTP_TYPE = 'http'
54
55
55
56
56 class hybrid_property(object):
57 class hybrid_property(object):
57 """
58 """
58 a property decorator that works both for instance and class
59 a property decorator that works both for instance and class
59 """
60 """
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 self.fget = fget
62 self.fget = fget
62 self.fset = fset
63 self.fset = fset
63 self.fdel = fdel
64 self.fdel = fdel
64 self.expr = expr or fget
65 self.expr = expr or fget
65 functools.update_wrapper(self, fget)
66 functools.update_wrapper(self, fget)
66
67
67 def __get__(self, instance, owner):
68 def __get__(self, instance, owner):
68 if instance is None:
69 if instance is None:
69 return self.expr(owner)
70 return self.expr(owner)
70 else:
71 else:
71 return self.fget(instance)
72 return self.fget(instance)
72
73
73 def __set__(self, instance, value):
74 def __set__(self, instance, value):
74 self.fset(instance, value)
75 self.fset(instance, value)
75
76
76 def __delete__(self, instance):
77 def __delete__(self, instance):
77 self.fdel(instance)
78 self.fdel(instance)
78
79
79
80
80 class LazyFormencode(object):
81 class LazyFormencode(object):
81 def __init__(self, formencode_obj, *args, **kwargs):
82 def __init__(self, formencode_obj, *args, **kwargs):
82 self.formencode_obj = formencode_obj
83 self.formencode_obj = formencode_obj
83 self.args = args
84 self.args = args
84 self.kwargs = kwargs
85 self.kwargs = kwargs
85
86
86 def __call__(self, *args, **kwargs):
87 def __call__(self, *args, **kwargs):
87 from inspect import isfunction
88 from inspect import isfunction
88 formencode_obj = self.formencode_obj
89 formencode_obj = self.formencode_obj
89 if isfunction(formencode_obj):
90 if isfunction(formencode_obj):
90 # case we wrap validators into functions
91 # case we wrap validators into functions
91 formencode_obj = self.formencode_obj(*args, **kwargs)
92 formencode_obj = self.formencode_obj(*args, **kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
93 return formencode_obj(*self.args, **self.kwargs)
93
94
94
95
95 class RhodeCodeAuthPluginBase(object):
96 class RhodeCodeAuthPluginBase(object):
96 # cache the authentication request for N amount of seconds. Some kind
97 # cache the authentication request for N amount of seconds. Some kind
97 # of authentication methods are very heavy and it's very efficient to cache
98 # of authentication methods are very heavy and it's very efficient to cache
98 # the result of a call. If it's set to None (default) cache is off
99 # the result of a call. If it's set to None (default) cache is off
99 AUTH_CACHE_TTL = None
100 AUTH_CACHE_TTL = None
100 AUTH_CACHE = {}
101 AUTH_CACHE = {}
101
102
102 auth_func_attrs = {
103 auth_func_attrs = {
103 "username": "unique username",
104 "username": "unique username",
104 "firstname": "first name",
105 "firstname": "first name",
105 "lastname": "last name",
106 "lastname": "last name",
106 "email": "email address",
107 "email": "email address",
107 "groups": '["list", "of", "groups"]',
108 "groups": '["list", "of", "groups"]',
108 "user_group_sync":
109 "user_group_sync":
109 'True|False defines if returned user groups should be synced',
110 'True|False defines if returned user groups should be synced',
110 "extern_name": "name in external source of record",
111 "extern_name": "name in external source of record",
111 "extern_type": "type of external source of record",
112 "extern_type": "type of external source of record",
112 "admin": 'True|False defines if user should be RhodeCode super admin',
113 "admin": 'True|False defines if user should be RhodeCode super admin',
113 "active":
114 "active":
114 'True|False defines active state of user internally for RhodeCode',
115 'True|False defines active state of user internally for RhodeCode',
115 "active_from_extern":
116 "active_from_extern":
116 "True|False\None, active state from the external auth, "
117 "True|False\None, active state from the external auth, "
117 "None means use definition from RhodeCode extern_type active value"
118 "None means use definition from RhodeCode extern_type active value"
118
119
119 }
120 }
120 # set on authenticate() method and via set_auth_type func.
121 # set on authenticate() method and via set_auth_type func.
121 auth_type = None
122 auth_type = None
122
123
123 # set on authenticate() method and via set_calling_scope_repo, this is a
124 # set on authenticate() method and via set_calling_scope_repo, this is a
124 # calling scope repository when doing authentication most likely on VCS
125 # calling scope repository when doing authentication most likely on VCS
125 # operations
126 # operations
126 acl_repo_name = None
127 acl_repo_name = None
127
128
128 # List of setting names to store encrypted. Plugins may override this list
129 # List of setting names to store encrypted. Plugins may override this list
129 # to store settings encrypted.
130 # to store settings encrypted.
130 _settings_encrypted = []
131 _settings_encrypted = []
131
132
132 # Mapping of python to DB settings model types. Plugins may override or
133 # Mapping of python to DB settings model types. Plugins may override or
133 # extend this mapping.
134 # extend this mapping.
134 _settings_type_map = {
135 _settings_type_map = {
135 colander.String: 'unicode',
136 colander.String: 'unicode',
136 colander.Integer: 'int',
137 colander.Integer: 'int',
137 colander.Boolean: 'bool',
138 colander.Boolean: 'bool',
138 colander.List: 'list',
139 colander.List: 'list',
139 }
140 }
140
141
141 # list of keys in settings that are unsafe to be logged, should be passwords
142 # list of keys in settings that are unsafe to be logged, should be passwords
142 # or other crucial credentials
143 # or other crucial credentials
143 _settings_unsafe_keys = []
144 _settings_unsafe_keys = []
144
145
145 def __init__(self, plugin_id):
146 def __init__(self, plugin_id):
146 self._plugin_id = plugin_id
147 self._plugin_id = plugin_id
147
148
148 def __str__(self):
149 def __str__(self):
149 return self.get_id()
150 return self.get_id()
150
151
151 def _get_setting_full_name(self, name):
152 def _get_setting_full_name(self, name):
152 """
153 """
153 Return the full setting name used for storing values in the database.
154 Return the full setting name used for storing values in the database.
154 """
155 """
155 # TODO: johbo: Using the name here is problematic. It would be good to
156 # TODO: johbo: Using the name here is problematic. It would be good to
156 # introduce either new models in the database to hold Plugin and
157 # introduce either new models in the database to hold Plugin and
157 # PluginSetting or to use the plugin id here.
158 # PluginSetting or to use the plugin id here.
158 return 'auth_{}_{}'.format(self.name, name)
159 return 'auth_{}_{}'.format(self.name, name)
159
160
160 def _get_setting_type(self, name):
161 def _get_setting_type(self, name):
161 """
162 """
162 Return the type of a setting. This type is defined by the SettingsModel
163 Return the type of a setting. This type is defined by the SettingsModel
163 and determines how the setting is stored in DB. Optionally the suffix
164 and determines how the setting is stored in DB. Optionally the suffix
164 `.encrypted` is appended to instruct SettingsModel to store it
165 `.encrypted` is appended to instruct SettingsModel to store it
165 encrypted.
166 encrypted.
166 """
167 """
167 schema_node = self.get_settings_schema().get(name)
168 schema_node = self.get_settings_schema().get(name)
168 db_type = self._settings_type_map.get(
169 db_type = self._settings_type_map.get(
169 type(schema_node.typ), 'unicode')
170 type(schema_node.typ), 'unicode')
170 if name in self._settings_encrypted:
171 if name in self._settings_encrypted:
171 db_type = '{}.encrypted'.format(db_type)
172 db_type = '{}.encrypted'.format(db_type)
172 return db_type
173 return db_type
173
174
174 @classmethod
175 @classmethod
175 def docs(cls):
176 def docs(cls):
176 """
177 """
177 Defines documentation url which helps with plugin setup
178 Defines documentation url which helps with plugin setup
178 """
179 """
179 return ''
180 return ''
180
181
181 @classmethod
182 @classmethod
182 def icon(cls):
183 def icon(cls):
183 """
184 """
184 Defines ICON in SVG format for authentication method
185 Defines ICON in SVG format for authentication method
185 """
186 """
186 return ''
187 return ''
187
188
188 def is_enabled(self):
189 def is_enabled(self):
189 """
190 """
190 Returns true if this plugin is enabled. An enabled plugin can be
191 Returns true if this plugin is enabled. An enabled plugin can be
191 configured in the admin interface but it is not consulted during
192 configured in the admin interface but it is not consulted during
192 authentication.
193 authentication.
193 """
194 """
194 auth_plugins = SettingsModel().get_auth_plugins()
195 auth_plugins = SettingsModel().get_auth_plugins()
195 return self.get_id() in auth_plugins
196 return self.get_id() in auth_plugins
196
197
197 def is_active(self, plugin_cached_settings=None):
198 def is_active(self, plugin_cached_settings=None):
198 """
199 """
199 Returns true if the plugin is activated. An activated plugin is
200 Returns true if the plugin is activated. An activated plugin is
200 consulted during authentication, assumed it is also enabled.
201 consulted during authentication, assumed it is also enabled.
201 """
202 """
202 return self.get_setting_by_name(
203 return self.get_setting_by_name(
203 'enabled', plugin_cached_settings=plugin_cached_settings)
204 'enabled', plugin_cached_settings=plugin_cached_settings)
204
205
205 def get_id(self):
206 def get_id(self):
206 """
207 """
207 Returns the plugin id.
208 Returns the plugin id.
208 """
209 """
209 return self._plugin_id
210 return self._plugin_id
210
211
211 def get_display_name(self):
212 def get_display_name(self):
212 """
213 """
213 Returns a translation string for displaying purposes.
214 Returns a translation string for displaying purposes.
214 """
215 """
215 raise NotImplementedError('Not implemented in base class')
216 raise NotImplementedError('Not implemented in base class')
216
217
217 def get_settings_schema(self):
218 def get_settings_schema(self):
218 """
219 """
219 Returns a colander schema, representing the plugin settings.
220 Returns a colander schema, representing the plugin settings.
220 """
221 """
221 return AuthnPluginSettingsSchemaBase()
222 return AuthnPluginSettingsSchemaBase()
222
223
223 def get_settings(self):
224 def get_settings(self):
224 """
225 """
225 Returns the plugin settings as dictionary.
226 Returns the plugin settings as dictionary.
226 """
227 """
227 settings = {}
228 settings = {}
228 raw_settings = SettingsModel().get_all_settings()
229 raw_settings = SettingsModel().get_all_settings()
229 for node in self.get_settings_schema():
230 for node in self.get_settings_schema():
230 settings[node.name] = self.get_setting_by_name(
231 settings[node.name] = self.get_setting_by_name(
231 node.name, plugin_cached_settings=raw_settings)
232 node.name, plugin_cached_settings=raw_settings)
232 return settings
233 return settings
233
234
234 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
235 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
235 """
236 """
236 Returns a plugin setting by name.
237 Returns a plugin setting by name.
237 """
238 """
238 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
239 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
239 if plugin_cached_settings:
240 if plugin_cached_settings:
240 plugin_settings = plugin_cached_settings
241 plugin_settings = plugin_cached_settings
241 else:
242 else:
242 plugin_settings = SettingsModel().get_all_settings()
243 plugin_settings = SettingsModel().get_all_settings()
243
244
244 if full_name in plugin_settings:
245 if full_name in plugin_settings:
245 return plugin_settings[full_name]
246 return plugin_settings[full_name]
246 else:
247 else:
247 return default
248 return default
248
249
249 def create_or_update_setting(self, name, value):
250 def create_or_update_setting(self, name, value):
250 """
251 """
251 Create or update a setting for this plugin in the persistent storage.
252 Create or update a setting for this plugin in the persistent storage.
252 """
253 """
253 full_name = self._get_setting_full_name(name)
254 full_name = self._get_setting_full_name(name)
254 type_ = self._get_setting_type(name)
255 type_ = self._get_setting_type(name)
255 db_setting = SettingsModel().create_or_update_setting(
256 db_setting = SettingsModel().create_or_update_setting(
256 full_name, value, type_)
257 full_name, value, type_)
257 return db_setting.app_settings_value
258 return db_setting.app_settings_value
258
259
259 def log_safe_settings(self, settings):
260 def log_safe_settings(self, settings):
260 """
261 """
261 returns a log safe representation of settings, without any secrets
262 returns a log safe representation of settings, without any secrets
262 """
263 """
263 settings_copy = copy.deepcopy(settings)
264 settings_copy = copy.deepcopy(settings)
264 for k in self._settings_unsafe_keys:
265 for k in self._settings_unsafe_keys:
265 if k in settings_copy:
266 if k in settings_copy:
266 del settings_copy[k]
267 del settings_copy[k]
267 return settings_copy
268 return settings_copy
268
269
269 @hybrid_property
270 @hybrid_property
270 def name(self):
271 def name(self):
271 """
272 """
272 Returns the name of this authentication plugin.
273 Returns the name of this authentication plugin.
273
274
274 :returns: string
275 :returns: string
275 """
276 """
276 raise NotImplementedError("Not implemented in base class")
277 raise NotImplementedError("Not implemented in base class")
277
278
278 def get_url_slug(self):
279 def get_url_slug(self):
279 """
280 """
280 Returns a slug which should be used when constructing URLs which refer
281 Returns a slug which should be used when constructing URLs which refer
281 to this plugin. By default it returns the plugin name. If the name is
282 to this plugin. By default it returns the plugin name. If the name is
282 not suitable for using it in an URL the plugin should override this
283 not suitable for using it in an URL the plugin should override this
283 method.
284 method.
284 """
285 """
285 return self.name
286 return self.name
286
287
287 @property
288 @property
288 def is_headers_auth(self):
289 def is_headers_auth(self):
289 """
290 """
290 Returns True if this authentication plugin uses HTTP headers as
291 Returns True if this authentication plugin uses HTTP headers as
291 authentication method.
292 authentication method.
292 """
293 """
293 return False
294 return False
294
295
295 @hybrid_property
296 @hybrid_property
296 def is_container_auth(self):
297 def is_container_auth(self):
297 """
298 """
298 Deprecated method that indicates if this authentication plugin uses
299 Deprecated method that indicates if this authentication plugin uses
299 HTTP headers as authentication method.
300 HTTP headers as authentication method.
300 """
301 """
301 warnings.warn(
302 warnings.warn(
302 'Use is_headers_auth instead.', category=DeprecationWarning)
303 'Use is_headers_auth instead.', category=DeprecationWarning)
303 return self.is_headers_auth
304 return self.is_headers_auth
304
305
305 @hybrid_property
306 @hybrid_property
306 def allows_creating_users(self):
307 def allows_creating_users(self):
307 """
308 """
308 Defines if Plugin allows users to be created on-the-fly when
309 Defines if Plugin allows users to be created on-the-fly when
309 authentication is called. Controls how external plugins should behave
310 authentication is called. Controls how external plugins should behave
310 in terms if they are allowed to create new users, or not. Base plugins
311 in terms if they are allowed to create new users, or not. Base plugins
311 should not be allowed to, but External ones should be !
312 should not be allowed to, but External ones should be !
312
313
313 :return: bool
314 :return: bool
314 """
315 """
315 return False
316 return False
316
317
317 def set_auth_type(self, auth_type):
318 def set_auth_type(self, auth_type):
318 self.auth_type = auth_type
319 self.auth_type = auth_type
319
320
320 def set_calling_scope_repo(self, acl_repo_name):
321 def set_calling_scope_repo(self, acl_repo_name):
321 self.acl_repo_name = acl_repo_name
322 self.acl_repo_name = acl_repo_name
322
323
323 def allows_authentication_from(
324 def allows_authentication_from(
324 self, user, allows_non_existing_user=True,
325 self, user, allows_non_existing_user=True,
325 allowed_auth_plugins=None, allowed_auth_sources=None):
326 allowed_auth_plugins=None, allowed_auth_sources=None):
326 """
327 """
327 Checks if this authentication module should accept a request for
328 Checks if this authentication module should accept a request for
328 the current user.
329 the current user.
329
330
330 :param user: user object fetched using plugin's get_user() method.
331 :param user: user object fetched using plugin's get_user() method.
331 :param allows_non_existing_user: if True, don't allow the
332 :param allows_non_existing_user: if True, don't allow the
332 user to be empty, meaning not existing in our database
333 user to be empty, meaning not existing in our database
333 :param allowed_auth_plugins: if provided, users extern_type will be
334 :param allowed_auth_plugins: if provided, users extern_type will be
334 checked against a list of provided extern types, which are plugin
335 checked against a list of provided extern types, which are plugin
335 auth_names in the end
336 auth_names in the end
336 :param allowed_auth_sources: authentication type allowed,
337 :param allowed_auth_sources: authentication type allowed,
337 `http` or `vcs` default is both.
338 `http` or `vcs` default is both.
338 defines if plugin will accept only http authentication vcs
339 defines if plugin will accept only http authentication vcs
339 authentication(git/hg) or both
340 authentication(git/hg) or both
340 :returns: boolean
341 :returns: boolean
341 """
342 """
342 if not user and not allows_non_existing_user:
343 if not user and not allows_non_existing_user:
343 log.debug('User is empty but plugin does not allow empty users,'
344 log.debug('User is empty but plugin does not allow empty users,'
344 'not allowed to authenticate')
345 'not allowed to authenticate')
345 return False
346 return False
346
347
347 expected_auth_plugins = allowed_auth_plugins or [self.name]
348 expected_auth_plugins = allowed_auth_plugins or [self.name]
348 if user and (user.extern_type and
349 if user and (user.extern_type and
349 user.extern_type not in expected_auth_plugins):
350 user.extern_type not in expected_auth_plugins):
350 log.debug(
351 log.debug(
351 'User `%s` is bound to `%s` auth type. Plugin allows only '
352 'User `%s` is bound to `%s` auth type. Plugin allows only '
352 '%s, skipping', user, user.extern_type, expected_auth_plugins)
353 '%s, skipping', user, user.extern_type, expected_auth_plugins)
353
354
354 return False
355 return False
355
356
356 # by default accept both
357 # by default accept both
357 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
358 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
358 if self.auth_type not in expected_auth_from:
359 if self.auth_type not in expected_auth_from:
359 log.debug('Current auth source is %s but plugin only allows %s',
360 log.debug('Current auth source is %s but plugin only allows %s',
360 self.auth_type, expected_auth_from)
361 self.auth_type, expected_auth_from)
361 return False
362 return False
362
363
363 return True
364 return True
364
365
365 def get_user(self, username=None, **kwargs):
366 def get_user(self, username=None, **kwargs):
366 """
367 """
367 Helper method for user fetching in plugins, by default it's using
368 Helper method for user fetching in plugins, by default it's using
368 simple fetch by username, but this method can be custimized in plugins
369 simple fetch by username, but this method can be custimized in plugins
369 eg. headers auth plugin to fetch user by environ params
370 eg. headers auth plugin to fetch user by environ params
370
371
371 :param username: username if given to fetch from database
372 :param username: username if given to fetch from database
372 :param kwargs: extra arguments needed for user fetching.
373 :param kwargs: extra arguments needed for user fetching.
373 """
374 """
374 user = None
375 user = None
375 log.debug(
376 log.debug(
376 'Trying to fetch user `%s` from RhodeCode database', username)
377 'Trying to fetch user `%s` from RhodeCode database', username)
377 if username:
378 if username:
378 user = User.get_by_username(username)
379 user = User.get_by_username(username)
379 if not user:
380 if not user:
380 log.debug('User not found, fallback to fetch user in '
381 log.debug('User not found, fallback to fetch user in '
381 'case insensitive mode')
382 'case insensitive mode')
382 user = User.get_by_username(username, case_insensitive=True)
383 user = User.get_by_username(username, case_insensitive=True)
383 else:
384 else:
384 log.debug('provided username:`%s` is empty skipping...', username)
385 log.debug('provided username:`%s` is empty skipping...', username)
385 if not user:
386 if not user:
386 log.debug('User `%s` not found in database', username)
387 log.debug('User `%s` not found in database', username)
387 else:
388 else:
388 log.debug('Got DB user:%s', user)
389 log.debug('Got DB user:%s', user)
389 return user
390 return user
390
391
391 def user_activation_state(self):
392 def user_activation_state(self):
392 """
393 """
393 Defines user activation state when creating new users
394 Defines user activation state when creating new users
394
395
395 :returns: boolean
396 :returns: boolean
396 """
397 """
397 raise NotImplementedError("Not implemented in base class")
398 raise NotImplementedError("Not implemented in base class")
398
399
399 def auth(self, userobj, username, passwd, settings, **kwargs):
400 def auth(self, userobj, username, passwd, settings, **kwargs):
400 """
401 """
401 Given a user object (which may be null), username, a plaintext
402 Given a user object (which may be null), username, a plaintext
402 password, and a settings object (containing all the keys needed as
403 password, and a settings object (containing all the keys needed as
403 listed in settings()), authenticate this user's login attempt.
404 listed in settings()), authenticate this user's login attempt.
404
405
405 Return None on failure. On success, return a dictionary of the form:
406 Return None on failure. On success, return a dictionary of the form:
406
407
407 see: RhodeCodeAuthPluginBase.auth_func_attrs
408 see: RhodeCodeAuthPluginBase.auth_func_attrs
408 This is later validated for correctness
409 This is later validated for correctness
409 """
410 """
410 raise NotImplementedError("not implemented in base class")
411 raise NotImplementedError("not implemented in base class")
411
412
412 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
413 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
413 """
414 """
414 Wrapper to call self.auth() that validates call on it
415 Wrapper to call self.auth() that validates call on it
415
416
416 :param userobj: userobj
417 :param userobj: userobj
417 :param username: username
418 :param username: username
418 :param passwd: plaintext password
419 :param passwd: plaintext password
419 :param settings: plugin settings
420 :param settings: plugin settings
420 """
421 """
421 auth = self.auth(userobj, username, passwd, settings, **kwargs)
422 auth = self.auth(userobj, username, passwd, settings, **kwargs)
422 if auth:
423 if auth:
423 auth['_plugin'] = self.name
424 auth['_plugin'] = self.name
424 auth['_ttl_cache'] = self.get_ttl_cache(settings)
425 auth['_ttl_cache'] = self.get_ttl_cache(settings)
425 # check if hash should be migrated ?
426 # check if hash should be migrated ?
426 new_hash = auth.get('_hash_migrate')
427 new_hash = auth.get('_hash_migrate')
427 if new_hash:
428 if new_hash:
428 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
429 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
429 if 'user_group_sync' not in auth:
430 if 'user_group_sync' not in auth:
430 auth['user_group_sync'] = False
431 auth['user_group_sync'] = False
431 return self._validate_auth_return(auth)
432 return self._validate_auth_return(auth)
432 return auth
433 return auth
433
434
434 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
435 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
435 new_hash_cypher = _RhodeCodeCryptoBCrypt()
436 new_hash_cypher = _RhodeCodeCryptoBCrypt()
436 # extra checks, so make sure new hash is correct.
437 # extra checks, so make sure new hash is correct.
437 password_encoded = safe_str(password)
438 password_encoded = safe_str(password)
438 if new_hash and new_hash_cypher.hash_check(
439 if new_hash and new_hash_cypher.hash_check(
439 password_encoded, new_hash):
440 password_encoded, new_hash):
440 cur_user = User.get_by_username(username)
441 cur_user = User.get_by_username(username)
441 cur_user.password = new_hash
442 cur_user.password = new_hash
442 Session().add(cur_user)
443 Session().add(cur_user)
443 Session().flush()
444 Session().flush()
444 log.info('Migrated user %s hash to bcrypt', cur_user)
445 log.info('Migrated user %s hash to bcrypt', cur_user)
445
446
446 def _validate_auth_return(self, ret):
447 def _validate_auth_return(self, ret):
447 if not isinstance(ret, dict):
448 if not isinstance(ret, dict):
448 raise Exception('returned value from auth must be a dict')
449 raise Exception('returned value from auth must be a dict')
449 for k in self.auth_func_attrs:
450 for k in self.auth_func_attrs:
450 if k not in ret:
451 if k not in ret:
451 raise Exception('Missing %s attribute from returned data' % k)
452 raise Exception('Missing %s attribute from returned data' % k)
452 return ret
453 return ret
453
454
454 def get_ttl_cache(self, settings=None):
455 def get_ttl_cache(self, settings=None):
455 plugin_settings = settings or self.get_settings()
456 plugin_settings = settings or self.get_settings()
456 # we set default to 30, we make a compromise here,
457 # we set default to 30, we make a compromise here,
457 # performance > security, mostly due to LDAP/SVN, majority
458 # performance > security, mostly due to LDAP/SVN, majority
458 # of users pick cache_ttl to be enabled
459 # of users pick cache_ttl to be enabled
459 from rhodecode.authentication import plugin_default_auth_ttl
460 from rhodecode.authentication import plugin_default_auth_ttl
460 cache_ttl = plugin_default_auth_ttl
461 cache_ttl = plugin_default_auth_ttl
461
462
462 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
463 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
463 # plugin cache set inside is more important than the settings value
464 # plugin cache set inside is more important than the settings value
464 cache_ttl = self.AUTH_CACHE_TTL
465 cache_ttl = self.AUTH_CACHE_TTL
465 elif plugin_settings.get('cache_ttl'):
466 elif plugin_settings.get('cache_ttl'):
466 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
467 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
467
468
468 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
469 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
469 return plugin_cache_active, cache_ttl
470 return plugin_cache_active, cache_ttl
470
471
471
472
472 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
473 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
473
474
474 @hybrid_property
475 @hybrid_property
475 def allows_creating_users(self):
476 def allows_creating_users(self):
476 return True
477 return True
477
478
478 def use_fake_password(self):
479 def use_fake_password(self):
479 """
480 """
480 Return a boolean that indicates whether or not we should set the user's
481 Return a boolean that indicates whether or not we should set the user's
481 password to a random value when it is authenticated by this plugin.
482 password to a random value when it is authenticated by this plugin.
482 If your plugin provides authentication, then you will generally
483 If your plugin provides authentication, then you will generally
483 want this.
484 want this.
484
485
485 :returns: boolean
486 :returns: boolean
486 """
487 """
487 raise NotImplementedError("Not implemented in base class")
488 raise NotImplementedError("Not implemented in base class")
488
489
489 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
490 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
490 # at this point _authenticate calls plugin's `auth()` function
491 # at this point _authenticate calls plugin's `auth()` function
491 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
492 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
492 userobj, username, passwd, settings, **kwargs)
493 userobj, username, passwd, settings, **kwargs)
493
494
494 if auth:
495 if auth:
495 # maybe plugin will clean the username ?
496 # maybe plugin will clean the username ?
496 # we should use the return value
497 # we should use the return value
497 username = auth['username']
498 username = auth['username']
498
499
499 # if external source tells us that user is not active, we should
500 # if external source tells us that user is not active, we should
500 # skip rest of the process. This can prevent from creating users in
501 # skip rest of the process. This can prevent from creating users in
501 # RhodeCode when using external authentication, but if it's
502 # RhodeCode when using external authentication, but if it's
502 # inactive user we shouldn't create that user anyway
503 # inactive user we shouldn't create that user anyway
503 if auth['active_from_extern'] is False:
504 if auth['active_from_extern'] is False:
504 log.warning(
505 log.warning(
505 "User %s authenticated against %s, but is inactive",
506 "User %s authenticated against %s, but is inactive",
506 username, self.__module__)
507 username, self.__module__)
507 return None
508 return None
508
509
509 cur_user = User.get_by_username(username, case_insensitive=True)
510 cur_user = User.get_by_username(username, case_insensitive=True)
510 is_user_existing = cur_user is not None
511 is_user_existing = cur_user is not None
511
512
512 if is_user_existing:
513 if is_user_existing:
513 log.debug('Syncing user `%s` from '
514 log.debug('Syncing user `%s` from '
514 '`%s` plugin', username, self.name)
515 '`%s` plugin', username, self.name)
515 else:
516 else:
516 log.debug('Creating non existing user `%s` from '
517 log.debug('Creating non existing user `%s` from '
517 '`%s` plugin', username, self.name)
518 '`%s` plugin', username, self.name)
518
519
519 if self.allows_creating_users:
520 if self.allows_creating_users:
520 log.debug('Plugin `%s` allows to '
521 log.debug('Plugin `%s` allows to '
521 'create new users', self.name)
522 'create new users', self.name)
522 else:
523 else:
523 log.debug('Plugin `%s` does not allow to '
524 log.debug('Plugin `%s` does not allow to '
524 'create new users', self.name)
525 'create new users', self.name)
525
526
526 user_parameters = {
527 user_parameters = {
527 'username': username,
528 'username': username,
528 'email': auth["email"],
529 'email': auth["email"],
529 'firstname': auth["firstname"],
530 'firstname': auth["firstname"],
530 'lastname': auth["lastname"],
531 'lastname': auth["lastname"],
531 'active': auth["active"],
532 'active': auth["active"],
532 'admin': auth["admin"],
533 'admin': auth["admin"],
533 'extern_name': auth["extern_name"],
534 'extern_name': auth["extern_name"],
534 'extern_type': self.name,
535 'extern_type': self.name,
535 'plugin': self,
536 'plugin': self,
536 'allow_to_create_user': self.allows_creating_users,
537 'allow_to_create_user': self.allows_creating_users,
537 }
538 }
538
539
539 if not is_user_existing:
540 if not is_user_existing:
540 if self.use_fake_password():
541 if self.use_fake_password():
541 # Randomize the PW because we don't need it, but don't want
542 # Randomize the PW because we don't need it, but don't want
542 # them blank either
543 # them blank either
543 passwd = PasswordGenerator().gen_password(length=16)
544 passwd = PasswordGenerator().gen_password(length=16)
544 user_parameters['password'] = passwd
545 user_parameters['password'] = passwd
545 else:
546 else:
546 # Since the password is required by create_or_update method of
547 # Since the password is required by create_or_update method of
547 # UserModel, we need to set it explicitly.
548 # UserModel, we need to set it explicitly.
548 # The create_or_update method is smart and recognises the
549 # The create_or_update method is smart and recognises the
549 # password hashes as well.
550 # password hashes as well.
550 user_parameters['password'] = cur_user.password
551 user_parameters['password'] = cur_user.password
551
552
552 # we either create or update users, we also pass the flag
553 # we either create or update users, we also pass the flag
553 # that controls if this method can actually do that.
554 # that controls if this method can actually do that.
554 # raises NotAllowedToCreateUserError if it cannot, and we try to.
555 # raises NotAllowedToCreateUserError if it cannot, and we try to.
555 user = UserModel().create_or_update(**user_parameters)
556 user = UserModel().create_or_update(**user_parameters)
556 Session().flush()
557 Session().flush()
557 # enforce user is just in given groups, all of them has to be ones
558 # enforce user is just in given groups, all of them has to be ones
558 # created from plugins. We store this info in _group_data JSON
559 # created from plugins. We store this info in _group_data JSON
559 # field
560 # field
560
561
561 if auth['user_group_sync']:
562 if auth['user_group_sync']:
562 try:
563 try:
563 groups = auth['groups'] or []
564 groups = auth['groups'] or []
564 log.debug(
565 log.debug(
565 'Performing user_group sync based on set `%s` '
566 'Performing user_group sync based on set `%s` '
566 'returned by `%s` plugin', groups, self.name)
567 'returned by `%s` plugin', groups, self.name)
567 UserGroupModel().enforce_groups(user, groups, self.name)
568 UserGroupModel().enforce_groups(user, groups, self.name)
568 except Exception:
569 except Exception:
569 # for any reason group syncing fails, we should
570 # for any reason group syncing fails, we should
570 # proceed with login
571 # proceed with login
571 log.error(traceback.format_exc())
572 log.error(traceback.format_exc())
572
573
573 Session().commit()
574 Session().commit()
574 return auth
575 return auth
575
576
576
577
577 class AuthLdapBase(object):
578 class AuthLdapBase(object):
578
579
579 @classmethod
580 @classmethod
580 def _build_servers(cls, ldap_server_type, ldap_server, port):
581 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
582
581 def host_resolver(host, port, full_resolve=True):
583 def host_resolver(host, port, full_resolve=True):
582 """
584 """
583 Main work for this function is to prevent ldap connection issues,
585 Main work for this function is to prevent ldap connection issues,
584 and detect them early using a "greenified" sockets
586 and detect them early using a "greenified" sockets
585 """
587 """
586 host = host.strip()
588 host = host.strip()
587 if not full_resolve:
589 if not full_resolve:
588 return '{}:{}'.format(host, port)
590 return '{}:{}'.format(host, port)
589
591
590 log.debug('LDAP: Resolving IP for LDAP host %s', host)
592 log.debug('LDAP: Resolving IP for LDAP host %s', host)
591 try:
593 try:
592 ip = socket.gethostbyname(host)
594 ip = socket.gethostbyname(host)
593 log.debug('Got LDAP server %s ip %s', host, ip)
595 log.debug('Got LDAP server %s ip %s', host, ip)
594 except Exception:
596 except Exception:
595 raise LdapConnectionError(
597 raise LdapConnectionError(
596 'Failed to resolve host: `{}`'.format(host))
598 'Failed to resolve host: `{}`'.format(host))
597
599
598 log.debug('LDAP: Checking if IP %s is accessible', ip)
600 log.debug('LDAP: Checking if IP %s is accessible', ip)
599 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
601 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
600 try:
602 try:
601 s.connect((ip, int(port)))
603 s.connect((ip, int(port)))
602 s.shutdown(socket.SHUT_RD)
604 s.shutdown(socket.SHUT_RD)
603 except Exception:
605 except Exception:
604 raise LdapConnectionError(
606 raise LdapConnectionError(
605 'Failed to connect to host: `{}:{}`'.format(host, port))
607 'Failed to connect to host: `{}:{}`'.format(host, port))
606
608
607 return '{}:{}'.format(host, port)
609 return '{}:{}'.format(host, port)
608
610
609 if len(ldap_server) == 1:
611 if len(ldap_server) == 1:
610 # in case of single server use resolver to detect potential
612 # in case of single server use resolver to detect potential
611 # connection issues
613 # connection issues
612 full_resolve = True
614 full_resolve = True
613 else:
615 else:
614 full_resolve = False
616 full_resolve = False
615
617
616 return ', '.join(
618 return ', '.join(
617 ["{}://{}".format(
619 ["{}://{}".format(
618 ldap_server_type,
620 ldap_server_type,
619 host_resolver(host, port, full_resolve=full_resolve))
621 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
620 for host in ldap_server])
622 for host in ldap_server])
621
623
622 @classmethod
624 @classmethod
623 def _get_server_list(cls, servers):
625 def _get_server_list(cls, servers):
624 return map(string.strip, servers.split(','))
626 return map(string.strip, servers.split(','))
625
627
626 @classmethod
628 @classmethod
627 def get_uid(cls, username, server_addresses):
629 def get_uid(cls, username, server_addresses):
628 uid = username
630 uid = username
629 for server_addr in server_addresses:
631 for server_addr in server_addresses:
630 uid = chop_at(username, "@%s" % server_addr)
632 uid = chop_at(username, "@%s" % server_addr)
631 return uid
633 return uid
632
634
635 @classmethod
636 def validate_username(cls, username):
637 if "," in username:
638 raise LdapUsernameError(
639 "invalid character `,` in username: `{}`".format(username))
640
641 @classmethod
642 def validate_password(cls, username, password):
643 if not password:
644 msg = "Authenticating user %s with blank password not allowed"
645 log.warning(msg, username)
646 raise LdapPasswordError(msg)
647
633
648
634 def loadplugin(plugin_id):
649 def loadplugin(plugin_id):
635 """
650 """
636 Loads and returns an instantiated authentication plugin.
651 Loads and returns an instantiated authentication plugin.
637 Returns the RhodeCodeAuthPluginBase subclass on success,
652 Returns the RhodeCodeAuthPluginBase subclass on success,
638 or None on failure.
653 or None on failure.
639 """
654 """
640 # TODO: Disusing pyramids thread locals to retrieve the registry.
655 # TODO: Disusing pyramids thread locals to retrieve the registry.
641 authn_registry = get_authn_registry()
656 authn_registry = get_authn_registry()
642 plugin = authn_registry.get_plugin(plugin_id)
657 plugin = authn_registry.get_plugin(plugin_id)
643 if plugin is None:
658 if plugin is None:
644 log.error('Authentication plugin not found: "%s"', plugin_id)
659 log.error('Authentication plugin not found: "%s"', plugin_id)
645 return plugin
660 return plugin
646
661
647
662
648 def get_authn_registry(registry=None):
663 def get_authn_registry(registry=None):
649 registry = registry or get_current_registry()
664 registry = registry or get_current_registry()
650 authn_registry = registry.getUtility(IAuthnPluginRegistry)
665 authn_registry = registry.getUtility(IAuthnPluginRegistry)
651 return authn_registry
666 return authn_registry
652
667
653
668
654 def authenticate(username, password, environ=None, auth_type=None,
669 def authenticate(username, password, environ=None, auth_type=None,
655 skip_missing=False, registry=None, acl_repo_name=None):
670 skip_missing=False, registry=None, acl_repo_name=None):
656 """
671 """
657 Authentication function used for access control,
672 Authentication function used for access control,
658 It tries to authenticate based on enabled authentication modules.
673 It tries to authenticate based on enabled authentication modules.
659
674
660 :param username: username can be empty for headers auth
675 :param username: username can be empty for headers auth
661 :param password: password can be empty for headers auth
676 :param password: password can be empty for headers auth
662 :param environ: environ headers passed for headers auth
677 :param environ: environ headers passed for headers auth
663 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
678 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
664 :param skip_missing: ignores plugins that are in db but not in environment
679 :param skip_missing: ignores plugins that are in db but not in environment
665 :returns: None if auth failed, plugin_user dict if auth is correct
680 :returns: None if auth failed, plugin_user dict if auth is correct
666 """
681 """
667 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
682 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
668 raise ValueError('auth type must be on of http, vcs got "%s" instead'
683 raise ValueError('auth type must be on of http, vcs got "%s" instead'
669 % auth_type)
684 % auth_type)
670 headers_only = environ and not (username and password)
685 headers_only = environ and not (username and password)
671
686
672 authn_registry = get_authn_registry(registry)
687 authn_registry = get_authn_registry(registry)
673 plugins_to_check = authn_registry.get_plugins_for_authentication()
688 plugins_to_check = authn_registry.get_plugins_for_authentication()
674 log.debug('Starting ordered authentication chain using %s plugins',
689 log.debug('Starting ordered authentication chain using %s plugins',
675 [x.name for x in plugins_to_check])
690 [x.name for x in plugins_to_check])
676 for plugin in plugins_to_check:
691 for plugin in plugins_to_check:
677 plugin.set_auth_type(auth_type)
692 plugin.set_auth_type(auth_type)
678 plugin.set_calling_scope_repo(acl_repo_name)
693 plugin.set_calling_scope_repo(acl_repo_name)
679
694
680 if headers_only and not plugin.is_headers_auth:
695 if headers_only and not plugin.is_headers_auth:
681 log.debug('Auth type is for headers only and plugin `%s` is not '
696 log.debug('Auth type is for headers only and plugin `%s` is not '
682 'headers plugin, skipping...', plugin.get_id())
697 'headers plugin, skipping...', plugin.get_id())
683 continue
698 continue
684
699
685 log.debug('Trying authentication using ** %s **', plugin.get_id())
700 log.debug('Trying authentication using ** %s **', plugin.get_id())
686
701
687 # load plugin settings from RhodeCode database
702 # load plugin settings from RhodeCode database
688 plugin_settings = plugin.get_settings()
703 plugin_settings = plugin.get_settings()
689 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
704 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
690 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
705 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
691
706
692 # use plugin's method of user extraction.
707 # use plugin's method of user extraction.
693 user = plugin.get_user(username, environ=environ,
708 user = plugin.get_user(username, environ=environ,
694 settings=plugin_settings)
709 settings=plugin_settings)
695 display_user = user.username if user else username
710 display_user = user.username if user else username
696 log.debug(
711 log.debug(
697 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
712 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
698
713
699 if not plugin.allows_authentication_from(user):
714 if not plugin.allows_authentication_from(user):
700 log.debug('Plugin %s does not accept user `%s` for authentication',
715 log.debug('Plugin %s does not accept user `%s` for authentication',
701 plugin.get_id(), display_user)
716 plugin.get_id(), display_user)
702 continue
717 continue
703 else:
718 else:
704 log.debug('Plugin %s accepted user `%s` for authentication',
719 log.debug('Plugin %s accepted user `%s` for authentication',
705 plugin.get_id(), display_user)
720 plugin.get_id(), display_user)
706
721
707 log.info('Authenticating user `%s` using %s plugin',
722 log.info('Authenticating user `%s` using %s plugin',
708 display_user, plugin.get_id())
723 display_user, plugin.get_id())
709
724
710 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
725 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
711
726
712 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
727 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
713 plugin.get_id(), plugin_cache_active, cache_ttl)
728 plugin.get_id(), plugin_cache_active, cache_ttl)
714
729
715 user_id = user.user_id if user else None
730 user_id = user.user_id if user else None
716 # don't cache for empty users
731 # don't cache for empty users
717 plugin_cache_active = plugin_cache_active and user_id
732 plugin_cache_active = plugin_cache_active and user_id
718 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
733 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
719 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
734 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
720
735
721 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
736 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
722 expiration_time=cache_ttl,
737 expiration_time=cache_ttl,
723 condition=plugin_cache_active)
738 condition=plugin_cache_active)
724 def compute_auth(
739 def compute_auth(
725 cache_name, plugin_name, username, password):
740 cache_name, plugin_name, username, password):
726
741
727 # _authenticate is a wrapper for .auth() method of plugin.
742 # _authenticate is a wrapper for .auth() method of plugin.
728 # it checks if .auth() sends proper data.
743 # it checks if .auth() sends proper data.
729 # For RhodeCodeExternalAuthPlugin it also maps users to
744 # For RhodeCodeExternalAuthPlugin it also maps users to
730 # Database and maps the attributes returned from .auth()
745 # Database and maps the attributes returned from .auth()
731 # to RhodeCode database. If this function returns data
746 # to RhodeCode database. If this function returns data
732 # then auth is correct.
747 # then auth is correct.
733 log.debug('Running plugin `%s` _authenticate method '
748 log.debug('Running plugin `%s` _authenticate method '
734 'using username and password', plugin.get_id())
749 'using username and password', plugin.get_id())
735 return plugin._authenticate(
750 return plugin._authenticate(
736 user, username, password, plugin_settings,
751 user, username, password, plugin_settings,
737 environ=environ or {})
752 environ=environ or {})
738
753
739 start = time.time()
754 start = time.time()
740 # for environ based auth, password can be empty, but then the validation is
755 # for environ based auth, password can be empty, but then the validation is
741 # on the server that fills in the env data needed for authentication
756 # on the server that fills in the env data needed for authentication
742 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
757 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
743
758
744 auth_time = time.time() - start
759 auth_time = time.time() - start
745 log.debug('Authentication for plugin `%s` completed in %.3fs, '
760 log.debug('Authentication for plugin `%s` completed in %.3fs, '
746 'expiration time of fetched cache %.1fs.',
761 'expiration time of fetched cache %.1fs.',
747 plugin.get_id(), auth_time, cache_ttl)
762 plugin.get_id(), auth_time, cache_ttl)
748
763
749 log.debug('PLUGIN USER DATA: %s', plugin_user)
764 log.debug('PLUGIN USER DATA: %s', plugin_user)
750
765
751 if plugin_user:
766 if plugin_user:
752 log.debug('Plugin returned proper authentication data')
767 log.debug('Plugin returned proper authentication data')
753 return plugin_user
768 return plugin_user
754 # we failed to Auth because .auth() method didn't return proper user
769 # we failed to Auth because .auth() method didn't return proper user
755 log.debug("User `%s` failed to authenticate against %s",
770 log.debug("User `%s` failed to authenticate against %s",
756 display_user, plugin.get_id())
771 display_user, plugin.get_id())
757
772
758 # case when we failed to authenticate against all defined plugins
773 # case when we failed to authenticate against all defined plugins
759 return None
774 return None
760
775
761
776
762 def chop_at(s, sub, inclusive=False):
777 def chop_at(s, sub, inclusive=False):
763 """Truncate string ``s`` at the first occurrence of ``sub``.
778 """Truncate string ``s`` at the first occurrence of ``sub``.
764
779
765 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
780 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
766
781
767 >>> chop_at("plutocratic brats", "rat")
782 >>> chop_at("plutocratic brats", "rat")
768 'plutoc'
783 'plutoc'
769 >>> chop_at("plutocratic brats", "rat", True)
784 >>> chop_at("plutocratic brats", "rat", True)
770 'plutocrat'
785 'plutocrat'
771 """
786 """
772 pos = s.find(sub)
787 pos = s.find(sub)
773 if pos == -1:
788 if pos == -1:
774 return s
789 return s
775 if inclusive:
790 if inclusive:
776 return s[:pos+len(sub)]
791 return s[:pos+len(sub)]
777 return s[:pos]
792 return s[:pos]
@@ -1,517 +1,528 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 os
26 import logging
25 import logging
27 import traceback
26 import traceback
28
27
29 import colander
28 import colander
30 from rhodecode.translation import _
29 from rhodecode.translation import _
31 from rhodecode.authentication.base import (
30 from rhodecode.authentication.base import (
32 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.lib.colander_utils import strip_whitespace
34 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.exceptions import (
35 from rhodecode.lib.exceptions import (
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 )
37 )
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.model.db import User
39 from rhodecode.model.db import User
41 from rhodecode.model.validators import Missing
40 from rhodecode.model.validators import Missing
42
41
43 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
44
43
45 try:
44 try:
46 import ldap
45 import ldap
47 except ImportError:
46 except ImportError:
48 # means that python-ldap is not installed, we use Missing object to mark
47 # means that python-ldap is not installed, we use Missing object to mark
49 # ldap lib is Missing
48 # ldap lib is Missing
50 ldap = Missing
49 ldap = Missing
51
50
52
51
53 class LdapError(Exception):
52 class LdapError(Exception):
54 pass
53 pass
55
54
56
55
57 def plugin_factory(plugin_id, *args, **kwds):
56 def plugin_factory(plugin_id, *args, **kwds):
58 """
57 """
59 Factory function that is called during plugin discovery.
58 Factory function that is called during plugin discovery.
60 It returns the plugin instance.
59 It returns the plugin instance.
61 """
60 """
62 plugin = RhodeCodeAuthPlugin(plugin_id)
61 plugin = RhodeCodeAuthPlugin(plugin_id)
63 return plugin
62 return plugin
64
63
65
64
66 class LdapAuthnResource(AuthnPluginResourceBase):
65 class LdapAuthnResource(AuthnPluginResourceBase):
67 pass
66 pass
68
67
69
68
69 class AuthLdap(AuthLdapBase):
70 default_tls_cert_dir = '/etc/openldap/cacerts'
71
72 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
73 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
74 tls_cert_dir=None, ldap_version=3,
75 search_scope='SUBTREE', attr_login='uid',
76 ldap_filter='', timeout=None):
77 if ldap == Missing:
78 raise LdapImportError("Missing or incompatible ldap library")
79
80 self.debug = False
81 self.timeout = timeout or 60 * 5
82 self.ldap_version = ldap_version
83 self.ldap_server_type = 'ldap'
84
85 self.TLS_KIND = tls_kind
86
87 if self.TLS_KIND == 'LDAPS':
88 port = port or 689
89 self.ldap_server_type += 's'
90
91 OPT_X_TLS_DEMAND = 2
92 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
93 self.TLS_CERT_FILE = tls_cert_file or ''
94 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
95
96 # split server into list
97 self.SERVER_ADDRESSES = self._get_server_list(server)
98 self.LDAP_SERVER_PORT = port
99
100 # USE FOR READ ONLY BIND TO LDAP SERVER
101 self.attr_login = attr_login
102
103 self.LDAP_BIND_DN = safe_str(bind_dn)
104 self.LDAP_BIND_PASS = safe_str(bind_pass)
105
106 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
107 self.BASE_DN = safe_str(base_dn)
108 self.LDAP_FILTER = safe_str(ldap_filter)
109
110 def _get_ldap_conn(self):
111
112 if self.debug:
113 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
114
115 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
116 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
117
118 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
119 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
120
121 if self.TLS_KIND != 'PLAIN':
122 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
123
124 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
125 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
126
127 # init connection now
128 ldap_servers = self._build_servers(
129 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
130 log.debug('initializing LDAP connection to:%s', ldap_servers)
131 ldap_conn = ldap.initialize(ldap_servers)
132 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
133 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
134 ldap_conn.timeout = self.timeout
135
136 if self.ldap_version == 2:
137 ldap_conn.protocol = ldap.VERSION2
138 else:
139 ldap_conn.protocol = ldap.VERSION3
140
141 if self.TLS_KIND == 'START_TLS':
142 ldap_conn.start_tls_s()
143
144 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
145 log.debug('Trying simple_bind with password and given login DN: %s',
146 self.LDAP_BIND_DN)
147 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
148
149 return ldap_conn
150
151 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
152 try:
153 log.debug('Trying simple bind with %s', dn)
154 server.simple_bind_s(dn, safe_str(password))
155 user = server.search_ext_s(
156 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
157 _, attrs = user
158 return attrs
159
160 except ldap.INVALID_CREDENTIALS:
161 log.debug(
162 "LDAP rejected password for user '%s': %s, org_exc:",
163 username, dn, exc_info=True)
164
165 def authenticate_ldap(self, username, password):
166 """
167 Authenticate a user via LDAP and return his/her LDAP properties.
168
169 Raises AuthenticationError if the credentials are rejected, or
170 EnvironmentError if the LDAP server can't be reached.
171
172 :param username: username
173 :param password: password
174 """
175
176 uid = self.get_uid(username, self.SERVER_ADDRESSES)
177 user_attrs = {}
178 dn = ''
179
180 self.validate_password(username, password)
181 self.validate_username(username)
182
183 ldap_conn = None
184 try:
185 ldap_conn = self._get_ldap_conn()
186 filter_ = '(&%s(%s=%s))' % (
187 self.LDAP_FILTER, self.attr_login, username)
188 log.debug("Authenticating %r filter %s", self.BASE_DN, filter_)
189
190 lobjects = ldap_conn.search_ext_s(
191 self.BASE_DN, self.SEARCH_SCOPE, filter_)
192
193 if not lobjects:
194 log.debug("No matching LDAP objects for authentication "
195 "of UID:'%s' username:(%s)", uid, username)
196 raise ldap.NO_SUCH_OBJECT()
197
198 log.debug('Found matching ldap object, trying to authenticate')
199 for (dn, _attrs) in lobjects:
200 if dn is None:
201 continue
202
203 user_attrs = self.fetch_attrs_from_simple_bind(
204 ldap_conn, dn, username, password)
205 if user_attrs:
206 break
207 else:
208 raise LdapPasswordError(
209 'Failed to authenticate user `{}`'
210 'with given password'.format(username))
211
212 except ldap.NO_SUCH_OBJECT:
213 log.debug("LDAP says no such user '%s' (%s), org_exc:",
214 uid, username, exc_info=True)
215 raise LdapUsernameError('Unable to find user')
216 except ldap.SERVER_DOWN:
217 org_exc = traceback.format_exc()
218 raise LdapConnectionError(
219 "LDAP can't access authentication "
220 "server, org_exc:%s" % org_exc)
221 finally:
222 if ldap_conn:
223 log.debug('ldap: connection release')
224 try:
225 ldap_conn.unbind_s()
226 except Exception:
227 # for any reason this can raise exception we must catch it
228 # to not crush the server
229 pass
230
231 return dn, user_attrs
232
233
70 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
234 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
71 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
235 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
72 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
236 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
73 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
237 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
74
238
75 host = colander.SchemaNode(
239 host = colander.SchemaNode(
76 colander.String(),
240 colander.String(),
77 default='',
241 default='',
78 description=_('Host[s] of the LDAP Server \n'
242 description=_('Host[s] of the LDAP Server \n'
79 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
243 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
80 'Multiple servers can be specified using commas'),
244 'Multiple servers can be specified using commas'),
81 preparer=strip_whitespace,
245 preparer=strip_whitespace,
82 title=_('LDAP Host'),
246 title=_('LDAP Host'),
83 widget='string')
247 widget='string')
84 port = colander.SchemaNode(
248 port = colander.SchemaNode(
85 colander.Int(),
249 colander.Int(),
86 default=389,
250 default=389,
87 description=_('Custom port that the LDAP server is listening on. '
251 description=_('Custom port that the LDAP server is listening on. '
88 'Default value is: 389'),
252 'Default value is: 389'),
89 preparer=strip_whitespace,
253 preparer=strip_whitespace,
90 title=_('Port'),
254 title=_('Port'),
91 validator=colander.Range(min=0, max=65536),
255 validator=colander.Range(min=0, max=65536),
92 widget='int')
256 widget='int')
93
257
94 timeout = colander.SchemaNode(
258 timeout = colander.SchemaNode(
95 colander.Int(),
259 colander.Int(),
96 default=60 * 5,
260 default=60 * 5,
97 description=_('Timeout for LDAP connection'),
261 description=_('Timeout for LDAP connection'),
98 preparer=strip_whitespace,
262 preparer=strip_whitespace,
99 title=_('Connection timeout'),
263 title=_('Connection timeout'),
100 validator=colander.Range(min=1),
264 validator=colander.Range(min=1),
101 widget='int')
265 widget='int')
102
266
103 dn_user = colander.SchemaNode(
267 dn_user = colander.SchemaNode(
104 colander.String(),
268 colander.String(),
105 default='',
269 default='',
106 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
270 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
107 'e.g., cn=admin,dc=mydomain,dc=com, or '
271 'e.g., cn=admin,dc=mydomain,dc=com, or '
108 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
272 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
109 missing='',
273 missing='',
110 preparer=strip_whitespace,
274 preparer=strip_whitespace,
111 title=_('Account'),
275 title=_('Account'),
112 widget='string')
276 widget='string')
113 dn_pass = colander.SchemaNode(
277 dn_pass = colander.SchemaNode(
114 colander.String(),
278 colander.String(),
115 default='',
279 default='',
116 description=_('Password to authenticate for given user DN.'),
280 description=_('Password to authenticate for given user DN.'),
117 missing='',
281 missing='',
118 preparer=strip_whitespace,
282 preparer=strip_whitespace,
119 title=_('Password'),
283 title=_('Password'),
120 widget='password')
284 widget='password')
121 tls_kind = colander.SchemaNode(
285 tls_kind = colander.SchemaNode(
122 colander.String(),
286 colander.String(),
123 default=tls_kind_choices[0],
287 default=tls_kind_choices[0],
124 description=_('TLS Type'),
288 description=_('TLS Type'),
125 title=_('Connection Security'),
289 title=_('Connection Security'),
126 validator=colander.OneOf(tls_kind_choices),
290 validator=colander.OneOf(tls_kind_choices),
127 widget='select')
291 widget='select')
128 tls_reqcert = colander.SchemaNode(
292 tls_reqcert = colander.SchemaNode(
129 colander.String(),
293 colander.String(),
130 default=tls_reqcert_choices[0],
294 default=tls_reqcert_choices[0],
131 description=_('Require Cert over TLS?. Self-signed and custom '
295 description=_('Require Cert over TLS?. Self-signed and custom '
132 'certificates can be used when\n `RhodeCode Certificate` '
296 'certificates can be used when\n `RhodeCode Certificate` '
133 'found in admin > settings > system info page is extended.'),
297 'found in admin > settings > system info page is extended.'),
134 title=_('Certificate Checks'),
298 title=_('Certificate Checks'),
135 validator=colander.OneOf(tls_reqcert_choices),
299 validator=colander.OneOf(tls_reqcert_choices),
136 widget='select')
300 widget='select')
301 tls_cert_file = colander.SchemaNode(
302 colander.String(),
303 default='',
304 description=_('This specifies the PEM-format file path containing '
305 'certificates for use in TLS connection.\n'
306 'If not specified `TLS Cert dir` will be used'),
307 title=_('TLS Cert file'),
308 missing='',
309 widget='string')
310 tls_cert_dir = colander.SchemaNode(
311 colander.String(),
312 default=AuthLdap.default_tls_cert_dir,
313 description=_('This specifies the path of a directory that contains individual '
314 'CA certificates in separate files.'),
315 title=_('TLS Cert dir'),
316 widget='string')
137 base_dn = colander.SchemaNode(
317 base_dn = colander.SchemaNode(
138 colander.String(),
318 colander.String(),
139 default='',
319 default='',
140 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
320 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
141 'in it to be replaced with current user credentials \n'
321 'in it to be replaced with current user credentials \n'
142 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
322 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
143 missing='',
323 missing='',
144 preparer=strip_whitespace,
324 preparer=strip_whitespace,
145 title=_('Base DN'),
325 title=_('Base DN'),
146 widget='string')
326 widget='string')
147 filter = colander.SchemaNode(
327 filter = colander.SchemaNode(
148 colander.String(),
328 colander.String(),
149 default='',
329 default='',
150 description=_('Filter to narrow results \n'
330 description=_('Filter to narrow results \n'
151 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
331 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
152 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
332 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
153 missing='',
333 missing='',
154 preparer=strip_whitespace,
334 preparer=strip_whitespace,
155 title=_('LDAP Search Filter'),
335 title=_('LDAP Search Filter'),
156 widget='string')
336 widget='string')
157
337
158 search_scope = colander.SchemaNode(
338 search_scope = colander.SchemaNode(
159 colander.String(),
339 colander.String(),
160 default=search_scope_choices[2],
340 default=search_scope_choices[2],
161 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
341 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
162 title=_('LDAP Search Scope'),
342 title=_('LDAP Search Scope'),
163 validator=colander.OneOf(search_scope_choices),
343 validator=colander.OneOf(search_scope_choices),
164 widget='select')
344 widget='select')
165 attr_login = colander.SchemaNode(
345 attr_login = colander.SchemaNode(
166 colander.String(),
346 colander.String(),
167 default='uid',
347 default='uid',
168 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
348 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
169 preparer=strip_whitespace,
349 preparer=strip_whitespace,
170 title=_('Login Attribute'),
350 title=_('Login Attribute'),
171 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
351 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
172 widget='string')
352 widget='string')
173 attr_firstname = colander.SchemaNode(
353 attr_firstname = colander.SchemaNode(
174 colander.String(),
354 colander.String(),
175 default='',
355 default='',
176 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
356 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
177 missing='',
357 missing='',
178 preparer=strip_whitespace,
358 preparer=strip_whitespace,
179 title=_('First Name Attribute'),
359 title=_('First Name Attribute'),
180 widget='string')
360 widget='string')
181 attr_lastname = colander.SchemaNode(
361 attr_lastname = colander.SchemaNode(
182 colander.String(),
362 colander.String(),
183 default='',
363 default='',
184 description=_('LDAP Attribute to map to last name (e.g., sn)'),
364 description=_('LDAP Attribute to map to last name (e.g., sn)'),
185 missing='',
365 missing='',
186 preparer=strip_whitespace,
366 preparer=strip_whitespace,
187 title=_('Last Name Attribute'),
367 title=_('Last Name Attribute'),
188 widget='string')
368 widget='string')
189 attr_email = colander.SchemaNode(
369 attr_email = colander.SchemaNode(
190 colander.String(),
370 colander.String(),
191 default='',
371 default='',
192 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
372 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
193 'Emails are a crucial part of RhodeCode. \n'
373 'Emails are a crucial part of RhodeCode. \n'
194 'If possible add a valid email attribute to ldap users.'),
374 'If possible add a valid email attribute to ldap users.'),
195 missing='',
375 missing='',
196 preparer=strip_whitespace,
376 preparer=strip_whitespace,
197 title=_('Email Attribute'),
377 title=_('Email Attribute'),
198 widget='string')
378 widget='string')
199
379
200
380
201 class AuthLdap(AuthLdapBase):
202
203 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
204 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
205 search_scope='SUBTREE', attr_login='uid',
206 ldap_filter='', timeout=None):
207 if ldap == Missing:
208 raise LdapImportError("Missing or incompatible ldap library")
209
210 self.debug = False
211 self.timeout = timeout or 60 * 5
212 self.ldap_version = ldap_version
213 self.ldap_server_type = 'ldap'
214
215 self.TLS_KIND = tls_kind
216
217 if self.TLS_KIND == 'LDAPS':
218 port = port or 689
219 self.ldap_server_type += 's'
220
221 OPT_X_TLS_DEMAND = 2
222 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
223 OPT_X_TLS_DEMAND)
224 self.LDAP_SERVER = server
225 # split server into list
226 self.SERVER_ADDRESSES = self._get_server_list(server)
227 self.LDAP_SERVER_PORT = port
228
229 # USE FOR READ ONLY BIND TO LDAP SERVER
230 self.attr_login = attr_login
231
232 self.LDAP_BIND_DN = safe_str(bind_dn)
233 self.LDAP_BIND_PASS = safe_str(bind_pass)
234
235 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
236 self.BASE_DN = safe_str(base_dn)
237 self.LDAP_FILTER = safe_str(ldap_filter)
238
239 def _get_ldap_conn(self):
240
241 if self.debug:
242 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
243
244 default_cert_path = os.environ.get('SSL_CERT_FILE')
245 default_cert_dir = os.environ.get('SSL_CERT_DIR', '/etc/openldap/cacerts')
246 if default_cert_path and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
247 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, default_cert_path)
248
249 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
250 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, default_cert_dir)
251
252 if self.TLS_KIND != 'PLAIN':
253 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
254
255 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
256 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
257
258 # init connection now
259 ldap_servers = self._build_servers(
260 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
261 log.debug('initializing LDAP connection to:%s', ldap_servers)
262 ldap_conn = ldap.initialize(ldap_servers)
263 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
264 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
265 ldap_conn.timeout = self.timeout
266
267 if self.ldap_version == 2:
268 ldap_conn.protocol = ldap.VERSION2
269 else:
270 ldap_conn.protocol = ldap.VERSION3
271
272 if self.TLS_KIND == 'START_TLS':
273 ldap_conn.start_tls_s()
274
275 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
276 log.debug('Trying simple_bind with password and given login DN: %s',
277 self.LDAP_BIND_DN)
278 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
279
280 return ldap_conn
281
282 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
283 try:
284 log.debug('Trying simple bind with %s', dn)
285 server.simple_bind_s(dn, safe_str(password))
286 user = server.search_ext_s(
287 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
288 _, attrs = user
289 return attrs
290
291 except ldap.INVALID_CREDENTIALS:
292 log.debug(
293 "LDAP rejected password for user '%s': %s, org_exc:",
294 username, dn, exc_info=True)
295
296 def authenticate_ldap(self, username, password):
297 """
298 Authenticate a user via LDAP and return his/her LDAP properties.
299
300 Raises AuthenticationError if the credentials are rejected, or
301 EnvironmentError if the LDAP server can't be reached.
302
303 :param username: username
304 :param password: password
305 """
306
307 uid = self.get_uid(username, self.SERVER_ADDRESSES)
308 user_attrs = {}
309 dn = ''
310
311 if not password:
312 msg = "Authenticating user %s with blank password not allowed"
313 log.warning(msg, username)
314 raise LdapPasswordError(msg)
315 if "," in username:
316 raise LdapUsernameError(
317 "invalid character `,` in username: `{}`".format(username))
318 ldap_conn = None
319 try:
320 ldap_conn = self._get_ldap_conn()
321 filter_ = '(&%s(%s=%s))' % (
322 self.LDAP_FILTER, self.attr_login, username)
323 log.debug(
324 "Authenticating %r filter %s", self.BASE_DN, filter_)
325 lobjects = ldap_conn.search_ext_s(
326 self.BASE_DN, self.SEARCH_SCOPE, filter_)
327
328 if not lobjects:
329 log.debug("No matching LDAP objects for authentication "
330 "of UID:'%s' username:(%s)", uid, username)
331 raise ldap.NO_SUCH_OBJECT()
332
333 log.debug('Found matching ldap object, trying to authenticate')
334 for (dn, _attrs) in lobjects:
335 if dn is None:
336 continue
337
338 user_attrs = self.fetch_attrs_from_simple_bind(
339 ldap_conn, dn, username, password)
340 if user_attrs:
341 break
342
343 else:
344 raise LdapPasswordError(
345 'Failed to authenticate user `{}`'
346 'with given password'.format(username))
347
348 except ldap.NO_SUCH_OBJECT:
349 log.debug("LDAP says no such user '%s' (%s), org_exc:",
350 uid, username, exc_info=True)
351 raise LdapUsernameError('Unable to find user')
352 except ldap.SERVER_DOWN:
353 org_exc = traceback.format_exc()
354 raise LdapConnectionError(
355 "LDAP can't access authentication "
356 "server, org_exc:%s" % org_exc)
357 finally:
358 if ldap_conn:
359 log.debug('ldap: connection release')
360 try:
361 ldap_conn.unbind_s()
362 except Exception:
363 # for any reason this can raise exception we must catch it
364 # to not crush the server
365 pass
366
367 return dn, user_attrs
368
369
370 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
381 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
371 # used to define dynamic binding in the
382 # used to define dynamic binding in the
372 DYNAMIC_BIND_VAR = '$login'
383 DYNAMIC_BIND_VAR = '$login'
373 _settings_unsafe_keys = ['dn_pass']
384 _settings_unsafe_keys = ['dn_pass']
374
385
375 def includeme(self, config):
386 def includeme(self, config):
376 config.add_authn_plugin(self)
387 config.add_authn_plugin(self)
377 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
388 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
378 config.add_view(
389 config.add_view(
379 'rhodecode.authentication.views.AuthnPluginViewBase',
390 'rhodecode.authentication.views.AuthnPluginViewBase',
380 attr='settings_get',
391 attr='settings_get',
381 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
392 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
382 request_method='GET',
393 request_method='GET',
383 route_name='auth_home',
394 route_name='auth_home',
384 context=LdapAuthnResource)
395 context=LdapAuthnResource)
385 config.add_view(
396 config.add_view(
386 'rhodecode.authentication.views.AuthnPluginViewBase',
397 'rhodecode.authentication.views.AuthnPluginViewBase',
387 attr='settings_post',
398 attr='settings_post',
388 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
399 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
389 request_method='POST',
400 request_method='POST',
390 route_name='auth_home',
401 route_name='auth_home',
391 context=LdapAuthnResource)
402 context=LdapAuthnResource)
392
403
393 def get_settings_schema(self):
404 def get_settings_schema(self):
394 return LdapSettingsSchema()
405 return LdapSettingsSchema()
395
406
396 def get_display_name(self):
407 def get_display_name(self):
397 return _('LDAP')
408 return _('LDAP')
398
409
399 @classmethod
410 @classmethod
400 def docs(cls):
411 def docs(cls):
401 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
412 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
402
413
403 @hybrid_property
414 @hybrid_property
404 def name(self):
415 def name(self):
405 return "ldap"
416 return "ldap"
406
417
407 def use_fake_password(self):
418 def use_fake_password(self):
408 return True
419 return True
409
420
410 def user_activation_state(self):
421 def user_activation_state(self):
411 def_user_perms = User.get_default_user().AuthUser().permissions['global']
422 def_user_perms = User.get_default_user().AuthUser().permissions['global']
412 return 'hg.extern_activate.auto' in def_user_perms
423 return 'hg.extern_activate.auto' in def_user_perms
413
424
414 def try_dynamic_binding(self, username, password, current_args):
425 def try_dynamic_binding(self, username, password, current_args):
415 """
426 """
416 Detects marker inside our original bind, and uses dynamic auth if
427 Detects marker inside our original bind, and uses dynamic auth if
417 present
428 present
418 """
429 """
419
430
420 org_bind = current_args['bind_dn']
431 org_bind = current_args['bind_dn']
421 passwd = current_args['bind_pass']
432 passwd = current_args['bind_pass']
422
433
423 def has_bind_marker(username):
434 def has_bind_marker(username):
424 if self.DYNAMIC_BIND_VAR in username:
435 if self.DYNAMIC_BIND_VAR in username:
425 return True
436 return True
426
437
427 # we only passed in user with "special" variable
438 # we only passed in user with "special" variable
428 if org_bind and has_bind_marker(org_bind) and not passwd:
439 if org_bind and has_bind_marker(org_bind) and not passwd:
429 log.debug('Using dynamic user/password binding for ldap '
440 log.debug('Using dynamic user/password binding for ldap '
430 'authentication. Replacing `%s` with username',
441 'authentication. Replacing `%s` with username',
431 self.DYNAMIC_BIND_VAR)
442 self.DYNAMIC_BIND_VAR)
432 current_args['bind_dn'] = org_bind.replace(
443 current_args['bind_dn'] = org_bind.replace(
433 self.DYNAMIC_BIND_VAR, username)
444 self.DYNAMIC_BIND_VAR, username)
434 current_args['bind_pass'] = password
445 current_args['bind_pass'] = password
435
446
436 return current_args
447 return current_args
437
448
438 def auth(self, userobj, username, password, settings, **kwargs):
449 def auth(self, userobj, username, password, settings, **kwargs):
439 """
450 """
440 Given a user object (which may be null), username, a plaintext password,
451 Given a user object (which may be null), username, a plaintext password,
441 and a settings object (containing all the keys needed as listed in
452 and a settings object (containing all the keys needed as listed in
442 settings()), authenticate this user's login attempt.
453 settings()), authenticate this user's login attempt.
443
454
444 Return None on failure. On success, return a dictionary of the form:
455 Return None on failure. On success, return a dictionary of the form:
445
456
446 see: RhodeCodeAuthPluginBase.auth_func_attrs
457 see: RhodeCodeAuthPluginBase.auth_func_attrs
447 This is later validated for correctness
458 This is later validated for correctness
448 """
459 """
449
460
450 if not username or not password:
461 if not username or not password:
451 log.debug('Empty username or password skipping...')
462 log.debug('Empty username or password skipping...')
452 return None
463 return None
453
464
454 ldap_args = {
465 ldap_args = {
455 'server': settings.get('host', ''),
466 'server': settings.get('host', ''),
456 'base_dn': settings.get('base_dn', ''),
467 'base_dn': settings.get('base_dn', ''),
457 'port': settings.get('port'),
468 'port': settings.get('port'),
458 'bind_dn': settings.get('dn_user'),
469 'bind_dn': settings.get('dn_user'),
459 'bind_pass': settings.get('dn_pass'),
470 'bind_pass': settings.get('dn_pass'),
460 'tls_kind': settings.get('tls_kind'),
471 'tls_kind': settings.get('tls_kind'),
461 'tls_reqcert': settings.get('tls_reqcert'),
472 'tls_reqcert': settings.get('tls_reqcert'),
473 'tls_cert_file': settings.get('tls_cert_file'),
474 'tls_cert_dir': settings.get('tls_cert_dir'),
462 'search_scope': settings.get('search_scope'),
475 'search_scope': settings.get('search_scope'),
463 'attr_login': settings.get('attr_login'),
476 'attr_login': settings.get('attr_login'),
464 'ldap_version': 3,
477 'ldap_version': 3,
465 'ldap_filter': settings.get('filter'),
478 'ldap_filter': settings.get('filter'),
466 'timeout': settings.get('timeout')
479 'timeout': settings.get('timeout')
467 }
480 }
468
481
469 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
482 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
470
483
471 log.debug('Checking for ldap authentication.')
484 log.debug('Checking for ldap authentication.')
472
485
473 try:
486 try:
474 aldap = AuthLdap(**ldap_args)
487 aldap = AuthLdap(**ldap_args)
475 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
488 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
476 log.debug('Got ldap DN response %s', user_dn)
489 log.debug('Got ldap DN response %s', user_dn)
477
490
478 def get_ldap_attr(k):
491 def get_ldap_attr(k):
479 return ldap_attrs.get(settings.get(k), [''])[0]
492 return ldap_attrs.get(settings.get(k), [''])[0]
480
493
481 # old attrs fetched from RhodeCode database
494 # old attrs fetched from RhodeCode database
482 admin = getattr(userobj, 'admin', False)
495 admin = getattr(userobj, 'admin', False)
483 active = getattr(userobj, 'active', True)
496 active = getattr(userobj, 'active', True)
484 email = getattr(userobj, 'email', '')
497 email = getattr(userobj, 'email', '')
485 username = getattr(userobj, 'username', username)
498 username = getattr(userobj, 'username', username)
486 firstname = getattr(userobj, 'firstname', '')
499 firstname = getattr(userobj, 'firstname', '')
487 lastname = getattr(userobj, 'lastname', '')
500 lastname = getattr(userobj, 'lastname', '')
488 extern_type = getattr(userobj, 'extern_type', '')
501 extern_type = getattr(userobj, 'extern_type', '')
489
502
490 groups = []
503 groups = []
491 user_attrs = {
504 user_attrs = {
492 'username': username,
505 'username': username,
493 'firstname': safe_unicode(
506 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
494 get_ldap_attr('attr_firstname') or firstname),
507 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
495 'lastname': safe_unicode(
496 get_ldap_attr('attr_lastname') or lastname),
497 'groups': groups,
508 'groups': groups,
498 'user_group_sync': False,
509 'user_group_sync': False,
499 'email': get_ldap_attr('attr_email') or email,
510 'email': get_ldap_attr('attr_email') or email,
500 'admin': admin,
511 'admin': admin,
501 'active': active,
512 'active': active,
502 'active_from_extern': None,
513 'active_from_extern': None,
503 'extern_name': user_dn,
514 'extern_name': user_dn,
504 'extern_type': extern_type,
515 'extern_type': extern_type,
505 }
516 }
517
506 log.debug('ldap user: %s', user_attrs)
518 log.debug('ldap user: %s', user_attrs)
507 log.info('user `%s` authenticated correctly', user_attrs['username'])
519 log.info('user `%s` authenticated correctly', user_attrs['username'])
508
520
509 return user_attrs
521 return user_attrs
510
522
511 except (LdapUsernameError, LdapPasswordError, LdapImportError):
523 except (LdapUsernameError, LdapPasswordError, LdapImportError):
512 log.exception("LDAP related exception")
524 log.exception("LDAP related exception")
513 return None
525 return None
514 except (Exception,):
526 except (Exception,):
515 log.exception("Other exception")
527 log.exception("Other exception")
516 return None
528 return None
517
General Comments 0
You need to be logged in to leave comments. Login now