##// END OF EJS Templates
auth: refactor code and simplified instructions....
marcink -
r1454:01fbc7af default
parent child Browse files
Show More
@@ -1,629 +1,647 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 logging
26 import logging
27 import time
27 import time
28 import traceback
28 import traceback
29 import warnings
29 import warnings
30
30
31 from pyramid.threadlocal import get_current_registry
31 from pyramid.threadlocal import get_current_registry
32 from sqlalchemy.ext.hybrid import hybrid_property
32 from sqlalchemy.ext.hybrid import hybrid_property
33
33
34 from rhodecode.authentication.interface import IAuthnPluginRegistry
34 from rhodecode.authentication.interface import IAuthnPluginRegistry
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.lib import caches
36 from rhodecode.lib import caches
37 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
37 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
38 from rhodecode.lib.utils2 import md5_safe, safe_int
38 from rhodecode.lib.utils2 import md5_safe, safe_int
39 from rhodecode.lib.utils2 import safe_str
39 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
42 from rhodecode.model.settings import SettingsModel
42 from rhodecode.model.settings import SettingsModel
43 from rhodecode.model.user import UserModel
43 from rhodecode.model.user import UserModel
44 from rhodecode.model.user_group import UserGroupModel
44 from rhodecode.model.user_group import UserGroupModel
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49 # auth types that authenticate() function can receive
49 # auth types that authenticate() function can receive
50 VCS_TYPE = 'vcs'
50 VCS_TYPE = 'vcs'
51 HTTP_TYPE = 'http'
51 HTTP_TYPE = 'http'
52
52
53
53
54 class LazyFormencode(object):
54 class LazyFormencode(object):
55 def __init__(self, formencode_obj, *args, **kwargs):
55 def __init__(self, formencode_obj, *args, **kwargs):
56 self.formencode_obj = formencode_obj
56 self.formencode_obj = formencode_obj
57 self.args = args
57 self.args = args
58 self.kwargs = kwargs
58 self.kwargs = kwargs
59
59
60 def __call__(self, *args, **kwargs):
60 def __call__(self, *args, **kwargs):
61 from inspect import isfunction
61 from inspect import isfunction
62 formencode_obj = self.formencode_obj
62 formencode_obj = self.formencode_obj
63 if isfunction(formencode_obj):
63 if isfunction(formencode_obj):
64 # case we wrap validators into functions
64 # case we wrap validators into functions
65 formencode_obj = self.formencode_obj(*args, **kwargs)
65 formencode_obj = self.formencode_obj(*args, **kwargs)
66 return formencode_obj(*self.args, **self.kwargs)
66 return formencode_obj(*self.args, **self.kwargs)
67
67
68
68
69 class RhodeCodeAuthPluginBase(object):
69 class RhodeCodeAuthPluginBase(object):
70 # cache the authentication request for N amount of seconds. Some kind
70 # cache the authentication request for N amount of seconds. Some kind
71 # of authentication methods are very heavy and it's very efficient to cache
71 # of authentication methods are very heavy and it's very efficient to cache
72 # the result of a call. If it's set to None (default) cache is off
72 # the result of a call. If it's set to None (default) cache is off
73 AUTH_CACHE_TTL = None
73 AUTH_CACHE_TTL = None
74 AUTH_CACHE = {}
74 AUTH_CACHE = {}
75
75
76 auth_func_attrs = {
76 auth_func_attrs = {
77 "username": "unique username",
77 "username": "unique username",
78 "firstname": "first name",
78 "firstname": "first name",
79 "lastname": "last name",
79 "lastname": "last name",
80 "email": "email address",
80 "email": "email address",
81 "groups": '["list", "of", "groups"]',
81 "groups": '["list", "of", "groups"]',
82 "extern_name": "name in external source of record",
82 "extern_name": "name in external source of record",
83 "extern_type": "type of external source of record",
83 "extern_type": "type of external source of record",
84 "admin": 'True|False defines if user should be RhodeCode super admin',
84 "admin": 'True|False defines if user should be RhodeCode super admin',
85 "active":
85 "active":
86 'True|False defines active state of user internally for RhodeCode',
86 'True|False defines active state of user internally for RhodeCode',
87 "active_from_extern":
87 "active_from_extern":
88 "True|False\None, active state from the external auth, "
88 "True|False\None, active state from the external auth, "
89 "None means use definition from RhodeCode extern_type active value"
89 "None means use definition from RhodeCode extern_type active value"
90 }
90 }
91 # set on authenticate() method and via set_auth_type func.
91 # set on authenticate() method and via set_auth_type func.
92 auth_type = None
92 auth_type = None
93
93
94 # List of setting names to store encrypted. Plugins may override this list
94 # List of setting names to store encrypted. Plugins may override this list
95 # to store settings encrypted.
95 # to store settings encrypted.
96 _settings_encrypted = []
96 _settings_encrypted = []
97
97
98 # Mapping of python to DB settings model types. Plugins may override or
98 # Mapping of python to DB settings model types. Plugins may override or
99 # extend this mapping.
99 # extend this mapping.
100 _settings_type_map = {
100 _settings_type_map = {
101 colander.String: 'unicode',
101 colander.String: 'unicode',
102 colander.Integer: 'int',
102 colander.Integer: 'int',
103 colander.Boolean: 'bool',
103 colander.Boolean: 'bool',
104 colander.List: 'list',
104 colander.List: 'list',
105 }
105 }
106
106
107 def __init__(self, plugin_id):
107 def __init__(self, plugin_id):
108 self._plugin_id = plugin_id
108 self._plugin_id = plugin_id
109
109
110 def __str__(self):
110 def __str__(self):
111 return self.get_id()
111 return self.get_id()
112
112
113 def _get_setting_full_name(self, name):
113 def _get_setting_full_name(self, name):
114 """
114 """
115 Return the full setting name used for storing values in the database.
115 Return the full setting name used for storing values in the database.
116 """
116 """
117 # TODO: johbo: Using the name here is problematic. It would be good to
117 # TODO: johbo: Using the name here is problematic. It would be good to
118 # introduce either new models in the database to hold Plugin and
118 # introduce either new models in the database to hold Plugin and
119 # PluginSetting or to use the plugin id here.
119 # PluginSetting or to use the plugin id here.
120 return 'auth_{}_{}'.format(self.name, name)
120 return 'auth_{}_{}'.format(self.name, name)
121
121
122 def _get_setting_type(self, name):
122 def _get_setting_type(self, name):
123 """
123 """
124 Return the type of a setting. This type is defined by the SettingsModel
124 Return the type of a setting. This type is defined by the SettingsModel
125 and determines how the setting is stored in DB. Optionally the suffix
125 and determines how the setting is stored in DB. Optionally the suffix
126 `.encrypted` is appended to instruct SettingsModel to store it
126 `.encrypted` is appended to instruct SettingsModel to store it
127 encrypted.
127 encrypted.
128 """
128 """
129 schema_node = self.get_settings_schema().get(name)
129 schema_node = self.get_settings_schema().get(name)
130 db_type = self._settings_type_map.get(
130 db_type = self._settings_type_map.get(
131 type(schema_node.typ), 'unicode')
131 type(schema_node.typ), 'unicode')
132 if name in self._settings_encrypted:
132 if name in self._settings_encrypted:
133 db_type = '{}.encrypted'.format(db_type)
133 db_type = '{}.encrypted'.format(db_type)
134 return db_type
134 return db_type
135
135
136 def is_enabled(self):
136 def is_enabled(self):
137 """
137 """
138 Returns true if this plugin is enabled. An enabled plugin can be
138 Returns true if this plugin is enabled. An enabled plugin can be
139 configured in the admin interface but it is not consulted during
139 configured in the admin interface but it is not consulted during
140 authentication.
140 authentication.
141 """
141 """
142 auth_plugins = SettingsModel().get_auth_plugins()
142 auth_plugins = SettingsModel().get_auth_plugins()
143 return self.get_id() in auth_plugins
143 return self.get_id() in auth_plugins
144
144
145 def is_active(self):
145 def is_active(self):
146 """
146 """
147 Returns true if the plugin is activated. An activated plugin is
147 Returns true if the plugin is activated. An activated plugin is
148 consulted during authentication, assumed it is also enabled.
148 consulted during authentication, assumed it is also enabled.
149 """
149 """
150 return self.get_setting_by_name('enabled')
150 return self.get_setting_by_name('enabled')
151
151
152 def get_id(self):
152 def get_id(self):
153 """
153 """
154 Returns the plugin id.
154 Returns the plugin id.
155 """
155 """
156 return self._plugin_id
156 return self._plugin_id
157
157
158 def get_display_name(self):
158 def get_display_name(self):
159 """
159 """
160 Returns a translation string for displaying purposes.
160 Returns a translation string for displaying purposes.
161 """
161 """
162 raise NotImplementedError('Not implemented in base class')
162 raise NotImplementedError('Not implemented in base class')
163
163
164 def get_settings_schema(self):
164 def get_settings_schema(self):
165 """
165 """
166 Returns a colander schema, representing the plugin settings.
166 Returns a colander schema, representing the plugin settings.
167 """
167 """
168 return AuthnPluginSettingsSchemaBase()
168 return AuthnPluginSettingsSchemaBase()
169
169
170 def get_setting_by_name(self, name, default=None):
170 def get_setting_by_name(self, name, default=None):
171 """
171 """
172 Returns a plugin setting by name.
172 Returns a plugin setting by name.
173 """
173 """
174 full_name = self._get_setting_full_name(name)
174 full_name = self._get_setting_full_name(name)
175 db_setting = SettingsModel().get_setting_by_name(full_name)
175 db_setting = SettingsModel().get_setting_by_name(full_name)
176 return db_setting.app_settings_value if db_setting else default
176 return db_setting.app_settings_value if db_setting else default
177
177
178 def create_or_update_setting(self, name, value):
178 def create_or_update_setting(self, name, value):
179 """
179 """
180 Create or update a setting for this plugin in the persistent storage.
180 Create or update a setting for this plugin in the persistent storage.
181 """
181 """
182 full_name = self._get_setting_full_name(name)
182 full_name = self._get_setting_full_name(name)
183 type_ = self._get_setting_type(name)
183 type_ = self._get_setting_type(name)
184 db_setting = SettingsModel().create_or_update_setting(
184 db_setting = SettingsModel().create_or_update_setting(
185 full_name, value, type_)
185 full_name, value, type_)
186 return db_setting.app_settings_value
186 return db_setting.app_settings_value
187
187
188 def get_settings(self):
188 def get_settings(self):
189 """
189 """
190 Returns the plugin settings as dictionary.
190 Returns the plugin settings as dictionary.
191 """
191 """
192 settings = {}
192 settings = {}
193 for node in self.get_settings_schema():
193 for node in self.get_settings_schema():
194 settings[node.name] = self.get_setting_by_name(node.name)
194 settings[node.name] = self.get_setting_by_name(node.name)
195 return settings
195 return settings
196
196
197 @property
197 @property
198 def validators(self):
198 def validators(self):
199 """
199 """
200 Exposes RhodeCode validators modules
200 Exposes RhodeCode validators modules
201 """
201 """
202 # this is a hack to overcome issues with pylons threadlocals and
202 # this is a hack to overcome issues with pylons threadlocals and
203 # translator object _() not beein registered properly.
203 # translator object _() not beein registered properly.
204 class LazyCaller(object):
204 class LazyCaller(object):
205 def __init__(self, name):
205 def __init__(self, name):
206 self.validator_name = name
206 self.validator_name = name
207
207
208 def __call__(self, *args, **kwargs):
208 def __call__(self, *args, **kwargs):
209 from rhodecode.model import validators as v
209 from rhodecode.model import validators as v
210 obj = getattr(v, self.validator_name)
210 obj = getattr(v, self.validator_name)
211 # log.debug('Initializing lazy formencode object: %s', obj)
211 # log.debug('Initializing lazy formencode object: %s', obj)
212 return LazyFormencode(obj, *args, **kwargs)
212 return LazyFormencode(obj, *args, **kwargs)
213
213
214 class ProxyGet(object):
214 class ProxyGet(object):
215 def __getattribute__(self, name):
215 def __getattribute__(self, name):
216 return LazyCaller(name)
216 return LazyCaller(name)
217
217
218 return ProxyGet()
218 return ProxyGet()
219
219
220 @hybrid_property
220 @hybrid_property
221 def name(self):
221 def name(self):
222 """
222 """
223 Returns the name of this authentication plugin.
223 Returns the name of this authentication plugin.
224
224
225 :returns: string
225 :returns: string
226 """
226 """
227 raise NotImplementedError("Not implemented in base class")
227 raise NotImplementedError("Not implemented in base class")
228
228
229 def get_url_slug(self):
229 def get_url_slug(self):
230 """
230 """
231 Returns a slug which should be used when constructing URLs which refer
231 Returns a slug which should be used when constructing URLs which refer
232 to this plugin. By default it returns the plugin name. If the name is
232 to this plugin. By default it returns the plugin name. If the name is
233 not suitable for using it in an URL the plugin should override this
233 not suitable for using it in an URL the plugin should override this
234 method.
234 method.
235 """
235 """
236 return self.name
236 return self.name
237
237
238 @property
238 @property
239 def is_headers_auth(self):
239 def is_headers_auth(self):
240 """
240 """
241 Returns True if this authentication plugin uses HTTP headers as
241 Returns True if this authentication plugin uses HTTP headers as
242 authentication method.
242 authentication method.
243 """
243 """
244 return False
244 return False
245
245
246 @hybrid_property
246 @hybrid_property
247 def is_container_auth(self):
247 def is_container_auth(self):
248 """
248 """
249 Deprecated method that indicates if this authentication plugin uses
249 Deprecated method that indicates if this authentication plugin uses
250 HTTP headers as authentication method.
250 HTTP headers as authentication method.
251 """
251 """
252 warnings.warn(
252 warnings.warn(
253 'Use is_headers_auth instead.', category=DeprecationWarning)
253 'Use is_headers_auth instead.', category=DeprecationWarning)
254 return self.is_headers_auth
254 return self.is_headers_auth
255
255
256 @hybrid_property
256 @hybrid_property
257 def allows_creating_users(self):
257 def allows_creating_users(self):
258 """
258 """
259 Defines if Plugin allows users to be created on-the-fly when
259 Defines if Plugin allows users to be created on-the-fly when
260 authentication is called. Controls how external plugins should behave
260 authentication is called. Controls how external plugins should behave
261 in terms if they are allowed to create new users, or not. Base plugins
261 in terms if they are allowed to create new users, or not. Base plugins
262 should not be allowed to, but External ones should be !
262 should not be allowed to, but External ones should be !
263
263
264 :return: bool
264 :return: bool
265 """
265 """
266 return False
266 return False
267
267
268 def set_auth_type(self, auth_type):
268 def set_auth_type(self, auth_type):
269 self.auth_type = auth_type
269 self.auth_type = auth_type
270
270
271 def allows_authentication_from(
271 def allows_authentication_from(
272 self, user, allows_non_existing_user=True,
272 self, user, allows_non_existing_user=True,
273 allowed_auth_plugins=None, allowed_auth_sources=None):
273 allowed_auth_plugins=None, allowed_auth_sources=None):
274 """
274 """
275 Checks if this authentication module should accept a request for
275 Checks if this authentication module should accept a request for
276 the current user.
276 the current user.
277
277
278 :param user: user object fetched using plugin's get_user() method.
278 :param user: user object fetched using plugin's get_user() method.
279 :param allows_non_existing_user: if True, don't allow the
279 :param allows_non_existing_user: if True, don't allow the
280 user to be empty, meaning not existing in our database
280 user to be empty, meaning not existing in our database
281 :param allowed_auth_plugins: if provided, users extern_type will be
281 :param allowed_auth_plugins: if provided, users extern_type will be
282 checked against a list of provided extern types, which are plugin
282 checked against a list of provided extern types, which are plugin
283 auth_names in the end
283 auth_names in the end
284 :param allowed_auth_sources: authentication type allowed,
284 :param allowed_auth_sources: authentication type allowed,
285 `http` or `vcs` default is both.
285 `http` or `vcs` default is both.
286 defines if plugin will accept only http authentication vcs
286 defines if plugin will accept only http authentication vcs
287 authentication(git/hg) or both
287 authentication(git/hg) or both
288 :returns: boolean
288 :returns: boolean
289 """
289 """
290 if not user and not allows_non_existing_user:
290 if not user and not allows_non_existing_user:
291 log.debug('User is empty but plugin does not allow empty users,'
291 log.debug('User is empty but plugin does not allow empty users,'
292 'not allowed to authenticate')
292 'not allowed to authenticate')
293 return False
293 return False
294
294
295 expected_auth_plugins = allowed_auth_plugins or [self.name]
295 expected_auth_plugins = allowed_auth_plugins or [self.name]
296 if user and (user.extern_type and
296 if user and (user.extern_type and
297 user.extern_type not in expected_auth_plugins):
297 user.extern_type not in expected_auth_plugins):
298 log.debug(
298 log.debug(
299 'User `%s` is bound to `%s` auth type. Plugin allows only '
299 'User `%s` is bound to `%s` auth type. Plugin allows only '
300 '%s, skipping', user, user.extern_type, expected_auth_plugins)
300 '%s, skipping', user, user.extern_type, expected_auth_plugins)
301
301
302 return False
302 return False
303
303
304 # by default accept both
304 # by default accept both
305 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
305 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
306 if self.auth_type not in expected_auth_from:
306 if self.auth_type not in expected_auth_from:
307 log.debug('Current auth source is %s but plugin only allows %s',
307 log.debug('Current auth source is %s but plugin only allows %s',
308 self.auth_type, expected_auth_from)
308 self.auth_type, expected_auth_from)
309 return False
309 return False
310
310
311 return True
311 return True
312
312
313 def get_user(self, username=None, **kwargs):
313 def get_user(self, username=None, **kwargs):
314 """
314 """
315 Helper method for user fetching in plugins, by default it's using
315 Helper method for user fetching in plugins, by default it's using
316 simple fetch by username, but this method can be custimized in plugins
316 simple fetch by username, but this method can be custimized in plugins
317 eg. headers auth plugin to fetch user by environ params
317 eg. headers auth plugin to fetch user by environ params
318
318
319 :param username: username if given to fetch from database
319 :param username: username if given to fetch from database
320 :param kwargs: extra arguments needed for user fetching.
320 :param kwargs: extra arguments needed for user fetching.
321 """
321 """
322 user = None
322 user = None
323 log.debug(
323 log.debug(
324 'Trying to fetch user `%s` from RhodeCode database', username)
324 'Trying to fetch user `%s` from RhodeCode database', username)
325 if username:
325 if username:
326 user = User.get_by_username(username)
326 user = User.get_by_username(username)
327 if not user:
327 if not user:
328 log.debug('User not found, fallback to fetch user in '
328 log.debug('User not found, fallback to fetch user in '
329 'case insensitive mode')
329 'case insensitive mode')
330 user = User.get_by_username(username, case_insensitive=True)
330 user = User.get_by_username(username, case_insensitive=True)
331 else:
331 else:
332 log.debug('provided username:`%s` is empty skipping...', username)
332 log.debug('provided username:`%s` is empty skipping...', username)
333 if not user:
333 if not user:
334 log.debug('User `%s` not found in database', username)
334 log.debug('User `%s` not found in database', username)
335 return user
335 return user
336
336
337 def user_activation_state(self):
337 def user_activation_state(self):
338 """
338 """
339 Defines user activation state when creating new users
339 Defines user activation state when creating new users
340
340
341 :returns: boolean
341 :returns: boolean
342 """
342 """
343 raise NotImplementedError("Not implemented in base class")
343 raise NotImplementedError("Not implemented in base class")
344
344
345 def auth(self, userobj, username, passwd, settings, **kwargs):
345 def auth(self, userobj, username, passwd, settings, **kwargs):
346 """
346 """
347 Given a user object (which may be null), username, a plaintext
347 Given a user object (which may be null), username, a plaintext
348 password, and a settings object (containing all the keys needed as
348 password, and a settings object (containing all the keys needed as
349 listed in settings()), authenticate this user's login attempt.
349 listed in settings()), authenticate this user's login attempt.
350
350
351 Return None on failure. On success, return a dictionary of the form:
351 Return None on failure. On success, return a dictionary of the form:
352
352
353 see: RhodeCodeAuthPluginBase.auth_func_attrs
353 see: RhodeCodeAuthPluginBase.auth_func_attrs
354 This is later validated for correctness
354 This is later validated for correctness
355 """
355 """
356 raise NotImplementedError("not implemented in base class")
356 raise NotImplementedError("not implemented in base class")
357
357
358 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
358 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
359 """
359 """
360 Wrapper to call self.auth() that validates call on it
360 Wrapper to call self.auth() that validates call on it
361
361
362 :param userobj: userobj
362 :param userobj: userobj
363 :param username: username
363 :param username: username
364 :param passwd: plaintext password
364 :param passwd: plaintext password
365 :param settings: plugin settings
365 :param settings: plugin settings
366 """
366 """
367 auth = self.auth(userobj, username, passwd, settings, **kwargs)
367 auth = self.auth(userobj, username, passwd, settings, **kwargs)
368 if auth:
368 if auth:
369 # check if hash should be migrated ?
369 # check if hash should be migrated ?
370 new_hash = auth.get('_hash_migrate')
370 new_hash = auth.get('_hash_migrate')
371 if new_hash:
371 if new_hash:
372 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
372 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
373 return self._validate_auth_return(auth)
373 return self._validate_auth_return(auth)
374 return auth
374 return auth
375
375
376 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
376 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
377 new_hash_cypher = _RhodeCodeCryptoBCrypt()
377 new_hash_cypher = _RhodeCodeCryptoBCrypt()
378 # extra checks, so make sure new hash is correct.
378 # extra checks, so make sure new hash is correct.
379 password_encoded = safe_str(password)
379 password_encoded = safe_str(password)
380 if new_hash and new_hash_cypher.hash_check(
380 if new_hash and new_hash_cypher.hash_check(
381 password_encoded, new_hash):
381 password_encoded, new_hash):
382 cur_user = User.get_by_username(username)
382 cur_user = User.get_by_username(username)
383 cur_user.password = new_hash
383 cur_user.password = new_hash
384 Session().add(cur_user)
384 Session().add(cur_user)
385 Session().flush()
385 Session().flush()
386 log.info('Migrated user %s hash to bcrypt', cur_user)
386 log.info('Migrated user %s hash to bcrypt', cur_user)
387
387
388 def _validate_auth_return(self, ret):
388 def _validate_auth_return(self, ret):
389 if not isinstance(ret, dict):
389 if not isinstance(ret, dict):
390 raise Exception('returned value from auth must be a dict')
390 raise Exception('returned value from auth must be a dict')
391 for k in self.auth_func_attrs:
391 for k in self.auth_func_attrs:
392 if k not in ret:
392 if k not in ret:
393 raise Exception('Missing %s attribute from returned data' % k)
393 raise Exception('Missing %s attribute from returned data' % k)
394 return ret
394 return ret
395
395
396
396
397 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
397 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
398
398
399 @hybrid_property
399 @hybrid_property
400 def allows_creating_users(self):
400 def allows_creating_users(self):
401 return True
401 return True
402
402
403 def use_fake_password(self):
403 def use_fake_password(self):
404 """
404 """
405 Return a boolean that indicates whether or not we should set the user's
405 Return a boolean that indicates whether or not we should set the user's
406 password to a random value when it is authenticated by this plugin.
406 password to a random value when it is authenticated by this plugin.
407 If your plugin provides authentication, then you will generally
407 If your plugin provides authentication, then you will generally
408 want this.
408 want this.
409
409
410 :returns: boolean
410 :returns: boolean
411 """
411 """
412 raise NotImplementedError("Not implemented in base class")
412 raise NotImplementedError("Not implemented in base class")
413
413
414 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
414 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
415 # at this point _authenticate calls plugin's `auth()` function
415 # at this point _authenticate calls plugin's `auth()` function
416 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
416 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
417 userobj, username, passwd, settings, **kwargs)
417 userobj, username, passwd, settings, **kwargs)
418 if auth:
418 if auth:
419 # maybe plugin will clean the username ?
419 # maybe plugin will clean the username ?
420 # we should use the return value
420 # we should use the return value
421 username = auth['username']
421 username = auth['username']
422
422
423 # if external source tells us that user is not active, we should
423 # if external source tells us that user is not active, we should
424 # skip rest of the process. This can prevent from creating users in
424 # skip rest of the process. This can prevent from creating users in
425 # RhodeCode when using external authentication, but if it's
425 # RhodeCode when using external authentication, but if it's
426 # inactive user we shouldn't create that user anyway
426 # inactive user we shouldn't create that user anyway
427 if auth['active_from_extern'] is False:
427 if auth['active_from_extern'] is False:
428 log.warning(
428 log.warning(
429 "User %s authenticated against %s, but is inactive",
429 "User %s authenticated against %s, but is inactive",
430 username, self.__module__)
430 username, self.__module__)
431 return None
431 return None
432
432
433 cur_user = User.get_by_username(username, case_insensitive=True)
433 cur_user = User.get_by_username(username, case_insensitive=True)
434 is_user_existing = cur_user is not None
434 is_user_existing = cur_user is not None
435
435
436 if is_user_existing:
436 if is_user_existing:
437 log.debug('Syncing user `%s` from '
437 log.debug('Syncing user `%s` from '
438 '`%s` plugin', username, self.name)
438 '`%s` plugin', username, self.name)
439 else:
439 else:
440 log.debug('Creating non existing user `%s` from '
440 log.debug('Creating non existing user `%s` from '
441 '`%s` plugin', username, self.name)
441 '`%s` plugin', username, self.name)
442
442
443 if self.allows_creating_users:
443 if self.allows_creating_users:
444 log.debug('Plugin `%s` allows to '
444 log.debug('Plugin `%s` allows to '
445 'create new users', self.name)
445 'create new users', self.name)
446 else:
446 else:
447 log.debug('Plugin `%s` does not allow to '
447 log.debug('Plugin `%s` does not allow to '
448 'create new users', self.name)
448 'create new users', self.name)
449
449
450 user_parameters = {
450 user_parameters = {
451 'username': username,
451 'username': username,
452 'email': auth["email"],
452 'email': auth["email"],
453 'firstname': auth["firstname"],
453 'firstname': auth["firstname"],
454 'lastname': auth["lastname"],
454 'lastname': auth["lastname"],
455 'active': auth["active"],
455 'active': auth["active"],
456 'admin': auth["admin"],
456 'admin': auth["admin"],
457 'extern_name': auth["extern_name"],
457 'extern_name': auth["extern_name"],
458 'extern_type': self.name,
458 'extern_type': self.name,
459 'plugin': self,
459 'plugin': self,
460 'allow_to_create_user': self.allows_creating_users,
460 'allow_to_create_user': self.allows_creating_users,
461 }
461 }
462
462
463 if not is_user_existing:
463 if not is_user_existing:
464 if self.use_fake_password():
464 if self.use_fake_password():
465 # Randomize the PW because we don't need it, but don't want
465 # Randomize the PW because we don't need it, but don't want
466 # them blank either
466 # them blank either
467 passwd = PasswordGenerator().gen_password(length=16)
467 passwd = PasswordGenerator().gen_password(length=16)
468 user_parameters['password'] = passwd
468 user_parameters['password'] = passwd
469 else:
469 else:
470 # Since the password is required by create_or_update method of
470 # Since the password is required by create_or_update method of
471 # UserModel, we need to set it explicitly.
471 # UserModel, we need to set it explicitly.
472 # The create_or_update method is smart and recognises the
472 # The create_or_update method is smart and recognises the
473 # password hashes as well.
473 # password hashes as well.
474 user_parameters['password'] = cur_user.password
474 user_parameters['password'] = cur_user.password
475
475
476 # we either create or update users, we also pass the flag
476 # we either create or update users, we also pass the flag
477 # that controls if this method can actually do that.
477 # that controls if this method can actually do that.
478 # raises NotAllowedToCreateUserError if it cannot, and we try to.
478 # raises NotAllowedToCreateUserError if it cannot, and we try to.
479 user = UserModel().create_or_update(**user_parameters)
479 user = UserModel().create_or_update(**user_parameters)
480 Session().flush()
480 Session().flush()
481 # enforce user is just in given groups, all of them has to be ones
481 # enforce user is just in given groups, all of them has to be ones
482 # created from plugins. We store this info in _group_data JSON
482 # created from plugins. We store this info in _group_data JSON
483 # field
483 # field
484 try:
484 try:
485 groups = auth['groups'] or []
485 groups = auth['groups'] or []
486 UserGroupModel().enforce_groups(user, groups, self.name)
486 UserGroupModel().enforce_groups(user, groups, self.name)
487 except Exception:
487 except Exception:
488 # for any reason group syncing fails, we should
488 # for any reason group syncing fails, we should
489 # proceed with login
489 # proceed with login
490 log.error(traceback.format_exc())
490 log.error(traceback.format_exc())
491 Session().commit()
491 Session().commit()
492 return auth
492 return auth
493
493
494
494
495 def loadplugin(plugin_id):
495 def loadplugin(plugin_id):
496 """
496 """
497 Loads and returns an instantiated authentication plugin.
497 Loads and returns an instantiated authentication plugin.
498 Returns the RhodeCodeAuthPluginBase subclass on success,
498 Returns the RhodeCodeAuthPluginBase subclass on success,
499 or None on failure.
499 or None on failure.
500 """
500 """
501 # TODO: Disusing pyramids thread locals to retrieve the registry.
501 # TODO: Disusing pyramids thread locals to retrieve the registry.
502 authn_registry = get_authn_registry()
502 authn_registry = get_authn_registry()
503 plugin = authn_registry.get_plugin(plugin_id)
503 plugin = authn_registry.get_plugin(plugin_id)
504 if plugin is None:
504 if plugin is None:
505 log.error('Authentication plugin not found: "%s"', plugin_id)
505 log.error('Authentication plugin not found: "%s"', plugin_id)
506 return plugin
506 return plugin
507
507
508
508
509 def get_authn_registry(registry=None):
509 def get_authn_registry(registry=None):
510 registry = registry or get_current_registry()
510 registry = registry or get_current_registry()
511 authn_registry = registry.getUtility(IAuthnPluginRegistry)
511 authn_registry = registry.getUtility(IAuthnPluginRegistry)
512 return authn_registry
512 return authn_registry
513
513
514
514
515 def get_auth_cache_manager(custom_ttl=None):
515 def get_auth_cache_manager(custom_ttl=None):
516 return caches.get_cache_manager(
516 return caches.get_cache_manager(
517 'auth_plugins', 'rhodecode.authentication', custom_ttl)
517 'auth_plugins', 'rhodecode.authentication', custom_ttl)
518
518
519
519
520 def authenticate(username, password, environ=None, auth_type=None,
520 def authenticate(username, password, environ=None, auth_type=None,
521 skip_missing=False, registry=None):
521 skip_missing=False, registry=None):
522 """
522 """
523 Authentication function used for access control,
523 Authentication function used for access control,
524 It tries to authenticate based on enabled authentication modules.
524 It tries to authenticate based on enabled authentication modules.
525
525
526 :param username: username can be empty for headers auth
526 :param username: username can be empty for headers auth
527 :param password: password can be empty for headers auth
527 :param password: password can be empty for headers auth
528 :param environ: environ headers passed for headers auth
528 :param environ: environ headers passed for headers auth
529 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
529 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
530 :param skip_missing: ignores plugins that are in db but not in environment
530 :param skip_missing: ignores plugins that are in db but not in environment
531 :returns: None if auth failed, plugin_user dict if auth is correct
531 :returns: None if auth failed, plugin_user dict if auth is correct
532 """
532 """
533 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
533 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
534 raise ValueError('auth type must be on of http, vcs got "%s" instead'
534 raise ValueError('auth type must be on of http, vcs got "%s" instead'
535 % auth_type)
535 % auth_type)
536 headers_only = environ and not (username and password)
536 headers_only = environ and not (username and password)
537
537
538 authn_registry = get_authn_registry(registry)
538 authn_registry = get_authn_registry(registry)
539 for plugin in authn_registry.get_plugins_for_authentication():
539 for plugin in authn_registry.get_plugins_for_authentication():
540 plugin.set_auth_type(auth_type)
540 plugin.set_auth_type(auth_type)
541 user = plugin.get_user(username)
541 user = plugin.get_user(username)
542 display_user = user.username if user else username
542 display_user = user.username if user else username
543
543
544 if headers_only and not plugin.is_headers_auth:
544 if headers_only and not plugin.is_headers_auth:
545 log.debug('Auth type is for headers only and plugin `%s` is not '
545 log.debug('Auth type is for headers only and plugin `%s` is not '
546 'headers plugin, skipping...', plugin.get_id())
546 'headers plugin, skipping...', plugin.get_id())
547 continue
547 continue
548
548
549 # load plugin settings from RhodeCode database
549 # load plugin settings from RhodeCode database
550 plugin_settings = plugin.get_settings()
550 plugin_settings = plugin.get_settings()
551 log.debug('Plugin settings:%s', plugin_settings)
551 log.debug('Plugin settings:%s', plugin_settings)
552
552
553 log.debug('Trying authentication using ** %s **', plugin.get_id())
553 log.debug('Trying authentication using ** %s **', plugin.get_id())
554 # use plugin's method of user extraction.
554 # use plugin's method of user extraction.
555 user = plugin.get_user(username, environ=environ,
555 user = plugin.get_user(username, environ=environ,
556 settings=plugin_settings)
556 settings=plugin_settings)
557 display_user = user.username if user else username
557 display_user = user.username if user else username
558 log.debug(
558 log.debug(
559 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
559 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
560
560
561 if not plugin.allows_authentication_from(user):
561 if not plugin.allows_authentication_from(user):
562 log.debug('Plugin %s does not accept user `%s` for authentication',
562 log.debug('Plugin %s does not accept user `%s` for authentication',
563 plugin.get_id(), display_user)
563 plugin.get_id(), display_user)
564 continue
564 continue
565 else:
565 else:
566 log.debug('Plugin %s accepted user `%s` for authentication',
566 log.debug('Plugin %s accepted user `%s` for authentication',
567 plugin.get_id(), display_user)
567 plugin.get_id(), display_user)
568
568
569 log.info('Authenticating user `%s` using %s plugin',
569 log.info('Authenticating user `%s` using %s plugin',
570 display_user, plugin.get_id())
570 display_user, plugin.get_id())
571
571
572 _cache_ttl = 0
572 _cache_ttl = 0
573
573
574 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
574 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
575 # plugin cache set inside is more important than the settings value
575 # plugin cache set inside is more important than the settings value
576 _cache_ttl = plugin.AUTH_CACHE_TTL
576 _cache_ttl = plugin.AUTH_CACHE_TTL
577 elif plugin_settings.get('cache_ttl'):
577 elif plugin_settings.get('cache_ttl'):
578 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
578 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
579
579
580 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
580 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
581
581
582 # get instance of cache manager configured for a namespace
582 # get instance of cache manager configured for a namespace
583 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
583 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
584
584
585 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
585 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
586 plugin.get_id(), plugin_cache_active, _cache_ttl)
586 plugin.get_id(), plugin_cache_active, _cache_ttl)
587
587
588 # for environ based password can be empty, but then the validation is
588 # for environ based password can be empty, but then the validation is
589 # on the server that fills in the env data needed for authentication
589 # on the server that fills in the env data needed for authentication
590 _password_hash = md5_safe(plugin.name + username + (password or ''))
590 _password_hash = md5_safe(plugin.name + username + (password or ''))
591
591
592 # _authenticate is a wrapper for .auth() method of plugin.
592 # _authenticate is a wrapper for .auth() method of plugin.
593 # it checks if .auth() sends proper data.
593 # it checks if .auth() sends proper data.
594 # For RhodeCodeExternalAuthPlugin it also maps users to
594 # For RhodeCodeExternalAuthPlugin it also maps users to
595 # Database and maps the attributes returned from .auth()
595 # Database and maps the attributes returned from .auth()
596 # to RhodeCode database. If this function returns data
596 # to RhodeCode database. If this function returns data
597 # then auth is correct.
597 # then auth is correct.
598 start = time.time()
598 start = time.time()
599 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
599 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
600
600
601 def auth_func():
601 def auth_func():
602 """
602 """
603 This function is used internally in Cache of Beaker to calculate
603 This function is used internally in Cache of Beaker to calculate
604 Results
604 Results
605 """
605 """
606 return plugin._authenticate(
606 return plugin._authenticate(
607 user, username, password, plugin_settings,
607 user, username, password, plugin_settings,
608 environ=environ or {})
608 environ=environ or {})
609
609
610 if plugin_cache_active:
610 if plugin_cache_active:
611 plugin_user = cache_manager.get(
611 plugin_user = cache_manager.get(
612 _password_hash, createfunc=auth_func)
612 _password_hash, createfunc=auth_func)
613 else:
613 else:
614 plugin_user = auth_func()
614 plugin_user = auth_func()
615
615
616 auth_time = time.time() - start
616 auth_time = time.time() - start
617 log.debug('Authentication for plugin `%s` completed in %.3fs, '
617 log.debug('Authentication for plugin `%s` completed in %.3fs, '
618 'expiration time of fetched cache %.1fs.',
618 'expiration time of fetched cache %.1fs.',
619 plugin.get_id(), auth_time, _cache_ttl)
619 plugin.get_id(), auth_time, _cache_ttl)
620
620
621 log.debug('PLUGIN USER DATA: %s', plugin_user)
621 log.debug('PLUGIN USER DATA: %s', plugin_user)
622
622
623 if plugin_user:
623 if plugin_user:
624 log.debug('Plugin returned proper authentication data')
624 log.debug('Plugin returned proper authentication data')
625 return plugin_user
625 return plugin_user
626 # we failed to Auth because .auth() method didn't return proper user
626 # we failed to Auth because .auth() method didn't return proper user
627 log.debug("User `%s` failed to authenticate against %s",
627 log.debug("User `%s` failed to authenticate against %s",
628 display_user, plugin.get_id())
628 display_user, plugin.get_id())
629 return None
629 return None
630
631
632 def chop_at(s, sub, inclusive=False):
633 """Truncate string ``s`` at the first occurrence of ``sub``.
634
635 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
636
637 >>> chop_at("plutocratic brats", "rat")
638 'plutoc'
639 >>> chop_at("plutocratic brats", "rat", True)
640 'plutocrat'
641 """
642 pos = s.find(sub)
643 if pos == -1:
644 return s
645 if inclusive:
646 return s[:pos+len(sub)]
647 return s[:pos]
@@ -1,284 +1,283 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 pylons.i18n.translation import lazy_ugettext as _
31 from rhodecode.translation import _
32 from sqlalchemy.ext.hybrid import hybrid_property
32 from rhodecode.authentication.base import (
33
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.ext_json import json, formatted_json
37 from rhodecode.lib.ext_json import json, formatted_json
39 from rhodecode.model.db import User
38 from rhodecode.model.db import User
40
39
41 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
42
41
43
42
44 def plugin_factory(plugin_id, *args, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
45 """
44 """
46 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
47 It returns the plugin instance.
46 It returns the plugin instance.
48 """
47 """
49 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
50 return plugin
49 return plugin
51
50
52
51
53 class CrowdAuthnResource(AuthnPluginResourceBase):
52 class CrowdAuthnResource(AuthnPluginResourceBase):
54 pass
53 pass
55
54
56
55
57 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
58 host = colander.SchemaNode(
57 host = colander.SchemaNode(
59 colander.String(),
58 colander.String(),
60 default='127.0.0.1',
59 default='127.0.0.1',
61 description=_('The FQDN or IP of the Atlassian CROWD Server'),
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
62 preparer=strip_whitespace,
61 preparer=strip_whitespace,
63 title=_('Host'),
62 title=_('Host'),
64 widget='string')
63 widget='string')
65 port = colander.SchemaNode(
64 port = colander.SchemaNode(
66 colander.Int(),
65 colander.Int(),
67 default=8095,
66 default=8095,
68 description=_('The Port in use by the Atlassian CROWD Server'),
67 description=_('The Port in use by the Atlassian CROWD Server'),
69 preparer=strip_whitespace,
68 preparer=strip_whitespace,
70 title=_('Port'),
69 title=_('Port'),
71 validator=colander.Range(min=0, max=65536),
70 validator=colander.Range(min=0, max=65536),
72 widget='int')
71 widget='int')
73 app_name = colander.SchemaNode(
72 app_name = colander.SchemaNode(
74 colander.String(),
73 colander.String(),
75 default='',
74 default='',
76 description=_('The Application Name to authenticate to CROWD'),
75 description=_('The Application Name to authenticate to CROWD'),
77 preparer=strip_whitespace,
76 preparer=strip_whitespace,
78 title=_('Application Name'),
77 title=_('Application Name'),
79 widget='string')
78 widget='string')
80 app_password = colander.SchemaNode(
79 app_password = colander.SchemaNode(
81 colander.String(),
80 colander.String(),
82 default='',
81 default='',
83 description=_('The password to authenticate to CROWD'),
82 description=_('The password to authenticate to CROWD'),
84 preparer=strip_whitespace,
83 preparer=strip_whitespace,
85 title=_('Application Password'),
84 title=_('Application Password'),
86 widget='password')
85 widget='password')
87 admin_groups = colander.SchemaNode(
86 admin_groups = colander.SchemaNode(
88 colander.String(),
87 colander.String(),
89 default='',
88 default='',
90 description=_('A comma separated list of group names that identify '
89 description=_('A comma separated list of group names that identify '
91 'users as RhodeCode Administrators'),
90 'users as RhodeCode Administrators'),
92 missing='',
91 missing='',
93 preparer=strip_whitespace,
92 preparer=strip_whitespace,
94 title=_('Admin Groups'),
93 title=_('Admin Groups'),
95 widget='string')
94 widget='string')
96
95
97
96
98 class CrowdServer(object):
97 class CrowdServer(object):
99 def __init__(self, *args, **kwargs):
98 def __init__(self, *args, **kwargs):
100 """
99 """
101 Create a new CrowdServer object that points to IP/Address 'host',
100 Create a new CrowdServer object that points to IP/Address 'host',
102 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
103 passwd can be set here or with set_credentials. If unspecified,
102 passwd can be set here or with set_credentials. If unspecified,
104 "version" defaults to "latest".
103 "version" defaults to "latest".
105
104
106 example::
105 example::
107
106
108 cserver = CrowdServer(host="127.0.0.1",
107 cserver = CrowdServer(host="127.0.0.1",
109 port="8095",
108 port="8095",
110 user="some_app",
109 user="some_app",
111 passwd="some_passwd",
110 passwd="some_passwd",
112 version="1")
111 version="1")
113 """
112 """
114 if not "port" in kwargs:
113 if not "port" in kwargs:
115 kwargs["port"] = "8095"
114 kwargs["port"] = "8095"
116 self._logger = kwargs.get("logger", logging.getLogger(__name__))
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
117 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
118 kwargs.get("host", "127.0.0.1"),
117 kwargs.get("host", "127.0.0.1"),
119 kwargs.get("port", "8095"))
118 kwargs.get("port", "8095"))
120 self.set_credentials(kwargs.get("user", ""),
119 self.set_credentials(kwargs.get("user", ""),
121 kwargs.get("passwd", ""))
120 kwargs.get("passwd", ""))
122 self._version = kwargs.get("version", "latest")
121 self._version = kwargs.get("version", "latest")
123 self._url_list = None
122 self._url_list = None
124 self._appname = "crowd"
123 self._appname = "crowd"
125
124
126 def set_credentials(self, user, passwd):
125 def set_credentials(self, user, passwd):
127 self.user = user
126 self.user = user
128 self.passwd = passwd
127 self.passwd = passwd
129 self._make_opener()
128 self._make_opener()
130
129
131 def _make_opener(self):
130 def _make_opener(self):
132 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
133 mgr.add_password(None, self._uri, self.user, self.passwd)
132 mgr.add_password(None, self._uri, self.user, self.passwd)
134 handler = urllib2.HTTPBasicAuthHandler(mgr)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
135 self.opener = urllib2.build_opener(handler)
134 self.opener = urllib2.build_opener(handler)
136
135
137 def _request(self, url, body=None, headers=None,
136 def _request(self, url, body=None, headers=None,
138 method=None, noformat=False,
137 method=None, noformat=False,
139 empty_response_ok=False):
138 empty_response_ok=False):
140 _headers = {"Content-type": "application/json",
139 _headers = {"Content-type": "application/json",
141 "Accept": "application/json"}
140 "Accept": "application/json"}
142 if self.user and self.passwd:
141 if self.user and self.passwd:
143 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
144 _headers["Authorization"] = "Basic %s" % authstring
143 _headers["Authorization"] = "Basic %s" % authstring
145 if headers:
144 if headers:
146 _headers.update(headers)
145 _headers.update(headers)
147 log.debug("Sent crowd: \n%s"
146 log.debug("Sent crowd: \n%s"
148 % (formatted_json({"url": url, "body": body,
147 % (formatted_json({"url": url, "body": body,
149 "headers": _headers})))
148 "headers": _headers})))
150 request = urllib2.Request(url, body, _headers)
149 request = urllib2.Request(url, body, _headers)
151 if method:
150 if method:
152 request.get_method = lambda: method
151 request.get_method = lambda: method
153
152
154 global msg
153 global msg
155 msg = ""
154 msg = ""
156 try:
155 try:
157 rdoc = self.opener.open(request)
156 rdoc = self.opener.open(request)
158 msg = "".join(rdoc.readlines())
157 msg = "".join(rdoc.readlines())
159 if not msg and empty_response_ok:
158 if not msg and empty_response_ok:
160 rval = {}
159 rval = {}
161 rval["status"] = True
160 rval["status"] = True
162 rval["error"] = "Response body was empty"
161 rval["error"] = "Response body was empty"
163 elif not noformat:
162 elif not noformat:
164 rval = json.loads(msg)
163 rval = json.loads(msg)
165 rval["status"] = True
164 rval["status"] = True
166 else:
165 else:
167 rval = "".join(rdoc.readlines())
166 rval = "".join(rdoc.readlines())
168 except Exception as e:
167 except Exception as e:
169 if not noformat:
168 if not noformat:
170 rval = {"status": False,
169 rval = {"status": False,
171 "body": body,
170 "body": body,
172 "error": str(e) + "\n" + msg}
171 "error": str(e) + "\n" + msg}
173 else:
172 else:
174 rval = None
173 rval = None
175 return rval
174 return rval
176
175
177 def user_auth(self, username, password):
176 def user_auth(self, username, password):
178 """Authenticate a user against crowd. Returns brief information about
177 """Authenticate a user against crowd. Returns brief information about
179 the user."""
178 the user."""
180 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
181 % (self._uri, self._version, username))
180 % (self._uri, self._version, username))
182 body = json.dumps({"value": password})
181 body = json.dumps({"value": password})
183 return self._request(url, body)
182 return self._request(url, body)
184
183
185 def user_groups(self, username):
184 def user_groups(self, username):
186 """Retrieve a list of groups to which this user belongs."""
185 """Retrieve a list of groups to which this user belongs."""
187 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
188 % (self._uri, self._version, username))
187 % (self._uri, self._version, username))
189 return self._request(url)
188 return self._request(url)
190
189
191
190
192 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
193
192
194 def includeme(self, config):
193 def includeme(self, config):
195 config.add_authn_plugin(self)
194 config.add_authn_plugin(self)
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
195 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_view(
196 config.add_view(
198 'rhodecode.authentication.views.AuthnPluginViewBase',
197 'rhodecode.authentication.views.AuthnPluginViewBase',
199 attr='settings_get',
198 attr='settings_get',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
199 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 request_method='GET',
200 request_method='GET',
202 route_name='auth_home',
201 route_name='auth_home',
203 context=CrowdAuthnResource)
202 context=CrowdAuthnResource)
204 config.add_view(
203 config.add_view(
205 'rhodecode.authentication.views.AuthnPluginViewBase',
204 'rhodecode.authentication.views.AuthnPluginViewBase',
206 attr='settings_post',
205 attr='settings_post',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
206 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 request_method='POST',
207 request_method='POST',
209 route_name='auth_home',
208 route_name='auth_home',
210 context=CrowdAuthnResource)
209 context=CrowdAuthnResource)
211
210
212 def get_settings_schema(self):
211 def get_settings_schema(self):
213 return CrowdSettingsSchema()
212 return CrowdSettingsSchema()
214
213
215 def get_display_name(self):
214 def get_display_name(self):
216 return _('CROWD')
215 return _('CROWD')
217
216
218 @hybrid_property
217 @hybrid_property
219 def name(self):
218 def name(self):
220 return "crowd"
219 return "crowd"
221
220
222 def use_fake_password(self):
221 def use_fake_password(self):
223 return True
222 return True
224
223
225 def user_activation_state(self):
224 def user_activation_state(self):
226 def_user_perms = User.get_default_user().AuthUser.permissions['global']
225 def_user_perms = User.get_default_user().AuthUser.permissions['global']
227 return 'hg.extern_activate.auto' in def_user_perms
226 return 'hg.extern_activate.auto' in def_user_perms
228
227
229 def auth(self, userobj, username, password, settings, **kwargs):
228 def auth(self, userobj, username, password, settings, **kwargs):
230 """
229 """
231 Given a user object (which may be null), username, a plaintext password,
230 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()),
231 and a settings object (containing all the keys needed as listed in settings()),
233 authenticate this user's login attempt.
232 authenticate this user's login attempt.
234
233
235 Return None on failure. On success, return a dictionary of the form:
234 Return None on failure. On success, return a dictionary of the form:
236
235
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
236 see: RhodeCodeAuthPluginBase.auth_func_attrs
238 This is later validated for correctness
237 This is later validated for correctness
239 """
238 """
240 if not username or not password:
239 if not username or not password:
241 log.debug('Empty username or password skipping...')
240 log.debug('Empty username or password skipping...')
242 return None
241 return None
243
242
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
243 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
245 server = CrowdServer(**settings)
244 server = CrowdServer(**settings)
246 server.set_credentials(settings["app_name"], settings["app_password"])
245 server.set_credentials(settings["app_name"], settings["app_password"])
247 crowd_user = server.user_auth(username, password)
246 crowd_user = server.user_auth(username, password)
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
247 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
249 if not crowd_user["status"]:
248 if not crowd_user["status"]:
250 return None
249 return None
251
250
252 res = server.user_groups(crowd_user["name"])
251 res = server.user_groups(crowd_user["name"])
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
252 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
253 crowd_user["groups"] = [x["name"] for x in res["groups"]]
255
254
256 # old attrs fetched from RhodeCode database
255 # old attrs fetched from RhodeCode database
257 admin = getattr(userobj, 'admin', False)
256 admin = getattr(userobj, 'admin', False)
258 active = getattr(userobj, 'active', True)
257 active = getattr(userobj, 'active', True)
259 email = getattr(userobj, 'email', '')
258 email = getattr(userobj, 'email', '')
260 username = getattr(userobj, 'username', username)
259 username = getattr(userobj, 'username', username)
261 firstname = getattr(userobj, 'firstname', '')
260 firstname = getattr(userobj, 'firstname', '')
262 lastname = getattr(userobj, 'lastname', '')
261 lastname = getattr(userobj, 'lastname', '')
263 extern_type = getattr(userobj, 'extern_type', '')
262 extern_type = getattr(userobj, 'extern_type', '')
264
263
265 user_attrs = {
264 user_attrs = {
266 'username': username,
265 'username': username,
267 'firstname': crowd_user["first-name"] or firstname,
266 'firstname': crowd_user["first-name"] or firstname,
268 'lastname': crowd_user["last-name"] or lastname,
267 'lastname': crowd_user["last-name"] or lastname,
269 'groups': crowd_user["groups"],
268 'groups': crowd_user["groups"],
270 'email': crowd_user["email"] or email,
269 'email': crowd_user["email"] or email,
271 'admin': admin,
270 'admin': admin,
272 'active': active,
271 'active': active,
273 'active_from_extern': crowd_user.get('active'),
272 'active_from_extern': crowd_user.get('active'),
274 'extern_name': crowd_user["name"],
273 'extern_name': crowd_user["name"],
275 'extern_type': extern_type,
274 'extern_type': extern_type,
276 }
275 }
277
276
278 # set an admin if we're in admin_groups of crowd
277 # set an admin if we're in admin_groups of crowd
279 for group in settings["admin_groups"]:
278 for group in settings["admin_groups"]:
280 if group in user_attrs["groups"]:
279 if group in user_attrs["groups"]:
281 user_attrs["admin"] = True
280 user_attrs["admin"] = True
282 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
281 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
283 log.info('user %s authenticated correctly' % user_attrs['username'])
282 log.info('user %s authenticated correctly' % user_attrs['username'])
284 return user_attrs
283 return user_attrs
@@ -1,225 +1,224 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 sqlalchemy.ext.hybrid import hybrid_property
24 from rhodecode.translation import _
25
25 from rhodecode.authentication.base import (
26 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
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 from rhodecode.translation import _
33
32
34
33
35 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
36
35
37
36
38 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
39 """
38 """
40 Factory function that is called during plugin discovery.
39 Factory function that is called during plugin discovery.
41 It returns the plugin instance.
40 It returns the plugin instance.
42 """
41 """
43 plugin = RhodeCodeAuthPlugin(plugin_id)
42 plugin = RhodeCodeAuthPlugin(plugin_id)
44 return plugin
43 return plugin
45
44
46
45
47 class HeadersAuthnResource(AuthnPluginResourceBase):
46 class HeadersAuthnResource(AuthnPluginResourceBase):
48 pass
47 pass
49
48
50
49
51 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
52 header = colander.SchemaNode(
51 header = colander.SchemaNode(
53 colander.String(),
52 colander.String(),
54 default='REMOTE_USER',
53 default='REMOTE_USER',
55 description=_('Header to extract the user from'),
54 description=_('Header to extract the user from'),
56 preparer=strip_whitespace,
55 preparer=strip_whitespace,
57 title=_('Header'),
56 title=_('Header'),
58 widget='string')
57 widget='string')
59 fallback_header = colander.SchemaNode(
58 fallback_header = colander.SchemaNode(
60 colander.String(),
59 colander.String(),
61 default='HTTP_X_FORWARDED_USER',
60 default='HTTP_X_FORWARDED_USER',
62 description=_('Header to extract the user from when main one fails'),
61 description=_('Header to extract the user from when main one fails'),
63 preparer=strip_whitespace,
62 preparer=strip_whitespace,
64 title=_('Fallback header'),
63 title=_('Fallback header'),
65 widget='string')
64 widget='string')
66 clean_username = colander.SchemaNode(
65 clean_username = colander.SchemaNode(
67 colander.Boolean(),
66 colander.Boolean(),
68 default=True,
67 default=True,
69 description=_('Perform cleaning of user, if passed user has @ in '
68 description=_('Perform cleaning of user, if passed user has @ in '
70 'username then first part before @ is taken. '
69 'username then first part before @ is taken. '
71 'If there\'s \\ in the username only the part after '
70 'If there\'s \\ in the username only the part after '
72 ' \\ is taken'),
71 ' \\ is taken'),
73 missing=False,
72 missing=False,
74 title=_('Clean username'),
73 title=_('Clean username'),
75 widget='bool')
74 widget='bool')
76
75
77
76
78 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
79
78
80 def includeme(self, config):
79 def includeme(self, config):
81 config.add_authn_plugin(self)
80 config.add_authn_plugin(self)
82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
83 config.add_view(
82 config.add_view(
84 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
85 attr='settings_get',
84 attr='settings_get',
86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
87 request_method='GET',
86 request_method='GET',
88 route_name='auth_home',
87 route_name='auth_home',
89 context=HeadersAuthnResource)
88 context=HeadersAuthnResource)
90 config.add_view(
89 config.add_view(
91 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
92 attr='settings_post',
91 attr='settings_post',
93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
94 request_method='POST',
93 request_method='POST',
95 route_name='auth_home',
94 route_name='auth_home',
96 context=HeadersAuthnResource)
95 context=HeadersAuthnResource)
97
96
98 def get_display_name(self):
97 def get_display_name(self):
99 return _('Headers')
98 return _('Headers')
100
99
101 def get_settings_schema(self):
100 def get_settings_schema(self):
102 return HeadersSettingsSchema()
101 return HeadersSettingsSchema()
103
102
104 @hybrid_property
103 @hybrid_property
105 def name(self):
104 def name(self):
106 return 'headers'
105 return 'headers'
107
106
108 @property
107 @property
109 def is_headers_auth(self):
108 def is_headers_auth(self):
110 return True
109 return True
111
110
112 def use_fake_password(self):
111 def use_fake_password(self):
113 return True
112 return True
114
113
115 def user_activation_state(self):
114 def user_activation_state(self):
116 def_user_perms = User.get_default_user().AuthUser.permissions['global']
115 def_user_perms = User.get_default_user().AuthUser.permissions['global']
117 return 'hg.extern_activate.auto' in def_user_perms
116 return 'hg.extern_activate.auto' in def_user_perms
118
117
119 def _clean_username(self, username):
118 def _clean_username(self, username):
120 # Removing realm and domain from username
119 # Removing realm and domain from username
121 username = username.split('@')[0]
120 username = username.split('@')[0]
122 username = username.rsplit('\\')[-1]
121 username = username.rsplit('\\')[-1]
123 return username
122 return username
124
123
125 def _get_username(self, environ, settings):
124 def _get_username(self, environ, settings):
126 username = None
125 username = None
127 environ = environ or {}
126 environ = environ or {}
128 if not environ:
127 if not environ:
129 log.debug('got empty environ: %s' % environ)
128 log.debug('got empty environ: %s' % environ)
130
129
131 settings = settings or {}
130 settings = settings or {}
132 if settings.get('header'):
131 if settings.get('header'):
133 header = settings.get('header')
132 header = settings.get('header')
134 username = environ.get(header)
133 username = environ.get(header)
135 log.debug('extracted %s:%s' % (header, username))
134 log.debug('extracted %s:%s' % (header, username))
136
135
137 # fallback mode
136 # fallback mode
138 if not username and settings.get('fallback_header'):
137 if not username and settings.get('fallback_header'):
139 header = settings.get('fallback_header')
138 header = settings.get('fallback_header')
140 username = environ.get(header)
139 username = environ.get(header)
141 log.debug('extracted %s:%s' % (header, username))
140 log.debug('extracted %s:%s' % (header, username))
142
141
143 if username and str2bool(settings.get('clean_username')):
142 if username and str2bool(settings.get('clean_username')):
144 log.debug('Received username `%s` from headers' % username)
143 log.debug('Received username `%s` from headers' % username)
145 username = self._clean_username(username)
144 username = self._clean_username(username)
146 log.debug('New cleanup user is:%s' % username)
145 log.debug('New cleanup user is:%s' % username)
147 return username
146 return username
148
147
149 def get_user(self, username=None, **kwargs):
148 def get_user(self, username=None, **kwargs):
150 """
149 """
151 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
152 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
153 eg. headers auth plugin to fetch user by environ params
152 eg. headers auth plugin to fetch user by environ params
154 :param username: username if given to fetch
153 :param username: username if given to fetch
155 :param kwargs: extra arguments needed for user fetching.
154 :param kwargs: extra arguments needed for user fetching.
156 """
155 """
157 environ = kwargs.get('environ') or {}
156 environ = kwargs.get('environ') or {}
158 settings = kwargs.get('settings') or {}
157 settings = kwargs.get('settings') or {}
159 username = self._get_username(environ, settings)
158 username = self._get_username(environ, settings)
160 # we got the username, so use default method now
159 # we got the username, so use default method now
161 return super(RhodeCodeAuthPlugin, self).get_user(username)
160 return super(RhodeCodeAuthPlugin, self).get_user(username)
162
161
163 def auth(self, userobj, username, password, settings, **kwargs):
162 def auth(self, userobj, username, password, settings, **kwargs):
164 """
163 """
165 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
166 from REMOTE_USER if this plugin is enabled, if that fails
165 from REMOTE_USER if this plugin is enabled, if that fails
167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
168 is set. clean_username extracts the username from this data if it's
167 is set. clean_username extracts the username from this data if it's
169 having @ in it.
168 having @ in it.
170 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:
171
170
172 see: RhodeCodeAuthPluginBase.auth_func_attrs
171 see: RhodeCodeAuthPluginBase.auth_func_attrs
173
172
174 :param userobj:
173 :param userobj:
175 :param username:
174 :param username:
176 :param password:
175 :param password:
177 :param settings:
176 :param settings:
178 :param kwargs:
177 :param kwargs:
179 """
178 """
180 environ = kwargs.get('environ')
179 environ = kwargs.get('environ')
181 if not environ:
180 if not environ:
182 log.debug('Empty environ data skipping...')
181 log.debug('Empty environ data skipping...')
183 return None
182 return None
184
183
185 if not userobj:
184 if not userobj:
186 userobj = self.get_user('', environ=environ, settings=settings)
185 userobj = self.get_user('', environ=environ, settings=settings)
187
186
188 # we don't care passed username/password for headers auth plugins.
187 # we don't care passed username/password for headers auth plugins.
189 # only way to log in is using environ
188 # only way to log in is using environ
190 username = None
189 username = None
191 if userobj:
190 if userobj:
192 username = getattr(userobj, 'username')
191 username = getattr(userobj, 'username')
193
192
194 if not username:
193 if not username:
195 # 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
196 # username from environ based on the settings
195 # username from environ based on the settings
197 username = self._get_username(environ, settings)
196 username = self._get_username(environ, settings)
198
197
199 # 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
200 if not username:
199 if not username:
201 return None
200 return None
202
201
203 # old attrs fetched from RhodeCode database
202 # old attrs fetched from RhodeCode database
204 admin = getattr(userobj, 'admin', False)
203 admin = getattr(userobj, 'admin', False)
205 active = getattr(userobj, 'active', True)
204 active = getattr(userobj, 'active', True)
206 email = getattr(userobj, 'email', '')
205 email = getattr(userobj, 'email', '')
207 firstname = getattr(userobj, 'firstname', '')
206 firstname = getattr(userobj, 'firstname', '')
208 lastname = getattr(userobj, 'lastname', '')
207 lastname = getattr(userobj, 'lastname', '')
209 extern_type = getattr(userobj, 'extern_type', '')
208 extern_type = getattr(userobj, 'extern_type', '')
210
209
211 user_attrs = {
210 user_attrs = {
212 'username': username,
211 'username': username,
213 'firstname': safe_unicode(firstname or username),
212 'firstname': safe_unicode(firstname or username),
214 'lastname': safe_unicode(lastname or ''),
213 'lastname': safe_unicode(lastname or ''),
215 'groups': [],
214 'groups': [],
216 'email': email or '',
215 'email': email or '',
217 'admin': admin or False,
216 'admin': admin or False,
218 'active': active,
217 'active': active,
219 'active_from_extern': True,
218 'active_from_extern': True,
220 'extern_name': username,
219 'extern_name': username,
221 'extern_type': extern_type,
220 'extern_type': extern_type,
222 }
221 }
223
222
224 log.info('user `%s` authenticated correctly' % user_attrs['username'])
223 log.info('user `%s` authenticated correctly' % user_attrs['username'])
225 return user_attrs
224 return user_attrs
@@ -1,167 +1,166 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 pylons.i18n.translation import lazy_ugettext as _
33 from rhodecode.translation import _
34 from sqlalchemy.ext.hybrid import hybrid_property
34 from rhodecode.authentication.base import (
35
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
39 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
40 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
41 from rhodecode.model.db import User
40 from rhodecode.model.db import User
42
41
43 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
44
43
45
44
46 def plugin_factory(plugin_id, *args, **kwds):
45 def plugin_factory(plugin_id, *args, **kwds):
47 """
46 """
48 Factory function that is called during plugin discovery.
47 Factory function that is called during plugin discovery.
49 It returns the plugin instance.
48 It returns the plugin instance.
50 """
49 """
51 plugin = RhodeCodeAuthPlugin(plugin_id)
50 plugin = RhodeCodeAuthPlugin(plugin_id)
52 return plugin
51 return plugin
53
52
54
53
55 class JasigCasAuthnResource(AuthnPluginResourceBase):
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
56 pass
55 pass
57
56
58
57
59 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
60 service_url = colander.SchemaNode(
59 service_url = colander.SchemaNode(
61 colander.String(),
60 colander.String(),
62 default='https://domain.com/cas/v1/tickets',
61 default='https://domain.com/cas/v1/tickets',
63 description=_('The url of the Jasig CAS REST service'),
62 description=_('The url of the Jasig CAS REST service'),
64 preparer=strip_whitespace,
63 preparer=strip_whitespace,
65 title=_('URL'),
64 title=_('URL'),
66 widget='string')
65 widget='string')
67
66
68
67
69 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
70
69
71 def includeme(self, config):
70 def includeme(self, config):
72 config.add_authn_plugin(self)
71 config.add_authn_plugin(self)
73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
74 config.add_view(
73 config.add_view(
75 'rhodecode.authentication.views.AuthnPluginViewBase',
74 'rhodecode.authentication.views.AuthnPluginViewBase',
76 attr='settings_get',
75 attr='settings_get',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
78 request_method='GET',
77 request_method='GET',
79 route_name='auth_home',
78 route_name='auth_home',
80 context=JasigCasAuthnResource)
79 context=JasigCasAuthnResource)
81 config.add_view(
80 config.add_view(
82 'rhodecode.authentication.views.AuthnPluginViewBase',
81 'rhodecode.authentication.views.AuthnPluginViewBase',
83 attr='settings_post',
82 attr='settings_post',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 request_method='POST',
84 request_method='POST',
86 route_name='auth_home',
85 route_name='auth_home',
87 context=JasigCasAuthnResource)
86 context=JasigCasAuthnResource)
88
87
89 def get_settings_schema(self):
88 def get_settings_schema(self):
90 return JasigCasSettingsSchema()
89 return JasigCasSettingsSchema()
91
90
92 def get_display_name(self):
91 def get_display_name(self):
93 return _('Jasig-CAS')
92 return _('Jasig-CAS')
94
93
95 @hybrid_property
94 @hybrid_property
96 def name(self):
95 def name(self):
97 return "jasig-cas"
96 return "jasig-cas"
98
97
99 @property
98 @property
100 def is_headers_auth(self):
99 def is_headers_auth(self):
101 return True
100 return True
102
101
103 def use_fake_password(self):
102 def use_fake_password(self):
104 return True
103 return True
105
104
106 def user_activation_state(self):
105 def user_activation_state(self):
107 def_user_perms = User.get_default_user().AuthUser.permissions['global']
106 def_user_perms = User.get_default_user().AuthUser.permissions['global']
108 return 'hg.extern_activate.auto' in def_user_perms
107 return 'hg.extern_activate.auto' in def_user_perms
109
108
110 def auth(self, userobj, username, password, settings, **kwargs):
109 def auth(self, userobj, username, password, settings, **kwargs):
111 """
110 """
112 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,
113 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()),
114 authenticate this user's login attempt.
113 authenticate this user's login attempt.
115
114
116 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:
117
116
118 see: RhodeCodeAuthPluginBase.auth_func_attrs
117 see: RhodeCodeAuthPluginBase.auth_func_attrs
119 This is later validated for correctness
118 This is later validated for correctness
120 """
119 """
121 if not username or not password:
120 if not username or not password:
122 log.debug('Empty username or password skipping...')
121 log.debug('Empty username or password skipping...')
123 return None
122 return None
124
123
125 log.debug("Jasig CAS settings: %s", settings)
124 log.debug("Jasig CAS settings: %s", settings)
126 params = urllib.urlencode({'username': username, 'password': password})
125 params = urllib.urlencode({'username': username, 'password': password})
127 headers = {"Content-type": "application/x-www-form-urlencoded",
126 headers = {"Content-type": "application/x-www-form-urlencoded",
128 "Accept": "text/plain",
127 "Accept": "text/plain",
129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
130 url = settings["service_url"]
129 url = settings["service_url"]
131
130
132 log.debug("Sent Jasig CAS: \n%s",
131 log.debug("Sent Jasig CAS: \n%s",
133 {"url": url, "body": params, "headers": headers})
132 {"url": url, "body": params, "headers": headers})
134 request = urllib2.Request(url, params, headers)
133 request = urllib2.Request(url, params, headers)
135 try:
134 try:
136 response = urllib2.urlopen(request)
135 response = urllib2.urlopen(request)
137 except urllib2.HTTPError as e:
136 except urllib2.HTTPError as e:
138 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)
139 return None
138 return None
140 except urllib2.URLError as e:
139 except urllib2.URLError as e:
141 log.debug("URLError when requesting Jasig CAS url: %s " % url)
140 log.debug("URLError when requesting Jasig CAS url: %s " % url)
142 return None
141 return None
143
142
144 # old attrs fetched from RhodeCode database
143 # old attrs fetched from RhodeCode database
145 admin = getattr(userobj, 'admin', False)
144 admin = getattr(userobj, 'admin', False)
146 active = getattr(userobj, 'active', True)
145 active = getattr(userobj, 'active', True)
147 email = getattr(userobj, 'email', '')
146 email = getattr(userobj, 'email', '')
148 username = getattr(userobj, 'username', username)
147 username = getattr(userobj, 'username', username)
149 firstname = getattr(userobj, 'firstname', '')
148 firstname = getattr(userobj, 'firstname', '')
150 lastname = getattr(userobj, 'lastname', '')
149 lastname = getattr(userobj, 'lastname', '')
151 extern_type = getattr(userobj, 'extern_type', '')
150 extern_type = getattr(userobj, 'extern_type', '')
152
151
153 user_attrs = {
152 user_attrs = {
154 'username': username,
153 'username': username,
155 'firstname': safe_unicode(firstname or username),
154 'firstname': safe_unicode(firstname or username),
156 'lastname': safe_unicode(lastname or ''),
155 'lastname': safe_unicode(lastname or ''),
157 'groups': [],
156 'groups': [],
158 'email': email or '',
157 'email': email or '',
159 'admin': admin or False,
158 'admin': admin or False,
160 'active': active,
159 'active': active,
161 'active_from_extern': True,
160 'active_from_extern': True,
162 'extern_name': username,
161 'extern_name': username,
163 'extern_type': extern_type,
162 'extern_type': extern_type,
164 }
163 }
165
164
166 log.info('user %s authenticated correctly' % user_attrs['username'])
165 log.info('user %s authenticated correctly' % user_attrs['username'])
167 return user_attrs
166 return user_attrs
@@ -1,464 +1,473 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 pylons.i18n.translation import lazy_ugettext as _
30 from rhodecode.translation import _
31 from sqlalchemy.ext.hybrid import hybrid_property
31 from rhodecode.authentication.base import (
32
32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
33 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.exceptions import (
36 from rhodecode.lib.exceptions import (
38 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
39 )
38 )
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 from rhodecode.model.db import User
40 from rhodecode.model.db import User
42 from rhodecode.model.validators import Missing
41 from rhodecode.model.validators import Missing
43
42
44 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
45
44
46 try:
45 try:
47 import ldap
46 import ldap
48 except ImportError:
47 except ImportError:
49 # 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
50 # ldap lib is Missing
49 # ldap lib is Missing
51 ldap = Missing
50 ldap = Missing
52
51
53
52
54 def plugin_factory(plugin_id, *args, **kwds):
53 def plugin_factory(plugin_id, *args, **kwds):
55 """
54 """
56 Factory function that is called during plugin discovery.
55 Factory function that is called during plugin discovery.
57 It returns the plugin instance.
56 It returns the plugin instance.
58 """
57 """
59 plugin = RhodeCodeAuthPlugin(plugin_id)
58 plugin = RhodeCodeAuthPlugin(plugin_id)
60 return plugin
59 return plugin
61
60
62
61
63 class LdapAuthnResource(AuthnPluginResourceBase):
62 class LdapAuthnResource(AuthnPluginResourceBase):
64 pass
63 pass
65
64
66
65
67 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
66 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
68 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
67 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
69 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
68 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
70 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
69 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
71
70
72 host = colander.SchemaNode(
71 host = colander.SchemaNode(
73 colander.String(),
72 colander.String(),
74 default='',
73 default='',
75 description=_('Host of the LDAP Server'),
74 description=_('Host of the LDAP Server \n'
75 '(e.g., 192.168.2.154, or ldap-server.domain.com'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('LDAP Host'),
77 title=_('LDAP Host'),
78 widget='string')
78 widget='string')
79 port = colander.SchemaNode(
79 port = colander.SchemaNode(
80 colander.Int(),
80 colander.Int(),
81 default=389,
81 default=389,
82 description=_('Port that the LDAP server is listening on'),
82 description=_('Custom port that the LDAP server is listening on. Default: 389'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Port'),
84 title=_('Port'),
85 validator=colander.Range(min=0, max=65536),
85 validator=colander.Range(min=0, max=65536),
86 widget='int')
86 widget='int')
87 dn_user = colander.SchemaNode(
87 dn_user = colander.SchemaNode(
88 colander.String(),
88 colander.String(),
89 default='',
89 default='',
90 description=_('User to connect to LDAP'),
90 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
91 'e.g., cn=admin,dc=mydomain,dc=com, or '
92 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
91 missing='',
93 missing='',
92 preparer=strip_whitespace,
94 preparer=strip_whitespace,
93 title=_('Account'),
95 title=_('Account'),
94 widget='string')
96 widget='string')
95 dn_pass = colander.SchemaNode(
97 dn_pass = colander.SchemaNode(
96 colander.String(),
98 colander.String(),
97 default='',
99 default='',
98 description=_('Password to connect to LDAP'),
100 description=_('Password to authenticate for given user DN.'),
99 missing='',
101 missing='',
100 preparer=strip_whitespace,
102 preparer=strip_whitespace,
101 title=_('Password'),
103 title=_('Password'),
102 widget='password')
104 widget='password')
103 tls_kind = colander.SchemaNode(
105 tls_kind = colander.SchemaNode(
104 colander.String(),
106 colander.String(),
105 default=tls_kind_choices[0],
107 default=tls_kind_choices[0],
106 description=_('TLS Type'),
108 description=_('TLS Type'),
107 title=_('Connection Security'),
109 title=_('Connection Security'),
108 validator=colander.OneOf(tls_kind_choices),
110 validator=colander.OneOf(tls_kind_choices),
109 widget='select')
111 widget='select')
110 tls_reqcert = colander.SchemaNode(
112 tls_reqcert = colander.SchemaNode(
111 colander.String(),
113 colander.String(),
112 default=tls_reqcert_choices[0],
114 default=tls_reqcert_choices[0],
113 description=_('Require Cert over TLS?'),
115 description=_('Require Cert over TLS?'),
114 title=_('Certificate Checks'),
116 title=_('Certificate Checks'),
115 validator=colander.OneOf(tls_reqcert_choices),
117 validator=colander.OneOf(tls_reqcert_choices),
116 widget='select')
118 widget='select')
117 base_dn = colander.SchemaNode(
119 base_dn = colander.SchemaNode(
118 colander.String(),
120 colander.String(),
119 default='',
121 default='',
120 description=_('Base DN to search (e.g., dc=mydomain,dc=com)'),
122 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
123 'in it to be replaced with current user credentials \n'
124 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
121 missing='',
125 missing='',
122 preparer=strip_whitespace,
126 preparer=strip_whitespace,
123 title=_('Base DN'),
127 title=_('Base DN'),
124 widget='string')
128 widget='string')
125 filter = colander.SchemaNode(
129 filter = colander.SchemaNode(
126 colander.String(),
130 colander.String(),
127 default='',
131 default='',
128 description=_('Filter to narrow results (e.g., ou=Users, etc)'),
132 description=_('Filter to narrow results \n'
133 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
134 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
129 missing='',
135 missing='',
130 preparer=strip_whitespace,
136 preparer=strip_whitespace,
131 title=_('LDAP Search Filter'),
137 title=_('LDAP Search Filter'),
132 widget='string')
138 widget='string')
139
133 search_scope = colander.SchemaNode(
140 search_scope = colander.SchemaNode(
134 colander.String(),
141 colander.String(),
135 default=search_scope_choices[0],
142 default=search_scope_choices[2],
136 description=_('How deep to search LDAP'),
143 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
137 title=_('LDAP Search Scope'),
144 title=_('LDAP Search Scope'),
138 validator=colander.OneOf(search_scope_choices),
145 validator=colander.OneOf(search_scope_choices),
139 widget='select')
146 widget='select')
140 attr_login = colander.SchemaNode(
147 attr_login = colander.SchemaNode(
141 colander.String(),
148 colander.String(),
142 default='',
149 default='uid',
143 description=_('LDAP Attribute to map to user name'),
150 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
144 preparer=strip_whitespace,
151 preparer=strip_whitespace,
145 title=_('Login Attribute'),
152 title=_('Login Attribute'),
146 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
153 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
147 widget='string')
154 widget='string')
148 attr_firstname = colander.SchemaNode(
155 attr_firstname = colander.SchemaNode(
149 colander.String(),
156 colander.String(),
150 default='',
157 default='',
151 description=_('LDAP Attribute to map to first name'),
158 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
152 missing='',
159 missing='',
153 preparer=strip_whitespace,
160 preparer=strip_whitespace,
154 title=_('First Name Attribute'),
161 title=_('First Name Attribute'),
155 widget='string')
162 widget='string')
156 attr_lastname = colander.SchemaNode(
163 attr_lastname = colander.SchemaNode(
157 colander.String(),
164 colander.String(),
158 default='',
165 default='',
159 description=_('LDAP Attribute to map to last name'),
166 description=_('LDAP Attribute to map to last name (e.g., sn)'),
160 missing='',
167 missing='',
161 preparer=strip_whitespace,
168 preparer=strip_whitespace,
162 title=_('Last Name Attribute'),
169 title=_('Last Name Attribute'),
163 widget='string')
170 widget='string')
164 attr_email = colander.SchemaNode(
171 attr_email = colander.SchemaNode(
165 colander.String(),
172 colander.String(),
166 default='',
173 default='',
167 description=_('LDAP Attribute to map to email address'),
174 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
175 'Emails are a crucial part of RhodeCode. \n'
176 'If possible add a valid email attribute to ldap users.'),
168 missing='',
177 missing='',
169 preparer=strip_whitespace,
178 preparer=strip_whitespace,
170 title=_('Email Attribute'),
179 title=_('Email Attribute'),
171 widget='string')
180 widget='string')
172
181
173
182
174 class AuthLdap(object):
183 class AuthLdap(object):
175
184
176 def _build_servers(self):
185 def _build_servers(self):
177 return ', '.join(
186 return ', '.join(
178 ["{}://{}:{}".format(
187 ["{}://{}:{}".format(
179 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
188 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
180 for host in self.SERVER_ADDRESSES])
189 for host in self.SERVER_ADDRESSES])
181
190
182 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
191 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
183 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
192 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
184 search_scope='SUBTREE', attr_login='uid',
193 search_scope='SUBTREE', attr_login='uid',
185 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'):
194 ldap_filter=None):
186 if ldap == Missing:
195 if ldap == Missing:
187 raise LdapImportError("Missing or incompatible ldap library")
196 raise LdapImportError("Missing or incompatible ldap library")
188
197
189 self.debug = False
198 self.debug = False
190 self.ldap_version = ldap_version
199 self.ldap_version = ldap_version
191 self.ldap_server_type = 'ldap'
200 self.ldap_server_type = 'ldap'
192
201
193 self.TLS_KIND = tls_kind
202 self.TLS_KIND = tls_kind
194
203
195 if self.TLS_KIND == 'LDAPS':
204 if self.TLS_KIND == 'LDAPS':
196 port = port or 689
205 port = port or 689
197 self.ldap_server_type += 's'
206 self.ldap_server_type += 's'
198
207
199 OPT_X_TLS_DEMAND = 2
208 OPT_X_TLS_DEMAND = 2
200 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
209 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
201 OPT_X_TLS_DEMAND)
210 OPT_X_TLS_DEMAND)
202 # split server into list
211 # split server into list
203 self.SERVER_ADDRESSES = server.split(',')
212 self.SERVER_ADDRESSES = server.split(',')
204 self.LDAP_SERVER_PORT = port
213 self.LDAP_SERVER_PORT = port
205
214
206 # USE FOR READ ONLY BIND TO LDAP SERVER
215 # USE FOR READ ONLY BIND TO LDAP SERVER
207 self.attr_login = attr_login
216 self.attr_login = attr_login
208
217
209 self.LDAP_BIND_DN = safe_str(bind_dn)
218 self.LDAP_BIND_DN = safe_str(bind_dn)
210 self.LDAP_BIND_PASS = safe_str(bind_pass)
219 self.LDAP_BIND_PASS = safe_str(bind_pass)
211 self.LDAP_SERVER = self._build_servers()
220 self.LDAP_SERVER = self._build_servers()
212 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
221 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
213 self.BASE_DN = safe_str(base_dn)
222 self.BASE_DN = safe_str(base_dn)
214 self.LDAP_FILTER = safe_str(ldap_filter)
223 self.LDAP_FILTER = safe_str(ldap_filter)
215
224
216 def _get_ldap_server(self):
225 def _get_ldap_server(self):
217 if self.debug:
226 if self.debug:
218 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
227 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
219 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
228 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
220 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
229 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
221 '/etc/openldap/cacerts')
230 '/etc/openldap/cacerts')
222 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
231 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
223 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
232 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
224 ldap.set_option(ldap.OPT_TIMEOUT, 20)
233 ldap.set_option(ldap.OPT_TIMEOUT, 20)
225 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
234 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
226 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
235 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
227 if self.TLS_KIND != 'PLAIN':
236 if self.TLS_KIND != 'PLAIN':
228 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
237 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
229 server = ldap.initialize(self.LDAP_SERVER)
238 server = ldap.initialize(self.LDAP_SERVER)
230 if self.ldap_version == 2:
239 if self.ldap_version == 2:
231 server.protocol = ldap.VERSION2
240 server.protocol = ldap.VERSION2
232 else:
241 else:
233 server.protocol = ldap.VERSION3
242 server.protocol = ldap.VERSION3
234
243
235 if self.TLS_KIND == 'START_TLS':
244 if self.TLS_KIND == 'START_TLS':
236 server.start_tls_s()
245 server.start_tls_s()
237
246
238 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
247 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
239 log.debug('Trying simple_bind with password and given DN: %s',
248 log.debug('Trying simple_bind with password and given login DN: %s',
240 self.LDAP_BIND_DN)
249 self.LDAP_BIND_DN)
241 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
250 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
242
251
243 return server
252 return server
244
253
245 def get_uid(self, username):
254 def get_uid(self, username):
246 from rhodecode.lib.helpers import chop_at
247 uid = username
255 uid = username
248 for server_addr in self.SERVER_ADDRESSES:
256 for server_addr in self.SERVER_ADDRESSES:
249 uid = chop_at(username, "@%s" % server_addr)
257 uid = chop_at(username, "@%s" % server_addr)
250 return uid
258 return uid
251
259
252 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
260 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
253 try:
261 try:
254 log.debug('Trying simple bind with %s', dn)
262 log.debug('Trying simple bind with %s', dn)
255 server.simple_bind_s(dn, safe_str(password))
263 server.simple_bind_s(dn, safe_str(password))
256 user = server.search_ext_s(
264 user = server.search_ext_s(
257 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
265 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
258 _, attrs = user
266 _, attrs = user
259 return attrs
267 return attrs
260
268
261 except ldap.INVALID_CREDENTIALS:
269 except ldap.INVALID_CREDENTIALS:
262 log.debug(
270 log.debug(
263 "LDAP rejected password for user '%s': %s, org_exc:",
271 "LDAP rejected password for user '%s': %s, org_exc:",
264 username, dn, exc_info=True)
272 username, dn, exc_info=True)
265
273
266 def authenticate_ldap(self, username, password):
274 def authenticate_ldap(self, username, password):
267 """
275 """
268 Authenticate a user via LDAP and return his/her LDAP properties.
276 Authenticate a user via LDAP and return his/her LDAP properties.
269
277
270 Raises AuthenticationError if the credentials are rejected, or
278 Raises AuthenticationError if the credentials are rejected, or
271 EnvironmentError if the LDAP server can't be reached.
279 EnvironmentError if the LDAP server can't be reached.
272
280
273 :param username: username
281 :param username: username
274 :param password: password
282 :param password: password
275 """
283 """
276
284
277 uid = self.get_uid(username)
285 uid = self.get_uid(username)
278
286
279 if not password:
287 if not password:
280 msg = "Authenticating user %s with blank password not allowed"
288 msg = "Authenticating user %s with blank password not allowed"
281 log.warning(msg, username)
289 log.warning(msg, username)
282 raise LdapPasswordError(msg)
290 raise LdapPasswordError(msg)
283 if "," in username:
291 if "," in username:
284 raise LdapUsernameError("invalid character in username: ,")
292 raise LdapUsernameError("invalid character in username: ,")
285 try:
293 try:
286 server = self._get_ldap_server()
294 server = self._get_ldap_server()
287 filter_ = '(&%s(%s=%s))' % (
295 filter_ = '(&%s(%s=%s))' % (
288 self.LDAP_FILTER, self.attr_login, username)
296 self.LDAP_FILTER, self.attr_login, username)
289 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
297 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
290 filter_, self.LDAP_SERVER)
298 filter_, self.LDAP_SERVER)
291 lobjects = server.search_ext_s(
299 lobjects = server.search_ext_s(
292 self.BASE_DN, self.SEARCH_SCOPE, filter_)
300 self.BASE_DN, self.SEARCH_SCOPE, filter_)
293
301
294 if not lobjects:
302 if not lobjects:
303 log.debug("No matching LDAP objects for authentication "
304 "of UID:'%s' username:(%s)", uid, username)
295 raise ldap.NO_SUCH_OBJECT()
305 raise ldap.NO_SUCH_OBJECT()
296
306
307 log.debug('Found matching ldap object, trying to authenticate')
297 for (dn, _attrs) in lobjects:
308 for (dn, _attrs) in lobjects:
298 if dn is None:
309 if dn is None:
299 continue
310 continue
300
311
301 user_attrs = self.fetch_attrs_from_simple_bind(
312 user_attrs = self.fetch_attrs_from_simple_bind(
302 server, dn, username, password)
313 server, dn, username, password)
303 if user_attrs:
314 if user_attrs:
304 break
315 break
305
316
306 else:
317 else:
307 log.debug("No matching LDAP objects for authentication "
308 "of '%s' (%s)", uid, username)
309 raise LdapPasswordError('Failed to authenticate user '
318 raise LdapPasswordError('Failed to authenticate user '
310 'with given password')
319 'with given password')
311
320
312 except ldap.NO_SUCH_OBJECT:
321 except ldap.NO_SUCH_OBJECT:
313 log.debug("LDAP says no such user '%s' (%s), org_exc:",
322 log.debug("LDAP says no such user '%s' (%s), org_exc:",
314 uid, username, exc_info=True)
323 uid, username, exc_info=True)
315 raise LdapUsernameError()
324 raise LdapUsernameError('Unable to find user')
316 except ldap.SERVER_DOWN:
325 except ldap.SERVER_DOWN:
317 org_exc = traceback.format_exc()
326 org_exc = traceback.format_exc()
318 raise LdapConnectionError(
327 raise LdapConnectionError(
319 "LDAP can't access authentication "
328 "LDAP can't access authentication "
320 "server, org_exc:%s" % org_exc)
329 "server, org_exc:%s" % org_exc)
321
330
322 return dn, user_attrs
331 return dn, user_attrs
323
332
324
333
325 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
334 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
326 # used to define dynamic binding in the
335 # used to define dynamic binding in the
327 DYNAMIC_BIND_VAR = '$login'
336 DYNAMIC_BIND_VAR = '$login'
328
337
329 def includeme(self, config):
338 def includeme(self, config):
330 config.add_authn_plugin(self)
339 config.add_authn_plugin(self)
331 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
340 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
332 config.add_view(
341 config.add_view(
333 'rhodecode.authentication.views.AuthnPluginViewBase',
342 'rhodecode.authentication.views.AuthnPluginViewBase',
334 attr='settings_get',
343 attr='settings_get',
335 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
344 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
336 request_method='GET',
345 request_method='GET',
337 route_name='auth_home',
346 route_name='auth_home',
338 context=LdapAuthnResource)
347 context=LdapAuthnResource)
339 config.add_view(
348 config.add_view(
340 'rhodecode.authentication.views.AuthnPluginViewBase',
349 'rhodecode.authentication.views.AuthnPluginViewBase',
341 attr='settings_post',
350 attr='settings_post',
342 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
351 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
343 request_method='POST',
352 request_method='POST',
344 route_name='auth_home',
353 route_name='auth_home',
345 context=LdapAuthnResource)
354 context=LdapAuthnResource)
346
355
347 def get_settings_schema(self):
356 def get_settings_schema(self):
348 return LdapSettingsSchema()
357 return LdapSettingsSchema()
349
358
350 def get_display_name(self):
359 def get_display_name(self):
351 return _('LDAP')
360 return _('LDAP')
352
361
353 @hybrid_property
362 @hybrid_property
354 def name(self):
363 def name(self):
355 return "ldap"
364 return "ldap"
356
365
357 def use_fake_password(self):
366 def use_fake_password(self):
358 return True
367 return True
359
368
360 def user_activation_state(self):
369 def user_activation_state(self):
361 def_user_perms = User.get_default_user().AuthUser.permissions['global']
370 def_user_perms = User.get_default_user().AuthUser.permissions['global']
362 return 'hg.extern_activate.auto' in def_user_perms
371 return 'hg.extern_activate.auto' in def_user_perms
363
372
364 def try_dynamic_binding(self, username, password, current_args):
373 def try_dynamic_binding(self, username, password, current_args):
365 """
374 """
366 Detects marker inside our original bind, and uses dynamic auth if
375 Detects marker inside our original bind, and uses dynamic auth if
367 present
376 present
368 """
377 """
369
378
370 org_bind = current_args['bind_dn']
379 org_bind = current_args['bind_dn']
371 passwd = current_args['bind_pass']
380 passwd = current_args['bind_pass']
372
381
373 def has_bind_marker(username):
382 def has_bind_marker(username):
374 if self.DYNAMIC_BIND_VAR in username:
383 if self.DYNAMIC_BIND_VAR in username:
375 return True
384 return True
376
385
377 # we only passed in user with "special" variable
386 # we only passed in user with "special" variable
378 if org_bind and has_bind_marker(org_bind) and not passwd:
387 if org_bind and has_bind_marker(org_bind) and not passwd:
379 log.debug('Using dynamic user/password binding for ldap '
388 log.debug('Using dynamic user/password binding for ldap '
380 'authentication. Replacing `%s` with username',
389 'authentication. Replacing `%s` with username',
381 self.DYNAMIC_BIND_VAR)
390 self.DYNAMIC_BIND_VAR)
382 current_args['bind_dn'] = org_bind.replace(
391 current_args['bind_dn'] = org_bind.replace(
383 self.DYNAMIC_BIND_VAR, username)
392 self.DYNAMIC_BIND_VAR, username)
384 current_args['bind_pass'] = password
393 current_args['bind_pass'] = password
385
394
386 return current_args
395 return current_args
387
396
388 def auth(self, userobj, username, password, settings, **kwargs):
397 def auth(self, userobj, username, password, settings, **kwargs):
389 """
398 """
390 Given a user object (which may be null), username, a plaintext password,
399 Given a user object (which may be null), username, a plaintext password,
391 and a settings object (containing all the keys needed as listed in
400 and a settings object (containing all the keys needed as listed in
392 settings()), authenticate this user's login attempt.
401 settings()), authenticate this user's login attempt.
393
402
394 Return None on failure. On success, return a dictionary of the form:
403 Return None on failure. On success, return a dictionary of the form:
395
404
396 see: RhodeCodeAuthPluginBase.auth_func_attrs
405 see: RhodeCodeAuthPluginBase.auth_func_attrs
397 This is later validated for correctness
406 This is later validated for correctness
398 """
407 """
399
408
400 if not username or not password:
409 if not username or not password:
401 log.debug('Empty username or password skipping...')
410 log.debug('Empty username or password skipping...')
402 return None
411 return None
403
412
404 ldap_args = {
413 ldap_args = {
405 'server': settings.get('host', ''),
414 'server': settings.get('host', ''),
406 'base_dn': settings.get('base_dn', ''),
415 'base_dn': settings.get('base_dn', ''),
407 'port': settings.get('port'),
416 'port': settings.get('port'),
408 'bind_dn': settings.get('dn_user'),
417 'bind_dn': settings.get('dn_user'),
409 'bind_pass': settings.get('dn_pass'),
418 'bind_pass': settings.get('dn_pass'),
410 'tls_kind': settings.get('tls_kind'),
419 'tls_kind': settings.get('tls_kind'),
411 'tls_reqcert': settings.get('tls_reqcert'),
420 'tls_reqcert': settings.get('tls_reqcert'),
412 'search_scope': settings.get('search_scope'),
421 'search_scope': settings.get('search_scope'),
413 'attr_login': settings.get('attr_login'),
422 'attr_login': settings.get('attr_login'),
414 'ldap_version': 3,
423 'ldap_version': 3,
415 'ldap_filter': settings.get('filter'),
424 'ldap_filter': settings.get('filter'),
416 }
425 }
417
426
418 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
427 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
419
428
420 log.debug('Checking for ldap authentication.')
429 log.debug('Checking for ldap authentication.')
421
430
422 try:
431 try:
423 aldap = AuthLdap(**ldap_args)
432 aldap = AuthLdap(**ldap_args)
424 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
433 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
425 log.debug('Got ldap DN response %s', user_dn)
434 log.debug('Got ldap DN response %s', user_dn)
426
435
427 def get_ldap_attr(k):
436 def get_ldap_attr(k):
428 return ldap_attrs.get(settings.get(k), [''])[0]
437 return ldap_attrs.get(settings.get(k), [''])[0]
429
438
430 # old attrs fetched from RhodeCode database
439 # old attrs fetched from RhodeCode database
431 admin = getattr(userobj, 'admin', False)
440 admin = getattr(userobj, 'admin', False)
432 active = getattr(userobj, 'active', True)
441 active = getattr(userobj, 'active', True)
433 email = getattr(userobj, 'email', '')
442 email = getattr(userobj, 'email', '')
434 username = getattr(userobj, 'username', username)
443 username = getattr(userobj, 'username', username)
435 firstname = getattr(userobj, 'firstname', '')
444 firstname = getattr(userobj, 'firstname', '')
436 lastname = getattr(userobj, 'lastname', '')
445 lastname = getattr(userobj, 'lastname', '')
437 extern_type = getattr(userobj, 'extern_type', '')
446 extern_type = getattr(userobj, 'extern_type', '')
438
447
439 groups = []
448 groups = []
440 user_attrs = {
449 user_attrs = {
441 'username': username,
450 'username': username,
442 'firstname': safe_unicode(
451 'firstname': safe_unicode(
443 get_ldap_attr('attr_firstname') or firstname),
452 get_ldap_attr('attr_firstname') or firstname),
444 'lastname': safe_unicode(
453 'lastname': safe_unicode(
445 get_ldap_attr('attr_lastname') or lastname),
454 get_ldap_attr('attr_lastname') or lastname),
446 'groups': groups,
455 'groups': groups,
447 'email': get_ldap_attr('attr_email') or email,
456 'email': get_ldap_attr('attr_email') or email,
448 'admin': admin,
457 'admin': admin,
449 'active': active,
458 'active': active,
450 "active_from_extern": None,
459 'active_from_extern': None,
451 'extern_name': user_dn,
460 'extern_name': user_dn,
452 'extern_type': extern_type,
461 'extern_type': extern_type,
453 }
462 }
454 log.debug('ldap user: %s', user_attrs)
463 log.debug('ldap user: %s', user_attrs)
455 log.info('user %s authenticated correctly', user_attrs['username'])
464 log.info('user %s authenticated correctly', user_attrs['username'])
456
465
457 return user_attrs
466 return user_attrs
458
467
459 except (LdapUsernameError, LdapPasswordError, LdapImportError):
468 except (LdapUsernameError, LdapPasswordError, LdapImportError):
460 log.exception("LDAP related exception")
469 log.exception("LDAP related exception")
461 return None
470 return None
462 except (Exception,):
471 except (Exception,):
463 log.exception("Other exception")
472 log.exception("Other exception")
464 return None
473 return None
@@ -1,160 +1,160 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 RhodeCode authentication library for PAM
22 RhodeCode authentication library for PAM
22 """
23 """
23
24
24 import colander
25 import colander
25 import grp
26 import grp
26 import logging
27 import logging
27 import pam
28 import pam
28 import pwd
29 import pwd
29 import re
30 import re
30 import socket
31 import socket
31
32
32 from pylons.i18n.translation import lazy_ugettext as _
33 from rhodecode.translation import _
33 from sqlalchemy.ext.hybrid import hybrid_property
34 from rhodecode.authentication.base import (
34
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
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 'email': email,
139 'email': email,
140 'admin': admin,
140 'admin': admin,
141 'active': active,
141 'active': active,
142 'active_from_extern': None,
142 'active_from_extern': None,
143 'extern_name': username,
143 'extern_name': username,
144 'extern_type': extern_type,
144 'extern_type': extern_type,
145 }
145 }
146
146
147 try:
147 try:
148 user_data = pwd.getpwnam(username)
148 user_data = pwd.getpwnam(username)
149 regex = settings["gecos"]
149 regex = settings["gecos"]
150 match = re.search(regex, user_data.pw_gecos)
150 match = re.search(regex, user_data.pw_gecos)
151 if match:
151 if match:
152 user_attrs["firstname"] = match.group('first_name')
152 user_attrs["firstname"] = match.group('first_name')
153 user_attrs["lastname"] = match.group('last_name')
153 user_attrs["lastname"] = match.group('last_name')
154 except Exception:
154 except Exception:
155 log.warning("Cannot extract additional info for PAM user")
155 log.warning("Cannot extract additional info for PAM user")
156 pass
156 pass
157
157
158 log.debug("pamuser: %s", user_attrs)
158 log.debug("pamuser: %s", user_attrs)
159 log.info('user %s authenticated correctly' % user_attrs['username'])
159 log.info('user %s authenticated correctly' % user_attrs['username'])
160 return user_attrs
160 return user_attrs
@@ -1,143 +1,142 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 pylons.i18n.translation import lazy_ugettext as _
27 from pylons.i18n.translation import lazy_ugettext as _
28 from sqlalchemy.ext.hybrid import hybrid_property
29
28
30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.lib.utils2 import safe_str
33 from rhodecode.model.db import User
32 from rhodecode.model.db import User
34
33
35 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
36
35
37
36
38 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
39 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
40 return plugin
39 return plugin
41
40
42
41
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 pass
43 pass
45
44
46
45
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48
47
49 def includeme(self, config):
48 def includeme(self, config):
50 config.add_authn_plugin(self)
49 config.add_authn_plugin(self)
51 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
52 config.add_view(
51 config.add_view(
53 'rhodecode.authentication.views.AuthnPluginViewBase',
52 'rhodecode.authentication.views.AuthnPluginViewBase',
54 attr='settings_get',
53 attr='settings_get',
55 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
56 request_method='GET',
55 request_method='GET',
57 route_name='auth_home',
56 route_name='auth_home',
58 context=RhodecodeAuthnResource)
57 context=RhodecodeAuthnResource)
59 config.add_view(
58 config.add_view(
60 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
61 attr='settings_post',
60 attr='settings_post',
62 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
63 request_method='POST',
62 request_method='POST',
64 route_name='auth_home',
63 route_name='auth_home',
65 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
66
65
67 def get_display_name(self):
66 def get_display_name(self):
68 return _('Rhodecode')
67 return _('Rhodecode')
69
68
70 @hybrid_property
69 @hybrid_property
71 def name(self):
70 def name(self):
72 return "rhodecode"
71 return "rhodecode"
73
72
74 def user_activation_state(self):
73 def user_activation_state(self):
75 def_user_perms = User.get_default_user().AuthUser.permissions['global']
74 def_user_perms = User.get_default_user().AuthUser.permissions['global']
76 return 'hg.register.auto_activate' in def_user_perms
75 return 'hg.register.auto_activate' in def_user_perms
77
76
78 def allows_authentication_from(
77 def allows_authentication_from(
79 self, user, allows_non_existing_user=True,
78 self, user, allows_non_existing_user=True,
80 allowed_auth_plugins=None, allowed_auth_sources=None):
79 allowed_auth_plugins=None, allowed_auth_sources=None):
81 """
80 """
82 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.
83 We know that user exists in our database.
82 We know that user exists in our database.
84 """
83 """
85 allows_non_existing_user = False
84 allows_non_existing_user = False
86 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
87 user, allows_non_existing_user=allows_non_existing_user)
86 user, allows_non_existing_user=allows_non_existing_user)
88
87
89 def auth(self, userobj, username, password, settings, **kwargs):
88 def auth(self, userobj, username, password, settings, **kwargs):
90 if not userobj:
89 if not userobj:
91 log.debug('userobj was:%s skipping' % (userobj, ))
90 log.debug('userobj was:%s skipping' % (userobj, ))
92 return None
91 return None
93 if userobj.extern_type != self.name:
92 if userobj.extern_type != self.name:
94 log.warning(
93 log.warning(
95 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
96 (userobj, userobj.extern_type, self.name))
95 (userobj, userobj.extern_type, self.name))
97 return None
96 return None
98
97
99 user_attrs = {
98 user_attrs = {
100 "username": userobj.username,
99 "username": userobj.username,
101 "firstname": userobj.firstname,
100 "firstname": userobj.firstname,
102 "lastname": userobj.lastname,
101 "lastname": userobj.lastname,
103 "groups": [],
102 "groups": [],
104 "email": userobj.email,
103 "email": userobj.email,
105 "admin": userobj.admin,
104 "admin": userobj.admin,
106 "active": userobj.active,
105 "active": userobj.active,
107 "active_from_extern": userobj.active,
106 "active_from_extern": userobj.active,
108 "extern_name": userobj.user_id,
107 "extern_name": userobj.user_id,
109 "extern_type": userobj.extern_type,
108 "extern_type": userobj.extern_type,
110 }
109 }
111
110
112 log.debug("User attributes:%s" % (user_attrs, ))
111 log.debug("User attributes:%s" % (user_attrs, ))
113 if userobj.active:
112 if userobj.active:
114 from rhodecode.lib import auth
113 from rhodecode.lib import auth
115 crypto_backend = auth.crypto_backend()
114 crypto_backend = auth.crypto_backend()
116 password_encoded = safe_str(password)
115 password_encoded = safe_str(password)
117 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
116 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
118 password_encoded, userobj.password)
117 password_encoded, userobj.password)
119
118
120 if password_match and new_hash:
119 if password_match and new_hash:
121 log.debug('user %s properly authenticated, but '
120 log.debug('user %s properly authenticated, but '
122 'requires hash change to bcrypt', userobj)
121 'requires hash change to bcrypt', userobj)
123 # if password match, and we use OLD deprecated hash,
122 # if password match, and we use OLD deprecated hash,
124 # we should migrate this user hash password to the new hash
123 # we should migrate this user hash password to the new hash
125 # we store the new returned by hash_check_with_upgrade function
124 # we store the new returned by hash_check_with_upgrade function
126 user_attrs['_hash_migrate'] = new_hash
125 user_attrs['_hash_migrate'] = new_hash
127
126
128 if userobj.username == User.DEFAULT_USER and userobj.active:
127 if userobj.username == User.DEFAULT_USER and userobj.active:
129 log.info(
128 log.info(
130 'user %s authenticated correctly as anonymous user', userobj)
129 'user %s authenticated correctly as anonymous user', userobj)
131 return user_attrs
130 return user_attrs
132
131
133 elif userobj.username == username and password_match:
132 elif userobj.username == username and password_match:
134 log.info('user %s authenticated correctly', userobj)
133 log.info('user %s authenticated correctly', userobj)
135 return user_attrs
134 return user_attrs
136 log.info("user %s had a bad password when "
135 log.info("user %s had a bad password when "
137 "authenticating on this plugin", userobj)
136 "authenticating on this plugin", userobj)
138 return None
137 return None
139 else:
138 else:
140 log.warning(
139 log.warning(
141 'user `%s` failed to authenticate via %s, reason: account not '
140 'user `%s` failed to authenticate via %s, reason: account not '
142 'active.', username, self.name)
141 'active.', username, self.name)
143 return None
142 return None
@@ -1,140 +1,139 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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 sqlalchemy.ext.hybrid import hybrid_property
28
29 from rhodecode.translation import _
27 from rhodecode.translation import _
30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE
28 from rhodecode.authentication.base import (
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.model.db import User, UserApiKeys
31 from rhodecode.model.db import User, UserApiKeys
33
32
34
33
35 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
36
35
37
36
38 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
39 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
40 return plugin
39 return plugin
41
40
42
41
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 pass
43 pass
45
44
46
45
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 """
47 """
49 Enables usage of authentication tokens for vcs operations.
48 Enables usage of authentication tokens for vcs operations.
50 """
49 """
51
50
52 def includeme(self, config):
51 def includeme(self, config):
53 config.add_authn_plugin(self)
52 config.add_authn_plugin(self)
54 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
55 config.add_view(
54 config.add_view(
56 'rhodecode.authentication.views.AuthnPluginViewBase',
55 'rhodecode.authentication.views.AuthnPluginViewBase',
57 attr='settings_get',
56 attr='settings_get',
58 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
59 request_method='GET',
58 request_method='GET',
60 route_name='auth_home',
59 route_name='auth_home',
61 context=RhodecodeAuthnResource)
60 context=RhodecodeAuthnResource)
62 config.add_view(
61 config.add_view(
63 'rhodecode.authentication.views.AuthnPluginViewBase',
62 'rhodecode.authentication.views.AuthnPluginViewBase',
64 attr='settings_post',
63 attr='settings_post',
65 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
66 request_method='POST',
65 request_method='POST',
67 route_name='auth_home',
66 route_name='auth_home',
68 context=RhodecodeAuthnResource)
67 context=RhodecodeAuthnResource)
69
68
70 def get_display_name(self):
69 def get_display_name(self):
71 return _('Rhodecode Token Auth')
70 return _('Rhodecode Token Auth')
72
71
73 @hybrid_property
72 @hybrid_property
74 def name(self):
73 def name(self):
75 return "authtoken"
74 return "authtoken"
76
75
77 def user_activation_state(self):
76 def user_activation_state(self):
78 def_user_perms = User.get_default_user().AuthUser.permissions['global']
77 def_user_perms = User.get_default_user().AuthUser.permissions['global']
79 return 'hg.register.auto_activate' in def_user_perms
78 return 'hg.register.auto_activate' in def_user_perms
80
79
81 def allows_authentication_from(
80 def allows_authentication_from(
82 self, user, allows_non_existing_user=True,
81 self, user, allows_non_existing_user=True,
83 allowed_auth_plugins=None, allowed_auth_sources=None):
82 allowed_auth_plugins=None, allowed_auth_sources=None):
84 """
83 """
85 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
86 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
87 authenticate against it. But only via vcs mode
86 authenticate against it. But only via vcs mode
88 """
87 """
89 from rhodecode.authentication.base import get_authn_registry
88 from rhodecode.authentication.base import get_authn_registry
90 authn_registry = get_authn_registry()
89 authn_registry = get_authn_registry()
91
90
92 active_plugins = set(
91 active_plugins = set(
93 [x.name for x in authn_registry.get_plugins_for_authentication()])
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
94 active_plugins.discard(self.name)
93 active_plugins.discard(self.name)
95
94
96 allowed_auth_plugins = [self.name] + list(active_plugins)
95 allowed_auth_plugins = [self.name] + list(active_plugins)
97 # only for vcs operations
96 # only for vcs operations
98 allowed_auth_sources = [VCS_TYPE]
97 allowed_auth_sources = [VCS_TYPE]
99
98
100 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
101 user, allows_non_existing_user=False,
100 user, allows_non_existing_user=False,
102 allowed_auth_plugins=allowed_auth_plugins,
101 allowed_auth_plugins=allowed_auth_plugins,
103 allowed_auth_sources=allowed_auth_sources)
102 allowed_auth_sources=allowed_auth_sources)
104
103
105 def auth(self, userobj, username, password, settings, **kwargs):
104 def auth(self, userobj, username, password, settings, **kwargs):
106 if not userobj:
105 if not userobj:
107 log.debug('userobj was:%s skipping' % (userobj, ))
106 log.debug('userobj was:%s skipping' % (userobj, ))
108 return None
107 return None
109
108
110 user_attrs = {
109 user_attrs = {
111 "username": userobj.username,
110 "username": userobj.username,
112 "firstname": userobj.firstname,
111 "firstname": userobj.firstname,
113 "lastname": userobj.lastname,
112 "lastname": userobj.lastname,
114 "groups": [],
113 "groups": [],
115 "email": userobj.email,
114 "email": userobj.email,
116 "admin": userobj.admin,
115 "admin": userobj.admin,
117 "active": userobj.active,
116 "active": userobj.active,
118 "active_from_extern": userobj.active,
117 "active_from_extern": userobj.active,
119 "extern_name": userobj.user_id,
118 "extern_name": userobj.user_id,
120 "extern_type": userobj.extern_type,
119 "extern_type": userobj.extern_type,
121 }
120 }
122
121
123 log.debug('Authenticating user with args %s', user_attrs)
122 log.debug('Authenticating user with args %s', user_attrs)
124 if userobj.active:
123 if userobj.active:
125 token_match = userobj.authenticate_by_token(
124 token_match = userobj.authenticate_by_token(
126 password, roles=[UserApiKeys.ROLE_VCS])
125 password, roles=[UserApiKeys.ROLE_VCS])
127
126
128 if userobj.username == username and token_match:
127 if userobj.username == username and token_match:
129 log.info(
128 log.info(
130 'user `%s` successfully authenticated via %s',
129 'user `%s` successfully authenticated via %s',
131 user_attrs['username'], self.name)
130 user_attrs['username'], self.name)
132 return user_attrs
131 return user_attrs
133 log.error(
132 log.error(
134 'user `%s` failed to authenticate via %s, reason: bad or '
133 'user `%s` failed to authenticate via %s, reason: bad or '
135 'inactive token.', username, self.name)
134 'inactive token.', username, self.name)
136 else:
135 else:
137 log.warning(
136 log.warning(
138 'user `%s` failed to authenticate via %s, reason: account not '
137 'user `%s` failed to authenticate via %s, reason: account not '
139 'active.', username, self.name)
138 'active.', username, self.name)
140 return None
139 return None
@@ -1,51 +1,52 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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
22
23 from rhodecode.translation import _
23 from rhodecode.translation import _
24
24
25
25
26 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
26 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
27 """
27 """
28 This base schema is intended for use in authentication plugins.
28 This base schema is intended for use in authentication plugins.
29 It adds a few default settings (e.g., "enabled"), so that plugin
29 It adds a few default settings (e.g., "enabled"), so that plugin
30 authors don't have to maintain a bunch of boilerplate.
30 authors don't have to maintain a bunch of boilerplate.
31 """
31 """
32 enabled = colander.SchemaNode(
32 enabled = colander.SchemaNode(
33 colander.Bool(),
33 colander.Bool(),
34 default=False,
34 default=False,
35 description=_('Enable or disable this authentication plugin.'),
35 description=_('Enable or disable this authentication plugin.'),
36 missing=False,
36 missing=False,
37 title=_('Enabled'),
37 title=_('Enabled'),
38 widget='bool',
38 widget='bool',
39 )
39 )
40 cache_ttl = colander.SchemaNode(
40 cache_ttl = colander.SchemaNode(
41 colander.Int(),
41 colander.Int(),
42 default=0,
42 default=0,
43 description=_('Amount of seconds to cache the authentication '
43 description=_('Amount of seconds to cache the authentication response'
44 'call for this plugin. Useful for long calls like '
44 'call for this plugin. \n'
45 'LDAP to improve the responsiveness of the '
45 'Useful for long calls like LDAP to improve the '
46 'authentication system (0 means disabled).'),
46 'performance of the authentication system '
47 '(0 means disabled).'),
47 missing=0,
48 missing=0,
48 title=_('Auth Cache TTL'),
49 title=_('Auth Cache TTL'),
49 validator=colander.Range(min=0, max=None),
50 validator=colander.Range(min=0, max=None),
50 widget='int',
51 widget='int',
51 )
52 )
@@ -1,93 +1,98 b''
1 # Copyright (C) 2016-2017 RhodeCode GmbH
1 # Copyright (C) 2016-2017 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import logging
18 import logging
19
19
20 from datetime import datetime
20 from datetime import datetime
21 from pyramid.threadlocal import get_current_request
21 from pyramid.threadlocal import get_current_request
22 from rhodecode.lib.utils2 import AttributeDict
22 from rhodecode.lib.utils2 import AttributeDict
23
23
24
24
25 # this is a user object to be used for events caused by the system (eg. shell)
25 # this is a user object to be used for events caused by the system (eg. shell)
26 SYSTEM_USER = AttributeDict(dict(
26 SYSTEM_USER = AttributeDict(dict(
27 username='__SYSTEM__'
27 username='__SYSTEM__'
28 ))
28 ))
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class RhodecodeEvent(object):
33 class RhodecodeEvent(object):
34 """
34 """
35 Base event class for all Rhodecode events
35 Base event class for all Rhodecode events
36 """
36 """
37 name = "RhodeCodeEvent"
37 name = "RhodeCodeEvent"
38
38
39 def __init__(self):
39 def __init__(self):
40 self.request = get_current_request()
40 self.request = get_current_request()
41 self.utc_timestamp = datetime.utcnow()
41 self.utc_timestamp = datetime.utcnow()
42
42
43 @property
43 @property
44 def auth_user(self):
44 def auth_user(self):
45 if not self.request:
45 if not self.request:
46 return
46 return
47
47
48 user = getattr(self.request, 'user', None)
48 user = getattr(self.request, 'user', None)
49 if user:
49 if user:
50 return user
50 return user
51
51
52 api_user = getattr(self.request, 'rpc_user', None)
52 api_user = getattr(self.request, 'rpc_user', None)
53 if api_user:
53 if api_user:
54 return api_user
54 return api_user
55
55
56 @property
56 @property
57 def actor(self):
57 def actor(self):
58 auth_user = self.auth_user
58 auth_user = self.auth_user
59 if auth_user:
59 if auth_user:
60 return auth_user.get_instance()
60 instance = auth_user.get_instance()
61 if not instance:
62 return AttributeDict(dict(
63 username=auth_user.username
64 ))
65
61 return SYSTEM_USER
66 return SYSTEM_USER
62
67
63 @property
68 @property
64 def actor_ip(self):
69 def actor_ip(self):
65 auth_user = self.auth_user
70 auth_user = self.auth_user
66 if auth_user:
71 if auth_user:
67 return auth_user.ip_addr
72 return auth_user.ip_addr
68 return '<no ip available>'
73 return '<no ip available>'
69
74
70 @property
75 @property
71 def server_url(self):
76 def server_url(self):
72 default = '<no server_url available>'
77 default = '<no server_url available>'
73 if self.request:
78 if self.request:
74 from rhodecode.lib import helpers as h
79 from rhodecode.lib import helpers as h
75 try:
80 try:
76 return h.url('home', qualified=True)
81 return h.url('home', qualified=True)
77 except Exception:
82 except Exception:
78 log.exception('Failed to fetch URL for server')
83 log.exception('Failed to fetch URL for server')
79 return default
84 return default
80
85
81 return default
86 return default
82
87
83 def as_dict(self):
88 def as_dict(self):
84 data = {
89 data = {
85 'name': self.name,
90 'name': self.name,
86 'utc_timestamp': self.utc_timestamp,
91 'utc_timestamp': self.utc_timestamp,
87 'actor_ip': self.actor_ip,
92 'actor_ip': self.actor_ip,
88 'actor': {
93 'actor': {
89 'username': self.actor.username
94 'username': self.actor.username
90 },
95 },
91 'server_url': self.server_url
96 'server_url': self.server_url
92 }
97 }
93 return data
98 return data
@@ -1,378 +1,379 b''
1
1
2 // Contains the style definitions used for .main-content
2 // Contains the style definitions used for .main-content
3 // elements which are mainly around the admin settings.
3 // elements which are mainly around the admin settings.
4
4
5
5
6 // TODO: johbo: Integrate in a better way, this is for "main content" which
6 // TODO: johbo: Integrate in a better way, this is for "main content" which
7 // should not have a limit on the width.
7 // should not have a limit on the width.
8 .main-content-full {
8 .main-content-full {
9 clear: both;
9 clear: both;
10 }
10 }
11
11
12
12
13 .main-content {
13 .main-content {
14 max-width: @maincontent-maxwidth;
14 max-width: @maincontent-maxwidth;
15
15
16 h3,
16 h3,
17 // TODO: johbo: Change templates to use h3 instead of h4 here
17 // TODO: johbo: Change templates to use h3 instead of h4 here
18 h4 {
18 h4 {
19 line-height: 1em;
19 line-height: 1em;
20 }
20 }
21
21
22 // TODO: johbo: Check if we can do that on a global level
22 // TODO: johbo: Check if we can do that on a global level
23 table {
23 table {
24 th {
24 th {
25 padding: 0;
25 padding: 0;
26 }
26 }
27 td.field{
27 td.field{
28 .help-block{
28 .help-block{
29 margin-left: 0;
29 margin-left: 0;
30 }
30 }
31 }
31 }
32 }
32 }
33
33
34 // TODO: johbo: Tweak this into the general styling, for a full width
34 // TODO: johbo: Tweak this into the general styling, for a full width
35 // textarea
35 // textarea
36 .textarea-full {
36 .textarea-full {
37 // 2x 10px padding and 2x 1px border
37 // 2x 10px padding and 2x 1px border
38 margin-right: 22px;
38 margin-right: 22px;
39 }
39 }
40
40
41 }
41 }
42
42
43
43
44 // TODO: johbo: duplicated, think about a mixins.less
44 // TODO: johbo: duplicated, think about a mixins.less
45 .block-left{
45 .block-left{
46 float: left;
46 float: left;
47 }
47 }
48
48
49 .form {
49 .form {
50 .checkboxes {
50 .checkboxes {
51 // TODO: johbo: Should be changed in .checkboxes already
51 // TODO: johbo: Should be changed in .checkboxes already
52 width: auto;
52 width: auto;
53 }
53 }
54
54
55 // TODO: johbo: some settings pages are broken and don't have the .buttons
55 // TODO: johbo: some settings pages are broken and don't have the .buttons
56 // inside the .fields, tweak those templates and remove this.
56 // inside the .fields, tweak those templates and remove this.
57 .buttons {
57 .buttons {
58 margin-top: @textmargin;
58 margin-top: @textmargin;
59 }
59 }
60
60
61 .help-block {
61 .help-block {
62 display: block;
62 display: block;
63 margin-left: @label-width;
63 margin-left: @label-width;
64 white-space: pre;
64 }
65 }
65
66
66 .action_button {
67 .action_button {
67 color: @grey4;
68 color: @grey4;
68 }
69 }
69 }
70 }
70
71
71 .main-content-full-width {
72 .main-content-full-width {
72 .main-content;
73 .main-content;
73 width: 100%;
74 width: 100%;
74 min-width: 100%;
75 min-width: 100%;
75 }
76 }
76
77
77 .field {
78 .field {
78 clear: left;
79 clear: left;
79 margin-bottom: @padding;
80 margin-bottom: @padding;
80
81
81 }
82 }
82
83
83 .fields {
84 .fields {
84 label {
85 label {
85 color: @grey2;
86 color: @grey2;
86 }
87 }
87
88
88 .field {
89 .field {
89 clear: right;
90 clear: right;
90 margin-bottom: @textmargin;
91 margin-bottom: @textmargin;
91 width: 100%;
92 width: 100%;
92
93
93 .label {
94 .label {
94 float: left;
95 float: left;
95 margin-right: @form-vertical-margin;
96 margin-right: @form-vertical-margin;
96 margin-top: 0;
97 margin-top: 0;
97 padding-top: @input-padding-px + @border-thickness-inputs;
98 padding-top: @input-padding-px + @border-thickness-inputs;
98 width: @label-width - @form-vertical-margin;
99 width: @label-width - @form-vertical-margin;
99 }
100 }
100 // used in forms for fields that show just text
101 // used in forms for fields that show just text
101 .label-text {
102 .label-text {
102 .label;
103 .label;
103 padding-top: 5px;
104 padding-top: 5px;
104 }
105 }
105 // Used to position content on the right side of a .label
106 // Used to position content on the right side of a .label
106 .content,
107 .content,
107 .side-by-side-selector {
108 .side-by-side-selector {
108 padding-top: @input-padding-px + @input-border-thickness;
109 padding-top: @input-padding-px + @input-border-thickness;
109 }
110 }
110
111
111 .checkboxes,
112 .checkboxes,
112 .input,
113 .input,
113 .select,
114 .select,
114 .textarea,
115 .textarea,
115 .content {
116 .content {
116 float: none;
117 float: none;
117 margin-left: @label-width;
118 margin-left: @label-width;
118
119
119 .help-block{
120 .help-block {
120 margin-left: 0;
121 margin-left: 0;
121 }
122 }
122 }
123 }
123
124
124 .checkboxes,
125 .checkboxes,
125 .input,
126 .input,
126 .select {
127 .select {
127 .help-block {
128 .help-block {
128 display: block;
129 display: block;
129 }
130 }
130 }
131 }
131
132
132 .checkboxes,
133 .checkboxes,
133 .radios {
134 .radios {
134 // TODO: johbo: We get a 4px margin from the from-bootstrap,
135 // TODO: johbo: We get a 4px margin from the from-bootstrap,
135 // compensating here to align well with labels on the left.
136 // compensating here to align well with labels on the left.
136 padding-top: @input-padding-px + @input-border-thickness - 3px;
137 padding-top: @input-padding-px + @input-border-thickness - 3px;
137 }
138 }
138
139
139 .checkbox,
140 .checkbox,
140 .radio {
141 .radio {
141 display: block;
142 display: block;
142 width: auto;
143 width: auto;
143 }
144 }
144
145
145 .checkbox + .checkbox {
146 .checkbox + .checkbox {
146 display: block;
147 display: block;
147 }
148 }
148
149
149 .input,
150 .input,
150 .select {
151 .select {
151 .help-block,
152 .help-block,
152 .info-block {
153 .info-block {
153 margin-top: @form-vertical-margin / 2;
154 margin-top: @form-vertical-margin / 2;
154 }
155 }
155 }
156 }
156
157
157 .input {
158 .input {
158 .medium {
159 .medium {
159 width: @fields-input-m;
160 width: @fields-input-m;
160 }
161 }
161 .large {
162 .large {
162 width: @fields-input-l;
163 width: @fields-input-l;
163 }
164 }
164
165
165 .text-as-placeholder {
166 .text-as-placeholder {
166 padding-top: @input-padding-px + @border-thickness-inputs;
167 padding-top: @input-padding-px + @border-thickness-inputs;
167 }
168 }
168 }
169 }
169
170
170 // TODO: johbo: Try to find a better integration of this bit.
171 // TODO: johbo: Try to find a better integration of this bit.
171 // When using a select2 inside of a field, it should not have the
172 // When using a select2 inside of a field, it should not have the
172 // top margin.
173 // top margin.
173 .select .drop-menu {
174 .select .drop-menu {
174 margin-top: 0;
175 margin-top: 0;
175 }
176 }
176
177
177 .textarea {
178 .textarea {
178 float: none;
179 float: none;
179
180
180 textarea {
181 textarea {
181 // TODO: johbo: From somewhere we get a clear which does
182 // TODO: johbo: From somewhere we get a clear which does
182 // more harm than good here.
183 // more harm than good here.
183 clear: none;
184 clear: none;
184 }
185 }
185
186
186 .CodeMirror {
187 .CodeMirror {
187 // TODO: johbo: Tweak to position the .help-block nicer,
188 // TODO: johbo: Tweak to position the .help-block nicer,
188 // figure out how to apply for .text-block instead.
189 // figure out how to apply for .text-block instead.
189 margin-bottom: 10px;
190 margin-bottom: 10px;
190 }
191 }
191
192
192 // TODO: johbo: Check if we can remove the grey background on
193 // TODO: johbo: Check if we can remove the grey background on
193 // the global level and remove this if possible.
194 // the global level and remove this if possible.
194 .help-block {
195 .help-block {
195 background: transparent;
196 background: transparent;
196 padding: 0;
197 padding: 0;
197 }
198 }
198 }
199 }
199
200
200 &.tag_patterns,
201 &.tag_patterns,
201 &.branch_patterns {
202 &.branch_patterns {
202
203
203 input {
204 input {
204 max-width: 430px;
205 max-width: 430px;
205 }
206 }
206 }
207 }
207 }
208 }
208
209
209 .field-sm {
210 .field-sm {
210 .label {
211 .label {
211 padding-top: @input-padding-px / 2 + @input-border-thickness;
212 padding-top: @input-padding-px / 2 + @input-border-thickness;
212 }
213 }
213 .checkboxes,
214 .checkboxes,
214 .radios {
215 .radios {
215 // TODO: johbo: We get a 4px margin from the from-bootstrap,
216 // TODO: johbo: We get a 4px margin from the from-bootstrap,
216 // compensating here to align well with labels on the left.
217 // compensating here to align well with labels on the left.
217 padding-top: @input-padding-px / 2 + @input-border-thickness - 3px;
218 padding-top: @input-padding-px / 2 + @input-border-thickness - 3px;
218 }
219 }
219 }
220 }
220
221
221 .field.customhooks {
222 .field.customhooks {
222 .label {
223 .label {
223 padding-top: 0;
224 padding-top: 0;
224 }
225 }
225 .input-wrapper {
226 .input-wrapper {
226 padding-right: 25px;
227 padding-right: 25px;
227
228
228 input {
229 input {
229 width: 100%;
230 width: 100%;
230 }
231 }
231 }
232 }
232 .input {
233 .input {
233 padding-right: 25px;
234 padding-right: 25px;
234 }
235 }
235 }
236 }
236
237
237 .buttons {
238 .buttons {
238 // TODO: johbo: define variable for this value.
239 // TODO: johbo: define variable for this value.
239 // Note that this should be 40px but since most elements add some
240 // Note that this should be 40px but since most elements add some
240 // space in the bottom, we are with 20 closer to 40.
241 // space in the bottom, we are with 20 closer to 40.
241 margin-top: 20px;
242 margin-top: 20px;
242 clear: both;
243 clear: both;
243 margin-bottom: @padding;
244 margin-bottom: @padding;
244 }
245 }
245
246
246 .desc{
247 .desc{
247 margin-right: @textmargin;
248 margin-right: @textmargin;
248 }
249 }
249
250
250 input,
251 input,
251 .drop-menu {
252 .drop-menu {
252 margin-right: @padding/3;
253 margin-right: @padding/3;
253 }
254 }
254
255
255 }
256 }
256
257
257 .form-vertical .fields .field {
258 .form-vertical .fields .field {
258
259
259 .label {
260 .label {
260 float: none;
261 float: none;
261 width: auto;
262 width: auto;
262 }
263 }
263
264
264 .checkboxes,
265 .checkboxes,
265 .input,
266 .input,
266 .select,
267 .select,
267 .textarea {
268 .textarea {
268 margin-left: 0;
269 margin-left: 0;
269 }
270 }
270
271
271 // TODO: johbo: had to tweak the width here to make it big enough for
272 // TODO: johbo: had to tweak the width here to make it big enough for
272 // the license.
273 // the license.
273 .textarea.editor {
274 .textarea.editor {
274 max-width: none;
275 max-width: none;
275 }
276 }
276
277
277 .textarea.large textarea {
278 .textarea.large textarea {
278 min-height: 200px;
279 min-height: 200px;
279 }
280 }
280
281
281 .help-block {
282 .help-block {
282 margin-left: 0;
283 margin-left: 0;
283 }
284 }
284 }
285 }
285
286
286
287
287
288
288
289
289 .main-content {
290 .main-content {
290 .block-left;
291 .block-left;
291
292
292 .section {
293 .section {
293 margin-bottom: @space;
294 margin-bottom: @space;
294 }
295 }
295
296
296
297
297 // Table aligning same way as forms in admin section, e.g.
298 // Table aligning same way as forms in admin section, e.g.
298 // python packages table
299 // python packages table
299 table.formalign {
300 table.formalign {
300 float: left;
301 float: left;
301 width: auto;
302 width: auto;
302
303
303 .label {
304 .label {
304 width: @label-width;
305 width: @label-width;
305 }
306 }
306
307
307 }
308 }
308
309
309
310
310 table.issuetracker {
311 table.issuetracker {
311
312
312 color: @text-color;
313 color: @text-color;
313
314
314 .issue-tracker-example {
315 .issue-tracker-example {
315 color: @grey4;
316 color: @grey4;
316 }
317 }
317 }
318 }
318
319
319 .side-by-side-selector{
320 .side-by-side-selector{
320 .left-group,
321 .left-group,
321 .middle-group,
322 .middle-group,
322 .right-group{
323 .right-group{
323 float: left;
324 float: left;
324 }
325 }
325
326
326 .left-group,
327 .left-group,
327 .right-group{
328 .right-group{
328 width: 45%;
329 width: 45%;
329 text-align: center;
330 text-align: center;
330
331
331 label{
332 label{
332 width: 100%;
333 width: 100%;
333 text-align: left;
334 text-align: left;
334 }
335 }
335
336
336 select{
337 select{
337 width: 100%;
338 width: 100%;
338 background: none;
339 background: none;
339 border-color: @border-highlight-color;
340 border-color: @border-highlight-color;
340 color: @text-color;
341 color: @text-color;
341 font-family: @text-light;
342 font-family: @text-light;
342 font-size: @basefontsize;
343 font-size: @basefontsize;
343 color: @grey1;
344 color: @grey1;
344 padding: @textmargin/2;
345 padding: @textmargin/2;
345 }
346 }
346
347
347 select:after{
348 select:after{
348 content: "";
349 content: "";
349 }
350 }
350
351
351 }
352 }
352
353
353 .middle-group{
354 .middle-group{
354 width: 10%;
355 width: 10%;
355 text-align: center;
356 text-align: center;
356 padding-top: 4em;
357 padding-top: 4em;
357 i {
358 i {
358 font-size: 18px;
359 font-size: 18px;
359 cursor: pointer;
360 cursor: pointer;
360 line-height: 2em;
361 line-height: 2em;
361 }
362 }
362 }
363 }
363
364
364 }
365 }
365
366
366 .permissions_boxes{
367 .permissions_boxes{
367 label, .label{
368 label, .label{
368 margin-right: @textmargin/2;
369 margin-right: @textmargin/2;
369 }
370 }
370 }
371 }
371
372
372 .radios{
373 .radios{
373 label{
374 label{
374 margin-right: @textmargin;
375 margin-right: @textmargin;
375 }
376 }
376 }
377 }
377 }
378 }
378
379
@@ -1,116 +1,117 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('Authentication Settings')}
5 ${_('Authentication Settings')}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}}
7 &middot; ${h.branding(c.rhodecode_name)}}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 ${h.link_to(_('Admin'),h.url('admin_home'))}
12 ${h.link_to(_('Admin'),h.url('admin_home'))}
13 &raquo;
13 &raquo;
14 ${h.link_to(_('Authentication Plugins'),request.resource_path(resource.__parent__, route_name='auth_home'))}
14 ${h.link_to(_('Authentication Plugins'),request.resource_path(resource.__parent__, route_name='auth_home'))}
15 &raquo;
15 &raquo;
16 ${resource.display_name}
16 ${resource.display_name}
17 </%def>
17 </%def>
18
18
19 <%def name="menu_bar_nav()">
19 <%def name="menu_bar_nav()">
20 ${self.menu_items(active='admin')}
20 ${self.menu_items(active='admin')}
21 </%def>
21 </%def>
22
22
23 <%def name="main()">
23 <%def name="main()">
24 <div class="box">
24 <div class="box">
25 <div class="title">
25 <div class="title">
26 ${self.breadcrumbs()}
26 ${self.breadcrumbs()}
27 </div>
27 </div>
28 <div class='sidebar-col-wrapper'>
28 <div class='sidebar-col-wrapper'>
29
29
30 ## TODO: This is repeated in the auth root template and should be merged
30 ## TODO: This is repeated in the auth root template and should be merged
31 ## into a single solution.
31 ## into a single solution.
32 <div class="sidebar">
32 <div class="sidebar">
33 <ul class="nav nav-pills nav-stacked">
33 <ul class="nav nav-pills nav-stacked">
34 % for item in resource.get_root().get_nav_list():
34 % for item in resource.get_root().get_nav_list():
35 <li ${'class=active' if item == resource else ''}>
35 <li ${'class=active' if item == resource else ''}>
36 <a href="${request.resource_path(item, route_name='auth_home')}">${item.display_name}</a>
36 <a href="${request.resource_path(item, route_name='auth_home')}">${item.display_name}</a>
37 </li>
37 </li>
38 % endfor
38 % endfor
39 </ul>
39 </ul>
40 </div>
40 </div>
41
41
42 <div class="main-content-full-width">
42 <div class="main-content-full-width">
43 <div class="panel panel-default">
43 <div class="panel panel-default">
44 <div class="panel-heading">
44 <div class="panel-heading">
45 <h3 class="panel-title">${_('Plugin')}: ${resource.display_name}</h3>
45 <h3 class="panel-title">${_('Plugin')}: ${resource.display_name}</h3>
46 </div>
46 </div>
47 <div class="panel-body">
47 <div class="panel-body">
48 <div class="plugin_form">
48 <div class="plugin_form">
49 <div class="fields">
49 <div class="fields">
50 ${h.secure_form(request.resource_path(resource, route_name='auth_home'))}
50 ${h.secure_form(request.resource_path(resource, route_name='auth_home'))}
51 <div class="form">
51 <div class="form">
52
52
53 %for node in plugin.get_settings_schema():
53 %for node in plugin.get_settings_schema():
54 <% label_css_class = ("label-checkbox" if (node.widget == "bool") else "") %>
54 <% label_css_class = ("label-checkbox" if (node.widget == "bool") else "") %>
55 <div class="field">
55 <div class="field">
56 <div class="label ${label_css_class}"><label for="${node.name}">${node.title}</label></div>
56 <div class="label ${label_css_class}"><label for="${node.name}">${node.title}</label></div>
57 <div class="input">
57 <div class="input">
58 %if node.widget in ["string", "int", "unicode"]:
58 %if node.widget in ["string", "int", "unicode"]:
59 ${h.text(node.name, defaults.get(node.name), class_="medium")}
59 ${h.text(node.name, defaults.get(node.name), class_="medium")}
60 %elif node.widget == "password":
60 %elif node.widget == "password":
61 ${h.password(node.name, defaults.get(node.name), class_="medium")}
61 ${h.password(node.name, defaults.get(node.name), class_="medium")}
62 %elif node.widget == "bool":
62 %elif node.widget == "bool":
63 <div class="checkbox">${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
63 <div class="checkbox">${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
64 %elif node.widget == "select":
64 %elif node.widget == "select":
65 ${h.select(node.name, defaults.get(node.name), node.validator.choices)}
65 ${h.select(node.name, defaults.get(node.name), node.validator.choices)}
66 %elif node.widget == "readonly":
66 %elif node.widget == "readonly":
67 ${node.default}
67 ${node.default}
68 %else:
68 %else:
69 This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select].
69 This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select].
70 %endif
70 %endif
71 %if node.name in errors:
71 %if node.name in errors:
72 <span class="error-message">${errors.get(node.name)}</span>
72 <span class="error-message">${errors.get(node.name)}</span>
73 <br />
73 <br />
74 %endif
74 %endif
75 <p class="help-block">${node.description}</p>
75 <p class="help-block">${node.description}</p>
76 </div>
76 </div>
77 </div>
77 </div>
78 %endfor
78 %endfor
79
79
80 ## Allow derived templates to add something below the form
80 ## Allow derived templates to add something below the form
81 ## input fields
81 ## input fields
82 %if hasattr(next, 'below_form_fields'):
82 %if hasattr(next, 'below_form_fields'):
83 ${next.below_form_fields()}
83 ${next.below_form_fields()}
84 %endif
84 %endif
85
85
86 <div class="buttons">
86 <div class="buttons">
87 ${h.submit('save',_('Save'),class_="btn")}
87 ${h.submit('save',_('Save'),class_="btn")}
88 </div>
88 </div>
89
89
90 </div>
90 </div>
91 ${h.end_form()}
91 ${h.end_form()}
92 </div>
92 </div>
93 </div>
93 </div>
94 </div>
94 </div>
95 </div>
95 </div>
96 </div>
96 </div>
97
97
98 </div>
98 </div>
99 </div>
99 </div>
100
100
101 ## TODO: Ugly hack to get ldap select elements to work.
101 ## TODO: Ugly hack to get ldap select elements to work.
102 ## Find a solution to integrate this nicely.
102 ## Find a solution to integrate this nicely.
103 <script>
103 <script>
104 $(document).ready(function() {
104 $(document).ready(function() {
105 var select2Options = {
105 var select2Options = {
106 containerCssClass: 'drop-menu',
106 containerCssClass: 'drop-menu',
107 dropdownCssClass: 'drop-menu-dropdown',
107 dropdownCssClass: 'drop-menu-dropdown',
108 dropdownAutoWidth: true,
108 dropdownAutoWidth: true,
109 minimumResultsForSearch: -1
109 minimumResultsForSearch: -1
110 };
110 };
111 $("#tls_kind").select2(select2Options);
111 $("#tls_kind").select2(select2Options);
112 $("#tls_reqcert").select2(select2Options);
112 $("#tls_reqcert").select2(select2Options);
113 $("#search_scope").select2(select2Options);
113 $("#search_scope").select2(select2Options);
114 $("#group_extraction_type").select2(select2Options);
114 });
115 });
115 </script>
116 </script>
116 </%def>
117 </%def>
General Comments 0
You need to be logged in to leave comments. Login now