##// END OF EJS Templates
authentication: introduce a group sync flag for plugins....
marcink -
r2495:4f076134 default
parent child Browse files
Show More
@@ -1,713 +1,719 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
24
25 import colander
25 import colander
26 import copy
26 import copy
27 import logging
27 import logging
28 import time
28 import time
29 import traceback
29 import traceback
30 import warnings
30 import warnings
31 import functools
31 import functools
32
32
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from zope.cachedescriptors.property import Lazy as LazyProperty
34 from zope.cachedescriptors.property import Lazy as LazyProperty
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 caches
38 from rhodecode.lib import caches
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.utils2 import safe_int
40 from rhodecode.lib.utils2 import safe_int
41 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.user import UserModel
45 from rhodecode.model.user import UserModel
46 from rhodecode.model.user_group import UserGroupModel
46 from rhodecode.model.user_group import UserGroupModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51 # auth types that authenticate() function can receive
51 # auth types that authenticate() function can receive
52 VCS_TYPE = 'vcs'
52 VCS_TYPE = 'vcs'
53 HTTP_TYPE = 'http'
53 HTTP_TYPE = 'http'
54
54
55
55
56 class hybrid_property(object):
56 class hybrid_property(object):
57 """
57 """
58 a property decorator that works both for instance and class
58 a property decorator that works both for instance and class
59 """
59 """
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 self.fget = fget
61 self.fget = fget
62 self.fset = fset
62 self.fset = fset
63 self.fdel = fdel
63 self.fdel = fdel
64 self.expr = expr or fget
64 self.expr = expr or fget
65 functools.update_wrapper(self, fget)
65 functools.update_wrapper(self, fget)
66
66
67 def __get__(self, instance, owner):
67 def __get__(self, instance, owner):
68 if instance is None:
68 if instance is None:
69 return self.expr(owner)
69 return self.expr(owner)
70 else:
70 else:
71 return self.fget(instance)
71 return self.fget(instance)
72
72
73 def __set__(self, instance, value):
73 def __set__(self, instance, value):
74 self.fset(instance, value)
74 self.fset(instance, value)
75
75
76 def __delete__(self, instance):
76 def __delete__(self, instance):
77 self.fdel(instance)
77 self.fdel(instance)
78
78
79
79
80
81 class LazyFormencode(object):
80 class LazyFormencode(object):
82 def __init__(self, formencode_obj, *args, **kwargs):
81 def __init__(self, formencode_obj, *args, **kwargs):
83 self.formencode_obj = formencode_obj
82 self.formencode_obj = formencode_obj
84 self.args = args
83 self.args = args
85 self.kwargs = kwargs
84 self.kwargs = kwargs
86
85
87 def __call__(self, *args, **kwargs):
86 def __call__(self, *args, **kwargs):
88 from inspect import isfunction
87 from inspect import isfunction
89 formencode_obj = self.formencode_obj
88 formencode_obj = self.formencode_obj
90 if isfunction(formencode_obj):
89 if isfunction(formencode_obj):
91 # case we wrap validators into functions
90 # case we wrap validators into functions
92 formencode_obj = self.formencode_obj(*args, **kwargs)
91 formencode_obj = self.formencode_obj(*args, **kwargs)
93 return formencode_obj(*self.args, **self.kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
94
93
95
94
96 class RhodeCodeAuthPluginBase(object):
95 class RhodeCodeAuthPluginBase(object):
97 # cache the authentication request for N amount of seconds. Some kind
96 # cache the authentication request for N amount of seconds. Some kind
98 # of authentication methods are very heavy and it's very efficient to cache
97 # of authentication methods are very heavy and it's very efficient to cache
99 # the result of a call. If it's set to None (default) cache is off
98 # the result of a call. If it's set to None (default) cache is off
100 AUTH_CACHE_TTL = None
99 AUTH_CACHE_TTL = None
101 AUTH_CACHE = {}
100 AUTH_CACHE = {}
102
101
103 auth_func_attrs = {
102 auth_func_attrs = {
104 "username": "unique username",
103 "username": "unique username",
105 "firstname": "first name",
104 "firstname": "first name",
106 "lastname": "last name",
105 "lastname": "last name",
107 "email": "email address",
106 "email": "email address",
108 "groups": '["list", "of", "groups"]',
107 "groups": '["list", "of", "groups"]',
108 "user_group_sync":
109 'True|False defines if returned user groups should be synced',
109 "extern_name": "name in external source of record",
110 "extern_name": "name in external source of record",
110 "extern_type": "type of external source of record",
111 "extern_type": "type of external source of record",
111 "admin": 'True|False defines if user should be RhodeCode super admin',
112 "admin": 'True|False defines if user should be RhodeCode super admin',
112 "active":
113 "active":
113 'True|False defines active state of user internally for RhodeCode',
114 'True|False defines active state of user internally for RhodeCode',
114 "active_from_extern":
115 "active_from_extern":
115 "True|False\None, active state from the external auth, "
116 "True|False\None, active state from the external auth, "
116 "None means use definition from RhodeCode extern_type active value"
117 "None means use definition from RhodeCode extern_type active value"
118
117 }
119 }
118 # set on authenticate() method and via set_auth_type func.
120 # set on authenticate() method and via set_auth_type func.
119 auth_type = None
121 auth_type = None
120
122
121 # set on authenticate() method and via set_calling_scope_repo, this is a
123 # set on authenticate() method and via set_calling_scope_repo, this is a
122 # calling scope repository when doing authentication most likely on VCS
124 # calling scope repository when doing authentication most likely on VCS
123 # operations
125 # operations
124 acl_repo_name = None
126 acl_repo_name = None
125
127
126 # List of setting names to store encrypted. Plugins may override this list
128 # List of setting names to store encrypted. Plugins may override this list
127 # to store settings encrypted.
129 # to store settings encrypted.
128 _settings_encrypted = []
130 _settings_encrypted = []
129
131
130 # Mapping of python to DB settings model types. Plugins may override or
132 # Mapping of python to DB settings model types. Plugins may override or
131 # extend this mapping.
133 # extend this mapping.
132 _settings_type_map = {
134 _settings_type_map = {
133 colander.String: 'unicode',
135 colander.String: 'unicode',
134 colander.Integer: 'int',
136 colander.Integer: 'int',
135 colander.Boolean: 'bool',
137 colander.Boolean: 'bool',
136 colander.List: 'list',
138 colander.List: 'list',
137 }
139 }
138
140
139 # list of keys in settings that are unsafe to be logged, should be passwords
141 # list of keys in settings that are unsafe to be logged, should be passwords
140 # or other crucial credentials
142 # or other crucial credentials
141 _settings_unsafe_keys = []
143 _settings_unsafe_keys = []
142
144
143 def __init__(self, plugin_id):
145 def __init__(self, plugin_id):
144 self._plugin_id = plugin_id
146 self._plugin_id = plugin_id
145
147
146 def __str__(self):
148 def __str__(self):
147 return self.get_id()
149 return self.get_id()
148
150
149 def _get_setting_full_name(self, name):
151 def _get_setting_full_name(self, name):
150 """
152 """
151 Return the full setting name used for storing values in the database.
153 Return the full setting name used for storing values in the database.
152 """
154 """
153 # TODO: johbo: Using the name here is problematic. It would be good to
155 # TODO: johbo: Using the name here is problematic. It would be good to
154 # introduce either new models in the database to hold Plugin and
156 # introduce either new models in the database to hold Plugin and
155 # PluginSetting or to use the plugin id here.
157 # PluginSetting or to use the plugin id here.
156 return 'auth_{}_{}'.format(self.name, name)
158 return 'auth_{}_{}'.format(self.name, name)
157
159
158 def _get_setting_type(self, name):
160 def _get_setting_type(self, name):
159 """
161 """
160 Return the type of a setting. This type is defined by the SettingsModel
162 Return the type of a setting. This type is defined by the SettingsModel
161 and determines how the setting is stored in DB. Optionally the suffix
163 and determines how the setting is stored in DB. Optionally the suffix
162 `.encrypted` is appended to instruct SettingsModel to store it
164 `.encrypted` is appended to instruct SettingsModel to store it
163 encrypted.
165 encrypted.
164 """
166 """
165 schema_node = self.get_settings_schema().get(name)
167 schema_node = self.get_settings_schema().get(name)
166 db_type = self._settings_type_map.get(
168 db_type = self._settings_type_map.get(
167 type(schema_node.typ), 'unicode')
169 type(schema_node.typ), 'unicode')
168 if name in self._settings_encrypted:
170 if name in self._settings_encrypted:
169 db_type = '{}.encrypted'.format(db_type)
171 db_type = '{}.encrypted'.format(db_type)
170 return db_type
172 return db_type
171
173
172 @LazyProperty
174 @LazyProperty
173 def plugin_settings(self):
175 def plugin_settings(self):
174 settings = SettingsModel().get_all_settings()
176 settings = SettingsModel().get_all_settings()
175 return settings
177 return settings
176
178
177 def is_enabled(self):
179 def is_enabled(self):
178 """
180 """
179 Returns true if this plugin is enabled. An enabled plugin can be
181 Returns true if this plugin is enabled. An enabled plugin can be
180 configured in the admin interface but it is not consulted during
182 configured in the admin interface but it is not consulted during
181 authentication.
183 authentication.
182 """
184 """
183 auth_plugins = SettingsModel().get_auth_plugins()
185 auth_plugins = SettingsModel().get_auth_plugins()
184 return self.get_id() in auth_plugins
186 return self.get_id() in auth_plugins
185
187
186 def is_active(self):
188 def is_active(self):
187 """
189 """
188 Returns true if the plugin is activated. An activated plugin is
190 Returns true if the plugin is activated. An activated plugin is
189 consulted during authentication, assumed it is also enabled.
191 consulted during authentication, assumed it is also enabled.
190 """
192 """
191 return self.get_setting_by_name('enabled')
193 return self.get_setting_by_name('enabled')
192
194
193 def get_id(self):
195 def get_id(self):
194 """
196 """
195 Returns the plugin id.
197 Returns the plugin id.
196 """
198 """
197 return self._plugin_id
199 return self._plugin_id
198
200
199 def get_display_name(self):
201 def get_display_name(self):
200 """
202 """
201 Returns a translation string for displaying purposes.
203 Returns a translation string for displaying purposes.
202 """
204 """
203 raise NotImplementedError('Not implemented in base class')
205 raise NotImplementedError('Not implemented in base class')
204
206
205 def get_settings_schema(self):
207 def get_settings_schema(self):
206 """
208 """
207 Returns a colander schema, representing the plugin settings.
209 Returns a colander schema, representing the plugin settings.
208 """
210 """
209 return AuthnPluginSettingsSchemaBase()
211 return AuthnPluginSettingsSchemaBase()
210
212
211 def get_setting_by_name(self, name, default=None, cache=True):
213 def get_setting_by_name(self, name, default=None, cache=True):
212 """
214 """
213 Returns a plugin setting by name.
215 Returns a plugin setting by name.
214 """
216 """
215 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
217 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
216 if cache:
218 if cache:
217 plugin_settings = self.plugin_settings
219 plugin_settings = self.plugin_settings
218 else:
220 else:
219 plugin_settings = SettingsModel().get_all_settings()
221 plugin_settings = SettingsModel().get_all_settings()
220
222
221 if full_name in plugin_settings:
223 if full_name in plugin_settings:
222 return plugin_settings[full_name]
224 return plugin_settings[full_name]
223 else:
225 else:
224 return default
226 return default
225
227
226 def create_or_update_setting(self, name, value):
228 def create_or_update_setting(self, name, value):
227 """
229 """
228 Create or update a setting for this plugin in the persistent storage.
230 Create or update a setting for this plugin in the persistent storage.
229 """
231 """
230 full_name = self._get_setting_full_name(name)
232 full_name = self._get_setting_full_name(name)
231 type_ = self._get_setting_type(name)
233 type_ = self._get_setting_type(name)
232 db_setting = SettingsModel().create_or_update_setting(
234 db_setting = SettingsModel().create_or_update_setting(
233 full_name, value, type_)
235 full_name, value, type_)
234 return db_setting.app_settings_value
236 return db_setting.app_settings_value
235
237
236 def get_settings(self):
238 def get_settings(self):
237 """
239 """
238 Returns the plugin settings as dictionary.
240 Returns the plugin settings as dictionary.
239 """
241 """
240 settings = {}
242 settings = {}
241 for node in self.get_settings_schema():
243 for node in self.get_settings_schema():
242 settings[node.name] = self.get_setting_by_name(node.name)
244 settings[node.name] = self.get_setting_by_name(node.name)
243 return settings
245 return settings
244
246
245 def log_safe_settings(self, settings):
247 def log_safe_settings(self, settings):
246 """
248 """
247 returns a log safe representation of settings, without any secrets
249 returns a log safe representation of settings, without any secrets
248 """
250 """
249 settings_copy = copy.deepcopy(settings)
251 settings_copy = copy.deepcopy(settings)
250 for k in self._settings_unsafe_keys:
252 for k in self._settings_unsafe_keys:
251 if k in settings_copy:
253 if k in settings_copy:
252 del settings_copy[k]
254 del settings_copy[k]
253 return settings_copy
255 return settings_copy
254
256
255 @hybrid_property
257 @hybrid_property
256 def name(self):
258 def name(self):
257 """
259 """
258 Returns the name of this authentication plugin.
260 Returns the name of this authentication plugin.
259
261
260 :returns: string
262 :returns: string
261 """
263 """
262 raise NotImplementedError("Not implemented in base class")
264 raise NotImplementedError("Not implemented in base class")
263
265
264 def get_url_slug(self):
266 def get_url_slug(self):
265 """
267 """
266 Returns a slug which should be used when constructing URLs which refer
268 Returns a slug which should be used when constructing URLs which refer
267 to this plugin. By default it returns the plugin name. If the name is
269 to this plugin. By default it returns the plugin name. If the name is
268 not suitable for using it in an URL the plugin should override this
270 not suitable for using it in an URL the plugin should override this
269 method.
271 method.
270 """
272 """
271 return self.name
273 return self.name
272
274
273 @property
275 @property
274 def is_headers_auth(self):
276 def is_headers_auth(self):
275 """
277 """
276 Returns True if this authentication plugin uses HTTP headers as
278 Returns True if this authentication plugin uses HTTP headers as
277 authentication method.
279 authentication method.
278 """
280 """
279 return False
281 return False
280
282
281 @hybrid_property
283 @hybrid_property
282 def is_container_auth(self):
284 def is_container_auth(self):
283 """
285 """
284 Deprecated method that indicates if this authentication plugin uses
286 Deprecated method that indicates if this authentication plugin uses
285 HTTP headers as authentication method.
287 HTTP headers as authentication method.
286 """
288 """
287 warnings.warn(
289 warnings.warn(
288 'Use is_headers_auth instead.', category=DeprecationWarning)
290 'Use is_headers_auth instead.', category=DeprecationWarning)
289 return self.is_headers_auth
291 return self.is_headers_auth
290
292
291 @hybrid_property
293 @hybrid_property
292 def allows_creating_users(self):
294 def allows_creating_users(self):
293 """
295 """
294 Defines if Plugin allows users to be created on-the-fly when
296 Defines if Plugin allows users to be created on-the-fly when
295 authentication is called. Controls how external plugins should behave
297 authentication is called. Controls how external plugins should behave
296 in terms if they are allowed to create new users, or not. Base plugins
298 in terms if they are allowed to create new users, or not. Base plugins
297 should not be allowed to, but External ones should be !
299 should not be allowed to, but External ones should be !
298
300
299 :return: bool
301 :return: bool
300 """
302 """
301 return False
303 return False
302
304
303 def set_auth_type(self, auth_type):
305 def set_auth_type(self, auth_type):
304 self.auth_type = auth_type
306 self.auth_type = auth_type
305
307
306 def set_calling_scope_repo(self, acl_repo_name):
308 def set_calling_scope_repo(self, acl_repo_name):
307 self.acl_repo_name = acl_repo_name
309 self.acl_repo_name = acl_repo_name
308
310
309 def allows_authentication_from(
311 def allows_authentication_from(
310 self, user, allows_non_existing_user=True,
312 self, user, allows_non_existing_user=True,
311 allowed_auth_plugins=None, allowed_auth_sources=None):
313 allowed_auth_plugins=None, allowed_auth_sources=None):
312 """
314 """
313 Checks if this authentication module should accept a request for
315 Checks if this authentication module should accept a request for
314 the current user.
316 the current user.
315
317
316 :param user: user object fetched using plugin's get_user() method.
318 :param user: user object fetched using plugin's get_user() method.
317 :param allows_non_existing_user: if True, don't allow the
319 :param allows_non_existing_user: if True, don't allow the
318 user to be empty, meaning not existing in our database
320 user to be empty, meaning not existing in our database
319 :param allowed_auth_plugins: if provided, users extern_type will be
321 :param allowed_auth_plugins: if provided, users extern_type will be
320 checked against a list of provided extern types, which are plugin
322 checked against a list of provided extern types, which are plugin
321 auth_names in the end
323 auth_names in the end
322 :param allowed_auth_sources: authentication type allowed,
324 :param allowed_auth_sources: authentication type allowed,
323 `http` or `vcs` default is both.
325 `http` or `vcs` default is both.
324 defines if plugin will accept only http authentication vcs
326 defines if plugin will accept only http authentication vcs
325 authentication(git/hg) or both
327 authentication(git/hg) or both
326 :returns: boolean
328 :returns: boolean
327 """
329 """
328 if not user and not allows_non_existing_user:
330 if not user and not allows_non_existing_user:
329 log.debug('User is empty but plugin does not allow empty users,'
331 log.debug('User is empty but plugin does not allow empty users,'
330 'not allowed to authenticate')
332 'not allowed to authenticate')
331 return False
333 return False
332
334
333 expected_auth_plugins = allowed_auth_plugins or [self.name]
335 expected_auth_plugins = allowed_auth_plugins or [self.name]
334 if user and (user.extern_type and
336 if user and (user.extern_type and
335 user.extern_type not in expected_auth_plugins):
337 user.extern_type not in expected_auth_plugins):
336 log.debug(
338 log.debug(
337 'User `%s` is bound to `%s` auth type. Plugin allows only '
339 'User `%s` is bound to `%s` auth type. Plugin allows only '
338 '%s, skipping', user, user.extern_type, expected_auth_plugins)
340 '%s, skipping', user, user.extern_type, expected_auth_plugins)
339
341
340 return False
342 return False
341
343
342 # by default accept both
344 # by default accept both
343 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
345 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
344 if self.auth_type not in expected_auth_from:
346 if self.auth_type not in expected_auth_from:
345 log.debug('Current auth source is %s but plugin only allows %s',
347 log.debug('Current auth source is %s but plugin only allows %s',
346 self.auth_type, expected_auth_from)
348 self.auth_type, expected_auth_from)
347 return False
349 return False
348
350
349 return True
351 return True
350
352
351 def get_user(self, username=None, **kwargs):
353 def get_user(self, username=None, **kwargs):
352 """
354 """
353 Helper method for user fetching in plugins, by default it's using
355 Helper method for user fetching in plugins, by default it's using
354 simple fetch by username, but this method can be custimized in plugins
356 simple fetch by username, but this method can be custimized in plugins
355 eg. headers auth plugin to fetch user by environ params
357 eg. headers auth plugin to fetch user by environ params
356
358
357 :param username: username if given to fetch from database
359 :param username: username if given to fetch from database
358 :param kwargs: extra arguments needed for user fetching.
360 :param kwargs: extra arguments needed for user fetching.
359 """
361 """
360 user = None
362 user = None
361 log.debug(
363 log.debug(
362 'Trying to fetch user `%s` from RhodeCode database', username)
364 'Trying to fetch user `%s` from RhodeCode database', username)
363 if username:
365 if username:
364 user = User.get_by_username(username)
366 user = User.get_by_username(username)
365 if not user:
367 if not user:
366 log.debug('User not found, fallback to fetch user in '
368 log.debug('User not found, fallback to fetch user in '
367 'case insensitive mode')
369 'case insensitive mode')
368 user = User.get_by_username(username, case_insensitive=True)
370 user = User.get_by_username(username, case_insensitive=True)
369 else:
371 else:
370 log.debug('provided username:`%s` is empty skipping...', username)
372 log.debug('provided username:`%s` is empty skipping...', username)
371 if not user:
373 if not user:
372 log.debug('User `%s` not found in database', username)
374 log.debug('User `%s` not found in database', username)
373 else:
375 else:
374 log.debug('Got DB user:%s', user)
376 log.debug('Got DB user:%s', user)
375 return user
377 return user
376
378
377 def user_activation_state(self):
379 def user_activation_state(self):
378 """
380 """
379 Defines user activation state when creating new users
381 Defines user activation state when creating new users
380
382
381 :returns: boolean
383 :returns: boolean
382 """
384 """
383 raise NotImplementedError("Not implemented in base class")
385 raise NotImplementedError("Not implemented in base class")
384
386
385 def auth(self, userobj, username, passwd, settings, **kwargs):
387 def auth(self, userobj, username, passwd, settings, **kwargs):
386 """
388 """
387 Given a user object (which may be null), username, a plaintext
389 Given a user object (which may be null), username, a plaintext
388 password, and a settings object (containing all the keys needed as
390 password, and a settings object (containing all the keys needed as
389 listed in settings()), authenticate this user's login attempt.
391 listed in settings()), authenticate this user's login attempt.
390
392
391 Return None on failure. On success, return a dictionary of the form:
393 Return None on failure. On success, return a dictionary of the form:
392
394
393 see: RhodeCodeAuthPluginBase.auth_func_attrs
395 see: RhodeCodeAuthPluginBase.auth_func_attrs
394 This is later validated for correctness
396 This is later validated for correctness
395 """
397 """
396 raise NotImplementedError("not implemented in base class")
398 raise NotImplementedError("not implemented in base class")
397
399
398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
400 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
399 """
401 """
400 Wrapper to call self.auth() that validates call on it
402 Wrapper to call self.auth() that validates call on it
401
403
402 :param userobj: userobj
404 :param userobj: userobj
403 :param username: username
405 :param username: username
404 :param passwd: plaintext password
406 :param passwd: plaintext password
405 :param settings: plugin settings
407 :param settings: plugin settings
406 """
408 """
407 auth = self.auth(userobj, username, passwd, settings, **kwargs)
409 auth = self.auth(userobj, username, passwd, settings, **kwargs)
408 if auth:
410 if auth:
409 auth['_plugin'] = self.name
411 auth['_plugin'] = self.name
410 auth['_ttl_cache'] = self.get_ttl_cache(settings)
412 auth['_ttl_cache'] = self.get_ttl_cache(settings)
411 # check if hash should be migrated ?
413 # check if hash should be migrated ?
412 new_hash = auth.get('_hash_migrate')
414 new_hash = auth.get('_hash_migrate')
413 if new_hash:
415 if new_hash:
414 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
416 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
417 if 'user_group_sync' not in auth:
418 auth['user_group_sync'] = False
415 return self._validate_auth_return(auth)
419 return self._validate_auth_return(auth)
416
417 return auth
420 return auth
418
421
419 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
422 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
420 new_hash_cypher = _RhodeCodeCryptoBCrypt()
423 new_hash_cypher = _RhodeCodeCryptoBCrypt()
421 # extra checks, so make sure new hash is correct.
424 # extra checks, so make sure new hash is correct.
422 password_encoded = safe_str(password)
425 password_encoded = safe_str(password)
423 if new_hash and new_hash_cypher.hash_check(
426 if new_hash and new_hash_cypher.hash_check(
424 password_encoded, new_hash):
427 password_encoded, new_hash):
425 cur_user = User.get_by_username(username)
428 cur_user = User.get_by_username(username)
426 cur_user.password = new_hash
429 cur_user.password = new_hash
427 Session().add(cur_user)
430 Session().add(cur_user)
428 Session().flush()
431 Session().flush()
429 log.info('Migrated user %s hash to bcrypt', cur_user)
432 log.info('Migrated user %s hash to bcrypt', cur_user)
430
433
431 def _validate_auth_return(self, ret):
434 def _validate_auth_return(self, ret):
432 if not isinstance(ret, dict):
435 if not isinstance(ret, dict):
433 raise Exception('returned value from auth must be a dict')
436 raise Exception('returned value from auth must be a dict')
434 for k in self.auth_func_attrs:
437 for k in self.auth_func_attrs:
435 if k not in ret:
438 if k not in ret:
436 raise Exception('Missing %s attribute from returned data' % k)
439 raise Exception('Missing %s attribute from returned data' % k)
437 return ret
440 return ret
438
441
439 def get_ttl_cache(self, settings=None):
442 def get_ttl_cache(self, settings=None):
440 plugin_settings = settings or self.get_settings()
443 plugin_settings = settings or self.get_settings()
441 cache_ttl = 0
444 cache_ttl = 0
442
445
443 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
446 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
444 # plugin cache set inside is more important than the settings value
447 # plugin cache set inside is more important than the settings value
445 cache_ttl = self.AUTH_CACHE_TTL
448 cache_ttl = self.AUTH_CACHE_TTL
446 elif plugin_settings.get('cache_ttl'):
449 elif plugin_settings.get('cache_ttl'):
447 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
450 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
448
451
449 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
452 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
450 return plugin_cache_active, cache_ttl
453 return plugin_cache_active, cache_ttl
451
454
452
455
453 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
456 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
454
457
455 @hybrid_property
458 @hybrid_property
456 def allows_creating_users(self):
459 def allows_creating_users(self):
457 return True
460 return True
458
461
459 def use_fake_password(self):
462 def use_fake_password(self):
460 """
463 """
461 Return a boolean that indicates whether or not we should set the user's
464 Return a boolean that indicates whether or not we should set the user's
462 password to a random value when it is authenticated by this plugin.
465 password to a random value when it is authenticated by this plugin.
463 If your plugin provides authentication, then you will generally
466 If your plugin provides authentication, then you will generally
464 want this.
467 want this.
465
468
466 :returns: boolean
469 :returns: boolean
467 """
470 """
468 raise NotImplementedError("Not implemented in base class")
471 raise NotImplementedError("Not implemented in base class")
469
472
470 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
473 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
471 # at this point _authenticate calls plugin's `auth()` function
474 # at this point _authenticate calls plugin's `auth()` function
472 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
475 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
473 userobj, username, passwd, settings, **kwargs)
476 userobj, username, passwd, settings, **kwargs)
474
477
475 if auth:
478 if auth:
476 # maybe plugin will clean the username ?
479 # maybe plugin will clean the username ?
477 # we should use the return value
480 # we should use the return value
478 username = auth['username']
481 username = auth['username']
479
482
480 # if external source tells us that user is not active, we should
483 # if external source tells us that user is not active, we should
481 # skip rest of the process. This can prevent from creating users in
484 # skip rest of the process. This can prevent from creating users in
482 # RhodeCode when using external authentication, but if it's
485 # RhodeCode when using external authentication, but if it's
483 # inactive user we shouldn't create that user anyway
486 # inactive user we shouldn't create that user anyway
484 if auth['active_from_extern'] is False:
487 if auth['active_from_extern'] is False:
485 log.warning(
488 log.warning(
486 "User %s authenticated against %s, but is inactive",
489 "User %s authenticated against %s, but is inactive",
487 username, self.__module__)
490 username, self.__module__)
488 return None
491 return None
489
492
490 cur_user = User.get_by_username(username, case_insensitive=True)
493 cur_user = User.get_by_username(username, case_insensitive=True)
491 is_user_existing = cur_user is not None
494 is_user_existing = cur_user is not None
492
495
493 if is_user_existing:
496 if is_user_existing:
494 log.debug('Syncing user `%s` from '
497 log.debug('Syncing user `%s` from '
495 '`%s` plugin', username, self.name)
498 '`%s` plugin', username, self.name)
496 else:
499 else:
497 log.debug('Creating non existing user `%s` from '
500 log.debug('Creating non existing user `%s` from '
498 '`%s` plugin', username, self.name)
501 '`%s` plugin', username, self.name)
499
502
500 if self.allows_creating_users:
503 if self.allows_creating_users:
501 log.debug('Plugin `%s` allows to '
504 log.debug('Plugin `%s` allows to '
502 'create new users', self.name)
505 'create new users', self.name)
503 else:
506 else:
504 log.debug('Plugin `%s` does not allow to '
507 log.debug('Plugin `%s` does not allow to '
505 'create new users', self.name)
508 'create new users', self.name)
506
509
507 user_parameters = {
510 user_parameters = {
508 'username': username,
511 'username': username,
509 'email': auth["email"],
512 'email': auth["email"],
510 'firstname': auth["firstname"],
513 'firstname': auth["firstname"],
511 'lastname': auth["lastname"],
514 'lastname': auth["lastname"],
512 'active': auth["active"],
515 'active': auth["active"],
513 'admin': auth["admin"],
516 'admin': auth["admin"],
514 'extern_name': auth["extern_name"],
517 'extern_name': auth["extern_name"],
515 'extern_type': self.name,
518 'extern_type': self.name,
516 'plugin': self,
519 'plugin': self,
517 'allow_to_create_user': self.allows_creating_users,
520 'allow_to_create_user': self.allows_creating_users,
518 }
521 }
519
522
520 if not is_user_existing:
523 if not is_user_existing:
521 if self.use_fake_password():
524 if self.use_fake_password():
522 # Randomize the PW because we don't need it, but don't want
525 # Randomize the PW because we don't need it, but don't want
523 # them blank either
526 # them blank either
524 passwd = PasswordGenerator().gen_password(length=16)
527 passwd = PasswordGenerator().gen_password(length=16)
525 user_parameters['password'] = passwd
528 user_parameters['password'] = passwd
526 else:
529 else:
527 # Since the password is required by create_or_update method of
530 # Since the password is required by create_or_update method of
528 # UserModel, we need to set it explicitly.
531 # UserModel, we need to set it explicitly.
529 # The create_or_update method is smart and recognises the
532 # The create_or_update method is smart and recognises the
530 # password hashes as well.
533 # password hashes as well.
531 user_parameters['password'] = cur_user.password
534 user_parameters['password'] = cur_user.password
532
535
533 # we either create or update users, we also pass the flag
536 # we either create or update users, we also pass the flag
534 # that controls if this method can actually do that.
537 # that controls if this method can actually do that.
535 # raises NotAllowedToCreateUserError if it cannot, and we try to.
538 # raises NotAllowedToCreateUserError if it cannot, and we try to.
536 user = UserModel().create_or_update(**user_parameters)
539 user = UserModel().create_or_update(**user_parameters)
537 Session().flush()
540 Session().flush()
538 # enforce user is just in given groups, all of them has to be ones
541 # enforce user is just in given groups, all of them has to be ones
539 # created from plugins. We store this info in _group_data JSON
542 # created from plugins. We store this info in _group_data JSON
540 # field
543 # field
541 try:
544
542 groups = auth['groups'] or []
545 if auth['user_group_sync']:
543 log.debug(
546 try:
544 'Performing user_group sync based on set `%s` '
547 groups = auth['groups'] or []
545 'returned by this plugin', groups)
548 log.debug(
546 UserGroupModel().enforce_groups(user, groups, self.name)
549 'Performing user_group sync based on set `%s` '
547 except Exception:
550 'returned by `%s` plugin', groups, self.name)
548 # for any reason group syncing fails, we should
551 UserGroupModel().enforce_groups(user, groups, self.name)
549 # proceed with login
552 except Exception:
550 log.error(traceback.format_exc())
553 # for any reason group syncing fails, we should
554 # proceed with login
555 log.error(traceback.format_exc())
556
551 Session().commit()
557 Session().commit()
552 return auth
558 return auth
553
559
554
560
555 def loadplugin(plugin_id):
561 def loadplugin(plugin_id):
556 """
562 """
557 Loads and returns an instantiated authentication plugin.
563 Loads and returns an instantiated authentication plugin.
558 Returns the RhodeCodeAuthPluginBase subclass on success,
564 Returns the RhodeCodeAuthPluginBase subclass on success,
559 or None on failure.
565 or None on failure.
560 """
566 """
561 # TODO: Disusing pyramids thread locals to retrieve the registry.
567 # TODO: Disusing pyramids thread locals to retrieve the registry.
562 authn_registry = get_authn_registry()
568 authn_registry = get_authn_registry()
563 plugin = authn_registry.get_plugin(plugin_id)
569 plugin = authn_registry.get_plugin(plugin_id)
564 if plugin is None:
570 if plugin is None:
565 log.error('Authentication plugin not found: "%s"', plugin_id)
571 log.error('Authentication plugin not found: "%s"', plugin_id)
566 return plugin
572 return plugin
567
573
568
574
569 def get_authn_registry(registry=None):
575 def get_authn_registry(registry=None):
570 registry = registry or get_current_registry()
576 registry = registry or get_current_registry()
571 authn_registry = registry.getUtility(IAuthnPluginRegistry)
577 authn_registry = registry.getUtility(IAuthnPluginRegistry)
572 return authn_registry
578 return authn_registry
573
579
574
580
575 def get_auth_cache_manager(custom_ttl=None):
581 def get_auth_cache_manager(custom_ttl=None):
576 return caches.get_cache_manager(
582 return caches.get_cache_manager(
577 'auth_plugins', 'rhodecode.authentication', custom_ttl)
583 'auth_plugins', 'rhodecode.authentication', custom_ttl)
578
584
579
585
580 def get_perms_cache_manager(custom_ttl=None):
586 def get_perms_cache_manager(custom_ttl=None):
581 return caches.get_cache_manager(
587 return caches.get_cache_manager(
582 'auth_plugins', 'rhodecode.permissions', custom_ttl)
588 'auth_plugins', 'rhodecode.permissions', custom_ttl)
583
589
584
590
585 def authenticate(username, password, environ=None, auth_type=None,
591 def authenticate(username, password, environ=None, auth_type=None,
586 skip_missing=False, registry=None, acl_repo_name=None):
592 skip_missing=False, registry=None, acl_repo_name=None):
587 """
593 """
588 Authentication function used for access control,
594 Authentication function used for access control,
589 It tries to authenticate based on enabled authentication modules.
595 It tries to authenticate based on enabled authentication modules.
590
596
591 :param username: username can be empty for headers auth
597 :param username: username can be empty for headers auth
592 :param password: password can be empty for headers auth
598 :param password: password can be empty for headers auth
593 :param environ: environ headers passed for headers auth
599 :param environ: environ headers passed for headers auth
594 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
600 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
595 :param skip_missing: ignores plugins that are in db but not in environment
601 :param skip_missing: ignores plugins that are in db but not in environment
596 :returns: None if auth failed, plugin_user dict if auth is correct
602 :returns: None if auth failed, plugin_user dict if auth is correct
597 """
603 """
598 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
604 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
599 raise ValueError('auth type must be on of http, vcs got "%s" instead'
605 raise ValueError('auth type must be on of http, vcs got "%s" instead'
600 % auth_type)
606 % auth_type)
601 headers_only = environ and not (username and password)
607 headers_only = environ and not (username and password)
602
608
603 authn_registry = get_authn_registry(registry)
609 authn_registry = get_authn_registry(registry)
604 plugins_to_check = authn_registry.get_plugins_for_authentication()
610 plugins_to_check = authn_registry.get_plugins_for_authentication()
605 log.debug('Starting ordered authentication chain using %s plugins',
611 log.debug('Starting ordered authentication chain using %s plugins',
606 plugins_to_check)
612 plugins_to_check)
607 for plugin in plugins_to_check:
613 for plugin in plugins_to_check:
608 plugin.set_auth_type(auth_type)
614 plugin.set_auth_type(auth_type)
609 plugin.set_calling_scope_repo(acl_repo_name)
615 plugin.set_calling_scope_repo(acl_repo_name)
610
616
611 if headers_only and not plugin.is_headers_auth:
617 if headers_only and not plugin.is_headers_auth:
612 log.debug('Auth type is for headers only and plugin `%s` is not '
618 log.debug('Auth type is for headers only and plugin `%s` is not '
613 'headers plugin, skipping...', plugin.get_id())
619 'headers plugin, skipping...', plugin.get_id())
614 continue
620 continue
615
621
616 # load plugin settings from RhodeCode database
622 # load plugin settings from RhodeCode database
617 plugin_settings = plugin.get_settings()
623 plugin_settings = plugin.get_settings()
618 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
624 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
619 log.debug('Plugin settings:%s', plugin_sanitized_settings)
625 log.debug('Plugin settings:%s', plugin_sanitized_settings)
620
626
621 log.debug('Trying authentication using ** %s **', plugin.get_id())
627 log.debug('Trying authentication using ** %s **', plugin.get_id())
622 # use plugin's method of user extraction.
628 # use plugin's method of user extraction.
623 user = plugin.get_user(username, environ=environ,
629 user = plugin.get_user(username, environ=environ,
624 settings=plugin_settings)
630 settings=plugin_settings)
625 display_user = user.username if user else username
631 display_user = user.username if user else username
626 log.debug(
632 log.debug(
627 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
633 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
628
634
629 if not plugin.allows_authentication_from(user):
635 if not plugin.allows_authentication_from(user):
630 log.debug('Plugin %s does not accept user `%s` for authentication',
636 log.debug('Plugin %s does not accept user `%s` for authentication',
631 plugin.get_id(), display_user)
637 plugin.get_id(), display_user)
632 continue
638 continue
633 else:
639 else:
634 log.debug('Plugin %s accepted user `%s` for authentication',
640 log.debug('Plugin %s accepted user `%s` for authentication',
635 plugin.get_id(), display_user)
641 plugin.get_id(), display_user)
636
642
637 log.info('Authenticating user `%s` using %s plugin',
643 log.info('Authenticating user `%s` using %s plugin',
638 display_user, plugin.get_id())
644 display_user, plugin.get_id())
639
645
640 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
646 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
641
647
642 # get instance of cache manager configured for a namespace
648 # get instance of cache manager configured for a namespace
643 cache_manager = get_auth_cache_manager(custom_ttl=cache_ttl)
649 cache_manager = get_auth_cache_manager(custom_ttl=cache_ttl)
644
650
645 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
651 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
646 plugin.get_id(), plugin_cache_active, cache_ttl)
652 plugin.get_id(), plugin_cache_active, cache_ttl)
647
653
648 # for environ based password can be empty, but then the validation is
654 # for environ based password can be empty, but then the validation is
649 # on the server that fills in the env data needed for authentication
655 # on the server that fills in the env data needed for authentication
650
656
651 _password_hash = caches.compute_key_from_params(
657 _password_hash = caches.compute_key_from_params(
652 plugin.name, username, (password or ''))
658 plugin.name, username, (password or ''))
653
659
654 # _authenticate is a wrapper for .auth() method of plugin.
660 # _authenticate is a wrapper for .auth() method of plugin.
655 # it checks if .auth() sends proper data.
661 # it checks if .auth() sends proper data.
656 # For RhodeCodeExternalAuthPlugin it also maps users to
662 # For RhodeCodeExternalAuthPlugin it also maps users to
657 # Database and maps the attributes returned from .auth()
663 # Database and maps the attributes returned from .auth()
658 # to RhodeCode database. If this function returns data
664 # to RhodeCode database. If this function returns data
659 # then auth is correct.
665 # then auth is correct.
660 start = time.time()
666 start = time.time()
661 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
667 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
662
668
663 def auth_func():
669 def auth_func():
664 """
670 """
665 This function is used internally in Cache of Beaker to calculate
671 This function is used internally in Cache of Beaker to calculate
666 Results
672 Results
667 """
673 """
668 log.debug('auth: calculating password access now...')
674 log.debug('auth: calculating password access now...')
669 return plugin._authenticate(
675 return plugin._authenticate(
670 user, username, password, plugin_settings,
676 user, username, password, plugin_settings,
671 environ=environ or {})
677 environ=environ or {})
672
678
673 if plugin_cache_active:
679 if plugin_cache_active:
674 log.debug('Trying to fetch cached auth by %s', _password_hash[:6])
680 log.debug('Trying to fetch cached auth by `...%s`', _password_hash[:6])
675 plugin_user = cache_manager.get(
681 plugin_user = cache_manager.get(
676 _password_hash, createfunc=auth_func)
682 _password_hash, createfunc=auth_func)
677 else:
683 else:
678 plugin_user = auth_func()
684 plugin_user = auth_func()
679
685
680 auth_time = time.time() - start
686 auth_time = time.time() - start
681 log.debug('Authentication for plugin `%s` completed in %.3fs, '
687 log.debug('Authentication for plugin `%s` completed in %.3fs, '
682 'expiration time of fetched cache %.1fs.',
688 'expiration time of fetched cache %.1fs.',
683 plugin.get_id(), auth_time, cache_ttl)
689 plugin.get_id(), auth_time, cache_ttl)
684
690
685 log.debug('PLUGIN USER DATA: %s', plugin_user)
691 log.debug('PLUGIN USER DATA: %s', plugin_user)
686
692
687 if plugin_user:
693 if plugin_user:
688 log.debug('Plugin returned proper authentication data')
694 log.debug('Plugin returned proper authentication data')
689 return plugin_user
695 return plugin_user
690 # we failed to Auth because .auth() method didn't return proper user
696 # we failed to Auth because .auth() method didn't return proper user
691 log.debug("User `%s` failed to authenticate against %s",
697 log.debug("User `%s` failed to authenticate against %s",
692 display_user, plugin.get_id())
698 display_user, plugin.get_id())
693
699
694 # case when we failed to authenticate against all defined plugins
700 # case when we failed to authenticate against all defined plugins
695 return None
701 return None
696
702
697
703
698 def chop_at(s, sub, inclusive=False):
704 def chop_at(s, sub, inclusive=False):
699 """Truncate string ``s`` at the first occurrence of ``sub``.
705 """Truncate string ``s`` at the first occurrence of ``sub``.
700
706
701 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
707 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
702
708
703 >>> chop_at("plutocratic brats", "rat")
709 >>> chop_at("plutocratic brats", "rat")
704 'plutoc'
710 'plutoc'
705 >>> chop_at("plutocratic brats", "rat", True)
711 >>> chop_at("plutocratic brats", "rat", True)
706 'plutocrat'
712 'plutocrat'
707 """
713 """
708 pos = s.find(sub)
714 pos = s.find(sub)
709 if pos == -1:
715 if pos == -1:
710 return s
716 return s
711 if inclusive:
717 if inclusive:
712 return s[:pos+len(sub)]
718 return s[:pos+len(sub)]
713 return s[:pos]
719 return s[:pos]
@@ -1,284 +1,285 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 Atlassian CROWD
22 RhodeCode authentication plugin for Atlassian CROWD
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import base64
27 import base64
28 import logging
28 import logging
29 import urllib2
29 import urllib2
30
30
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.ext_json import json, formatted_json
37 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
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 CrowdAuthnResource(AuthnPluginResourceBase):
52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 host = colander.SchemaNode(
57 host = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='127.0.0.1',
59 default='127.0.0.1',
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('Host'),
62 title=_('Host'),
63 widget='string')
63 widget='string')
64 port = colander.SchemaNode(
64 port = colander.SchemaNode(
65 colander.Int(),
65 colander.Int(),
66 default=8095,
66 default=8095,
67 description=_('The Port in use by the Atlassian CROWD Server'),
67 description=_('The Port in use by the Atlassian CROWD Server'),
68 preparer=strip_whitespace,
68 preparer=strip_whitespace,
69 title=_('Port'),
69 title=_('Port'),
70 validator=colander.Range(min=0, max=65536),
70 validator=colander.Range(min=0, max=65536),
71 widget='int')
71 widget='int')
72 app_name = colander.SchemaNode(
72 app_name = colander.SchemaNode(
73 colander.String(),
73 colander.String(),
74 default='',
74 default='',
75 description=_('The Application Name to authenticate to CROWD'),
75 description=_('The Application Name to authenticate to CROWD'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('Application Name'),
77 title=_('Application Name'),
78 widget='string')
78 widget='string')
79 app_password = colander.SchemaNode(
79 app_password = colander.SchemaNode(
80 colander.String(),
80 colander.String(),
81 default='',
81 default='',
82 description=_('The password to authenticate to CROWD'),
82 description=_('The password to authenticate to CROWD'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Application Password'),
84 title=_('Application Password'),
85 widget='password')
85 widget='password')
86 admin_groups = colander.SchemaNode(
86 admin_groups = colander.SchemaNode(
87 colander.String(),
87 colander.String(),
88 default='',
88 default='',
89 description=_('A comma separated list of group names that identify '
89 description=_('A comma separated list of group names that identify '
90 'users as RhodeCode Administrators'),
90 'users as RhodeCode Administrators'),
91 missing='',
91 missing='',
92 preparer=strip_whitespace,
92 preparer=strip_whitespace,
93 title=_('Admin Groups'),
93 title=_('Admin Groups'),
94 widget='string')
94 widget='string')
95
95
96
96
97 class CrowdServer(object):
97 class CrowdServer(object):
98 def __init__(self, *args, **kwargs):
98 def __init__(self, *args, **kwargs):
99 """
99 """
100 Create a new CrowdServer object that points to IP/Address 'host',
100 Create a new CrowdServer object that points to IP/Address 'host',
101 on the given port, and using the given method (https/http). user and
101 on the given port, and using the given method (https/http). user and
102 passwd can be set here or with set_credentials. If unspecified,
102 passwd can be set here or with set_credentials. If unspecified,
103 "version" defaults to "latest".
103 "version" defaults to "latest".
104
104
105 example::
105 example::
106
106
107 cserver = CrowdServer(host="127.0.0.1",
107 cserver = CrowdServer(host="127.0.0.1",
108 port="8095",
108 port="8095",
109 user="some_app",
109 user="some_app",
110 passwd="some_passwd",
110 passwd="some_passwd",
111 version="1")
111 version="1")
112 """
112 """
113 if not "port" in kwargs:
113 if not "port" in kwargs:
114 kwargs["port"] = "8095"
114 kwargs["port"] = "8095"
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 kwargs.get("host", "127.0.0.1"),
117 kwargs.get("host", "127.0.0.1"),
118 kwargs.get("port", "8095"))
118 kwargs.get("port", "8095"))
119 self.set_credentials(kwargs.get("user", ""),
119 self.set_credentials(kwargs.get("user", ""),
120 kwargs.get("passwd", ""))
120 kwargs.get("passwd", ""))
121 self._version = kwargs.get("version", "latest")
121 self._version = kwargs.get("version", "latest")
122 self._url_list = None
122 self._url_list = None
123 self._appname = "crowd"
123 self._appname = "crowd"
124
124
125 def set_credentials(self, user, passwd):
125 def set_credentials(self, user, passwd):
126 self.user = user
126 self.user = user
127 self.passwd = passwd
127 self.passwd = passwd
128 self._make_opener()
128 self._make_opener()
129
129
130 def _make_opener(self):
130 def _make_opener(self):
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
132 mgr.add_password(None, self._uri, self.user, self.passwd)
132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
134 self.opener = urllib2.build_opener(handler)
134 self.opener = urllib2.build_opener(handler)
135
135
136 def _request(self, url, body=None, headers=None,
136 def _request(self, url, body=None, headers=None,
137 method=None, noformat=False,
137 method=None, noformat=False,
138 empty_response_ok=False):
138 empty_response_ok=False):
139 _headers = {"Content-type": "application/json",
139 _headers = {"Content-type": "application/json",
140 "Accept": "application/json"}
140 "Accept": "application/json"}
141 if self.user and self.passwd:
141 if self.user and self.passwd:
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 _headers["Authorization"] = "Basic %s" % authstring
143 _headers["Authorization"] = "Basic %s" % authstring
144 if headers:
144 if headers:
145 _headers.update(headers)
145 _headers.update(headers)
146 log.debug("Sent crowd: \n%s"
146 log.debug("Sent crowd: \n%s"
147 % (formatted_json({"url": url, "body": body,
147 % (formatted_json({"url": url, "body": body,
148 "headers": _headers})))
148 "headers": _headers})))
149 request = urllib2.Request(url, body, _headers)
149 request = urllib2.Request(url, body, _headers)
150 if method:
150 if method:
151 request.get_method = lambda: method
151 request.get_method = lambda: method
152
152
153 global msg
153 global msg
154 msg = ""
154 msg = ""
155 try:
155 try:
156 rdoc = self.opener.open(request)
156 rdoc = self.opener.open(request)
157 msg = "".join(rdoc.readlines())
157 msg = "".join(rdoc.readlines())
158 if not msg and empty_response_ok:
158 if not msg and empty_response_ok:
159 rval = {}
159 rval = {}
160 rval["status"] = True
160 rval["status"] = True
161 rval["error"] = "Response body was empty"
161 rval["error"] = "Response body was empty"
162 elif not noformat:
162 elif not noformat:
163 rval = json.loads(msg)
163 rval = json.loads(msg)
164 rval["status"] = True
164 rval["status"] = True
165 else:
165 else:
166 rval = "".join(rdoc.readlines())
166 rval = "".join(rdoc.readlines())
167 except Exception as e:
167 except Exception as e:
168 if not noformat:
168 if not noformat:
169 rval = {"status": False,
169 rval = {"status": False,
170 "body": body,
170 "body": body,
171 "error": str(e) + "\n" + msg}
171 "error": str(e) + "\n" + msg}
172 else:
172 else:
173 rval = None
173 rval = None
174 return rval
174 return rval
175
175
176 def user_auth(self, username, password):
176 def user_auth(self, username, password):
177 """Authenticate a user against crowd. Returns brief information about
177 """Authenticate a user against crowd. Returns brief information about
178 the user."""
178 the user."""
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 % (self._uri, self._version, username))
180 % (self._uri, self._version, username))
181 body = json.dumps({"value": password})
181 body = json.dumps({"value": password})
182 return self._request(url, body)
182 return self._request(url, body)
183
183
184 def user_groups(self, username):
184 def user_groups(self, username):
185 """Retrieve a list of groups to which this user belongs."""
185 """Retrieve a list of groups to which this user belongs."""
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 % (self._uri, self._version, username))
187 % (self._uri, self._version, username))
188 return self._request(url)
188 return self._request(url)
189
189
190
190
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 _settings_unsafe_keys = ['app_password']
192 _settings_unsafe_keys = ['app_password']
193
193
194 def includeme(self, config):
194 def includeme(self, config):
195 config.add_authn_plugin(self)
195 config.add_authn_plugin(self)
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_view(
197 config.add_view(
198 'rhodecode.authentication.views.AuthnPluginViewBase',
198 'rhodecode.authentication.views.AuthnPluginViewBase',
199 attr='settings_get',
199 attr='settings_get',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 request_method='GET',
201 request_method='GET',
202 route_name='auth_home',
202 route_name='auth_home',
203 context=CrowdAuthnResource)
203 context=CrowdAuthnResource)
204 config.add_view(
204 config.add_view(
205 'rhodecode.authentication.views.AuthnPluginViewBase',
205 'rhodecode.authentication.views.AuthnPluginViewBase',
206 attr='settings_post',
206 attr='settings_post',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 request_method='POST',
208 request_method='POST',
209 route_name='auth_home',
209 route_name='auth_home',
210 context=CrowdAuthnResource)
210 context=CrowdAuthnResource)
211
211
212 def get_settings_schema(self):
212 def get_settings_schema(self):
213 return CrowdSettingsSchema()
213 return CrowdSettingsSchema()
214
214
215 def get_display_name(self):
215 def get_display_name(self):
216 return _('CROWD')
216 return _('CROWD')
217
217
218 @hybrid_property
218 @hybrid_property
219 def name(self):
219 def name(self):
220 return "crowd"
220 return "crowd"
221
221
222 def use_fake_password(self):
222 def use_fake_password(self):
223 return True
223 return True
224
224
225 def user_activation_state(self):
225 def user_activation_state(self):
226 def_user_perms = User.get_default_user().AuthUser().permissions['global']
226 def_user_perms = User.get_default_user().AuthUser().permissions['global']
227 return 'hg.extern_activate.auto' in def_user_perms
227 return 'hg.extern_activate.auto' in def_user_perms
228
228
229 def auth(self, userobj, username, password, settings, **kwargs):
229 def auth(self, userobj, username, password, settings, **kwargs):
230 """
230 """
231 Given a user object (which may be null), username, a plaintext password,
231 Given a user object (which may be null), username, a plaintext password,
232 and a settings object (containing all the keys needed as listed in settings()),
232 and a settings object (containing all the keys needed as listed in settings()),
233 authenticate this user's login attempt.
233 authenticate this user's login attempt.
234
234
235 Return None on failure. On success, return a dictionary of the form:
235 Return None on failure. On success, return a dictionary of the form:
236
236
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
238 This is later validated for correctness
238 This is later validated for correctness
239 """
239 """
240 if not username or not password:
240 if not username or not password:
241 log.debug('Empty username or password skipping...')
241 log.debug('Empty username or password skipping...')
242 return None
242 return None
243
243
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
245 server = CrowdServer(**settings)
245 server = CrowdServer(**settings)
246 server.set_credentials(settings["app_name"], settings["app_password"])
246 server.set_credentials(settings["app_name"], settings["app_password"])
247 crowd_user = server.user_auth(username, password)
247 crowd_user = server.user_auth(username, password)
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
249 if not crowd_user["status"]:
249 if not crowd_user["status"]:
250 return None
250 return None
251
251
252 res = server.user_groups(crowd_user["name"])
252 res = server.user_groups(crowd_user["name"])
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
255
255
256 # old attrs fetched from RhodeCode database
256 # old attrs fetched from RhodeCode database
257 admin = getattr(userobj, 'admin', False)
257 admin = getattr(userobj, 'admin', False)
258 active = getattr(userobj, 'active', True)
258 active = getattr(userobj, 'active', True)
259 email = getattr(userobj, 'email', '')
259 email = getattr(userobj, 'email', '')
260 username = getattr(userobj, 'username', username)
260 username = getattr(userobj, 'username', username)
261 firstname = getattr(userobj, 'firstname', '')
261 firstname = getattr(userobj, 'firstname', '')
262 lastname = getattr(userobj, 'lastname', '')
262 lastname = getattr(userobj, 'lastname', '')
263 extern_type = getattr(userobj, 'extern_type', '')
263 extern_type = getattr(userobj, 'extern_type', '')
264
264
265 user_attrs = {
265 user_attrs = {
266 'username': username,
266 'username': username,
267 'firstname': crowd_user["first-name"] or firstname,
267 'firstname': crowd_user["first-name"] or firstname,
268 'lastname': crowd_user["last-name"] or lastname,
268 'lastname': crowd_user["last-name"] or lastname,
269 'groups': crowd_user["groups"],
269 'groups': crowd_user["groups"],
270 'user_group_sync': True,
270 'email': crowd_user["email"] or email,
271 'email': crowd_user["email"] or email,
271 'admin': admin,
272 'admin': admin,
272 'active': active,
273 'active': active,
273 'active_from_extern': crowd_user.get('active'),
274 'active_from_extern': crowd_user.get('active'),
274 'extern_name': crowd_user["name"],
275 'extern_name': crowd_user["name"],
275 'extern_type': extern_type,
276 'extern_type': extern_type,
276 }
277 }
277
278
278 # set an admin if we're in admin_groups of crowd
279 # set an admin if we're in admin_groups of crowd
279 for group in settings["admin_groups"]:
280 for group in settings["admin_groups"]:
280 if group in user_attrs["groups"]:
281 if group in user_attrs["groups"]:
281 user_attrs["admin"] = True
282 user_attrs["admin"] = True
282 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
283 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
283 log.info('user %s authenticated correctly' % user_attrs['username'])
284 log.info('user %s authenticated correctly' % user_attrs['username'])
284 return user_attrs
285 return user_attrs
@@ -1,224 +1,225 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 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, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
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
78
79 def includeme(self, config):
79 def includeme(self, config):
80 config.add_authn_plugin(self)
80 config.add_authn_plugin(self)
81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 config.add_view(
82 config.add_view(
83 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 attr='settings_get',
84 attr='settings_get',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 request_method='GET',
86 request_method='GET',
87 route_name='auth_home',
87 route_name='auth_home',
88 context=HeadersAuthnResource)
88 context=HeadersAuthnResource)
89 config.add_view(
89 config.add_view(
90 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 attr='settings_post',
91 attr='settings_post',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 request_method='POST',
93 request_method='POST',
94 route_name='auth_home',
94 route_name='auth_home',
95 context=HeadersAuthnResource)
95 context=HeadersAuthnResource)
96
96
97 def get_display_name(self):
97 def get_display_name(self):
98 return _('Headers')
98 return _('Headers')
99
99
100 def get_settings_schema(self):
100 def get_settings_schema(self):
101 return HeadersSettingsSchema()
101 return HeadersSettingsSchema()
102
102
103 @hybrid_property
103 @hybrid_property
104 def name(self):
104 def name(self):
105 return 'headers'
105 return 'headers'
106
106
107 @property
107 @property
108 def is_headers_auth(self):
108 def is_headers_auth(self):
109 return True
109 return True
110
110
111 def use_fake_password(self):
111 def use_fake_password(self):
112 return True
112 return True
113
113
114 def user_activation_state(self):
114 def user_activation_state(self):
115 def_user_perms = User.get_default_user().AuthUser().permissions['global']
115 def_user_perms = User.get_default_user().AuthUser().permissions['global']
116 return 'hg.extern_activate.auto' in def_user_perms
116 return 'hg.extern_activate.auto' in def_user_perms
117
117
118 def _clean_username(self, username):
118 def _clean_username(self, username):
119 # Removing realm and domain from username
119 # Removing realm and domain from username
120 username = username.split('@')[0]
120 username = username.split('@')[0]
121 username = username.rsplit('\\')[-1]
121 username = username.rsplit('\\')[-1]
122 return username
122 return username
123
123
124 def _get_username(self, environ, settings):
124 def _get_username(self, environ, settings):
125 username = None
125 username = None
126 environ = environ or {}
126 environ = environ or {}
127 if not environ:
127 if not environ:
128 log.debug('got empty environ: %s' % environ)
128 log.debug('got empty environ: %s' % environ)
129
129
130 settings = settings or {}
130 settings = settings or {}
131 if settings.get('header'):
131 if settings.get('header'):
132 header = settings.get('header')
132 header = settings.get('header')
133 username = environ.get(header)
133 username = environ.get(header)
134 log.debug('extracted %s:%s' % (header, username))
134 log.debug('extracted %s:%s' % (header, username))
135
135
136 # fallback mode
136 # fallback mode
137 if not username and settings.get('fallback_header'):
137 if not username and settings.get('fallback_header'):
138 header = settings.get('fallback_header')
138 header = settings.get('fallback_header')
139 username = environ.get(header)
139 username = environ.get(header)
140 log.debug('extracted %s:%s' % (header, username))
140 log.debug('extracted %s:%s' % (header, username))
141
141
142 if username and str2bool(settings.get('clean_username')):
142 if username and str2bool(settings.get('clean_username')):
143 log.debug('Received username `%s` from headers' % username)
143 log.debug('Received username `%s` from headers' % username)
144 username = self._clean_username(username)
144 username = self._clean_username(username)
145 log.debug('New cleanup user is:%s' % username)
145 log.debug('New cleanup user is:%s' % username)
146 return username
146 return username
147
147
148 def get_user(self, username=None, **kwargs):
148 def get_user(self, username=None, **kwargs):
149 """
149 """
150 Helper method for user fetching in plugins, by default it's using
150 Helper method for user fetching in plugins, by default it's using
151 simple fetch by username, but this method can be custimized in plugins
151 simple fetch by username, but this method can be custimized in plugins
152 eg. headers auth plugin to fetch user by environ params
152 eg. headers auth plugin to fetch user by environ params
153 :param username: username if given to fetch
153 :param username: username if given to fetch
154 :param kwargs: extra arguments needed for user fetching.
154 :param kwargs: extra arguments needed for user fetching.
155 """
155 """
156 environ = kwargs.get('environ') or {}
156 environ = kwargs.get('environ') or {}
157 settings = kwargs.get('settings') or {}
157 settings = kwargs.get('settings') or {}
158 username = self._get_username(environ, settings)
158 username = self._get_username(environ, settings)
159 # we got the username, so use default method now
159 # we got the username, so use default method now
160 return super(RhodeCodeAuthPlugin, self).get_user(username)
160 return super(RhodeCodeAuthPlugin, self).get_user(username)
161
161
162 def auth(self, userobj, username, password, settings, **kwargs):
162 def auth(self, userobj, username, password, settings, **kwargs):
163 """
163 """
164 Get's the headers_auth username (or email). It tries to get username
164 Get's the headers_auth username (or email). It tries to get username
165 from REMOTE_USER if this plugin is enabled, if that fails
165 from REMOTE_USER if this plugin is enabled, if that fails
166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 is set. clean_username extracts the username from this data if it's
167 is set. clean_username extracts the username from this data if it's
168 having @ in it.
168 having @ in it.
169 Return None on failure. On success, return a dictionary of the form:
169 Return None on failure. On success, return a dictionary of the form:
170
170
171 see: RhodeCodeAuthPluginBase.auth_func_attrs
171 see: RhodeCodeAuthPluginBase.auth_func_attrs
172
172
173 :param userobj:
173 :param userobj:
174 :param username:
174 :param username:
175 :param password:
175 :param password:
176 :param settings:
176 :param settings:
177 :param kwargs:
177 :param kwargs:
178 """
178 """
179 environ = kwargs.get('environ')
179 environ = kwargs.get('environ')
180 if not environ:
180 if not environ:
181 log.debug('Empty environ data skipping...')
181 log.debug('Empty environ data skipping...')
182 return None
182 return None
183
183
184 if not userobj:
184 if not userobj:
185 userobj = self.get_user('', environ=environ, settings=settings)
185 userobj = self.get_user('', environ=environ, settings=settings)
186
186
187 # we don't care passed username/password for headers auth plugins.
187 # we don't care passed username/password for headers auth plugins.
188 # only way to log in is using environ
188 # only way to log in is using environ
189 username = None
189 username = None
190 if userobj:
190 if userobj:
191 username = getattr(userobj, 'username')
191 username = getattr(userobj, 'username')
192
192
193 if not username:
193 if not username:
194 # we don't have any objects in DB user doesn't exist extract
194 # we don't have any objects in DB user doesn't exist extract
195 # username from environ based on the settings
195 # username from environ based on the settings
196 username = self._get_username(environ, settings)
196 username = self._get_username(environ, settings)
197
197
198 # if cannot fetch username, it's a no-go for this plugin to proceed
198 # if cannot fetch username, it's a no-go for this plugin to proceed
199 if not username:
199 if not username:
200 return None
200 return None
201
201
202 # old attrs fetched from RhodeCode database
202 # old attrs fetched from RhodeCode database
203 admin = getattr(userobj, 'admin', False)
203 admin = getattr(userobj, 'admin', False)
204 active = getattr(userobj, 'active', True)
204 active = getattr(userobj, 'active', True)
205 email = getattr(userobj, 'email', '')
205 email = getattr(userobj, 'email', '')
206 firstname = getattr(userobj, 'firstname', '')
206 firstname = getattr(userobj, 'firstname', '')
207 lastname = getattr(userobj, 'lastname', '')
207 lastname = getattr(userobj, 'lastname', '')
208 extern_type = getattr(userobj, 'extern_type', '')
208 extern_type = getattr(userobj, 'extern_type', '')
209
209
210 user_attrs = {
210 user_attrs = {
211 'username': username,
211 'username': username,
212 'firstname': safe_unicode(firstname or username),
212 'firstname': safe_unicode(firstname or username),
213 'lastname': safe_unicode(lastname or ''),
213 'lastname': safe_unicode(lastname or ''),
214 'groups': [],
214 'groups': [],
215 'user_group_sync': False,
215 'email': email or '',
216 'email': email or '',
216 'admin': admin or False,
217 'admin': admin or False,
217 'active': active,
218 'active': active,
218 'active_from_extern': True,
219 'active_from_extern': True,
219 'extern_name': username,
220 'extern_name': username,
220 'extern_type': extern_type,
221 'extern_type': extern_type,
221 }
222 }
222
223
223 log.info('user `%s` authenticated correctly' % user_attrs['username'])
224 log.info('user `%s` authenticated correctly' % user_attrs['username'])
224 return user_attrs
225 return user_attrs
@@ -1,166 +1,167 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 Jasig CAS
22 RhodeCode authentication plugin for Jasig CAS
23 http://www.jasig.org/cas
23 http://www.jasig.org/cas
24 """
24 """
25
25
26
26
27 import colander
27 import colander
28 import logging
28 import logging
29 import rhodecode
29 import rhodecode
30 import urllib
30 import urllib
31 import urllib2
31 import urllib2
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 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def plugin_factory(plugin_id, *args, **kwds):
45 def plugin_factory(plugin_id, *args, **kwds):
46 """
46 """
47 Factory function that is called during plugin discovery.
47 Factory function that is called during plugin discovery.
48 It returns the plugin instance.
48 It returns the plugin instance.
49 """
49 """
50 plugin = RhodeCodeAuthPlugin(plugin_id)
50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 return plugin
51 return plugin
52
52
53
53
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 pass
55 pass
56
56
57
57
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 service_url = colander.SchemaNode(
59 service_url = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 default='https://domain.com/cas/v1/tickets',
61 default='https://domain.com/cas/v1/tickets',
62 description=_('The url of the Jasig CAS REST service'),
62 description=_('The url of the Jasig CAS REST service'),
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 title=_('URL'),
64 title=_('URL'),
65 widget='string')
65 widget='string')
66
66
67
67
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69
69
70 def includeme(self, config):
70 def includeme(self, config):
71 config.add_authn_plugin(self)
71 config.add_authn_plugin(self)
72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 config.add_view(
73 config.add_view(
74 'rhodecode.authentication.views.AuthnPluginViewBase',
74 'rhodecode.authentication.views.AuthnPluginViewBase',
75 attr='settings_get',
75 attr='settings_get',
76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
77 request_method='GET',
77 request_method='GET',
78 route_name='auth_home',
78 route_name='auth_home',
79 context=JasigCasAuthnResource)
79 context=JasigCasAuthnResource)
80 config.add_view(
80 config.add_view(
81 'rhodecode.authentication.views.AuthnPluginViewBase',
81 'rhodecode.authentication.views.AuthnPluginViewBase',
82 attr='settings_post',
82 attr='settings_post',
83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 request_method='POST',
84 request_method='POST',
85 route_name='auth_home',
85 route_name='auth_home',
86 context=JasigCasAuthnResource)
86 context=JasigCasAuthnResource)
87
87
88 def get_settings_schema(self):
88 def get_settings_schema(self):
89 return JasigCasSettingsSchema()
89 return JasigCasSettingsSchema()
90
90
91 def get_display_name(self):
91 def get_display_name(self):
92 return _('Jasig-CAS')
92 return _('Jasig-CAS')
93
93
94 @hybrid_property
94 @hybrid_property
95 def name(self):
95 def name(self):
96 return "jasig-cas"
96 return "jasig-cas"
97
97
98 @property
98 @property
99 def is_headers_auth(self):
99 def is_headers_auth(self):
100 return True
100 return True
101
101
102 def use_fake_password(self):
102 def use_fake_password(self):
103 return True
103 return True
104
104
105 def user_activation_state(self):
105 def user_activation_state(self):
106 def_user_perms = User.get_default_user().AuthUser().permissions['global']
106 def_user_perms = User.get_default_user().AuthUser().permissions['global']
107 return 'hg.extern_activate.auto' in def_user_perms
107 return 'hg.extern_activate.auto' in def_user_perms
108
108
109 def auth(self, userobj, username, password, settings, **kwargs):
109 def auth(self, userobj, username, password, settings, **kwargs):
110 """
110 """
111 Given a user object (which may be null), username, a plaintext password,
111 Given a user object (which may be null), username, a plaintext password,
112 and a settings object (containing all the keys needed as listed in settings()),
112 and a settings object (containing all the keys needed as listed in settings()),
113 authenticate this user's login attempt.
113 authenticate this user's login attempt.
114
114
115 Return None on failure. On success, return a dictionary of the form:
115 Return None on failure. On success, return a dictionary of the form:
116
116
117 see: RhodeCodeAuthPluginBase.auth_func_attrs
117 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 This is later validated for correctness
118 This is later validated for correctness
119 """
119 """
120 if not username or not password:
120 if not username or not password:
121 log.debug('Empty username or password skipping...')
121 log.debug('Empty username or password skipping...')
122 return None
122 return None
123
123
124 log.debug("Jasig CAS settings: %s", settings)
124 log.debug("Jasig CAS settings: %s", settings)
125 params = urllib.urlencode({'username': username, 'password': password})
125 params = urllib.urlencode({'username': username, 'password': password})
126 headers = {"Content-type": "application/x-www-form-urlencoded",
126 headers = {"Content-type": "application/x-www-form-urlencoded",
127 "Accept": "text/plain",
127 "Accept": "text/plain",
128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 url = settings["service_url"]
129 url = settings["service_url"]
130
130
131 log.debug("Sent Jasig CAS: \n%s",
131 log.debug("Sent Jasig CAS: \n%s",
132 {"url": url, "body": params, "headers": headers})
132 {"url": url, "body": params, "headers": headers})
133 request = urllib2.Request(url, params, headers)
133 request = urllib2.Request(url, params, headers)
134 try:
134 try:
135 response = urllib2.urlopen(request)
135 response = urllib2.urlopen(request)
136 except urllib2.HTTPError as e:
136 except urllib2.HTTPError as e:
137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
138 return None
138 return None
139 except urllib2.URLError as e:
139 except urllib2.URLError as e:
140 log.debug("URLError when requesting Jasig CAS url: %s " % url)
140 log.debug("URLError when requesting Jasig CAS url: %s " % url)
141 return None
141 return None
142
142
143 # old attrs fetched from RhodeCode database
143 # old attrs fetched from RhodeCode database
144 admin = getattr(userobj, 'admin', False)
144 admin = getattr(userobj, 'admin', False)
145 active = getattr(userobj, 'active', True)
145 active = getattr(userobj, 'active', True)
146 email = getattr(userobj, 'email', '')
146 email = getattr(userobj, 'email', '')
147 username = getattr(userobj, 'username', username)
147 username = getattr(userobj, 'username', username)
148 firstname = getattr(userobj, 'firstname', '')
148 firstname = getattr(userobj, 'firstname', '')
149 lastname = getattr(userobj, 'lastname', '')
149 lastname = getattr(userobj, 'lastname', '')
150 extern_type = getattr(userobj, 'extern_type', '')
150 extern_type = getattr(userobj, 'extern_type', '')
151
151
152 user_attrs = {
152 user_attrs = {
153 'username': username,
153 'username': username,
154 'firstname': safe_unicode(firstname or username),
154 'firstname': safe_unicode(firstname or username),
155 'lastname': safe_unicode(lastname or ''),
155 'lastname': safe_unicode(lastname or ''),
156 'groups': [],
156 'groups': [],
157 'user_group_sync': False,
157 'email': email or '',
158 'email': email or '',
158 'admin': admin or False,
159 'admin': admin or False,
159 'active': active,
160 'active': active,
160 'active_from_extern': True,
161 'active_from_extern': True,
161 'extern_name': username,
162 'extern_name': username,
162 'extern_type': extern_type,
163 'extern_type': extern_type,
163 }
164 }
164
165
165 log.info('user %s authenticated correctly' % user_attrs['username'])
166 log.info('user %s authenticated correctly' % user_attrs['username'])
166 return user_attrs
167 return user_attrs
@@ -1,480 +1,481 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
25
26 import colander
26 import colander
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from rhodecode.translation import _
30 from rhodecode.translation import _
31 from rhodecode.authentication.base import (
31 from rhodecode.authentication.base import (
32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.exceptions import (
36 from rhodecode.lib.exceptions import (
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 )
38 )
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.model.validators import Missing
41 from rhodecode.model.validators import Missing
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45 try:
45 try:
46 import ldap
46 import ldap
47 except ImportError:
47 except ImportError:
48 # means that python-ldap is not installed, we use Missing object to mark
48 # means that python-ldap is not installed, we use Missing object to mark
49 # ldap lib is Missing
49 # ldap lib is Missing
50 ldap = Missing
50 ldap = Missing
51
51
52
52
53 def plugin_factory(plugin_id, *args, **kwds):
53 def plugin_factory(plugin_id, *args, **kwds):
54 """
54 """
55 Factory function that is called during plugin discovery.
55 Factory function that is called during plugin discovery.
56 It returns the plugin instance.
56 It returns the plugin instance.
57 """
57 """
58 plugin = RhodeCodeAuthPlugin(plugin_id)
58 plugin = RhodeCodeAuthPlugin(plugin_id)
59 return plugin
59 return plugin
60
60
61
61
62 class LdapAuthnResource(AuthnPluginResourceBase):
62 class LdapAuthnResource(AuthnPluginResourceBase):
63 pass
63 pass
64
64
65
65
66 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
66 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
67 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
67 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
68 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
68 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
69 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
69 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
70
70
71 host = colander.SchemaNode(
71 host = colander.SchemaNode(
72 colander.String(),
72 colander.String(),
73 default='',
73 default='',
74 description=_('Host[s] of the LDAP Server \n'
74 description=_('Host[s] of the LDAP Server \n'
75 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
75 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
76 'Multiple servers can be specified using commas'),
76 'Multiple servers can be specified using commas'),
77 preparer=strip_whitespace,
77 preparer=strip_whitespace,
78 title=_('LDAP Host'),
78 title=_('LDAP Host'),
79 widget='string')
79 widget='string')
80 port = colander.SchemaNode(
80 port = colander.SchemaNode(
81 colander.Int(),
81 colander.Int(),
82 default=389,
82 default=389,
83 description=_('Custom port that the LDAP server is listening on. '
83 description=_('Custom port that the LDAP server is listening on. '
84 'Default value is: 389'),
84 'Default value is: 389'),
85 preparer=strip_whitespace,
85 preparer=strip_whitespace,
86 title=_('Port'),
86 title=_('Port'),
87 validator=colander.Range(min=0, max=65536),
87 validator=colander.Range(min=0, max=65536),
88 widget='int')
88 widget='int')
89 dn_user = colander.SchemaNode(
89 dn_user = colander.SchemaNode(
90 colander.String(),
90 colander.String(),
91 default='',
91 default='',
92 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
92 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
93 'e.g., cn=admin,dc=mydomain,dc=com, or '
93 'e.g., cn=admin,dc=mydomain,dc=com, or '
94 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
94 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
95 missing='',
95 missing='',
96 preparer=strip_whitespace,
96 preparer=strip_whitespace,
97 title=_('Account'),
97 title=_('Account'),
98 widget='string')
98 widget='string')
99 dn_pass = colander.SchemaNode(
99 dn_pass = colander.SchemaNode(
100 colander.String(),
100 colander.String(),
101 default='',
101 default='',
102 description=_('Password to authenticate for given user DN.'),
102 description=_('Password to authenticate for given user DN.'),
103 missing='',
103 missing='',
104 preparer=strip_whitespace,
104 preparer=strip_whitespace,
105 title=_('Password'),
105 title=_('Password'),
106 widget='password')
106 widget='password')
107 tls_kind = colander.SchemaNode(
107 tls_kind = colander.SchemaNode(
108 colander.String(),
108 colander.String(),
109 default=tls_kind_choices[0],
109 default=tls_kind_choices[0],
110 description=_('TLS Type'),
110 description=_('TLS Type'),
111 title=_('Connection Security'),
111 title=_('Connection Security'),
112 validator=colander.OneOf(tls_kind_choices),
112 validator=colander.OneOf(tls_kind_choices),
113 widget='select')
113 widget='select')
114 tls_reqcert = colander.SchemaNode(
114 tls_reqcert = colander.SchemaNode(
115 colander.String(),
115 colander.String(),
116 default=tls_reqcert_choices[0],
116 default=tls_reqcert_choices[0],
117 description=_('Require Cert over TLS?. Self-signed and custom '
117 description=_('Require Cert over TLS?. Self-signed and custom '
118 'certificates can be used when\n `RhodeCode Certificate` '
118 'certificates can be used when\n `RhodeCode Certificate` '
119 'found in admin > settings > system info page is extended.'),
119 'found in admin > settings > system info page is extended.'),
120 title=_('Certificate Checks'),
120 title=_('Certificate Checks'),
121 validator=colander.OneOf(tls_reqcert_choices),
121 validator=colander.OneOf(tls_reqcert_choices),
122 widget='select')
122 widget='select')
123 base_dn = colander.SchemaNode(
123 base_dn = colander.SchemaNode(
124 colander.String(),
124 colander.String(),
125 default='',
125 default='',
126 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
126 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
127 'in it to be replaced with current user credentials \n'
127 'in it to be replaced with current user credentials \n'
128 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
128 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
129 missing='',
129 missing='',
130 preparer=strip_whitespace,
130 preparer=strip_whitespace,
131 title=_('Base DN'),
131 title=_('Base DN'),
132 widget='string')
132 widget='string')
133 filter = colander.SchemaNode(
133 filter = colander.SchemaNode(
134 colander.String(),
134 colander.String(),
135 default='',
135 default='',
136 description=_('Filter to narrow results \n'
136 description=_('Filter to narrow results \n'
137 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
137 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
138 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
138 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
139 missing='',
139 missing='',
140 preparer=strip_whitespace,
140 preparer=strip_whitespace,
141 title=_('LDAP Search Filter'),
141 title=_('LDAP Search Filter'),
142 widget='string')
142 widget='string')
143
143
144 search_scope = colander.SchemaNode(
144 search_scope = colander.SchemaNode(
145 colander.String(),
145 colander.String(),
146 default=search_scope_choices[2],
146 default=search_scope_choices[2],
147 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
147 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
148 title=_('LDAP Search Scope'),
148 title=_('LDAP Search Scope'),
149 validator=colander.OneOf(search_scope_choices),
149 validator=colander.OneOf(search_scope_choices),
150 widget='select')
150 widget='select')
151 attr_login = colander.SchemaNode(
151 attr_login = colander.SchemaNode(
152 colander.String(),
152 colander.String(),
153 default='uid',
153 default='uid',
154 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
154 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
155 preparer=strip_whitespace,
155 preparer=strip_whitespace,
156 title=_('Login Attribute'),
156 title=_('Login Attribute'),
157 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
157 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
158 widget='string')
158 widget='string')
159 attr_firstname = colander.SchemaNode(
159 attr_firstname = colander.SchemaNode(
160 colander.String(),
160 colander.String(),
161 default='',
161 default='',
162 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
162 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
163 missing='',
163 missing='',
164 preparer=strip_whitespace,
164 preparer=strip_whitespace,
165 title=_('First Name Attribute'),
165 title=_('First Name Attribute'),
166 widget='string')
166 widget='string')
167 attr_lastname = colander.SchemaNode(
167 attr_lastname = colander.SchemaNode(
168 colander.String(),
168 colander.String(),
169 default='',
169 default='',
170 description=_('LDAP Attribute to map to last name (e.g., sn)'),
170 description=_('LDAP Attribute to map to last name (e.g., sn)'),
171 missing='',
171 missing='',
172 preparer=strip_whitespace,
172 preparer=strip_whitespace,
173 title=_('Last Name Attribute'),
173 title=_('Last Name Attribute'),
174 widget='string')
174 widget='string')
175 attr_email = colander.SchemaNode(
175 attr_email = colander.SchemaNode(
176 colander.String(),
176 colander.String(),
177 default='',
177 default='',
178 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
178 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
179 'Emails are a crucial part of RhodeCode. \n'
179 'Emails are a crucial part of RhodeCode. \n'
180 'If possible add a valid email attribute to ldap users.'),
180 'If possible add a valid email attribute to ldap users.'),
181 missing='',
181 missing='',
182 preparer=strip_whitespace,
182 preparer=strip_whitespace,
183 title=_('Email Attribute'),
183 title=_('Email Attribute'),
184 widget='string')
184 widget='string')
185
185
186
186
187 class AuthLdap(object):
187 class AuthLdap(object):
188
188
189 def _build_servers(self):
189 def _build_servers(self):
190 return ', '.join(
190 return ', '.join(
191 ["{}://{}:{}".format(
191 ["{}://{}:{}".format(
192 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
192 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
193 for host in self.SERVER_ADDRESSES])
193 for host in self.SERVER_ADDRESSES])
194
194
195 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
195 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
196 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
196 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
197 search_scope='SUBTREE', attr_login='uid',
197 search_scope='SUBTREE', attr_login='uid',
198 ldap_filter=''):
198 ldap_filter=''):
199 if ldap == Missing:
199 if ldap == Missing:
200 raise LdapImportError("Missing or incompatible ldap library")
200 raise LdapImportError("Missing or incompatible ldap library")
201
201
202 self.debug = False
202 self.debug = False
203 self.ldap_version = ldap_version
203 self.ldap_version = ldap_version
204 self.ldap_server_type = 'ldap'
204 self.ldap_server_type = 'ldap'
205
205
206 self.TLS_KIND = tls_kind
206 self.TLS_KIND = tls_kind
207
207
208 if self.TLS_KIND == 'LDAPS':
208 if self.TLS_KIND == 'LDAPS':
209 port = port or 689
209 port = port or 689
210 self.ldap_server_type += 's'
210 self.ldap_server_type += 's'
211
211
212 OPT_X_TLS_DEMAND = 2
212 OPT_X_TLS_DEMAND = 2
213 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
213 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
214 OPT_X_TLS_DEMAND)
214 OPT_X_TLS_DEMAND)
215 # split server into list
215 # split server into list
216 self.SERVER_ADDRESSES = server.split(',')
216 self.SERVER_ADDRESSES = server.split(',')
217 self.LDAP_SERVER_PORT = port
217 self.LDAP_SERVER_PORT = port
218
218
219 # USE FOR READ ONLY BIND TO LDAP SERVER
219 # USE FOR READ ONLY BIND TO LDAP SERVER
220 self.attr_login = attr_login
220 self.attr_login = attr_login
221
221
222 self.LDAP_BIND_DN = safe_str(bind_dn)
222 self.LDAP_BIND_DN = safe_str(bind_dn)
223 self.LDAP_BIND_PASS = safe_str(bind_pass)
223 self.LDAP_BIND_PASS = safe_str(bind_pass)
224 self.LDAP_SERVER = self._build_servers()
224 self.LDAP_SERVER = self._build_servers()
225 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
225 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
226 self.BASE_DN = safe_str(base_dn)
226 self.BASE_DN = safe_str(base_dn)
227 self.LDAP_FILTER = safe_str(ldap_filter)
227 self.LDAP_FILTER = safe_str(ldap_filter)
228
228
229 def _get_ldap_server(self):
229 def _get_ldap_server(self):
230 if self.debug:
230 if self.debug:
231 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
231 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
232 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
232 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
233 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
233 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
234 '/etc/openldap/cacerts')
234 '/etc/openldap/cacerts')
235 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
235 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
236 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
236 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
237 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 60 * 10)
237 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 60 * 10)
238 ldap.set_option(ldap.OPT_TIMEOUT, 60 * 10)
238 ldap.set_option(ldap.OPT_TIMEOUT, 60 * 10)
239
239
240 if self.TLS_KIND != 'PLAIN':
240 if self.TLS_KIND != 'PLAIN':
241 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
241 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
242 server = ldap.initialize(self.LDAP_SERVER)
242 server = ldap.initialize(self.LDAP_SERVER)
243 if self.ldap_version == 2:
243 if self.ldap_version == 2:
244 server.protocol = ldap.VERSION2
244 server.protocol = ldap.VERSION2
245 else:
245 else:
246 server.protocol = ldap.VERSION3
246 server.protocol = ldap.VERSION3
247
247
248 if self.TLS_KIND == 'START_TLS':
248 if self.TLS_KIND == 'START_TLS':
249 server.start_tls_s()
249 server.start_tls_s()
250
250
251 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
251 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
252 log.debug('Trying simple_bind with password and given login DN: %s',
252 log.debug('Trying simple_bind with password and given login DN: %s',
253 self.LDAP_BIND_DN)
253 self.LDAP_BIND_DN)
254 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
254 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
255
255
256 return server
256 return server
257
257
258 def get_uid(self, username):
258 def get_uid(self, username):
259 uid = username
259 uid = username
260 for server_addr in self.SERVER_ADDRESSES:
260 for server_addr in self.SERVER_ADDRESSES:
261 uid = chop_at(username, "@%s" % server_addr)
261 uid = chop_at(username, "@%s" % server_addr)
262 return uid
262 return uid
263
263
264 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
264 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
265 try:
265 try:
266 log.debug('Trying simple bind with %s', dn)
266 log.debug('Trying simple bind with %s', dn)
267 server.simple_bind_s(dn, safe_str(password))
267 server.simple_bind_s(dn, safe_str(password))
268 user = server.search_ext_s(
268 user = server.search_ext_s(
269 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
269 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
270 _, attrs = user
270 _, attrs = user
271 return attrs
271 return attrs
272
272
273 except ldap.INVALID_CREDENTIALS:
273 except ldap.INVALID_CREDENTIALS:
274 log.debug(
274 log.debug(
275 "LDAP rejected password for user '%s': %s, org_exc:",
275 "LDAP rejected password for user '%s': %s, org_exc:",
276 username, dn, exc_info=True)
276 username, dn, exc_info=True)
277
277
278 def authenticate_ldap(self, username, password):
278 def authenticate_ldap(self, username, password):
279 """
279 """
280 Authenticate a user via LDAP and return his/her LDAP properties.
280 Authenticate a user via LDAP and return his/her LDAP properties.
281
281
282 Raises AuthenticationError if the credentials are rejected, or
282 Raises AuthenticationError if the credentials are rejected, or
283 EnvironmentError if the LDAP server can't be reached.
283 EnvironmentError if the LDAP server can't be reached.
284
284
285 :param username: username
285 :param username: username
286 :param password: password
286 :param password: password
287 """
287 """
288
288
289 uid = self.get_uid(username)
289 uid = self.get_uid(username)
290
290
291 if not password:
291 if not password:
292 msg = "Authenticating user %s with blank password not allowed"
292 msg = "Authenticating user %s with blank password not allowed"
293 log.warning(msg, username)
293 log.warning(msg, username)
294 raise LdapPasswordError(msg)
294 raise LdapPasswordError(msg)
295 if "," in username:
295 if "," in username:
296 raise LdapUsernameError(
296 raise LdapUsernameError(
297 "invalid character `,` in username: `{}`".format(username))
297 "invalid character `,` in username: `{}`".format(username))
298 try:
298 try:
299 server = self._get_ldap_server()
299 server = self._get_ldap_server()
300 filter_ = '(&%s(%s=%s))' % (
300 filter_ = '(&%s(%s=%s))' % (
301 self.LDAP_FILTER, self.attr_login, username)
301 self.LDAP_FILTER, self.attr_login, username)
302 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
302 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
303 filter_, self.LDAP_SERVER)
303 filter_, self.LDAP_SERVER)
304 lobjects = server.search_ext_s(
304 lobjects = server.search_ext_s(
305 self.BASE_DN, self.SEARCH_SCOPE, filter_)
305 self.BASE_DN, self.SEARCH_SCOPE, filter_)
306
306
307 if not lobjects:
307 if not lobjects:
308 log.debug("No matching LDAP objects for authentication "
308 log.debug("No matching LDAP objects for authentication "
309 "of UID:'%s' username:(%s)", uid, username)
309 "of UID:'%s' username:(%s)", uid, username)
310 raise ldap.NO_SUCH_OBJECT()
310 raise ldap.NO_SUCH_OBJECT()
311
311
312 log.debug('Found matching ldap object, trying to authenticate')
312 log.debug('Found matching ldap object, trying to authenticate')
313 for (dn, _attrs) in lobjects:
313 for (dn, _attrs) in lobjects:
314 if dn is None:
314 if dn is None:
315 continue
315 continue
316
316
317 user_attrs = self.fetch_attrs_from_simple_bind(
317 user_attrs = self.fetch_attrs_from_simple_bind(
318 server, dn, username, password)
318 server, dn, username, password)
319 if user_attrs:
319 if user_attrs:
320 break
320 break
321
321
322 else:
322 else:
323 raise LdapPasswordError(
323 raise LdapPasswordError(
324 'Failed to authenticate user `{}`'
324 'Failed to authenticate user `{}`'
325 'with given password'.format(username))
325 'with given password'.format(username))
326
326
327 except ldap.NO_SUCH_OBJECT:
327 except ldap.NO_SUCH_OBJECT:
328 log.debug("LDAP says no such user '%s' (%s), org_exc:",
328 log.debug("LDAP says no such user '%s' (%s), org_exc:",
329 uid, username, exc_info=True)
329 uid, username, exc_info=True)
330 raise LdapUsernameError('Unable to find user')
330 raise LdapUsernameError('Unable to find user')
331 except ldap.SERVER_DOWN:
331 except ldap.SERVER_DOWN:
332 org_exc = traceback.format_exc()
332 org_exc = traceback.format_exc()
333 raise LdapConnectionError(
333 raise LdapConnectionError(
334 "LDAP can't access authentication "
334 "LDAP can't access authentication "
335 "server, org_exc:%s" % org_exc)
335 "server, org_exc:%s" % org_exc)
336
336
337 return dn, user_attrs
337 return dn, user_attrs
338
338
339
339
340 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
340 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
341 # used to define dynamic binding in the
341 # used to define dynamic binding in the
342 DYNAMIC_BIND_VAR = '$login'
342 DYNAMIC_BIND_VAR = '$login'
343 _settings_unsafe_keys = ['dn_pass']
343 _settings_unsafe_keys = ['dn_pass']
344
344
345 def includeme(self, config):
345 def includeme(self, config):
346 config.add_authn_plugin(self)
346 config.add_authn_plugin(self)
347 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
347 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
348 config.add_view(
348 config.add_view(
349 'rhodecode.authentication.views.AuthnPluginViewBase',
349 'rhodecode.authentication.views.AuthnPluginViewBase',
350 attr='settings_get',
350 attr='settings_get',
351 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
351 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
352 request_method='GET',
352 request_method='GET',
353 route_name='auth_home',
353 route_name='auth_home',
354 context=LdapAuthnResource)
354 context=LdapAuthnResource)
355 config.add_view(
355 config.add_view(
356 'rhodecode.authentication.views.AuthnPluginViewBase',
356 'rhodecode.authentication.views.AuthnPluginViewBase',
357 attr='settings_post',
357 attr='settings_post',
358 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
358 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
359 request_method='POST',
359 request_method='POST',
360 route_name='auth_home',
360 route_name='auth_home',
361 context=LdapAuthnResource)
361 context=LdapAuthnResource)
362
362
363 def get_settings_schema(self):
363 def get_settings_schema(self):
364 return LdapSettingsSchema()
364 return LdapSettingsSchema()
365
365
366 def get_display_name(self):
366 def get_display_name(self):
367 return _('LDAP')
367 return _('LDAP')
368
368
369 @hybrid_property
369 @hybrid_property
370 def name(self):
370 def name(self):
371 return "ldap"
371 return "ldap"
372
372
373 def use_fake_password(self):
373 def use_fake_password(self):
374 return True
374 return True
375
375
376 def user_activation_state(self):
376 def user_activation_state(self):
377 def_user_perms = User.get_default_user().AuthUser().permissions['global']
377 def_user_perms = User.get_default_user().AuthUser().permissions['global']
378 return 'hg.extern_activate.auto' in def_user_perms
378 return 'hg.extern_activate.auto' in def_user_perms
379
379
380 def try_dynamic_binding(self, username, password, current_args):
380 def try_dynamic_binding(self, username, password, current_args):
381 """
381 """
382 Detects marker inside our original bind, and uses dynamic auth if
382 Detects marker inside our original bind, and uses dynamic auth if
383 present
383 present
384 """
384 """
385
385
386 org_bind = current_args['bind_dn']
386 org_bind = current_args['bind_dn']
387 passwd = current_args['bind_pass']
387 passwd = current_args['bind_pass']
388
388
389 def has_bind_marker(username):
389 def has_bind_marker(username):
390 if self.DYNAMIC_BIND_VAR in username:
390 if self.DYNAMIC_BIND_VAR in username:
391 return True
391 return True
392
392
393 # we only passed in user with "special" variable
393 # we only passed in user with "special" variable
394 if org_bind and has_bind_marker(org_bind) and not passwd:
394 if org_bind and has_bind_marker(org_bind) and not passwd:
395 log.debug('Using dynamic user/password binding for ldap '
395 log.debug('Using dynamic user/password binding for ldap '
396 'authentication. Replacing `%s` with username',
396 'authentication. Replacing `%s` with username',
397 self.DYNAMIC_BIND_VAR)
397 self.DYNAMIC_BIND_VAR)
398 current_args['bind_dn'] = org_bind.replace(
398 current_args['bind_dn'] = org_bind.replace(
399 self.DYNAMIC_BIND_VAR, username)
399 self.DYNAMIC_BIND_VAR, username)
400 current_args['bind_pass'] = password
400 current_args['bind_pass'] = password
401
401
402 return current_args
402 return current_args
403
403
404 def auth(self, userobj, username, password, settings, **kwargs):
404 def auth(self, userobj, username, password, settings, **kwargs):
405 """
405 """
406 Given a user object (which may be null), username, a plaintext password,
406 Given a user object (which may be null), username, a plaintext password,
407 and a settings object (containing all the keys needed as listed in
407 and a settings object (containing all the keys needed as listed in
408 settings()), authenticate this user's login attempt.
408 settings()), authenticate this user's login attempt.
409
409
410 Return None on failure. On success, return a dictionary of the form:
410 Return None on failure. On success, return a dictionary of the form:
411
411
412 see: RhodeCodeAuthPluginBase.auth_func_attrs
412 see: RhodeCodeAuthPluginBase.auth_func_attrs
413 This is later validated for correctness
413 This is later validated for correctness
414 """
414 """
415
415
416 if not username or not password:
416 if not username or not password:
417 log.debug('Empty username or password skipping...')
417 log.debug('Empty username or password skipping...')
418 return None
418 return None
419
419
420 ldap_args = {
420 ldap_args = {
421 'server': settings.get('host', ''),
421 'server': settings.get('host', ''),
422 'base_dn': settings.get('base_dn', ''),
422 'base_dn': settings.get('base_dn', ''),
423 'port': settings.get('port'),
423 'port': settings.get('port'),
424 'bind_dn': settings.get('dn_user'),
424 'bind_dn': settings.get('dn_user'),
425 'bind_pass': settings.get('dn_pass'),
425 'bind_pass': settings.get('dn_pass'),
426 'tls_kind': settings.get('tls_kind'),
426 'tls_kind': settings.get('tls_kind'),
427 'tls_reqcert': settings.get('tls_reqcert'),
427 'tls_reqcert': settings.get('tls_reqcert'),
428 'search_scope': settings.get('search_scope'),
428 'search_scope': settings.get('search_scope'),
429 'attr_login': settings.get('attr_login'),
429 'attr_login': settings.get('attr_login'),
430 'ldap_version': 3,
430 'ldap_version': 3,
431 'ldap_filter': settings.get('filter'),
431 'ldap_filter': settings.get('filter'),
432 }
432 }
433
433
434 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
434 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
435
435
436 log.debug('Checking for ldap authentication.')
436 log.debug('Checking for ldap authentication.')
437
437
438 try:
438 try:
439 aldap = AuthLdap(**ldap_args)
439 aldap = AuthLdap(**ldap_args)
440 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
440 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
441 log.debug('Got ldap DN response %s', user_dn)
441 log.debug('Got ldap DN response %s', user_dn)
442
442
443 def get_ldap_attr(k):
443 def get_ldap_attr(k):
444 return ldap_attrs.get(settings.get(k), [''])[0]
444 return ldap_attrs.get(settings.get(k), [''])[0]
445
445
446 # old attrs fetched from RhodeCode database
446 # old attrs fetched from RhodeCode database
447 admin = getattr(userobj, 'admin', False)
447 admin = getattr(userobj, 'admin', False)
448 active = getattr(userobj, 'active', True)
448 active = getattr(userobj, 'active', True)
449 email = getattr(userobj, 'email', '')
449 email = getattr(userobj, 'email', '')
450 username = getattr(userobj, 'username', username)
450 username = getattr(userobj, 'username', username)
451 firstname = getattr(userobj, 'firstname', '')
451 firstname = getattr(userobj, 'firstname', '')
452 lastname = getattr(userobj, 'lastname', '')
452 lastname = getattr(userobj, 'lastname', '')
453 extern_type = getattr(userobj, 'extern_type', '')
453 extern_type = getattr(userobj, 'extern_type', '')
454
454
455 groups = []
455 groups = []
456 user_attrs = {
456 user_attrs = {
457 'username': username,
457 'username': username,
458 'firstname': safe_unicode(
458 'firstname': safe_unicode(
459 get_ldap_attr('attr_firstname') or firstname),
459 get_ldap_attr('attr_firstname') or firstname),
460 'lastname': safe_unicode(
460 'lastname': safe_unicode(
461 get_ldap_attr('attr_lastname') or lastname),
461 get_ldap_attr('attr_lastname') or lastname),
462 'groups': groups,
462 'groups': groups,
463 'user_group_sync': False,
463 'email': get_ldap_attr('attr_email') or email,
464 'email': get_ldap_attr('attr_email') or email,
464 'admin': admin,
465 'admin': admin,
465 'active': active,
466 'active': active,
466 'active_from_extern': None,
467 'active_from_extern': None,
467 'extern_name': user_dn,
468 'extern_name': user_dn,
468 'extern_type': extern_type,
469 'extern_type': extern_type,
469 }
470 }
470 log.debug('ldap user: %s', user_attrs)
471 log.debug('ldap user: %s', user_attrs)
471 log.info('user %s authenticated correctly', user_attrs['username'])
472 log.info('user %s authenticated correctly', user_attrs['username'])
472
473
473 return user_attrs
474 return user_attrs
474
475
475 except (LdapUsernameError, LdapPasswordError, LdapImportError):
476 except (LdapUsernameError, LdapPasswordError, LdapImportError):
476 log.exception("LDAP related exception")
477 log.exception("LDAP related exception")
477 return None
478 return None
478 except (Exception,):
479 except (Exception,):
479 log.exception("Other exception")
480 log.exception("Other exception")
480 return None
481 return None
@@ -1,160 +1,161 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 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, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
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 # PAM authentication can be slow. Repository operations involve a lot of
75 # PAM authentication can be slow. Repository operations involve a lot of
76 # auth calls. Little caching helps speedup push/pull operations significantly
76 # auth calls. Little caching helps speedup push/pull operations significantly
77 AUTH_CACHE_TTL = 4
77 AUTH_CACHE_TTL = 4
78
78
79 def includeme(self, config):
79 def includeme(self, config):
80 config.add_authn_plugin(self)
80 config.add_authn_plugin(self)
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
82 config.add_view(
82 config.add_view(
83 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 attr='settings_get',
84 attr='settings_get',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 request_method='GET',
86 request_method='GET',
87 route_name='auth_home',
87 route_name='auth_home',
88 context=PamAuthnResource)
88 context=PamAuthnResource)
89 config.add_view(
89 config.add_view(
90 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 attr='settings_post',
91 attr='settings_post',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 request_method='POST',
93 request_method='POST',
94 route_name='auth_home',
94 route_name='auth_home',
95 context=PamAuthnResource)
95 context=PamAuthnResource)
96
96
97 def get_display_name(self):
97 def get_display_name(self):
98 return _('PAM')
98 return _('PAM')
99
99
100 @hybrid_property
100 @hybrid_property
101 def name(self):
101 def name(self):
102 return "pam"
102 return "pam"
103
103
104 def get_settings_schema(self):
104 def get_settings_schema(self):
105 return PamSettingsSchema()
105 return PamSettingsSchema()
106
106
107 def use_fake_password(self):
107 def use_fake_password(self):
108 return True
108 return True
109
109
110 def auth(self, userobj, username, password, settings, **kwargs):
110 def auth(self, userobj, username, password, settings, **kwargs):
111 if not username or not password:
111 if not username or not password:
112 log.debug('Empty username or password skipping...')
112 log.debug('Empty username or password skipping...')
113 return None
113 return None
114
114
115 auth_result = pam.authenticate(username, password, settings["service"])
115 auth_result = pam.authenticate(username, password, settings["service"])
116
116
117 if not auth_result:
117 if not auth_result:
118 log.error("PAM was unable to authenticate user: %s" % (username, ))
118 log.error("PAM was unable to authenticate user: %s" % (username, ))
119 return None
119 return None
120
120
121 log.debug('Got PAM response %s' % (auth_result, ))
121 log.debug('Got PAM response %s' % (auth_result, ))
122
122
123 # old attrs fetched from RhodeCode database
123 # old attrs fetched from RhodeCode database
124 default_email = "%s@%s" % (username, socket.gethostname())
124 default_email = "%s@%s" % (username, socket.gethostname())
125 admin = getattr(userobj, 'admin', False)
125 admin = getattr(userobj, 'admin', False)
126 active = getattr(userobj, 'active', True)
126 active = getattr(userobj, 'active', True)
127 email = getattr(userobj, 'email', '') or default_email
127 email = getattr(userobj, 'email', '') or default_email
128 username = getattr(userobj, 'username', username)
128 username = getattr(userobj, 'username', username)
129 firstname = getattr(userobj, 'firstname', '')
129 firstname = getattr(userobj, 'firstname', '')
130 lastname = getattr(userobj, 'lastname', '')
130 lastname = getattr(userobj, 'lastname', '')
131 extern_type = getattr(userobj, 'extern_type', '')
131 extern_type = getattr(userobj, 'extern_type', '')
132
132
133 user_attrs = {
133 user_attrs = {
134 'username': username,
134 'username': username,
135 'firstname': firstname,
135 'firstname': firstname,
136 'lastname': lastname,
136 'lastname': lastname,
137 'groups': [g.gr_name for g in grp.getgrall()
137 'groups': [g.gr_name for g in grp.getgrall()
138 if username in g.gr_mem],
138 if username in g.gr_mem],
139 'user_group_sync': True,
139 'email': email,
140 'email': email,
140 'admin': admin,
141 'admin': admin,
141 'active': active,
142 'active': active,
142 'active_from_extern': None,
143 'active_from_extern': None,
143 'extern_name': username,
144 'extern_name': username,
144 'extern_type': extern_type,
145 'extern_type': extern_type,
145 }
146 }
146
147
147 try:
148 try:
148 user_data = pwd.getpwnam(username)
149 user_data = pwd.getpwnam(username)
149 regex = settings["gecos"]
150 regex = settings["gecos"]
150 match = re.search(regex, user_data.pw_gecos)
151 match = re.search(regex, user_data.pw_gecos)
151 if match:
152 if match:
152 user_attrs["firstname"] = match.group('first_name')
153 user_attrs["firstname"] = match.group('first_name')
153 user_attrs["lastname"] = match.group('last_name')
154 user_attrs["lastname"] = match.group('last_name')
154 except Exception:
155 except Exception:
155 log.warning("Cannot extract additional info for PAM user")
156 log.warning("Cannot extract additional info for PAM user")
156 pass
157 pass
157
158
158 log.debug("pamuser: %s", user_attrs)
159 log.debug("pamuser: %s", user_attrs)
159 log.info('user %s authenticated correctly' % user_attrs['username'])
160 log.info('user %s authenticated correctly' % user_attrs['username'])
160 return user_attrs
161 return user_attrs
@@ -1,142 +1,143 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 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 from rhodecode.translation import _
27 from rhodecode.translation import _
28
28
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.lib.utils2 import safe_str
32 from rhodecode.model.db import User
32 from rhodecode.model.db import User
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47
47
48 def includeme(self, config):
48 def includeme(self, config):
49 config.add_authn_plugin(self)
49 config.add_authn_plugin(self)
50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
51 config.add_view(
51 config.add_view(
52 'rhodecode.authentication.views.AuthnPluginViewBase',
52 'rhodecode.authentication.views.AuthnPluginViewBase',
53 attr='settings_get',
53 attr='settings_get',
54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
55 request_method='GET',
55 request_method='GET',
56 route_name='auth_home',
56 route_name='auth_home',
57 context=RhodecodeAuthnResource)
57 context=RhodecodeAuthnResource)
58 config.add_view(
58 config.add_view(
59 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 attr='settings_post',
60 attr='settings_post',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 request_method='POST',
62 request_method='POST',
63 route_name='auth_home',
63 route_name='auth_home',
64 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
65
65
66 def get_display_name(self):
66 def get_display_name(self):
67 return _('Rhodecode')
67 return _('Rhodecode')
68
68
69 @hybrid_property
69 @hybrid_property
70 def name(self):
70 def name(self):
71 return "rhodecode"
71 return "rhodecode"
72
72
73 def user_activation_state(self):
73 def user_activation_state(self):
74 def_user_perms = User.get_default_user().AuthUser().permissions['global']
74 def_user_perms = User.get_default_user().AuthUser().permissions['global']
75 return 'hg.register.auto_activate' in def_user_perms
75 return 'hg.register.auto_activate' in def_user_perms
76
76
77 def allows_authentication_from(
77 def allows_authentication_from(
78 self, user, allows_non_existing_user=True,
78 self, user, allows_non_existing_user=True,
79 allowed_auth_plugins=None, allowed_auth_sources=None):
79 allowed_auth_plugins=None, allowed_auth_sources=None):
80 """
80 """
81 Custom method for this auth that doesn't accept non existing users.
81 Custom method for this auth that doesn't accept non existing users.
82 We know that user exists in our database.
82 We know that user exists in our database.
83 """
83 """
84 allows_non_existing_user = False
84 allows_non_existing_user = False
85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
86 user, allows_non_existing_user=allows_non_existing_user)
86 user, allows_non_existing_user=allows_non_existing_user)
87
87
88 def auth(self, userobj, username, password, settings, **kwargs):
88 def auth(self, userobj, username, password, settings, **kwargs):
89 if not userobj:
89 if not userobj:
90 log.debug('userobj was:%s skipping' % (userobj, ))
90 log.debug('userobj was:%s skipping' % (userobj, ))
91 return None
91 return None
92 if userobj.extern_type != self.name:
92 if userobj.extern_type != self.name:
93 log.warning(
93 log.warning(
94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
95 (userobj, userobj.extern_type, self.name))
95 (userobj, userobj.extern_type, self.name))
96 return None
96 return None
97
97
98 user_attrs = {
98 user_attrs = {
99 "username": userobj.username,
99 "username": userobj.username,
100 "firstname": userobj.firstname,
100 "firstname": userobj.firstname,
101 "lastname": userobj.lastname,
101 "lastname": userobj.lastname,
102 "groups": [],
102 "groups": [],
103 'user_group_sync': False,
103 "email": userobj.email,
104 "email": userobj.email,
104 "admin": userobj.admin,
105 "admin": userobj.admin,
105 "active": userobj.active,
106 "active": userobj.active,
106 "active_from_extern": userobj.active,
107 "active_from_extern": userobj.active,
107 "extern_name": userobj.user_id,
108 "extern_name": userobj.user_id,
108 "extern_type": userobj.extern_type,
109 "extern_type": userobj.extern_type,
109 }
110 }
110
111
111 log.debug("User attributes:%s" % (user_attrs, ))
112 log.debug("User attributes:%s" % (user_attrs, ))
112 if userobj.active:
113 if userobj.active:
113 from rhodecode.lib import auth
114 from rhodecode.lib import auth
114 crypto_backend = auth.crypto_backend()
115 crypto_backend = auth.crypto_backend()
115 password_encoded = safe_str(password)
116 password_encoded = safe_str(password)
116 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
117 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
117 password_encoded, userobj.password or '')
118 password_encoded, userobj.password or '')
118
119
119 if password_match and new_hash:
120 if password_match and new_hash:
120 log.debug('user %s properly authenticated, but '
121 log.debug('user %s properly authenticated, but '
121 'requires hash change to bcrypt', userobj)
122 'requires hash change to bcrypt', userobj)
122 # if password match, and we use OLD deprecated hash,
123 # if password match, and we use OLD deprecated hash,
123 # we should migrate this user hash password to the new hash
124 # we should migrate this user hash password to the new hash
124 # we store the new returned by hash_check_with_upgrade function
125 # we store the new returned by hash_check_with_upgrade function
125 user_attrs['_hash_migrate'] = new_hash
126 user_attrs['_hash_migrate'] = new_hash
126
127
127 if userobj.username == User.DEFAULT_USER and userobj.active:
128 if userobj.username == User.DEFAULT_USER and userobj.active:
128 log.info(
129 log.info(
129 'user %s authenticated correctly as anonymous user', userobj)
130 'user %s authenticated correctly as anonymous user', userobj)
130 return user_attrs
131 return user_attrs
131
132
132 elif userobj.username == username and password_match:
133 elif userobj.username == username and password_match:
133 log.info('user %s authenticated correctly', userobj)
134 log.info('user %s authenticated correctly', userobj)
134 return user_attrs
135 return user_attrs
135 log.info("user %s had a bad password when "
136 log.info("user %s had a bad password when "
136 "authenticating on this plugin", userobj)
137 "authenticating on this plugin", userobj)
137 return None
138 return None
138 else:
139 else:
139 log.warning(
140 log.warning(
140 'user `%s` failed to authenticate via %s, reason: account not '
141 'user `%s` failed to authenticate via %s, reason: account not '
141 'active.', username, self.name)
142 'active.', username, self.name)
142 return None
143 return None
@@ -1,146 +1,147 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-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 token plugin for built in internal auth
22 RhodeCode authentication token plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.authentication.base import (
28 from rhodecode.authentication.base import (
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.model.db import User, UserApiKeys, Repository
31 from rhodecode.model.db import User, UserApiKeys, Repository
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, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 """
47 """
48 Enables usage of authentication tokens for vcs operations.
48 Enables usage of authentication tokens for vcs operations.
49 """
49 """
50
50
51 def includeme(self, config):
51 def includeme(self, config):
52 config.add_authn_plugin(self)
52 config.add_authn_plugin(self)
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 config.add_view(
54 config.add_view(
55 'rhodecode.authentication.views.AuthnPluginViewBase',
55 'rhodecode.authentication.views.AuthnPluginViewBase',
56 attr='settings_get',
56 attr='settings_get',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
58 request_method='GET',
58 request_method='GET',
59 route_name='auth_home',
59 route_name='auth_home',
60 context=RhodecodeAuthnResource)
60 context=RhodecodeAuthnResource)
61 config.add_view(
61 config.add_view(
62 'rhodecode.authentication.views.AuthnPluginViewBase',
62 'rhodecode.authentication.views.AuthnPluginViewBase',
63 attr='settings_post',
63 attr='settings_post',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
65 request_method='POST',
65 request_method='POST',
66 route_name='auth_home',
66 route_name='auth_home',
67 context=RhodecodeAuthnResource)
67 context=RhodecodeAuthnResource)
68
68
69 def get_display_name(self):
69 def get_display_name(self):
70 return _('Rhodecode Token Auth')
70 return _('Rhodecode Token Auth')
71
71
72 @hybrid_property
72 @hybrid_property
73 def name(self):
73 def name(self):
74 return "authtoken"
74 return "authtoken"
75
75
76 def user_activation_state(self):
76 def user_activation_state(self):
77 def_user_perms = User.get_default_user().AuthUser().permissions['global']
77 def_user_perms = User.get_default_user().AuthUser().permissions['global']
78 return 'hg.register.auto_activate' in def_user_perms
78 return 'hg.register.auto_activate' in def_user_perms
79
79
80 def allows_authentication_from(
80 def allows_authentication_from(
81 self, user, allows_non_existing_user=True,
81 self, user, allows_non_existing_user=True,
82 allowed_auth_plugins=None, allowed_auth_sources=None):
82 allowed_auth_plugins=None, allowed_auth_sources=None):
83 """
83 """
84 Custom method for this auth that doesn't accept empty users. And also
84 Custom method for this auth that doesn't accept empty users. And also
85 allows users from all other active plugins to use it and also
85 allows users from all other active plugins to use it and also
86 authenticate against it. But only via vcs mode
86 authenticate against it. But only via vcs mode
87 """
87 """
88 from rhodecode.authentication.base import get_authn_registry
88 from rhodecode.authentication.base import get_authn_registry
89 authn_registry = get_authn_registry()
89 authn_registry = get_authn_registry()
90
90
91 active_plugins = set(
91 active_plugins = set(
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
93 active_plugins.discard(self.name)
93 active_plugins.discard(self.name)
94
94
95 allowed_auth_plugins = [self.name] + list(active_plugins)
95 allowed_auth_plugins = [self.name] + list(active_plugins)
96 # only for vcs operations
96 # only for vcs operations
97 allowed_auth_sources = [VCS_TYPE]
97 allowed_auth_sources = [VCS_TYPE]
98
98
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
100 user, allows_non_existing_user=False,
100 user, allows_non_existing_user=False,
101 allowed_auth_plugins=allowed_auth_plugins,
101 allowed_auth_plugins=allowed_auth_plugins,
102 allowed_auth_sources=allowed_auth_sources)
102 allowed_auth_sources=allowed_auth_sources)
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 user_attrs = {
109 user_attrs = {
110 "username": userobj.username,
110 "username": userobj.username,
111 "firstname": userobj.firstname,
111 "firstname": userobj.firstname,
112 "lastname": userobj.lastname,
112 "lastname": userobj.lastname,
113 "groups": [],
113 "groups": [],
114 'user_group_sync': False,
114 "email": userobj.email,
115 "email": userobj.email,
115 "admin": userobj.admin,
116 "admin": userobj.admin,
116 "active": userobj.active,
117 "active": userobj.active,
117 "active_from_extern": userobj.active,
118 "active_from_extern": userobj.active,
118 "extern_name": userobj.user_id,
119 "extern_name": userobj.user_id,
119 "extern_type": userobj.extern_type,
120 "extern_type": userobj.extern_type,
120 }
121 }
121
122
122 log.debug('Authenticating user with args %s', user_attrs)
123 log.debug('Authenticating user with args %s', user_attrs)
123 if userobj.active:
124 if userobj.active:
124 # calling context repo for token scopes
125 # calling context repo for token scopes
125 scope_repo_id = None
126 scope_repo_id = None
126 if self.acl_repo_name:
127 if self.acl_repo_name:
127 repo = Repository.get_by_repo_name(self.acl_repo_name)
128 repo = Repository.get_by_repo_name(self.acl_repo_name)
128 scope_repo_id = repo.repo_id if repo else None
129 scope_repo_id = repo.repo_id if repo else None
129
130
130 token_match = userobj.authenticate_by_token(
131 token_match = userobj.authenticate_by_token(
131 password, roles=[UserApiKeys.ROLE_VCS],
132 password, roles=[UserApiKeys.ROLE_VCS],
132 scope_repo_id=scope_repo_id)
133 scope_repo_id=scope_repo_id)
133
134
134 if userobj.username == username and token_match:
135 if userobj.username == username and token_match:
135 log.info(
136 log.info(
136 'user `%s` successfully authenticated via %s',
137 'user `%s` successfully authenticated via %s',
137 user_attrs['username'], self.name)
138 user_attrs['username'], self.name)
138 return user_attrs
139 return user_attrs
139 log.error(
140 log.error(
140 'user `%s` failed to authenticate via %s, reason: bad or '
141 'user `%s` failed to authenticate via %s, reason: bad or '
141 'inactive token.', username, self.name)
142 'inactive token.', username, self.name)
142 else:
143 else:
143 log.warning(
144 log.warning(
144 'user `%s` failed to authenticate via %s, reason: account not '
145 'user `%s` failed to authenticate via %s, reason: account not '
145 'active.', username, self.name)
146 'active.', username, self.name)
146 return None
147 return None
General Comments 0
You need to be logged in to leave comments. Login now