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