##// END OF EJS Templates
logging: changed module in logs extra as it is a reserved keyword
super-admin -
r4818:6ebd323b default
parent child Browse files
Show More
@@ -1,232 +1,232 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 colander
21 import colander
22 import logging
22 import logging
23
23
24 from rhodecode.translation import _
24 from rhodecode.translation import _
25 from rhodecode.authentication.base import (
25 from rhodecode.authentication.base import (
26 RhodeCodeExternalAuthPlugin, hybrid_property)
26 RhodeCodeExternalAuthPlugin, hybrid_property)
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
28 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 from rhodecode.lib.colander_utils import strip_whitespace
29 from rhodecode.lib.colander_utils import strip_whitespace
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
30 from rhodecode.lib.utils2 import str2bool, safe_unicode
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwargs):
37 def plugin_factory(plugin_id, *args, **kwargs):
38 """
38 """
39 Factory function that is called during plugin discovery.
39 Factory function that is called during plugin discovery.
40 It returns the plugin instance.
40 It returns the plugin instance.
41 """
41 """
42 plugin = RhodeCodeAuthPlugin(plugin_id)
42 plugin = RhodeCodeAuthPlugin(plugin_id)
43 return plugin
43 return plugin
44
44
45
45
46 class HeadersAuthnResource(AuthnPluginResourceBase):
46 class HeadersAuthnResource(AuthnPluginResourceBase):
47 pass
47 pass
48
48
49
49
50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 header = colander.SchemaNode(
51 header = colander.SchemaNode(
52 colander.String(),
52 colander.String(),
53 default='REMOTE_USER',
53 default='REMOTE_USER',
54 description=_('Header to extract the user from'),
54 description=_('Header to extract the user from'),
55 preparer=strip_whitespace,
55 preparer=strip_whitespace,
56 title=_('Header'),
56 title=_('Header'),
57 widget='string')
57 widget='string')
58 fallback_header = colander.SchemaNode(
58 fallback_header = colander.SchemaNode(
59 colander.String(),
59 colander.String(),
60 default='HTTP_X_FORWARDED_USER',
60 default='HTTP_X_FORWARDED_USER',
61 description=_('Header to extract the user from when main one fails'),
61 description=_('Header to extract the user from when main one fails'),
62 preparer=strip_whitespace,
62 preparer=strip_whitespace,
63 title=_('Fallback header'),
63 title=_('Fallback header'),
64 widget='string')
64 widget='string')
65 clean_username = colander.SchemaNode(
65 clean_username = colander.SchemaNode(
66 colander.Boolean(),
66 colander.Boolean(),
67 default=True,
67 default=True,
68 description=_('Perform cleaning of user, if passed user has @ in '
68 description=_('Perform cleaning of user, if passed user has @ in '
69 'username then first part before @ is taken. '
69 'username then first part before @ is taken. '
70 'If there\'s \\ in the username only the part after '
70 'If there\'s \\ in the username only the part after '
71 ' \\ is taken'),
71 ' \\ is taken'),
72 missing=False,
72 missing=False,
73 title=_('Clean username'),
73 title=_('Clean username'),
74 widget='bool')
74 widget='bool')
75
75
76
76
77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78 uid = 'headers'
78 uid = 'headers'
79
79
80 def includeme(self, config):
80 def includeme(self, config):
81 config.add_authn_plugin(self)
81 config.add_authn_plugin(self)
82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
83 config.add_view(
83 config.add_view(
84 'rhodecode.authentication.views.AuthnPluginViewBase',
84 'rhodecode.authentication.views.AuthnPluginViewBase',
85 attr='settings_get',
85 attr='settings_get',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
87 request_method='GET',
87 request_method='GET',
88 route_name='auth_home',
88 route_name='auth_home',
89 context=HeadersAuthnResource)
89 context=HeadersAuthnResource)
90 config.add_view(
90 config.add_view(
91 'rhodecode.authentication.views.AuthnPluginViewBase',
91 'rhodecode.authentication.views.AuthnPluginViewBase',
92 attr='settings_post',
92 attr='settings_post',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
94 request_method='POST',
94 request_method='POST',
95 route_name='auth_home',
95 route_name='auth_home',
96 context=HeadersAuthnResource)
96 context=HeadersAuthnResource)
97
97
98 def get_display_name(self, load_from_settings=False):
98 def get_display_name(self, load_from_settings=False):
99 return _('Headers')
99 return _('Headers')
100
100
101 def get_settings_schema(self):
101 def get_settings_schema(self):
102 return HeadersSettingsSchema()
102 return HeadersSettingsSchema()
103
103
104 @hybrid_property
104 @hybrid_property
105 def name(self):
105 def name(self):
106 return u"headers"
106 return u"headers"
107
107
108 @property
108 @property
109 def is_headers_auth(self):
109 def is_headers_auth(self):
110 return True
110 return True
111
111
112 def use_fake_password(self):
112 def use_fake_password(self):
113 return True
113 return True
114
114
115 def user_activation_state(self):
115 def user_activation_state(self):
116 def_user_perms = User.get_default_user().AuthUser().permissions['global']
116 def_user_perms = User.get_default_user().AuthUser().permissions['global']
117 return 'hg.extern_activate.auto' in def_user_perms
117 return 'hg.extern_activate.auto' in def_user_perms
118
118
119 def _clean_username(self, username):
119 def _clean_username(self, username):
120 # Removing realm and domain from username
120 # Removing realm and domain from username
121 username = username.split('@')[0]
121 username = username.split('@')[0]
122 username = username.rsplit('\\')[-1]
122 username = username.rsplit('\\')[-1]
123 return username
123 return username
124
124
125 def _get_username(self, environ, settings):
125 def _get_username(self, environ, settings):
126 username = None
126 username = None
127 environ = environ or {}
127 environ = environ or {}
128 if not environ:
128 if not environ:
129 log.debug('got empty environ: %s', environ)
129 log.debug('got empty environ: %s', environ)
130
130
131 settings = settings or {}
131 settings = settings or {}
132 if settings.get('header'):
132 if settings.get('header'):
133 header = settings.get('header')
133 header = settings.get('header')
134 username = environ.get(header)
134 username = environ.get(header)
135 log.debug('extracted %s:%s', header, username)
135 log.debug('extracted %s:%s', header, username)
136
136
137 # fallback mode
137 # fallback mode
138 if not username and settings.get('fallback_header'):
138 if not username and settings.get('fallback_header'):
139 header = settings.get('fallback_header')
139 header = settings.get('fallback_header')
140 username = environ.get(header)
140 username = environ.get(header)
141 log.debug('extracted %s:%s', header, username)
141 log.debug('extracted %s:%s', header, username)
142
142
143 if username and str2bool(settings.get('clean_username')):
143 if username and str2bool(settings.get('clean_username')):
144 log.debug('Received username `%s` from headers', username)
144 log.debug('Received username `%s` from headers', username)
145 username = self._clean_username(username)
145 username = self._clean_username(username)
146 log.debug('New cleanup user is:%s', username)
146 log.debug('New cleanup user is:%s', username)
147 return username
147 return username
148
148
149 def get_user(self, username=None, **kwargs):
149 def get_user(self, username=None, **kwargs):
150 """
150 """
151 Helper method for user fetching in plugins, by default it's using
151 Helper method for user fetching in plugins, by default it's using
152 simple fetch by username, but this method can be custimized in plugins
152 simple fetch by username, but this method can be custimized in plugins
153 eg. headers auth plugin to fetch user by environ params
153 eg. headers auth plugin to fetch user by environ params
154 :param username: username if given to fetch
154 :param username: username if given to fetch
155 :param kwargs: extra arguments needed for user fetching.
155 :param kwargs: extra arguments needed for user fetching.
156 """
156 """
157 environ = kwargs.get('environ') or {}
157 environ = kwargs.get('environ') or {}
158 settings = kwargs.get('settings') or {}
158 settings = kwargs.get('settings') or {}
159 username = self._get_username(environ, settings)
159 username = self._get_username(environ, settings)
160 # we got the username, so use default method now
160 # we got the username, so use default method now
161 return super(RhodeCodeAuthPlugin, self).get_user(username)
161 return super(RhodeCodeAuthPlugin, self).get_user(username)
162
162
163 def auth(self, userobj, username, password, settings, **kwargs):
163 def auth(self, userobj, username, password, settings, **kwargs):
164 """
164 """
165 Get's the headers_auth username (or email). It tries to get username
165 Get's the headers_auth username (or email). It tries to get username
166 from REMOTE_USER if this plugin is enabled, if that fails
166 from REMOTE_USER if this plugin is enabled, if that fails
167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
168 is set. clean_username extracts the username from this data if it's
168 is set. clean_username extracts the username from this data if it's
169 having @ in it.
169 having @ in it.
170 Return None on failure. On success, return a dictionary of the form:
170 Return None on failure. On success, return a dictionary of the form:
171
171
172 see: RhodeCodeAuthPluginBase.auth_func_attrs
172 see: RhodeCodeAuthPluginBase.auth_func_attrs
173
173
174 :param userobj:
174 :param userobj:
175 :param username:
175 :param username:
176 :param password:
176 :param password:
177 :param settings:
177 :param settings:
178 :param kwargs:
178 :param kwargs:
179 """
179 """
180 environ = kwargs.get('environ')
180 environ = kwargs.get('environ')
181 if not environ:
181 if not environ:
182 log.debug('Empty environ data skipping...')
182 log.debug('Empty environ data skipping...')
183 return None
183 return None
184
184
185 if not userobj:
185 if not userobj:
186 userobj = self.get_user('', environ=environ, settings=settings)
186 userobj = self.get_user('', environ=environ, settings=settings)
187
187
188 # we don't care passed username/password for headers auth plugins.
188 # we don't care passed username/password for headers auth plugins.
189 # only way to log in is using environ
189 # only way to log in is using environ
190 username = None
190 username = None
191 if userobj:
191 if userobj:
192 username = getattr(userobj, 'username')
192 username = getattr(userobj, 'username')
193
193
194 if not username:
194 if not username:
195 # we don't have any objects in DB user doesn't exist extract
195 # we don't have any objects in DB user doesn't exist extract
196 # username from environ based on the settings
196 # username from environ based on the settings
197 username = self._get_username(environ, settings)
197 username = self._get_username(environ, settings)
198
198
199 # if cannot fetch username, it's a no-go for this plugin to proceed
199 # if cannot fetch username, it's a no-go for this plugin to proceed
200 if not username:
200 if not username:
201 return None
201 return None
202
202
203 # old attrs fetched from RhodeCode database
203 # old attrs fetched from RhodeCode database
204 admin = getattr(userobj, 'admin', False)
204 admin = getattr(userobj, 'admin', False)
205 active = getattr(userobj, 'active', True)
205 active = getattr(userobj, 'active', True)
206 email = getattr(userobj, 'email', '')
206 email = getattr(userobj, 'email', '')
207 firstname = getattr(userobj, 'firstname', '')
207 firstname = getattr(userobj, 'firstname', '')
208 lastname = getattr(userobj, 'lastname', '')
208 lastname = getattr(userobj, 'lastname', '')
209 extern_type = getattr(userobj, 'extern_type', '')
209 extern_type = getattr(userobj, 'extern_type', '')
210
210
211 user_attrs = {
211 user_attrs = {
212 'username': username,
212 'username': username,
213 'firstname': safe_unicode(firstname or username),
213 'firstname': safe_unicode(firstname or username),
214 'lastname': safe_unicode(lastname or ''),
214 'lastname': safe_unicode(lastname or ''),
215 'groups': [],
215 'groups': [],
216 'user_group_sync': False,
216 'user_group_sync': False,
217 'email': email or '',
217 'email': email or '',
218 'admin': admin or False,
218 'admin': admin or False,
219 'active': active,
219 'active': active,
220 'active_from_extern': True,
220 'active_from_extern': True,
221 'extern_name': username,
221 'extern_name': username,
222 'extern_type': extern_type,
222 'extern_type': extern_type,
223 }
223 }
224
224
225 log.info('user `%s` authenticated correctly', user_attrs['username'],
225 log.info('user `%s` authenticated correctly', user_attrs['username'],
226 extra={"action": "user_auth_ok", "module": "auth_headers", "username": user_attrs["username"]})
226 extra={"action": "user_auth_ok", "auth_module": "auth_headers", "username": user_attrs["username"]})
227 return user_attrs
227 return user_attrs
228
228
229
229
230 def includeme(config):
230 def includeme(config):
231 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
231 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
232 plugin_factory(plugin_id).includeme(config)
232 plugin_factory(plugin_id).includeme(config)
@@ -1,552 +1,552 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 logging
25 import logging
26 import traceback
26 import traceback
27
27
28 import colander
28 import colander
29 from rhodecode.translation import _
29 from rhodecode.translation import _
30 from rhodecode.authentication.base import (
30 from rhodecode.authentication.base import (
31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 from rhodecode.lib.colander_utils import strip_whitespace
34 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.exceptions import (
35 from rhodecode.lib.exceptions import (
36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 )
37 )
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 from rhodecode.model.db import User
39 from rhodecode.model.db import User
40 from rhodecode.model.validators import Missing
40 from rhodecode.model.validators import Missing
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44 try:
44 try:
45 import ldap
45 import ldap
46 except ImportError:
46 except ImportError:
47 # 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
48 # ldap lib is Missing
48 # ldap lib is Missing
49 ldap = Missing
49 ldap = Missing
50
50
51
51
52 class LdapError(Exception):
52 class LdapError(Exception):
53 pass
53 pass
54
54
55
55
56 def plugin_factory(plugin_id, *args, **kwargs):
56 def plugin_factory(plugin_id, *args, **kwargs):
57 """
57 """
58 Factory function that is called during plugin discovery.
58 Factory function that is called during plugin discovery.
59 It returns the plugin instance.
59 It returns the plugin instance.
60 """
60 """
61 plugin = RhodeCodeAuthPlugin(plugin_id)
61 plugin = RhodeCodeAuthPlugin(plugin_id)
62 return plugin
62 return plugin
63
63
64
64
65 class LdapAuthnResource(AuthnPluginResourceBase):
65 class LdapAuthnResource(AuthnPluginResourceBase):
66 pass
66 pass
67
67
68
68
69 class AuthLdap(AuthLdapBase):
69 class AuthLdap(AuthLdapBase):
70 default_tls_cert_dir = '/etc/openldap/cacerts'
70 default_tls_cert_dir = '/etc/openldap/cacerts'
71
71
72 scope_labels = {
72 scope_labels = {
73 ldap.SCOPE_BASE: 'SCOPE_BASE',
73 ldap.SCOPE_BASE: 'SCOPE_BASE',
74 ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
74 ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
75 ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
75 ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
76 }
76 }
77
77
78 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
78 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
79 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
79 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
80 tls_cert_dir=None, ldap_version=3,
80 tls_cert_dir=None, ldap_version=3,
81 search_scope='SUBTREE', attr_login='uid',
81 search_scope='SUBTREE', attr_login='uid',
82 ldap_filter='', timeout=None):
82 ldap_filter='', timeout=None):
83 if ldap == Missing:
83 if ldap == Missing:
84 raise LdapImportError("Missing or incompatible ldap library")
84 raise LdapImportError("Missing or incompatible ldap library")
85
85
86 self.debug = False
86 self.debug = False
87 self.timeout = timeout or 60 * 5
87 self.timeout = timeout or 60 * 5
88 self.ldap_version = ldap_version
88 self.ldap_version = ldap_version
89 self.ldap_server_type = 'ldap'
89 self.ldap_server_type = 'ldap'
90
90
91 self.TLS_KIND = tls_kind
91 self.TLS_KIND = tls_kind
92
92
93 if self.TLS_KIND == 'LDAPS':
93 if self.TLS_KIND == 'LDAPS':
94 port = port or 636
94 port = port or 636
95 self.ldap_server_type += 's'
95 self.ldap_server_type += 's'
96
96
97 OPT_X_TLS_DEMAND = 2
97 OPT_X_TLS_DEMAND = 2
98 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
98 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
99 self.TLS_CERT_FILE = tls_cert_file or ''
99 self.TLS_CERT_FILE = tls_cert_file or ''
100 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
100 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
101
101
102 # split server into list
102 # split server into list
103 self.SERVER_ADDRESSES = self._get_server_list(server)
103 self.SERVER_ADDRESSES = self._get_server_list(server)
104 self.LDAP_SERVER_PORT = port
104 self.LDAP_SERVER_PORT = port
105
105
106 # USE FOR READ ONLY BIND TO LDAP SERVER
106 # USE FOR READ ONLY BIND TO LDAP SERVER
107 self.attr_login = attr_login
107 self.attr_login = attr_login
108
108
109 self.LDAP_BIND_DN = safe_str(bind_dn)
109 self.LDAP_BIND_DN = safe_str(bind_dn)
110 self.LDAP_BIND_PASS = safe_str(bind_pass)
110 self.LDAP_BIND_PASS = safe_str(bind_pass)
111
111
112 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
112 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
113 self.BASE_DN = safe_str(base_dn)
113 self.BASE_DN = safe_str(base_dn)
114 self.LDAP_FILTER = safe_str(ldap_filter)
114 self.LDAP_FILTER = safe_str(ldap_filter)
115
115
116 def _get_ldap_conn(self):
116 def _get_ldap_conn(self):
117
117
118 if self.debug:
118 if self.debug:
119 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
119 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
120
120
121 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
121 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
122 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
122 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
123
123
124 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
124 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
125 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
125 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
126
126
127 if self.TLS_KIND != 'PLAIN':
127 if self.TLS_KIND != 'PLAIN':
128 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
128 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
129
129
130 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
130 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
131 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
131 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
132
132
133 # init connection now
133 # init connection now
134 ldap_servers = self._build_servers(
134 ldap_servers = self._build_servers(
135 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
135 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
136 log.debug('initializing LDAP connection to:%s', ldap_servers)
136 log.debug('initializing LDAP connection to:%s', ldap_servers)
137 ldap_conn = ldap.initialize(ldap_servers)
137 ldap_conn = ldap.initialize(ldap_servers)
138 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
138 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
139 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
139 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
140 ldap_conn.timeout = self.timeout
140 ldap_conn.timeout = self.timeout
141
141
142 if self.ldap_version == 2:
142 if self.ldap_version == 2:
143 ldap_conn.protocol = ldap.VERSION2
143 ldap_conn.protocol = ldap.VERSION2
144 else:
144 else:
145 ldap_conn.protocol = ldap.VERSION3
145 ldap_conn.protocol = ldap.VERSION3
146
146
147 if self.TLS_KIND == 'START_TLS':
147 if self.TLS_KIND == 'START_TLS':
148 ldap_conn.start_tls_s()
148 ldap_conn.start_tls_s()
149
149
150 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
150 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
151 log.debug('Trying simple_bind with password and given login DN: %r',
151 log.debug('Trying simple_bind with password and given login DN: %r',
152 self.LDAP_BIND_DN)
152 self.LDAP_BIND_DN)
153 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
153 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
154 log.debug('simple_bind successful')
154 log.debug('simple_bind successful')
155 return ldap_conn
155 return ldap_conn
156
156
157 def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
157 def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
158 scope = ldap.SCOPE_BASE
158 scope = ldap.SCOPE_BASE
159 scope_label = self.scope_labels.get(scope)
159 scope_label = self.scope_labels.get(scope)
160 ldap_filter = '(objectClass=*)'
160 ldap_filter = '(objectClass=*)'
161
161
162 try:
162 try:
163 log.debug('Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)',
163 log.debug('Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)',
164 dn, scope_label, ldap_filter)
164 dn, scope_label, ldap_filter)
165 ldap_conn.simple_bind_s(dn, safe_str(password))
165 ldap_conn.simple_bind_s(dn, safe_str(password))
166 response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=['*', '+'])
166 response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=['*', '+'])
167
167
168 if not response:
168 if not response:
169 log.error('search bind returned empty results: %r', response)
169 log.error('search bind returned empty results: %r', response)
170 return {}
170 return {}
171 else:
171 else:
172 _dn, attrs = response[0]
172 _dn, attrs = response[0]
173 return attrs
173 return attrs
174
174
175 except ldap.INVALID_CREDENTIALS:
175 except ldap.INVALID_CREDENTIALS:
176 log.debug("LDAP rejected password for user '%s': %s, org_exc:",
176 log.debug("LDAP rejected password for user '%s': %s, org_exc:",
177 username, dn, exc_info=True)
177 username, dn, exc_info=True)
178
178
179 def authenticate_ldap(self, username, password):
179 def authenticate_ldap(self, username, password):
180 """
180 """
181 Authenticate a user via LDAP and return his/her LDAP properties.
181 Authenticate a user via LDAP and return his/her LDAP properties.
182
182
183 Raises AuthenticationError if the credentials are rejected, or
183 Raises AuthenticationError if the credentials are rejected, or
184 EnvironmentError if the LDAP server can't be reached.
184 EnvironmentError if the LDAP server can't be reached.
185
185
186 :param username: username
186 :param username: username
187 :param password: password
187 :param password: password
188 """
188 """
189
189
190 uid = self.get_uid(username, self.SERVER_ADDRESSES)
190 uid = self.get_uid(username, self.SERVER_ADDRESSES)
191 user_attrs = {}
191 user_attrs = {}
192 dn = ''
192 dn = ''
193
193
194 self.validate_password(username, password)
194 self.validate_password(username, password)
195 self.validate_username(username)
195 self.validate_username(username)
196 scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
196 scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
197
197
198 ldap_conn = None
198 ldap_conn = None
199 try:
199 try:
200 ldap_conn = self._get_ldap_conn()
200 ldap_conn = self._get_ldap_conn()
201 filter_ = '(&%s(%s=%s))' % (
201 filter_ = '(&%s(%s=%s))' % (
202 self.LDAP_FILTER, self.attr_login, username)
202 self.LDAP_FILTER, self.attr_login, username)
203 log.debug("Authenticating %r filter %s and scope: %s",
203 log.debug("Authenticating %r filter %s and scope: %s",
204 self.BASE_DN, filter_, scope_label)
204 self.BASE_DN, filter_, scope_label)
205
205
206 ldap_objects = ldap_conn.search_ext_s(
206 ldap_objects = ldap_conn.search_ext_s(
207 self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=['*', '+'])
207 self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=['*', '+'])
208
208
209 if not ldap_objects:
209 if not ldap_objects:
210 log.debug("No matching LDAP objects for authentication "
210 log.debug("No matching LDAP objects for authentication "
211 "of UID:'%s' username:(%s)", uid, username)
211 "of UID:'%s' username:(%s)", uid, username)
212 raise ldap.NO_SUCH_OBJECT()
212 raise ldap.NO_SUCH_OBJECT()
213
213
214 log.debug('Found %s matching ldap object[s], trying to authenticate on each one now...', len(ldap_objects))
214 log.debug('Found %s matching ldap object[s], trying to authenticate on each one now...', len(ldap_objects))
215 for (dn, _attrs) in ldap_objects:
215 for (dn, _attrs) in ldap_objects:
216 if dn is None:
216 if dn is None:
217 continue
217 continue
218
218
219 user_attrs = self.fetch_attrs_from_simple_bind(
219 user_attrs = self.fetch_attrs_from_simple_bind(
220 ldap_conn, dn, username, password)
220 ldap_conn, dn, username, password)
221
221
222 if user_attrs:
222 if user_attrs:
223 log.debug('Got authenticated user attributes from DN:%s', dn)
223 log.debug('Got authenticated user attributes from DN:%s', dn)
224 break
224 break
225 else:
225 else:
226 raise LdapPasswordError(
226 raise LdapPasswordError(
227 'Failed to authenticate user `{}` with given password'.format(username))
227 'Failed to authenticate user `{}` with given password'.format(username))
228
228
229 except ldap.NO_SUCH_OBJECT:
229 except ldap.NO_SUCH_OBJECT:
230 log.debug("LDAP says no such user '%s' (%s), org_exc:",
230 log.debug("LDAP says no such user '%s' (%s), org_exc:",
231 uid, username, exc_info=True)
231 uid, username, exc_info=True)
232 raise LdapUsernameError('Unable to find user')
232 raise LdapUsernameError('Unable to find user')
233 except ldap.SERVER_DOWN:
233 except ldap.SERVER_DOWN:
234 org_exc = traceback.format_exc()
234 org_exc = traceback.format_exc()
235 raise LdapConnectionError(
235 raise LdapConnectionError(
236 "LDAP can't access authentication server, org_exc:%s" % org_exc)
236 "LDAP can't access authentication server, org_exc:%s" % org_exc)
237 finally:
237 finally:
238 if ldap_conn:
238 if ldap_conn:
239 log.debug('ldap: connection release')
239 log.debug('ldap: connection release')
240 try:
240 try:
241 ldap_conn.unbind_s()
241 ldap_conn.unbind_s()
242 except Exception:
242 except Exception:
243 # for any reason this can raise exception we must catch it
243 # for any reason this can raise exception we must catch it
244 # to not crush the server
244 # to not crush the server
245 pass
245 pass
246
246
247 return dn, user_attrs
247 return dn, user_attrs
248
248
249
249
250 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
250 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
251 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
251 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
252 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
252 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
253 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
253 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
254
254
255 host = colander.SchemaNode(
255 host = colander.SchemaNode(
256 colander.String(),
256 colander.String(),
257 default='',
257 default='',
258 description=_('Host[s] of the LDAP Server \n'
258 description=_('Host[s] of the LDAP Server \n'
259 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
259 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
260 'Multiple servers can be specified using commas'),
260 'Multiple servers can be specified using commas'),
261 preparer=strip_whitespace,
261 preparer=strip_whitespace,
262 title=_('LDAP Host'),
262 title=_('LDAP Host'),
263 widget='string')
263 widget='string')
264 port = colander.SchemaNode(
264 port = colander.SchemaNode(
265 colander.Int(),
265 colander.Int(),
266 default=389,
266 default=389,
267 description=_('Custom port that the LDAP server is listening on. '
267 description=_('Custom port that the LDAP server is listening on. '
268 'Default value is: 389, use 636 for LDAPS (SSL)'),
268 'Default value is: 389, use 636 for LDAPS (SSL)'),
269 preparer=strip_whitespace,
269 preparer=strip_whitespace,
270 title=_('Port'),
270 title=_('Port'),
271 validator=colander.Range(min=0, max=65536),
271 validator=colander.Range(min=0, max=65536),
272 widget='int')
272 widget='int')
273
273
274 timeout = colander.SchemaNode(
274 timeout = colander.SchemaNode(
275 colander.Int(),
275 colander.Int(),
276 default=60 * 5,
276 default=60 * 5,
277 description=_('Timeout for LDAP connection'),
277 description=_('Timeout for LDAP connection'),
278 preparer=strip_whitespace,
278 preparer=strip_whitespace,
279 title=_('Connection timeout'),
279 title=_('Connection timeout'),
280 validator=colander.Range(min=1),
280 validator=colander.Range(min=1),
281 widget='int')
281 widget='int')
282
282
283 dn_user = colander.SchemaNode(
283 dn_user = colander.SchemaNode(
284 colander.String(),
284 colander.String(),
285 default='',
285 default='',
286 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
286 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
287 'e.g., cn=admin,dc=mydomain,dc=com, or '
287 'e.g., cn=admin,dc=mydomain,dc=com, or '
288 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
288 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
289 missing='',
289 missing='',
290 preparer=strip_whitespace,
290 preparer=strip_whitespace,
291 title=_('Bind account'),
291 title=_('Bind account'),
292 widget='string')
292 widget='string')
293 dn_pass = colander.SchemaNode(
293 dn_pass = colander.SchemaNode(
294 colander.String(),
294 colander.String(),
295 default='',
295 default='',
296 description=_('Password to authenticate for given user DN.'),
296 description=_('Password to authenticate for given user DN.'),
297 missing='',
297 missing='',
298 preparer=strip_whitespace,
298 preparer=strip_whitespace,
299 title=_('Bind account password'),
299 title=_('Bind account password'),
300 widget='password')
300 widget='password')
301 tls_kind = colander.SchemaNode(
301 tls_kind = colander.SchemaNode(
302 colander.String(),
302 colander.String(),
303 default=tls_kind_choices[0],
303 default=tls_kind_choices[0],
304 description=_('TLS Type'),
304 description=_('TLS Type'),
305 title=_('Connection Security'),
305 title=_('Connection Security'),
306 validator=colander.OneOf(tls_kind_choices),
306 validator=colander.OneOf(tls_kind_choices),
307 widget='select')
307 widget='select')
308 tls_reqcert = colander.SchemaNode(
308 tls_reqcert = colander.SchemaNode(
309 colander.String(),
309 colander.String(),
310 default=tls_reqcert_choices[0],
310 default=tls_reqcert_choices[0],
311 description=_('Require Cert over TLS?. Self-signed and custom '
311 description=_('Require Cert over TLS?. Self-signed and custom '
312 'certificates can be used when\n `RhodeCode Certificate` '
312 'certificates can be used when\n `RhodeCode Certificate` '
313 'found in admin > settings > system info page is extended.'),
313 'found in admin > settings > system info page is extended.'),
314 title=_('Certificate Checks'),
314 title=_('Certificate Checks'),
315 validator=colander.OneOf(tls_reqcert_choices),
315 validator=colander.OneOf(tls_reqcert_choices),
316 widget='select')
316 widget='select')
317 tls_cert_file = colander.SchemaNode(
317 tls_cert_file = colander.SchemaNode(
318 colander.String(),
318 colander.String(),
319 default='',
319 default='',
320 description=_('This specifies the PEM-format file path containing '
320 description=_('This specifies the PEM-format file path containing '
321 'certificates for use in TLS connection.\n'
321 'certificates for use in TLS connection.\n'
322 'If not specified `TLS Cert dir` will be used'),
322 'If not specified `TLS Cert dir` will be used'),
323 title=_('TLS Cert file'),
323 title=_('TLS Cert file'),
324 missing='',
324 missing='',
325 widget='string')
325 widget='string')
326 tls_cert_dir = colander.SchemaNode(
326 tls_cert_dir = colander.SchemaNode(
327 colander.String(),
327 colander.String(),
328 default=AuthLdap.default_tls_cert_dir,
328 default=AuthLdap.default_tls_cert_dir,
329 description=_('This specifies the path of a directory that contains individual '
329 description=_('This specifies the path of a directory that contains individual '
330 'CA certificates in separate files.'),
330 'CA certificates in separate files.'),
331 title=_('TLS Cert dir'),
331 title=_('TLS Cert dir'),
332 widget='string')
332 widget='string')
333 base_dn = colander.SchemaNode(
333 base_dn = colander.SchemaNode(
334 colander.String(),
334 colander.String(),
335 default='',
335 default='',
336 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
336 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
337 'in it to be replaced with current user username \n'
337 'in it to be replaced with current user username \n'
338 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
338 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
339 missing='',
339 missing='',
340 preparer=strip_whitespace,
340 preparer=strip_whitespace,
341 title=_('Base DN'),
341 title=_('Base DN'),
342 widget='string')
342 widget='string')
343 filter = colander.SchemaNode(
343 filter = colander.SchemaNode(
344 colander.String(),
344 colander.String(),
345 default='',
345 default='',
346 description=_('Filter to narrow results \n'
346 description=_('Filter to narrow results \n'
347 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
347 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
348 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
348 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
349 missing='',
349 missing='',
350 preparer=strip_whitespace,
350 preparer=strip_whitespace,
351 title=_('LDAP Search Filter'),
351 title=_('LDAP Search Filter'),
352 widget='string')
352 widget='string')
353
353
354 search_scope = colander.SchemaNode(
354 search_scope = colander.SchemaNode(
355 colander.String(),
355 colander.String(),
356 default=search_scope_choices[2],
356 default=search_scope_choices[2],
357 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
357 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
358 title=_('LDAP Search Scope'),
358 title=_('LDAP Search Scope'),
359 validator=colander.OneOf(search_scope_choices),
359 validator=colander.OneOf(search_scope_choices),
360 widget='select')
360 widget='select')
361 attr_login = colander.SchemaNode(
361 attr_login = colander.SchemaNode(
362 colander.String(),
362 colander.String(),
363 default='uid',
363 default='uid',
364 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
364 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
365 preparer=strip_whitespace,
365 preparer=strip_whitespace,
366 title=_('Login Attribute'),
366 title=_('Login Attribute'),
367 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
367 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
368 widget='string')
368 widget='string')
369 attr_email = colander.SchemaNode(
369 attr_email = colander.SchemaNode(
370 colander.String(),
370 colander.String(),
371 default='',
371 default='',
372 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'
373 'Emails are a crucial part of RhodeCode. \n'
373 'Emails are a crucial part of RhodeCode. \n'
374 'If possible add a valid email attribute to ldap users.'),
374 'If possible add a valid email attribute to ldap users.'),
375 missing='',
375 missing='',
376 preparer=strip_whitespace,
376 preparer=strip_whitespace,
377 title=_('Email Attribute'),
377 title=_('Email Attribute'),
378 widget='string')
378 widget='string')
379 attr_firstname = colander.SchemaNode(
379 attr_firstname = colander.SchemaNode(
380 colander.String(),
380 colander.String(),
381 default='',
381 default='',
382 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
382 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
383 missing='',
383 missing='',
384 preparer=strip_whitespace,
384 preparer=strip_whitespace,
385 title=_('First Name Attribute'),
385 title=_('First Name Attribute'),
386 widget='string')
386 widget='string')
387 attr_lastname = colander.SchemaNode(
387 attr_lastname = colander.SchemaNode(
388 colander.String(),
388 colander.String(),
389 default='',
389 default='',
390 description=_('LDAP Attribute to map to last name (e.g., sn)'),
390 description=_('LDAP Attribute to map to last name (e.g., sn)'),
391 missing='',
391 missing='',
392 preparer=strip_whitespace,
392 preparer=strip_whitespace,
393 title=_('Last Name Attribute'),
393 title=_('Last Name Attribute'),
394 widget='string')
394 widget='string')
395
395
396
396
397 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
397 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
398 uid = 'ldap'
398 uid = 'ldap'
399 # used to define dynamic binding in the
399 # used to define dynamic binding in the
400 DYNAMIC_BIND_VAR = '$login'
400 DYNAMIC_BIND_VAR = '$login'
401 _settings_unsafe_keys = ['dn_pass']
401 _settings_unsafe_keys = ['dn_pass']
402
402
403 def includeme(self, config):
403 def includeme(self, config):
404 config.add_authn_plugin(self)
404 config.add_authn_plugin(self)
405 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
405 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
406 config.add_view(
406 config.add_view(
407 'rhodecode.authentication.views.AuthnPluginViewBase',
407 'rhodecode.authentication.views.AuthnPluginViewBase',
408 attr='settings_get',
408 attr='settings_get',
409 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
409 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
410 request_method='GET',
410 request_method='GET',
411 route_name='auth_home',
411 route_name='auth_home',
412 context=LdapAuthnResource)
412 context=LdapAuthnResource)
413 config.add_view(
413 config.add_view(
414 'rhodecode.authentication.views.AuthnPluginViewBase',
414 'rhodecode.authentication.views.AuthnPluginViewBase',
415 attr='settings_post',
415 attr='settings_post',
416 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
416 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
417 request_method='POST',
417 request_method='POST',
418 route_name='auth_home',
418 route_name='auth_home',
419 context=LdapAuthnResource)
419 context=LdapAuthnResource)
420
420
421 def get_settings_schema(self):
421 def get_settings_schema(self):
422 return LdapSettingsSchema()
422 return LdapSettingsSchema()
423
423
424 def get_display_name(self, load_from_settings=False):
424 def get_display_name(self, load_from_settings=False):
425 return _('LDAP')
425 return _('LDAP')
426
426
427 @classmethod
427 @classmethod
428 def docs(cls):
428 def docs(cls):
429 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
429 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
430
430
431 @hybrid_property
431 @hybrid_property
432 def name(self):
432 def name(self):
433 return u"ldap"
433 return u"ldap"
434
434
435 def use_fake_password(self):
435 def use_fake_password(self):
436 return True
436 return True
437
437
438 def user_activation_state(self):
438 def user_activation_state(self):
439 def_user_perms = User.get_default_user().AuthUser().permissions['global']
439 def_user_perms = User.get_default_user().AuthUser().permissions['global']
440 return 'hg.extern_activate.auto' in def_user_perms
440 return 'hg.extern_activate.auto' in def_user_perms
441
441
442 def try_dynamic_binding(self, username, password, current_args):
442 def try_dynamic_binding(self, username, password, current_args):
443 """
443 """
444 Detects marker inside our original bind, and uses dynamic auth if
444 Detects marker inside our original bind, and uses dynamic auth if
445 present
445 present
446 """
446 """
447
447
448 org_bind = current_args['bind_dn']
448 org_bind = current_args['bind_dn']
449 passwd = current_args['bind_pass']
449 passwd = current_args['bind_pass']
450
450
451 def has_bind_marker(username):
451 def has_bind_marker(username):
452 if self.DYNAMIC_BIND_VAR in username:
452 if self.DYNAMIC_BIND_VAR in username:
453 return True
453 return True
454
454
455 # we only passed in user with "special" variable
455 # we only passed in user with "special" variable
456 if org_bind and has_bind_marker(org_bind) and not passwd:
456 if org_bind and has_bind_marker(org_bind) and not passwd:
457 log.debug('Using dynamic user/password binding for ldap '
457 log.debug('Using dynamic user/password binding for ldap '
458 'authentication. Replacing `%s` with username',
458 'authentication. Replacing `%s` with username',
459 self.DYNAMIC_BIND_VAR)
459 self.DYNAMIC_BIND_VAR)
460 current_args['bind_dn'] = org_bind.replace(
460 current_args['bind_dn'] = org_bind.replace(
461 self.DYNAMIC_BIND_VAR, username)
461 self.DYNAMIC_BIND_VAR, username)
462 current_args['bind_pass'] = password
462 current_args['bind_pass'] = password
463
463
464 return current_args
464 return current_args
465
465
466 def auth(self, userobj, username, password, settings, **kwargs):
466 def auth(self, userobj, username, password, settings, **kwargs):
467 """
467 """
468 Given a user object (which may be null), username, a plaintext password,
468 Given a user object (which may be null), username, a plaintext password,
469 and a settings object (containing all the keys needed as listed in
469 and a settings object (containing all the keys needed as listed in
470 settings()), authenticate this user's login attempt.
470 settings()), authenticate this user's login attempt.
471
471
472 Return None on failure. On success, return a dictionary of the form:
472 Return None on failure. On success, return a dictionary of the form:
473
473
474 see: RhodeCodeAuthPluginBase.auth_func_attrs
474 see: RhodeCodeAuthPluginBase.auth_func_attrs
475 This is later validated for correctness
475 This is later validated for correctness
476 """
476 """
477
477
478 if not username or not password:
478 if not username or not password:
479 log.debug('Empty username or password skipping...')
479 log.debug('Empty username or password skipping...')
480 return None
480 return None
481
481
482 ldap_args = {
482 ldap_args = {
483 'server': settings.get('host', ''),
483 'server': settings.get('host', ''),
484 'base_dn': settings.get('base_dn', ''),
484 'base_dn': settings.get('base_dn', ''),
485 'port': settings.get('port'),
485 'port': settings.get('port'),
486 'bind_dn': settings.get('dn_user'),
486 'bind_dn': settings.get('dn_user'),
487 'bind_pass': settings.get('dn_pass'),
487 'bind_pass': settings.get('dn_pass'),
488 'tls_kind': settings.get('tls_kind'),
488 'tls_kind': settings.get('tls_kind'),
489 'tls_reqcert': settings.get('tls_reqcert'),
489 'tls_reqcert': settings.get('tls_reqcert'),
490 'tls_cert_file': settings.get('tls_cert_file'),
490 'tls_cert_file': settings.get('tls_cert_file'),
491 'tls_cert_dir': settings.get('tls_cert_dir'),
491 'tls_cert_dir': settings.get('tls_cert_dir'),
492 'search_scope': settings.get('search_scope'),
492 'search_scope': settings.get('search_scope'),
493 'attr_login': settings.get('attr_login'),
493 'attr_login': settings.get('attr_login'),
494 'ldap_version': 3,
494 'ldap_version': 3,
495 'ldap_filter': settings.get('filter'),
495 'ldap_filter': settings.get('filter'),
496 'timeout': settings.get('timeout')
496 'timeout': settings.get('timeout')
497 }
497 }
498
498
499 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
499 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
500
500
501 log.debug('Checking for ldap authentication.')
501 log.debug('Checking for ldap authentication.')
502
502
503 try:
503 try:
504 aldap = AuthLdap(**ldap_args)
504 aldap = AuthLdap(**ldap_args)
505 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
505 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
506 log.debug('Got ldap DN response %s', user_dn)
506 log.debug('Got ldap DN response %s', user_dn)
507
507
508 def get_ldap_attr(k):
508 def get_ldap_attr(k):
509 return ldap_attrs.get(settings.get(k), [''])[0]
509 return ldap_attrs.get(settings.get(k), [''])[0]
510
510
511 # old attrs fetched from RhodeCode database
511 # old attrs fetched from RhodeCode database
512 admin = getattr(userobj, 'admin', False)
512 admin = getattr(userobj, 'admin', False)
513 active = getattr(userobj, 'active', True)
513 active = getattr(userobj, 'active', True)
514 email = getattr(userobj, 'email', '')
514 email = getattr(userobj, 'email', '')
515 username = getattr(userobj, 'username', username)
515 username = getattr(userobj, 'username', username)
516 firstname = getattr(userobj, 'firstname', '')
516 firstname = getattr(userobj, 'firstname', '')
517 lastname = getattr(userobj, 'lastname', '')
517 lastname = getattr(userobj, 'lastname', '')
518 extern_type = getattr(userobj, 'extern_type', '')
518 extern_type = getattr(userobj, 'extern_type', '')
519
519
520 groups = []
520 groups = []
521
521
522 user_attrs = {
522 user_attrs = {
523 'username': username,
523 'username': username,
524 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
524 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
525 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
525 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
526 'groups': groups,
526 'groups': groups,
527 'user_group_sync': False,
527 'user_group_sync': False,
528 'email': get_ldap_attr('attr_email') or email,
528 'email': get_ldap_attr('attr_email') or email,
529 'admin': admin,
529 'admin': admin,
530 'active': active,
530 'active': active,
531 'active_from_extern': None,
531 'active_from_extern': None,
532 'extern_name': user_dn,
532 'extern_name': user_dn,
533 'extern_type': extern_type,
533 'extern_type': extern_type,
534 }
534 }
535
535
536 log.debug('ldap user: %s', user_attrs)
536 log.debug('ldap user: %s', user_attrs)
537 log.info('user `%s` authenticated correctly', user_attrs['username'],
537 log.info('user `%s` authenticated correctly', user_attrs['username'],
538 extra={"action": "user_auth_ok", "module": "auth_ldap", "username": user_attrs["username"]})
538 extra={"action": "user_auth_ok", "auth_module": "auth_ldap", "username": user_attrs["username"]})
539
539
540 return user_attrs
540 return user_attrs
541
541
542 except (LdapUsernameError, LdapPasswordError, LdapImportError):
542 except (LdapUsernameError, LdapPasswordError, LdapImportError):
543 log.exception("LDAP related exception")
543 log.exception("LDAP related exception")
544 return None
544 return None
545 except (Exception,):
545 except (Exception,):
546 log.exception("Other exception")
546 log.exception("Other exception")
547 return None
547 return None
548
548
549
549
550 def includeme(config):
550 def includeme(config):
551 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
551 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
552 plugin_factory(plugin_id).includeme(config)
552 plugin_factory(plugin_id).includeme(config)
@@ -1,172 +1,172 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 library for PAM
22 RhodeCode authentication library for PAM
23 """
23 """
24
24
25 import colander
25 import colander
26 import grp
26 import grp
27 import logging
27 import logging
28 import pam
28 import pam
29 import pwd
29 import pwd
30 import re
30 import re
31 import socket
31 import socket
32
32
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwargs):
43 def plugin_factory(plugin_id, *args, **kwargs):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class PamAuthnResource(AuthnPluginResourceBase):
52 class PamAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
57 service = colander.SchemaNode(
57 service = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='login',
59 default='login',
60 description=_('PAM service name to use for authentication.'),
60 description=_('PAM service name to use for authentication.'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('PAM service name'),
62 title=_('PAM service name'),
63 widget='string')
63 widget='string')
64 gecos = colander.SchemaNode(
64 gecos = colander.SchemaNode(
65 colander.String(),
65 colander.String(),
66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
67 description=_('Regular expression for extracting user name/email etc. '
67 description=_('Regular expression for extracting user name/email etc. '
68 'from Unix userinfo.'),
68 'from Unix userinfo.'),
69 preparer=strip_whitespace,
69 preparer=strip_whitespace,
70 title=_('Gecos Regex'),
70 title=_('Gecos Regex'),
71 widget='string')
71 widget='string')
72
72
73
73
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
75 uid = 'pam'
75 uid = 'pam'
76 # PAM authentication can be slow. Repository operations involve a lot of
76 # PAM authentication can be slow. Repository operations involve a lot of
77 # auth calls. Little caching helps speedup push/pull operations significantly
77 # auth calls. Little caching helps speedup push/pull operations significantly
78 AUTH_CACHE_TTL = 4
78 AUTH_CACHE_TTL = 4
79
79
80 def includeme(self, config):
80 def includeme(self, config):
81 config.add_authn_plugin(self)
81 config.add_authn_plugin(self)
82 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
82 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
83 config.add_view(
83 config.add_view(
84 'rhodecode.authentication.views.AuthnPluginViewBase',
84 'rhodecode.authentication.views.AuthnPluginViewBase',
85 attr='settings_get',
85 attr='settings_get',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
87 request_method='GET',
87 request_method='GET',
88 route_name='auth_home',
88 route_name='auth_home',
89 context=PamAuthnResource)
89 context=PamAuthnResource)
90 config.add_view(
90 config.add_view(
91 'rhodecode.authentication.views.AuthnPluginViewBase',
91 'rhodecode.authentication.views.AuthnPluginViewBase',
92 attr='settings_post',
92 attr='settings_post',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
94 request_method='POST',
94 request_method='POST',
95 route_name='auth_home',
95 route_name='auth_home',
96 context=PamAuthnResource)
96 context=PamAuthnResource)
97
97
98 def get_display_name(self, load_from_settings=False):
98 def get_display_name(self, load_from_settings=False):
99 return _('PAM')
99 return _('PAM')
100
100
101 @classmethod
101 @classmethod
102 def docs(cls):
102 def docs(cls):
103 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
103 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
104
104
105 @hybrid_property
105 @hybrid_property
106 def name(self):
106 def name(self):
107 return u"pam"
107 return u"pam"
108
108
109 def get_settings_schema(self):
109 def get_settings_schema(self):
110 return PamSettingsSchema()
110 return PamSettingsSchema()
111
111
112 def use_fake_password(self):
112 def use_fake_password(self):
113 return True
113 return True
114
114
115 def auth(self, userobj, username, password, settings, **kwargs):
115 def auth(self, userobj, username, password, settings, **kwargs):
116 if not username or not password:
116 if not username or not password:
117 log.debug('Empty username or password skipping...')
117 log.debug('Empty username or password skipping...')
118 return None
118 return None
119 _pam = pam.pam()
119 _pam = pam.pam()
120 auth_result = _pam.authenticate(username, password, settings["service"])
120 auth_result = _pam.authenticate(username, password, settings["service"])
121
121
122 if not auth_result:
122 if not auth_result:
123 log.error("PAM was unable to authenticate user: %s", username)
123 log.error("PAM was unable to authenticate user: %s", username)
124 return None
124 return None
125
125
126 log.debug('Got PAM response %s', auth_result)
126 log.debug('Got PAM response %s', auth_result)
127
127
128 # old attrs fetched from RhodeCode database
128 # old attrs fetched from RhodeCode database
129 default_email = "%s@%s" % (username, socket.gethostname())
129 default_email = "%s@%s" % (username, socket.gethostname())
130 admin = getattr(userobj, 'admin', False)
130 admin = getattr(userobj, 'admin', False)
131 active = getattr(userobj, 'active', True)
131 active = getattr(userobj, 'active', True)
132 email = getattr(userobj, 'email', '') or default_email
132 email = getattr(userobj, 'email', '') or default_email
133 username = getattr(userobj, 'username', username)
133 username = getattr(userobj, 'username', username)
134 firstname = getattr(userobj, 'firstname', '')
134 firstname = getattr(userobj, 'firstname', '')
135 lastname = getattr(userobj, 'lastname', '')
135 lastname = getattr(userobj, 'lastname', '')
136 extern_type = getattr(userobj, 'extern_type', '')
136 extern_type = getattr(userobj, 'extern_type', '')
137
137
138 user_attrs = {
138 user_attrs = {
139 'username': username,
139 'username': username,
140 'firstname': firstname,
140 'firstname': firstname,
141 'lastname': lastname,
141 'lastname': lastname,
142 'groups': [g.gr_name for g in grp.getgrall()
142 'groups': [g.gr_name for g in grp.getgrall()
143 if username in g.gr_mem],
143 if username in g.gr_mem],
144 'user_group_sync': True,
144 'user_group_sync': True,
145 'email': email,
145 'email': email,
146 'admin': admin,
146 'admin': admin,
147 'active': active,
147 'active': active,
148 'active_from_extern': None,
148 'active_from_extern': None,
149 'extern_name': username,
149 'extern_name': username,
150 'extern_type': extern_type,
150 'extern_type': extern_type,
151 }
151 }
152
152
153 try:
153 try:
154 user_data = pwd.getpwnam(username)
154 user_data = pwd.getpwnam(username)
155 regex = settings["gecos"]
155 regex = settings["gecos"]
156 match = re.search(regex, user_data.pw_gecos)
156 match = re.search(regex, user_data.pw_gecos)
157 if match:
157 if match:
158 user_attrs["firstname"] = match.group('first_name')
158 user_attrs["firstname"] = match.group('first_name')
159 user_attrs["lastname"] = match.group('last_name')
159 user_attrs["lastname"] = match.group('last_name')
160 except Exception:
160 except Exception:
161 log.warning("Cannot extract additional info for PAM user")
161 log.warning("Cannot extract additional info for PAM user")
162 pass
162 pass
163
163
164 log.debug("pamuser: %s", user_attrs)
164 log.debug("pamuser: %s", user_attrs)
165 log.info('user `%s` authenticated correctly', user_attrs['username'],
165 log.info('user `%s` authenticated correctly', user_attrs['username'],
166 extra={"action": "user_auth_ok", "module": "auth_pam", "username": user_attrs["username"]})
166 extra={"action": "user_auth_ok", "auth_module": "auth_pam", "username": user_attrs["username"]})
167 return user_attrs
167 return user_attrs
168
168
169
169
170 def includeme(config):
170 def includeme(config):
171 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
171 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
172 plugin_factory(plugin_id).includeme(config)
172 plugin_factory(plugin_id).includeme(config)
@@ -1,222 +1,222 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 built in internal auth
22 RhodeCode authentication plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 import colander
27 import colander
28
28
29 from rhodecode.translation import _
29 from rhodecode.translation import _
30 from rhodecode.lib.utils2 import safe_str
30 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.base import (
33 from rhodecode.authentication.base import (
34 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
34 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 def plugin_factory(plugin_id, *args, **kwargs):
40 def plugin_factory(plugin_id, *args, **kwargs):
41 plugin = RhodeCodeAuthPlugin(plugin_id)
41 plugin = RhodeCodeAuthPlugin(plugin_id)
42 return plugin
42 return plugin
43
43
44
44
45 class RhodecodeAuthnResource(AuthnPluginResourceBase):
45 class RhodecodeAuthnResource(AuthnPluginResourceBase):
46 pass
46 pass
47
47
48
48
49 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
49 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
50 uid = 'rhodecode'
50 uid = 'rhodecode'
51 AUTH_RESTRICTION_NONE = 'user_all'
51 AUTH_RESTRICTION_NONE = 'user_all'
52 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
52 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
53 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
53 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
54 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
54 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
55 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
55 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
56
56
57 def includeme(self, config):
57 def includeme(self, config):
58 config.add_authn_plugin(self)
58 config.add_authn_plugin(self)
59 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
59 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
60 config.add_view(
60 config.add_view(
61 'rhodecode.authentication.views.AuthnPluginViewBase',
61 'rhodecode.authentication.views.AuthnPluginViewBase',
62 attr='settings_get',
62 attr='settings_get',
63 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
63 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 request_method='GET',
64 request_method='GET',
65 route_name='auth_home',
65 route_name='auth_home',
66 context=RhodecodeAuthnResource)
66 context=RhodecodeAuthnResource)
67 config.add_view(
67 config.add_view(
68 'rhodecode.authentication.views.AuthnPluginViewBase',
68 'rhodecode.authentication.views.AuthnPluginViewBase',
69 attr='settings_post',
69 attr='settings_post',
70 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
70 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
71 request_method='POST',
71 request_method='POST',
72 route_name='auth_home',
72 route_name='auth_home',
73 context=RhodecodeAuthnResource)
73 context=RhodecodeAuthnResource)
74
74
75 def get_settings_schema(self):
75 def get_settings_schema(self):
76 return RhodeCodeSettingsSchema()
76 return RhodeCodeSettingsSchema()
77
77
78 def get_display_name(self, load_from_settings=False):
78 def get_display_name(self, load_from_settings=False):
79 return _('RhodeCode Internal')
79 return _('RhodeCode Internal')
80
80
81 @classmethod
81 @classmethod
82 def docs(cls):
82 def docs(cls):
83 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
83 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
84
84
85 @hybrid_property
85 @hybrid_property
86 def name(self):
86 def name(self):
87 return u"rhodecode"
87 return u"rhodecode"
88
88
89 def user_activation_state(self):
89 def user_activation_state(self):
90 def_user_perms = User.get_default_user().AuthUser().permissions['global']
90 def_user_perms = User.get_default_user().AuthUser().permissions['global']
91 return 'hg.register.auto_activate' in def_user_perms
91 return 'hg.register.auto_activate' in def_user_perms
92
92
93 def allows_authentication_from(
93 def allows_authentication_from(
94 self, user, allows_non_existing_user=True,
94 self, user, allows_non_existing_user=True,
95 allowed_auth_plugins=None, allowed_auth_sources=None):
95 allowed_auth_plugins=None, allowed_auth_sources=None):
96 """
96 """
97 Custom method for this auth that doesn't accept non existing users.
97 Custom method for this auth that doesn't accept non existing users.
98 We know that user exists in our database.
98 We know that user exists in our database.
99 """
99 """
100 allows_non_existing_user = False
100 allows_non_existing_user = False
101 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
101 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
102 user, allows_non_existing_user=allows_non_existing_user)
102 user, allows_non_existing_user=allows_non_existing_user)
103
103
104 def auth(self, userobj, username, password, settings, **kwargs):
104 def auth(self, userobj, username, password, settings, **kwargs):
105 if not userobj:
105 if not userobj:
106 log.debug('userobj was:%s skipping', userobj)
106 log.debug('userobj was:%s skipping', userobj)
107 return None
107 return None
108
108
109 if userobj.extern_type != self.name:
109 if userobj.extern_type != self.name:
110 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
110 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
111 userobj, userobj.extern_type, self.name)
111 userobj, userobj.extern_type, self.name)
112 return None
112 return None
113
113
114 # check scope of auth
114 # check scope of auth
115 scope_restriction = settings.get('scope_restriction', '')
115 scope_restriction = settings.get('scope_restriction', '')
116
116
117 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
117 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
118 and self.auth_type != HTTP_TYPE:
118 and self.auth_type != HTTP_TYPE:
119 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
119 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
120 userobj, self.auth_type, scope_restriction)
120 userobj, self.auth_type, scope_restriction)
121 return None
121 return None
122
122
123 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
123 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
124 and self.auth_type != VCS_TYPE:
124 and self.auth_type != VCS_TYPE:
125 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
125 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
126 userobj, self.auth_type, scope_restriction)
126 userobj, self.auth_type, scope_restriction)
127 return None
127 return None
128
128
129 # check super-admin restriction
129 # check super-admin restriction
130 auth_restriction = settings.get('auth_restriction', '')
130 auth_restriction = settings.get('auth_restriction', '')
131
131
132 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
132 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
133 and userobj.admin is False:
133 and userobj.admin is False:
134 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
134 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
135 userobj, auth_restriction)
135 userobj, auth_restriction)
136 return None
136 return None
137
137
138 user_attrs = {
138 user_attrs = {
139 "username": userobj.username,
139 "username": userobj.username,
140 "firstname": userobj.firstname,
140 "firstname": userobj.firstname,
141 "lastname": userobj.lastname,
141 "lastname": userobj.lastname,
142 "groups": [],
142 "groups": [],
143 'user_group_sync': False,
143 'user_group_sync': False,
144 "email": userobj.email,
144 "email": userobj.email,
145 "admin": userobj.admin,
145 "admin": userobj.admin,
146 "active": userobj.active,
146 "active": userobj.active,
147 "active_from_extern": userobj.active,
147 "active_from_extern": userobj.active,
148 "extern_name": userobj.user_id,
148 "extern_name": userobj.user_id,
149 "extern_type": userobj.extern_type,
149 "extern_type": userobj.extern_type,
150 }
150 }
151
151
152 log.debug("User attributes:%s", user_attrs)
152 log.debug("User attributes:%s", user_attrs)
153 if userobj.active:
153 if userobj.active:
154 from rhodecode.lib import auth
154 from rhodecode.lib import auth
155 crypto_backend = auth.crypto_backend()
155 crypto_backend = auth.crypto_backend()
156 password_encoded = safe_str(password)
156 password_encoded = safe_str(password)
157 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
157 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
158 password_encoded, userobj.password or '')
158 password_encoded, userobj.password or '')
159
159
160 if password_match and new_hash:
160 if password_match and new_hash:
161 log.debug('user %s properly authenticated, but '
161 log.debug('user %s properly authenticated, but '
162 'requires hash change to bcrypt', userobj)
162 'requires hash change to bcrypt', userobj)
163 # if password match, and we use OLD deprecated hash,
163 # if password match, and we use OLD deprecated hash,
164 # we should migrate this user hash password to the new hash
164 # we should migrate this user hash password to the new hash
165 # we store the new returned by hash_check_with_upgrade function
165 # we store the new returned by hash_check_with_upgrade function
166 user_attrs['_hash_migrate'] = new_hash
166 user_attrs['_hash_migrate'] = new_hash
167
167
168 if userobj.username == User.DEFAULT_USER and userobj.active:
168 if userobj.username == User.DEFAULT_USER and userobj.active:
169 log.info('user `%s` authenticated correctly as anonymous user',
169 log.info('user `%s` authenticated correctly as anonymous user',
170 userobj.username,
170 userobj.username,
171 extra={"action": "user_auth_ok", "module": "auth_rhodecode_anon", "username": userobj.username})
171 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
172 return user_attrs
172 return user_attrs
173
173
174 elif userobj.username == username and password_match:
174 elif userobj.username == username and password_match:
175 log.info('user `%s` authenticated correctly', userobj.username,
175 log.info('user `%s` authenticated correctly', userobj.username,
176 extra={"action": "user_auth_ok", "module": "auth_rhodecode", "username": userobj.username})
176 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
177 return user_attrs
177 return user_attrs
178 log.warning("user `%s` used a wrong password when "
178 log.warning("user `%s` used a wrong password when "
179 "authenticating on this plugin", userobj.username)
179 "authenticating on this plugin", userobj.username)
180 return None
180 return None
181 else:
181 else:
182 log.warning('user `%s` failed to authenticate via %s, reason: account not '
182 log.warning('user `%s` failed to authenticate via %s, reason: account not '
183 'active.', username, self.name)
183 'active.', username, self.name)
184 return None
184 return None
185
185
186
186
187 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
187 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
188
188
189 auth_restriction_choices = [
189 auth_restriction_choices = [
190 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
190 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
191 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
191 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
192 ]
192 ]
193
193
194 auth_scope_choices = [
194 auth_scope_choices = [
195 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
195 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
196 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
196 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
197 ]
197 ]
198
198
199 auth_restriction = colander.SchemaNode(
199 auth_restriction = colander.SchemaNode(
200 colander.String(),
200 colander.String(),
201 default=auth_restriction_choices[0],
201 default=auth_restriction_choices[0],
202 description=_('Allowed user types for authentication using this plugin.'),
202 description=_('Allowed user types for authentication using this plugin.'),
203 title=_('User restriction'),
203 title=_('User restriction'),
204 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
204 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
205 widget='select_with_labels',
205 widget='select_with_labels',
206 choices=auth_restriction_choices
206 choices=auth_restriction_choices
207 )
207 )
208 scope_restriction = colander.SchemaNode(
208 scope_restriction = colander.SchemaNode(
209 colander.String(),
209 colander.String(),
210 default=auth_scope_choices[0],
210 default=auth_scope_choices[0],
211 description=_('Allowed protocols for authentication using this plugin. '
211 description=_('Allowed protocols for authentication using this plugin. '
212 'VCS means GIT/HG/SVN. HTTP is web based login.'),
212 'VCS means GIT/HG/SVN. HTTP is web based login.'),
213 title=_('Scope restriction'),
213 title=_('Scope restriction'),
214 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
214 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
215 widget='select_with_labels',
215 widget='select_with_labels',
216 choices=auth_scope_choices
216 choices=auth_scope_choices
217 )
217 )
218
218
219
219
220 def includeme(config):
220 def includeme(config):
221 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
221 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
222 plugin_factory(plugin_id).includeme(config)
222 plugin_factory(plugin_id).includeme(config)
General Comments 0
You need to be logged in to leave comments. Login now