##// END OF EJS Templates
authn: Fix form validation of authentication plugins....
johbo -
r102:1a9dcffb default
parent child Browse files
Show More
@@ -1,607 +1,602 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 logging
26 26 import time
27 27 import traceback
28 28
29 29 from pyramid.threadlocal import get_current_registry
30 30 from sqlalchemy.ext.hybrid import hybrid_property
31 31
32 32 from rhodecode.authentication.interface import IAuthnPluginRegistry
33 33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 34 from rhodecode.lib import caches
35 35 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
36 36 from rhodecode.lib.utils2 import md5_safe, safe_int
37 37 from rhodecode.lib.utils2 import safe_str
38 38 from rhodecode.model.db import User
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.settings import SettingsModel
41 41 from rhodecode.model.user import UserModel
42 42 from rhodecode.model.user_group import UserGroupModel
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 # auth types that authenticate() function can receive
48 48 VCS_TYPE = 'vcs'
49 49 HTTP_TYPE = 'http'
50 50
51 51
52 52 class LazyFormencode(object):
53 53 def __init__(self, formencode_obj, *args, **kwargs):
54 54 self.formencode_obj = formencode_obj
55 55 self.args = args
56 56 self.kwargs = kwargs
57 57
58 58 def __call__(self, *args, **kwargs):
59 59 from inspect import isfunction
60 60 formencode_obj = self.formencode_obj
61 61 if isfunction(formencode_obj):
62 62 # case we wrap validators into functions
63 63 formencode_obj = self.formencode_obj(*args, **kwargs)
64 64 return formencode_obj(*self.args, **self.kwargs)
65 65
66 66
67 67 class RhodeCodeAuthPluginBase(object):
68 68 # cache the authentication request for N amount of seconds. Some kind
69 69 # of authentication methods are very heavy and it's very efficient to cache
70 70 # the result of a call. If it's set to None (default) cache is off
71 71 AUTH_CACHE_TTL = None
72 72 AUTH_CACHE = {}
73 73
74 74 auth_func_attrs = {
75 75 "username": "unique username",
76 76 "firstname": "first name",
77 77 "lastname": "last name",
78 78 "email": "email address",
79 79 "groups": '["list", "of", "groups"]',
80 80 "extern_name": "name in external source of record",
81 81 "extern_type": "type of external source of record",
82 82 "admin": 'True|False defines if user should be RhodeCode super admin',
83 83 "active":
84 84 'True|False defines active state of user internally for RhodeCode',
85 85 "active_from_extern":
86 86 "True|False\None, active state from the external auth, "
87 87 "None means use definition from RhodeCode extern_type active value"
88 88 }
89 89 # set on authenticate() method and via set_auth_type func.
90 90 auth_type = None
91 91
92 92 # List of setting names to store encrypted. Plugins may override this list
93 93 # to store settings encrypted.
94 94 _settings_encrypted = []
95 95
96 96 # Mapping of python to DB settings model types. Plugins may override or
97 97 # extend this mapping.
98 98 _settings_type_map = {
99 99 str: 'str',
100 100 int: 'int',
101 101 unicode: 'unicode',
102 102 bool: 'bool',
103 103 list: 'list',
104 104 }
105 105
106 106 def __init__(self, plugin_id):
107 107 self._plugin_id = plugin_id
108 108
109 109 def _get_setting_full_name(self, name):
110 110 """
111 111 Return the full setting name used for storing values in the database.
112 112 """
113 113 # TODO: johbo: Using the name here is problematic. It would be good to
114 114 # introduce either new models in the database to hold Plugin and
115 115 # PluginSetting or to use the plugin id here.
116 116 return 'auth_{}_{}'.format(self.name, name)
117 117
118 118 def _get_setting_type(self, name, value):
119 119 """
120 120 Get the type as used by the SettingsModel accordingly to type of passed
121 121 value. Optionally the suffix `.encrypted` is appended to instruct
122 122 SettingsModel to store it encrypted.
123 123 """
124 124 type_ = self._settings_type_map.get(type(value), 'unicode')
125 125 if name in self._settings_encrypted:
126 126 type_ = '{}.encrypted'.format(type_)
127 127 return type_
128 128
129 129 def is_enabled(self):
130 130 """
131 131 Returns true if this plugin is enabled. An enabled plugin can be
132 132 configured in the admin interface but it is not consulted during
133 133 authentication.
134 134 """
135 135 auth_plugins = SettingsModel().get_auth_plugins()
136 136 return self.get_id() in auth_plugins
137 137
138 138 def is_active(self):
139 139 """
140 140 Returns true if the plugin is activated. An activated plugin is
141 141 consulted during authentication, assumed it is also enabled.
142 142 """
143 143 return self.get_setting_by_name('enabled')
144 144
145 145 def get_id(self):
146 146 """
147 147 Returns the plugin id.
148 148 """
149 149 return self._plugin_id
150 150
151 151 def get_display_name(self):
152 152 """
153 153 Returns a translation string for displaying purposes.
154 154 """
155 155 raise NotImplementedError('Not implemented in base class')
156 156
157 157 def get_settings_schema(self):
158 158 """
159 159 Returns a colander schema, representing the plugin settings.
160 160 """
161 161 return AuthnPluginSettingsSchemaBase()
162 162
163 163 def get_setting_by_name(self, name):
164 164 """
165 165 Returns a plugin setting by name.
166 166 """
167 167 full_name = self._get_setting_full_name(name)
168 168 db_setting = SettingsModel().get_setting_by_name(full_name)
169 169 return db_setting.app_settings_value if db_setting else None
170 170
171 171 def create_or_update_setting(self, name, value):
172 172 """
173 173 Create or update a setting for this plugin in the persistent storage.
174 174 """
175 175 full_name = self._get_setting_full_name(name)
176 176 type_ = self._get_setting_type(name, value)
177 177 db_setting = SettingsModel().create_or_update_setting(
178 178 full_name, value, type_)
179 179 return db_setting.app_settings_value
180 180
181 181 def get_settings(self):
182 182 """
183 183 Returns the plugin settings as dictionary.
184 184 """
185 185 settings = {}
186 186 for node in self.get_settings_schema():
187 187 settings[node.name] = self.get_setting_by_name(node.name)
188 188 return settings
189 189
190 190 @property
191 191 def validators(self):
192 192 """
193 193 Exposes RhodeCode validators modules
194 194 """
195 195 # this is a hack to overcome issues with pylons threadlocals and
196 196 # translator object _() not beein registered properly.
197 197 class LazyCaller(object):
198 198 def __init__(self, name):
199 199 self.validator_name = name
200 200
201 201 def __call__(self, *args, **kwargs):
202 202 from rhodecode.model import validators as v
203 203 obj = getattr(v, self.validator_name)
204 204 # log.debug('Initializing lazy formencode object: %s', obj)
205 205 return LazyFormencode(obj, *args, **kwargs)
206 206
207 207 class ProxyGet(object):
208 208 def __getattribute__(self, name):
209 209 return LazyCaller(name)
210 210
211 211 return ProxyGet()
212 212
213 213 @hybrid_property
214 214 def name(self):
215 215 """
216 216 Returns the name of this authentication plugin.
217 217
218 218 :returns: string
219 219 """
220 220 raise NotImplementedError("Not implemented in base class")
221 221
222 222 @hybrid_property
223 223 def is_container_auth(self):
224 224 """
225 225 Returns bool if this module uses container auth.
226 226
227 227 This property will trigger an automatic call to authenticate on
228 228 a visit to the website or during a push/pull.
229 229
230 230 :returns: bool
231 231 """
232 232 return False
233 233
234 234 @hybrid_property
235 235 def allows_creating_users(self):
236 236 """
237 237 Defines if Plugin allows users to be created on-the-fly when
238 238 authentication is called. Controls how external plugins should behave
239 239 in terms if they are allowed to create new users, or not. Base plugins
240 240 should not be allowed to, but External ones should be !
241 241
242 242 :return: bool
243 243 """
244 244 return False
245 245
246 246 def set_auth_type(self, auth_type):
247 247 self.auth_type = auth_type
248 248
249 249 def allows_authentication_from(
250 250 self, user, allows_non_existing_user=True,
251 251 allowed_auth_plugins=None, allowed_auth_sources=None):
252 252 """
253 253 Checks if this authentication module should accept a request for
254 254 the current user.
255 255
256 256 :param user: user object fetched using plugin's get_user() method.
257 257 :param allows_non_existing_user: if True, don't allow the
258 258 user to be empty, meaning not existing in our database
259 259 :param allowed_auth_plugins: if provided, users extern_type will be
260 260 checked against a list of provided extern types, which are plugin
261 261 auth_names in the end
262 262 :param allowed_auth_sources: authentication type allowed,
263 263 `http` or `vcs` default is both.
264 264 defines if plugin will accept only http authentication vcs
265 265 authentication(git/hg) or both
266 266 :returns: boolean
267 267 """
268 268 if not user and not allows_non_existing_user:
269 269 log.debug('User is empty but plugin does not allow empty users,'
270 270 'not allowed to authenticate')
271 271 return False
272 272
273 273 expected_auth_plugins = allowed_auth_plugins or [self.name]
274 274 if user and (user.extern_type and
275 275 user.extern_type not in expected_auth_plugins):
276 276 log.debug(
277 277 'User `%s` is bound to `%s` auth type. Plugin allows only '
278 278 '%s, skipping', user, user.extern_type, expected_auth_plugins)
279 279
280 280 return False
281 281
282 282 # by default accept both
283 283 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
284 284 if self.auth_type not in expected_auth_from:
285 285 log.debug('Current auth source is %s but plugin only allows %s',
286 286 self.auth_type, expected_auth_from)
287 287 return False
288 288
289 289 return True
290 290
291 291 def get_user(self, username=None, **kwargs):
292 292 """
293 293 Helper method for user fetching in plugins, by default it's using
294 294 simple fetch by username, but this method can be custimized in plugins
295 295 eg. container auth plugin to fetch user by environ params
296 296
297 297 :param username: username if given to fetch from database
298 298 :param kwargs: extra arguments needed for user fetching.
299 299 """
300 300 user = None
301 301 log.debug(
302 302 'Trying to fetch user `%s` from RhodeCode database', username)
303 303 if username:
304 304 user = User.get_by_username(username)
305 305 if not user:
306 306 log.debug('User not found, fallback to fetch user in '
307 307 'case insensitive mode')
308 308 user = User.get_by_username(username, case_insensitive=True)
309 309 else:
310 310 log.debug('provided username:`%s` is empty skipping...', username)
311 311 if not user:
312 312 log.debug('User `%s` not found in database', username)
313 313 return user
314 314
315 315 def user_activation_state(self):
316 316 """
317 317 Defines user activation state when creating new users
318 318
319 319 :returns: boolean
320 320 """
321 321 raise NotImplementedError("Not implemented in base class")
322 322
323 323 def auth(self, userobj, username, passwd, settings, **kwargs):
324 324 """
325 325 Given a user object (which may be null), username, a plaintext
326 326 password, and a settings object (containing all the keys needed as
327 327 listed in settings()), authenticate this user's login attempt.
328 328
329 329 Return None on failure. On success, return a dictionary of the form:
330 330
331 331 see: RhodeCodeAuthPluginBase.auth_func_attrs
332 332 This is later validated for correctness
333 333 """
334 334 raise NotImplementedError("not implemented in base class")
335 335
336 336 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
337 337 """
338 338 Wrapper to call self.auth() that validates call on it
339 339
340 340 :param userobj: userobj
341 341 :param username: username
342 342 :param passwd: plaintext password
343 343 :param settings: plugin settings
344 344 """
345 345 auth = self.auth(userobj, username, passwd, settings, **kwargs)
346 346 if auth:
347 347 # check if hash should be migrated ?
348 348 new_hash = auth.get('_hash_migrate')
349 349 if new_hash:
350 350 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
351 351 return self._validate_auth_return(auth)
352 352 return auth
353 353
354 354 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
355 355 new_hash_cypher = _RhodeCodeCryptoBCrypt()
356 356 # extra checks, so make sure new hash is correct.
357 357 password_encoded = safe_str(password)
358 358 if new_hash and new_hash_cypher.hash_check(
359 359 password_encoded, new_hash):
360 360 cur_user = User.get_by_username(username)
361 361 cur_user.password = new_hash
362 362 Session().add(cur_user)
363 363 Session().flush()
364 364 log.info('Migrated user %s hash to bcrypt', cur_user)
365 365
366 366 def _validate_auth_return(self, ret):
367 367 if not isinstance(ret, dict):
368 368 raise Exception('returned value from auth must be a dict')
369 369 for k in self.auth_func_attrs:
370 370 if k not in ret:
371 371 raise Exception('Missing %s attribute from returned data' % k)
372 372 return ret
373 373
374 374
375 375 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
376 376
377 377 @hybrid_property
378 378 def allows_creating_users(self):
379 379 return True
380 380
381 381 def use_fake_password(self):
382 382 """
383 383 Return a boolean that indicates whether or not we should set the user's
384 384 password to a random value when it is authenticated by this plugin.
385 385 If your plugin provides authentication, then you will generally
386 386 want this.
387 387
388 388 :returns: boolean
389 389 """
390 390 raise NotImplementedError("Not implemented in base class")
391 391
392 392 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
393 393 # at this point _authenticate calls plugin's `auth()` function
394 394 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
395 395 userobj, username, passwd, settings, **kwargs)
396 396 if auth:
397 397 # maybe plugin will clean the username ?
398 398 # we should use the return value
399 399 username = auth['username']
400 400
401 401 # if external source tells us that user is not active, we should
402 402 # skip rest of the process. This can prevent from creating users in
403 403 # RhodeCode when using external authentication, but if it's
404 404 # inactive user we shouldn't create that user anyway
405 405 if auth['active_from_extern'] is False:
406 406 log.warning(
407 407 "User %s authenticated against %s, but is inactive",
408 408 username, self.__module__)
409 409 return None
410 410
411 411 cur_user = User.get_by_username(username, case_insensitive=True)
412 412 is_user_existing = cur_user is not None
413 413
414 414 if is_user_existing:
415 415 log.debug('Syncing user `%s` from '
416 416 '`%s` plugin', username, self.name)
417 417 else:
418 418 log.debug('Creating non existing user `%s` from '
419 419 '`%s` plugin', username, self.name)
420 420
421 421 if self.allows_creating_users:
422 422 log.debug('Plugin `%s` allows to '
423 423 'create new users', self.name)
424 424 else:
425 425 log.debug('Plugin `%s` does not allow to '
426 426 'create new users', self.name)
427 427
428 428 user_parameters = {
429 429 'username': username,
430 430 'email': auth["email"],
431 431 'firstname': auth["firstname"],
432 432 'lastname': auth["lastname"],
433 433 'active': auth["active"],
434 434 'admin': auth["admin"],
435 435 'extern_name': auth["extern_name"],
436 436 'extern_type': self.name,
437 437 'plugin': self,
438 438 'allow_to_create_user': self.allows_creating_users,
439 439 }
440 440
441 441 if not is_user_existing:
442 442 if self.use_fake_password():
443 443 # Randomize the PW because we don't need it, but don't want
444 444 # them blank either
445 445 passwd = PasswordGenerator().gen_password(length=16)
446 446 user_parameters['password'] = passwd
447 447 else:
448 448 # Since the password is required by create_or_update method of
449 449 # UserModel, we need to set it explicitly.
450 450 # The create_or_update method is smart and recognises the
451 451 # password hashes as well.
452 452 user_parameters['password'] = cur_user.password
453 453
454 454 # we either create or update users, we also pass the flag
455 455 # that controls if this method can actually do that.
456 456 # raises NotAllowedToCreateUserError if it cannot, and we try to.
457 457 user = UserModel().create_or_update(**user_parameters)
458 458 Session().flush()
459 459 # enforce user is just in given groups, all of them has to be ones
460 460 # created from plugins. We store this info in _group_data JSON
461 461 # field
462 462 try:
463 463 groups = auth['groups'] or []
464 464 UserGroupModel().enforce_groups(user, groups, self.name)
465 465 except Exception:
466 466 # for any reason group syncing fails, we should
467 467 # proceed with login
468 468 log.error(traceback.format_exc())
469 469 Session().commit()
470 470 return auth
471 471
472 472
473 473 def loadplugin(plugin_id):
474 474 """
475 475 Loads and returns an instantiated authentication plugin.
476 476 Returns the RhodeCodeAuthPluginBase subclass on success,
477 raises exceptions on failure.
478
479 raises:
480 KeyError -- if no plugin available with given name
481 TypeError -- if the RhodeCodeAuthPlugin is not a subclass of
482 ours RhodeCodeAuthPluginBase
477 or None on failure.
483 478 """
484 479 # TODO: Disusing pyramids thread locals to retrieve the registry.
485 480 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
486 481 plugin = authn_registry.get_plugin(plugin_id)
487 482 if plugin is None:
488 483 log.error('Authentication plugin not found: "%s"', plugin_id)
489 484 return plugin
490 485
491 486
492 487 def get_auth_cache_manager(custom_ttl=None):
493 488 return caches.get_cache_manager(
494 489 'auth_plugins', 'rhodecode.authentication', custom_ttl)
495 490
496 491
497 492 def authenticate(username, password, environ=None, auth_type=None,
498 493 skip_missing=False):
499 494 """
500 495 Authentication function used for access control,
501 496 It tries to authenticate based on enabled authentication modules.
502 497
503 498 :param username: username can be empty for container auth
504 499 :param password: password can be empty for container auth
505 500 :param environ: environ headers passed for container auth
506 501 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
507 502 :param skip_missing: ignores plugins that are in db but not in environment
508 503 :returns: None if auth failed, plugin_user dict if auth is correct
509 504 """
510 505 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
511 506 raise ValueError('auth type must be on of http, vcs got "%s" instead'
512 507 % auth_type)
513 508 container_only = environ and not (username and password)
514 509
515 510 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
516 511 for plugin in authn_registry.get_plugins_for_authentication():
517 512 plugin.set_auth_type(auth_type)
518 513 user = plugin.get_user(username)
519 514 display_user = user.username if user else username
520 515
521 516 if container_only and not plugin.is_container_auth:
522 517 log.debug('Auth type is for container only and plugin `%s` is not '
523 518 'container plugin, skipping...', plugin.get_id())
524 519 continue
525 520
526 521 # load plugin settings from RhodeCode database
527 522 plugin_settings = plugin.get_settings()
528 523 log.debug('Plugin settings:%s', plugin_settings)
529 524
530 525 log.debug('Trying authentication using ** %s **', plugin.get_id())
531 526 # use plugin's method of user extraction.
532 527 user = plugin.get_user(username, environ=environ,
533 528 settings=plugin_settings)
534 529 display_user = user.username if user else username
535 530 log.debug(
536 531 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
537 532
538 533 if not plugin.allows_authentication_from(user):
539 534 log.debug('Plugin %s does not accept user `%s` for authentication',
540 535 plugin.get_id(), display_user)
541 536 continue
542 537 else:
543 538 log.debug('Plugin %s accepted user `%s` for authentication',
544 539 plugin.get_id(), display_user)
545 540
546 541 log.info('Authenticating user `%s` using %s plugin',
547 542 display_user, plugin.get_id())
548 543
549 544 _cache_ttl = 0
550 545
551 546 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
552 547 # plugin cache set inside is more important than the settings value
553 548 _cache_ttl = plugin.AUTH_CACHE_TTL
554 549 elif plugin_settings.get('auth_cache_ttl'):
555 550 _cache_ttl = safe_int(plugin_settings.get('auth_cache_ttl'), 0)
556 551
557 552 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
558 553
559 554 # get instance of cache manager configured for a namespace
560 555 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
561 556
562 557 log.debug('Cache for plugin `%s` active: %s', plugin.get_id(),
563 558 plugin_cache_active)
564 559
565 560 # for environ based password can be empty, but then the validation is
566 561 # on the server that fills in the env data needed for authentication
567 562 _password_hash = md5_safe(plugin.name + username + (password or ''))
568 563
569 564 # _authenticate is a wrapper for .auth() method of plugin.
570 565 # it checks if .auth() sends proper data.
571 566 # For RhodeCodeExternalAuthPlugin it also maps users to
572 567 # Database and maps the attributes returned from .auth()
573 568 # to RhodeCode database. If this function returns data
574 569 # then auth is correct.
575 570 start = time.time()
576 571 log.debug('Running plugin `%s` _authenticate method',
577 572 plugin.get_id())
578 573
579 574 def auth_func():
580 575 """
581 576 This function is used internally in Cache of Beaker to calculate
582 577 Results
583 578 """
584 579 return plugin._authenticate(
585 580 user, username, password, plugin_settings,
586 581 environ=environ or {})
587 582
588 583 if plugin_cache_active:
589 584 plugin_user = cache_manager.get(
590 585 _password_hash, createfunc=auth_func)
591 586 else:
592 587 plugin_user = auth_func()
593 588
594 589 auth_time = time.time() - start
595 590 log.debug('Authentication for plugin `%s` completed in %.3fs, '
596 591 'expiration time of fetched cache %.1fs.',
597 592 plugin.get_id(), auth_time, _cache_ttl)
598 593
599 594 log.debug('PLUGIN USER DATA: %s', plugin_user)
600 595
601 596 if plugin_user:
602 597 log.debug('Plugin returned proper authentication data')
603 598 return plugin_user
604 599 # we failed to Auth because .auth() method didn't return proper user
605 600 log.debug("User `%s` failed to authenticate against %s",
606 601 display_user, plugin.get_id())
607 602 return None
@@ -1,1075 +1,1076 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Set of generic validators
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 from collections import defaultdict
29 29
30 30 import formencode
31 31 import ipaddress
32 32 from formencode.validators import (
33 33 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
34 34 NotEmpty, IPAddress, CIDR, String, FancyValidator
35 35 )
36 36 from pylons.i18n.translation import _
37 37 from sqlalchemy.sql.expression import true
38 38 from sqlalchemy.util import OrderedSet
39 39 from webhelpers.pylonslib.secure_form import authentication_token
40 40
41 41 from rhodecode.config.routing import ADMIN_PREFIX
42 42 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
43 43 from rhodecode.lib.exceptions import LdapImportError
44 44 from rhodecode.lib.utils import repo_name_slug, make_db_config
45 45 from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5
46 46 from rhodecode.lib.vcs.backends.git.repository import GitRepository
47 47 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
48 48 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
49 49 from rhodecode.model.db import (
50 50 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
51 51 from rhodecode.model.settings import VcsSettingsModel
52 52
53 53 # silence warnings and pylint
54 54 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
55 55 NotEmpty, IPAddress, CIDR, String, FancyValidator
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class _Missing(object):
61 61 pass
62 62
63 63 Missing = _Missing()
64 64
65 65
66 66 class StateObj(object):
67 67 """
68 68 this is needed to translate the messages using _() in validators
69 69 """
70 70 _ = staticmethod(_)
71 71
72 72
73 73 def M(self, key, state=None, **kwargs):
74 74 """
75 75 returns string from self.message based on given key,
76 76 passed kw params are used to substitute %(named)s params inside
77 77 translated strings
78 78
79 79 :param msg:
80 80 :param state:
81 81 """
82 82 if state is None:
83 83 state = StateObj()
84 84 else:
85 85 state._ = staticmethod(_)
86 86 # inject validator into state object
87 87 return self.message(key, state, **kwargs)
88 88
89 89
90 90 def UniqueList(convert=None):
91 91 class _UniqueList(formencode.FancyValidator):
92 92 """
93 93 Unique List !
94 94 """
95 95 messages = {
96 96 'empty': _(u'Value cannot be an empty list'),
97 97 'missing_value': _(u'Value cannot be an empty list'),
98 98 }
99 99
100 100 def _to_python(self, value, state):
101 101 ret_val = []
102 102
103 103 def make_unique(value):
104 104 seen = []
105 105 return [c for c in value if not (c in seen or seen.append(c))]
106 106
107 107 if isinstance(value, list):
108 108 ret_val = make_unique(value)
109 109 elif isinstance(value, set):
110 110 ret_val = make_unique(list(value))
111 111 elif isinstance(value, tuple):
112 112 ret_val = make_unique(list(value))
113 113 elif value is None:
114 114 ret_val = []
115 115 else:
116 116 ret_val = [value]
117 117
118 118 if convert:
119 119 ret_val = map(convert, ret_val)
120 120 return ret_val
121 121
122 122 def empty_value(self, value):
123 123 return []
124 124
125 125 return _UniqueList
126 126
127 127
128 128 def UniqueListFromString():
129 129 class _UniqueListFromString(UniqueList()):
130 130 def _to_python(self, value, state):
131 131 if isinstance(value, basestring):
132 132 value = aslist(value, ',')
133 133 return super(_UniqueListFromString, self)._to_python(value, state)
134 134 return _UniqueListFromString
135 135
136 136
137 137 def ValidSvnPattern(section, repo_name=None):
138 138 class _validator(formencode.validators.FancyValidator):
139 139 messages = {
140 140 'pattern_exists': _(u'Pattern already exists'),
141 141 }
142 142
143 143 def validate_python(self, value, state):
144 144 if not value:
145 145 return
146 146 model = VcsSettingsModel(repo=repo_name)
147 147 ui_settings = model.get_svn_patterns(section=section)
148 148 for entry in ui_settings:
149 149 if value == entry.value:
150 150 msg = M(self, 'pattern_exists', state)
151 151 raise formencode.Invalid(msg, value, state)
152 152 return _validator
153 153
154 154
155 155 def ValidUsername(edit=False, old_data={}):
156 156 class _validator(formencode.validators.FancyValidator):
157 157 messages = {
158 158 'username_exists': _(u'Username "%(username)s" already exists'),
159 159 'system_invalid_username':
160 160 _(u'Username "%(username)s" is forbidden'),
161 161 'invalid_username':
162 162 _(u'Username may only contain alphanumeric characters '
163 163 u'underscores, periods or dashes and must begin with '
164 164 u'alphanumeric character or underscore')
165 165 }
166 166
167 167 def validate_python(self, value, state):
168 168 if value in ['default', 'new_user']:
169 169 msg = M(self, 'system_invalid_username', state, username=value)
170 170 raise formencode.Invalid(msg, value, state)
171 171 # check if user is unique
172 172 old_un = None
173 173 if edit:
174 174 old_un = User.get(old_data.get('user_id')).username
175 175
176 176 if old_un != value or not edit:
177 177 if User.get_by_username(value, case_insensitive=True):
178 178 msg = M(self, 'username_exists', state, username=value)
179 179 raise formencode.Invalid(msg, value, state)
180 180
181 181 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
182 182 is None):
183 183 msg = M(self, 'invalid_username', state)
184 184 raise formencode.Invalid(msg, value, state)
185 185 return _validator
186 186
187 187
188 188 def ValidRegex(msg=None):
189 189 class _validator(formencode.validators.Regex):
190 190 messages = {'invalid': msg or _(u'The input is not valid')}
191 191 return _validator
192 192
193 193
194 194 def ValidRepoUser():
195 195 class _validator(formencode.validators.FancyValidator):
196 196 messages = {
197 197 'invalid_username': _(u'Username %(username)s is not valid')
198 198 }
199 199
200 200 def validate_python(self, value, state):
201 201 try:
202 202 User.query().filter(User.active == true())\
203 203 .filter(User.username == value).one()
204 204 except Exception:
205 205 msg = M(self, 'invalid_username', state, username=value)
206 206 raise formencode.Invalid(
207 207 msg, value, state, error_dict={'username': msg}
208 208 )
209 209
210 210 return _validator
211 211
212 212
213 213 def ValidUserGroup(edit=False, old_data={}):
214 214 class _validator(formencode.validators.FancyValidator):
215 215 messages = {
216 216 'invalid_group': _(u'Invalid user group name'),
217 217 'group_exist': _(u'User group "%(usergroup)s" already exists'),
218 218 'invalid_usergroup_name':
219 219 _(u'user group name may only contain alphanumeric '
220 220 u'characters underscores, periods or dashes and must begin '
221 221 u'with alphanumeric character')
222 222 }
223 223
224 224 def validate_python(self, value, state):
225 225 if value in ['default']:
226 226 msg = M(self, 'invalid_group', state)
227 227 raise formencode.Invalid(
228 228 msg, value, state, error_dict={'users_group_name': msg}
229 229 )
230 230 # check if group is unique
231 231 old_ugname = None
232 232 if edit:
233 233 old_id = old_data.get('users_group_id')
234 234 old_ugname = UserGroup.get(old_id).users_group_name
235 235
236 236 if old_ugname != value or not edit:
237 237 is_existing_group = UserGroup.get_by_group_name(
238 238 value, case_insensitive=True)
239 239 if is_existing_group:
240 240 msg = M(self, 'group_exist', state, usergroup=value)
241 241 raise formencode.Invalid(
242 242 msg, value, state, error_dict={'users_group_name': msg}
243 243 )
244 244
245 245 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
246 246 msg = M(self, 'invalid_usergroup_name', state)
247 247 raise formencode.Invalid(
248 248 msg, value, state, error_dict={'users_group_name': msg}
249 249 )
250 250
251 251 return _validator
252 252
253 253
254 254 def ValidRepoGroup(edit=False, old_data={}, can_create_in_root=False):
255 255 class _validator(formencode.validators.FancyValidator):
256 256 messages = {
257 257 'group_parent_id': _(u'Cannot assign this group as parent'),
258 258 'group_exists': _(u'Group "%(group_name)s" already exists'),
259 259 'repo_exists': _(u'Repository with name "%(group_name)s" '
260 260 u'already exists'),
261 261 'permission_denied': _(u"no permission to store repository group"
262 262 u"in this location"),
263 263 'permission_denied_root': _(
264 264 u"no permission to store repository group "
265 265 u"in root location")
266 266 }
267 267
268 268 def _to_python(self, value, state):
269 269 group_name = repo_name_slug(value.get('group_name', ''))
270 270 group_parent_id = safe_int(value.get('group_parent_id'))
271 271 gr = RepoGroup.get(group_parent_id)
272 272 if gr:
273 273 parent_group_path = gr.full_path
274 274 # value needs to be aware of group name in order to check
275 275 # db key This is an actual just the name to store in the
276 276 # database
277 277 group_name_full = (
278 278 parent_group_path + RepoGroup.url_sep() + group_name)
279 279 else:
280 280 group_name_full = group_name
281 281
282 282 value['group_name'] = group_name
283 283 value['group_name_full'] = group_name_full
284 284 value['group_parent_id'] = group_parent_id
285 285 return value
286 286
287 287 def validate_python(self, value, state):
288 288
289 289 old_group_name = None
290 290 group_name = value.get('group_name')
291 291 group_name_full = value.get('group_name_full')
292 292 group_parent_id = safe_int(value.get('group_parent_id'))
293 293 if group_parent_id == -1:
294 294 group_parent_id = None
295 295
296 296 group_obj = RepoGroup.get(old_data.get('group_id'))
297 297 parent_group_changed = False
298 298 if edit:
299 299 old_group_name = group_obj.group_name
300 300 old_group_parent_id = group_obj.group_parent_id
301 301
302 302 if group_parent_id != old_group_parent_id:
303 303 parent_group_changed = True
304 304
305 305 # TODO: mikhail: the following if statement is not reached
306 306 # since group_parent_id's OneOf validation fails before.
307 307 # Can be removed.
308 308
309 309 # check against setting a parent of self
310 310 parent_of_self = (
311 311 old_data['group_id'] == group_parent_id
312 312 if group_parent_id else False
313 313 )
314 314 if parent_of_self:
315 315 msg = M(self, 'group_parent_id', state)
316 316 raise formencode.Invalid(
317 317 msg, value, state, error_dict={'group_parent_id': msg}
318 318 )
319 319
320 320 # group we're moving current group inside
321 321 child_group = None
322 322 if group_parent_id:
323 323 child_group = RepoGroup.query().filter(
324 324 RepoGroup.group_id == group_parent_id).scalar()
325 325
326 326 # do a special check that we cannot move a group to one of
327 327 # it's children
328 328 if edit and child_group:
329 329 parents = [x.group_id for x in child_group.parents]
330 330 move_to_children = old_data['group_id'] in parents
331 331 if move_to_children:
332 332 msg = M(self, 'group_parent_id', state)
333 333 raise formencode.Invalid(
334 334 msg, value, state, error_dict={'group_parent_id': msg})
335 335
336 336 # Check if we have permission to store in the parent.
337 337 # Only check if the parent group changed.
338 338 if parent_group_changed:
339 339 if child_group is None:
340 340 if not can_create_in_root:
341 341 msg = M(self, 'permission_denied_root', state)
342 342 raise formencode.Invalid(
343 343 msg, value, state,
344 344 error_dict={'group_parent_id': msg})
345 345 else:
346 346 valid = HasRepoGroupPermissionAny('group.admin')
347 347 forbidden = not valid(
348 348 child_group.group_name, 'can create group validator')
349 349 if forbidden:
350 350 msg = M(self, 'permission_denied', state)
351 351 raise formencode.Invalid(
352 352 msg, value, state,
353 353 error_dict={'group_parent_id': msg})
354 354
355 355 # if we change the name or it's new group, check for existing names
356 356 # or repositories with the same name
357 357 if old_group_name != group_name_full or not edit:
358 358 # check group
359 359 gr = RepoGroup.get_by_group_name(group_name_full)
360 360 if gr:
361 361 msg = M(self, 'group_exists', state, group_name=group_name)
362 362 raise formencode.Invalid(
363 363 msg, value, state, error_dict={'group_name': msg})
364 364
365 365 # check for same repo
366 366 repo = Repository.get_by_repo_name(group_name_full)
367 367 if repo:
368 368 msg = M(self, 'repo_exists', state, group_name=group_name)
369 369 raise formencode.Invalid(
370 370 msg, value, state, error_dict={'group_name': msg})
371 371
372 372 return _validator
373 373
374 374
375 375 def ValidPassword():
376 376 class _validator(formencode.validators.FancyValidator):
377 377 messages = {
378 378 'invalid_password':
379 379 _(u'Invalid characters (non-ascii) in password')
380 380 }
381 381
382 382 def validate_python(self, value, state):
383 383 try:
384 384 (value or '').decode('ascii')
385 385 except UnicodeError:
386 386 msg = M(self, 'invalid_password', state)
387 387 raise formencode.Invalid(msg, value, state,)
388 388 return _validator
389 389
390 390
391 391 def ValidOldPassword(username):
392 392 class _validator(formencode.validators.FancyValidator):
393 393 messages = {
394 394 'invalid_password': _(u'Invalid old password')
395 395 }
396 396
397 397 def validate_python(self, value, state):
398 398 from rhodecode.authentication.base import authenticate, HTTP_TYPE
399 399 if not authenticate(username, value, '', HTTP_TYPE):
400 400 msg = M(self, 'invalid_password', state)
401 401 raise formencode.Invalid(
402 402 msg, value, state, error_dict={'current_password': msg}
403 403 )
404 404 return _validator
405 405
406 406
407 407 def ValidPasswordsMatch(
408 408 passwd='new_password', passwd_confirmation='password_confirmation'):
409 409 class _validator(formencode.validators.FancyValidator):
410 410 messages = {
411 411 'password_mismatch': _(u'Passwords do not match'),
412 412 }
413 413
414 414 def validate_python(self, value, state):
415 415
416 416 pass_val = value.get('password') or value.get(passwd)
417 417 if pass_val != value[passwd_confirmation]:
418 418 msg = M(self, 'password_mismatch', state)
419 419 raise formencode.Invalid(
420 420 msg, value, state,
421 421 error_dict={passwd: msg, passwd_confirmation: msg}
422 422 )
423 423 return _validator
424 424
425 425
426 426 def ValidAuth():
427 427 class _validator(formencode.validators.FancyValidator):
428 428 messages = {
429 429 'invalid_password': _(u'invalid password'),
430 430 'invalid_username': _(u'invalid user name'),
431 431 'disabled_account': _(u'Your account is disabled')
432 432 }
433 433
434 434 def validate_python(self, value, state):
435 435 from rhodecode.authentication.base import authenticate, HTTP_TYPE
436 436
437 437 password = value['password']
438 438 username = value['username']
439 439
440 440 if not authenticate(username, password, '', HTTP_TYPE,
441 441 skip_missing=True):
442 442 user = User.get_by_username(username)
443 443 if user and not user.active:
444 444 log.warning('user %s is disabled', username)
445 445 msg = M(self, 'disabled_account', state)
446 446 raise formencode.Invalid(
447 447 msg, value, state, error_dict={'username': msg}
448 448 )
449 449 else:
450 450 log.warning('user `%s` failed to authenticate', username)
451 451 msg = M(self, 'invalid_username', state)
452 452 msg2 = M(self, 'invalid_password', state)
453 453 raise formencode.Invalid(
454 454 msg, value, state,
455 455 error_dict={'username': msg, 'password': msg2}
456 456 )
457 457 return _validator
458 458
459 459
460 460 def ValidAuthToken():
461 461 class _validator(formencode.validators.FancyValidator):
462 462 messages = {
463 463 'invalid_token': _(u'Token mismatch')
464 464 }
465 465
466 466 def validate_python(self, value, state):
467 467 if value != authentication_token():
468 468 msg = M(self, 'invalid_token', state)
469 469 raise formencode.Invalid(msg, value, state)
470 470 return _validator
471 471
472 472
473 473 def ValidRepoName(edit=False, old_data={}):
474 474 class _validator(formencode.validators.FancyValidator):
475 475 messages = {
476 476 'invalid_repo_name':
477 477 _(u'Repository name %(repo)s is disallowed'),
478 478 # top level
479 479 'repository_exists': _(u'Repository with name %(repo)s '
480 480 u'already exists'),
481 481 'group_exists': _(u'Repository group with name "%(repo)s" '
482 482 u'already exists'),
483 483 # inside a group
484 484 'repository_in_group_exists': _(u'Repository with name %(repo)s '
485 485 u'exists in group "%(group)s"'),
486 486 'group_in_group_exists': _(
487 487 u'Repository group with name "%(repo)s" '
488 488 u'exists in group "%(group)s"'),
489 489 }
490 490
491 491 def _to_python(self, value, state):
492 492 repo_name = repo_name_slug(value.get('repo_name', ''))
493 493 repo_group = value.get('repo_group')
494 494 if repo_group:
495 495 gr = RepoGroup.get(repo_group)
496 496 group_path = gr.full_path
497 497 group_name = gr.group_name
498 498 # value needs to be aware of group name in order to check
499 499 # db key This is an actual just the name to store in the
500 500 # database
501 501 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
502 502 else:
503 503 group_name = group_path = ''
504 504 repo_name_full = repo_name
505 505
506 506 value['repo_name'] = repo_name
507 507 value['repo_name_full'] = repo_name_full
508 508 value['group_path'] = group_path
509 509 value['group_name'] = group_name
510 510 return value
511 511
512 512 def validate_python(self, value, state):
513 513
514 514 repo_name = value.get('repo_name')
515 515 repo_name_full = value.get('repo_name_full')
516 516 group_path = value.get('group_path')
517 517 group_name = value.get('group_name')
518 518
519 519 if repo_name in [ADMIN_PREFIX, '']:
520 520 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
521 521 raise formencode.Invalid(
522 522 msg, value, state, error_dict={'repo_name': msg})
523 523
524 524 rename = old_data.get('repo_name') != repo_name_full
525 525 create = not edit
526 526 if rename or create:
527 527
528 528 if group_path:
529 529 if Repository.get_by_repo_name(repo_name_full):
530 530 msg = M(self, 'repository_in_group_exists', state,
531 531 repo=repo_name, group=group_name)
532 532 raise formencode.Invalid(
533 533 msg, value, state, error_dict={'repo_name': msg})
534 534 if RepoGroup.get_by_group_name(repo_name_full):
535 535 msg = M(self, 'group_in_group_exists', state,
536 536 repo=repo_name, group=group_name)
537 537 raise formencode.Invalid(
538 538 msg, value, state, error_dict={'repo_name': msg})
539 539 else:
540 540 if RepoGroup.get_by_group_name(repo_name_full):
541 541 msg = M(self, 'group_exists', state, repo=repo_name)
542 542 raise formencode.Invalid(
543 543 msg, value, state, error_dict={'repo_name': msg})
544 544
545 545 if Repository.get_by_repo_name(repo_name_full):
546 546 msg = M(
547 547 self, 'repository_exists', state, repo=repo_name)
548 548 raise formencode.Invalid(
549 549 msg, value, state, error_dict={'repo_name': msg})
550 550 return value
551 551 return _validator
552 552
553 553
554 554 def ValidForkName(*args, **kwargs):
555 555 return ValidRepoName(*args, **kwargs)
556 556
557 557
558 558 def SlugifyName():
559 559 class _validator(formencode.validators.FancyValidator):
560 560
561 561 def _to_python(self, value, state):
562 562 return repo_name_slug(value)
563 563
564 564 def validate_python(self, value, state):
565 565 pass
566 566
567 567 return _validator
568 568
569 569
570 570 def ValidCloneUri():
571 571 class InvalidCloneUrl(Exception):
572 572 allowed_prefixes = ()
573 573
574 574 def url_handler(repo_type, url):
575 575 config = make_db_config(clear_session=False)
576 576 if repo_type == 'hg':
577 577 allowed_prefixes = ('http', 'svn+http', 'git+http')
578 578
579 579 if 'http' in url[:4]:
580 580 # initially check if it's at least the proper URL
581 581 # or does it pass basic auth
582 582 MercurialRepository.check_url(url, config)
583 583 elif 'svn+http' in url[:8]: # svn->hg import
584 584 SubversionRepository.check_url(url, config)
585 585 elif 'git+http' in url[:8]: # git->hg import
586 586 raise NotImplementedError()
587 587 else:
588 588 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
589 589 'Allowed url must start with one of %s'
590 590 % (url, ','.join(allowed_prefixes)))
591 591 exc.allowed_prefixes = allowed_prefixes
592 592 raise exc
593 593
594 594 elif repo_type == 'git':
595 595 allowed_prefixes = ('http', 'svn+http', 'hg+http')
596 596 if 'http' in url[:4]:
597 597 # initially check if it's at least the proper URL
598 598 # or does it pass basic auth
599 599 GitRepository.check_url(url, config)
600 600 elif 'svn+http' in url[:8]: # svn->git import
601 601 raise NotImplementedError()
602 602 elif 'hg+http' in url[:8]: # hg->git import
603 603 raise NotImplementedError()
604 604 else:
605 605 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
606 606 'Allowed url must start with one of %s'
607 607 % (url, ','.join(allowed_prefixes)))
608 608 exc.allowed_prefixes = allowed_prefixes
609 609 raise exc
610 610
611 611 class _validator(formencode.validators.FancyValidator):
612 612 messages = {
613 613 'clone_uri': _(u'invalid clone url for %(rtype)s repository'),
614 614 'invalid_clone_uri': _(
615 615 u'Invalid clone url, provide a valid clone '
616 616 u'url starting with one of %(allowed_prefixes)s')
617 617 }
618 618
619 619 def validate_python(self, value, state):
620 620 repo_type = value.get('repo_type')
621 621 url = value.get('clone_uri')
622 622
623 623 if url:
624 624 try:
625 625 url_handler(repo_type, url)
626 626 except InvalidCloneUrl as e:
627 627 log.warning(e)
628 628 msg = M(self, 'invalid_clone_uri', rtype=repo_type,
629 629 allowed_prefixes=','.join(e.allowed_prefixes))
630 630 raise formencode.Invalid(msg, value, state,
631 631 error_dict={'clone_uri': msg})
632 632 except Exception:
633 633 log.exception('Url validation failed')
634 634 msg = M(self, 'clone_uri', rtype=repo_type)
635 635 raise formencode.Invalid(msg, value, state,
636 636 error_dict={'clone_uri': msg})
637 637 return _validator
638 638
639 639
640 640 def ValidForkType(old_data={}):
641 641 class _validator(formencode.validators.FancyValidator):
642 642 messages = {
643 643 'invalid_fork_type': _(u'Fork have to be the same type as parent')
644 644 }
645 645
646 646 def validate_python(self, value, state):
647 647 if old_data['repo_type'] != value:
648 648 msg = M(self, 'invalid_fork_type', state)
649 649 raise formencode.Invalid(
650 650 msg, value, state, error_dict={'repo_type': msg}
651 651 )
652 652 return _validator
653 653
654 654
655 655 def CanWriteGroup(old_data=None):
656 656 class _validator(formencode.validators.FancyValidator):
657 657 messages = {
658 658 'permission_denied': _(
659 659 u"You do not have the permission "
660 660 u"to create repositories in this group."),
661 661 'permission_denied_root': _(
662 662 u"You do not have the permission to store repositories in "
663 663 u"the root location.")
664 664 }
665 665
666 666 def _to_python(self, value, state):
667 667 # root location
668 668 if value in [-1, "-1"]:
669 669 return None
670 670 return value
671 671
672 672 def validate_python(self, value, state):
673 673 gr = RepoGroup.get(value)
674 674 gr_name = gr.group_name if gr else None # None means ROOT location
675 675 # create repositories with write permission on group is set to true
676 676 create_on_write = HasPermissionAny(
677 677 'hg.create.write_on_repogroup.true')()
678 678 group_admin = HasRepoGroupPermissionAny('group.admin')(
679 679 gr_name, 'can write into group validator')
680 680 group_write = HasRepoGroupPermissionAny('group.write')(
681 681 gr_name, 'can write into group validator')
682 682 forbidden = not (group_admin or (group_write and create_on_write))
683 683 can_create_repos = HasPermissionAny(
684 684 'hg.admin', 'hg.create.repository')
685 685 gid = (old_data['repo_group'].get('group_id')
686 686 if (old_data and 'repo_group' in old_data) else None)
687 687 value_changed = gid != safe_int(value)
688 688 new = not old_data
689 689 # do check if we changed the value, there's a case that someone got
690 690 # revoked write permissions to a repository, he still created, we
691 691 # don't need to check permission if he didn't change the value of
692 692 # groups in form box
693 693 if value_changed or new:
694 694 # parent group need to be existing
695 695 if gr and forbidden:
696 696 msg = M(self, 'permission_denied', state)
697 697 raise formencode.Invalid(
698 698 msg, value, state, error_dict={'repo_type': msg}
699 699 )
700 700 # check if we can write to root location !
701 701 elif gr is None and not can_create_repos():
702 702 msg = M(self, 'permission_denied_root', state)
703 703 raise formencode.Invalid(
704 704 msg, value, state, error_dict={'repo_type': msg}
705 705 )
706 706
707 707 return _validator
708 708
709 709
710 710 def ValidPerms(type_='repo'):
711 711 if type_ == 'repo_group':
712 712 EMPTY_PERM = 'group.none'
713 713 elif type_ == 'repo':
714 714 EMPTY_PERM = 'repository.none'
715 715 elif type_ == 'user_group':
716 716 EMPTY_PERM = 'usergroup.none'
717 717
718 718 class _validator(formencode.validators.FancyValidator):
719 719 messages = {
720 720 'perm_new_member_name':
721 721 _(u'This username or user group name is not valid')
722 722 }
723 723
724 724 def _to_python(self, value, state):
725 725 perm_updates = OrderedSet()
726 726 perm_additions = OrderedSet()
727 727 perm_deletions = OrderedSet()
728 728 # build a list of permission to update/delete and new permission
729 729
730 730 # Read the perm_new_member/perm_del_member attributes and group
731 731 # them by they IDs
732 732 new_perms_group = defaultdict(dict)
733 733 del_perms_group = defaultdict(dict)
734 734 for k, v in value.copy().iteritems():
735 735 if k.startswith('perm_del_member'):
736 736 # delete from org storage so we don't process that later
737 737 del value[k]
738 738 # part is `id`, `type`
739 739 _type, part = k.split('perm_del_member_')
740 740 args = part.split('_')
741 741 if len(args) == 2:
742 742 _key, pos = args
743 743 del_perms_group[pos][_key] = v
744 744 if k.startswith('perm_new_member'):
745 745 # delete from org storage so we don't process that later
746 746 del value[k]
747 747 # part is `id`, `type`, `perm`
748 748 _type, part = k.split('perm_new_member_')
749 749 args = part.split('_')
750 750 if len(args) == 2:
751 751 _key, pos = args
752 752 new_perms_group[pos][_key] = v
753 753
754 754 # store the deletes
755 755 for k in sorted(del_perms_group.keys()):
756 756 perm_dict = del_perms_group[k]
757 757 del_member = perm_dict.get('id')
758 758 del_type = perm_dict.get('type')
759 759 if del_member and del_type:
760 760 perm_deletions.add((del_member, None, del_type))
761 761
762 762 # store additions in order of how they were added in web form
763 763 for k in sorted(new_perms_group.keys()):
764 764 perm_dict = new_perms_group[k]
765 765 new_member = perm_dict.get('id')
766 766 new_type = perm_dict.get('type')
767 767 new_perm = perm_dict.get('perm')
768 768 if new_member and new_perm and new_type:
769 769 perm_additions.add((new_member, new_perm, new_type))
770 770
771 771 # get updates of permissions
772 772 # (read the existing radio button states)
773 773 for k, update_value in value.iteritems():
774 774 if k.startswith('u_perm_') or k.startswith('g_perm_'):
775 775 member = k[7:]
776 776 update_type = {'u': 'user',
777 777 'g': 'users_group'}[k[0]]
778 778 if member == User.DEFAULT_USER:
779 779 if str2bool(value.get('repo_private')):
780 780 # set none for default when updating to
781 781 # private repo protects agains form manipulation
782 782 update_value = EMPTY_PERM
783 783 perm_updates.add((member, update_value, update_type))
784 784 # check the deletes
785 785
786 786 value['perm_additions'] = list(perm_additions)
787 787 value['perm_updates'] = list(perm_updates)
788 788 value['perm_deletions'] = list(perm_deletions)
789 789
790 790 # validate users they exist and they are active !
791 791 for member_id, _perm, member_type in perm_additions:
792 792 try:
793 793 if member_type == 'user':
794 794 self.user_db = User.query()\
795 795 .filter(User.active == true())\
796 796 .filter(User.user_id == member_id).one()
797 797 if member_type == 'users_group':
798 798 self.user_db = UserGroup.query()\
799 799 .filter(UserGroup.users_group_active == true())\
800 800 .filter(UserGroup.users_group_id == member_id)\
801 801 .one()
802 802
803 803 except Exception:
804 804 log.exception('Updated permission failed: org_exc:')
805 805 msg = M(self, 'perm_new_member_type', state)
806 806 raise formencode.Invalid(
807 807 msg, value, state, error_dict={
808 808 'perm_new_member_name': msg}
809 809 )
810 810 return value
811 811 return _validator
812 812
813 813
814 814 def ValidSettings():
815 815 class _validator(formencode.validators.FancyValidator):
816 816 def _to_python(self, value, state):
817 817 # settings form for users that are not admin
818 818 # can't edit certain parameters, it's extra backup if they mangle
819 819 # with forms
820 820
821 821 forbidden_params = [
822 822 'user', 'repo_type', 'repo_enable_locking',
823 823 'repo_enable_downloads', 'repo_enable_statistics'
824 824 ]
825 825
826 826 for param in forbidden_params:
827 827 if param in value:
828 828 del value[param]
829 829 return value
830 830
831 831 def validate_python(self, value, state):
832 832 pass
833 833 return _validator
834 834
835 835
836 836 def ValidPath():
837 837 class _validator(formencode.validators.FancyValidator):
838 838 messages = {
839 839 'invalid_path': _(u'This is not a valid path')
840 840 }
841 841
842 842 def validate_python(self, value, state):
843 843 if not os.path.isdir(value):
844 844 msg = M(self, 'invalid_path', state)
845 845 raise formencode.Invalid(
846 846 msg, value, state, error_dict={'paths_root_path': msg}
847 847 )
848 848 return _validator
849 849
850 850
851 851 def UniqSystemEmail(old_data={}):
852 852 class _validator(formencode.validators.FancyValidator):
853 853 messages = {
854 854 'email_taken': _(u'This e-mail address is already taken')
855 855 }
856 856
857 857 def _to_python(self, value, state):
858 858 return value.lower()
859 859
860 860 def validate_python(self, value, state):
861 861 if (old_data.get('email') or '').lower() != value:
862 862 user = User.get_by_email(value, case_insensitive=True)
863 863 if user:
864 864 msg = M(self, 'email_taken', state)
865 865 raise formencode.Invalid(
866 866 msg, value, state, error_dict={'email': msg}
867 867 )
868 868 return _validator
869 869
870 870
871 871 def ValidSystemEmail():
872 872 class _validator(formencode.validators.FancyValidator):
873 873 messages = {
874 874 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
875 875 }
876 876
877 877 def _to_python(self, value, state):
878 878 return value.lower()
879 879
880 880 def validate_python(self, value, state):
881 881 user = User.get_by_email(value, case_insensitive=True)
882 882 if user is None:
883 883 msg = M(self, 'non_existing_email', state, email=value)
884 884 raise formencode.Invalid(
885 885 msg, value, state, error_dict={'email': msg}
886 886 )
887 887
888 888 return _validator
889 889
890 890
891 891 def NotReviewedRevisions(repo_id):
892 892 class _validator(formencode.validators.FancyValidator):
893 893 messages = {
894 894 'rev_already_reviewed':
895 895 _(u'Revisions %(revs)s are already part of pull request '
896 896 u'or have set status'),
897 897 }
898 898
899 899 def validate_python(self, value, state):
900 900 # check revisions if they are not reviewed, or a part of another
901 901 # pull request
902 902 statuses = ChangesetStatus.query()\
903 903 .filter(ChangesetStatus.revision.in_(value))\
904 904 .filter(ChangesetStatus.repo_id == repo_id)\
905 905 .all()
906 906
907 907 errors = []
908 908 for status in statuses:
909 909 if status.pull_request_id:
910 910 errors.append(['pull_req', status.revision[:12]])
911 911 elif status.status:
912 912 errors.append(['status', status.revision[:12]])
913 913
914 914 if errors:
915 915 revs = ','.join([x[1] for x in errors])
916 916 msg = M(self, 'rev_already_reviewed', state, revs=revs)
917 917 raise formencode.Invalid(
918 918 msg, value, state, error_dict={'revisions': revs})
919 919
920 920 return _validator
921 921
922 922
923 923 def ValidIp():
924 924 class _validator(CIDR):
925 925 messages = {
926 926 'badFormat': _(u'Please enter a valid IPv4 or IpV6 address'),
927 927 'illegalBits': _(
928 928 u'The network size (bits) must be within the range '
929 929 u'of 0-32 (not %(bits)r)'),
930 930 }
931 931
932 932 # we ovveride the default to_python() call
933 933 def to_python(self, value, state):
934 934 v = super(_validator, self).to_python(value, state)
935 935 v = v.strip()
936 936 net = ipaddress.ip_network(address=v, strict=False)
937 937 return str(net)
938 938
939 939 def validate_python(self, value, state):
940 940 try:
941 941 addr = value.strip()
942 942 # this raises an ValueError if address is not IpV4 or IpV6
943 943 ipaddress.ip_network(addr, strict=False)
944 944 except ValueError:
945 945 raise formencode.Invalid(self.message('badFormat', state),
946 946 value, state)
947 947
948 948 return _validator
949 949
950 950
951 951 def FieldKey():
952 952 class _validator(formencode.validators.FancyValidator):
953 953 messages = {
954 954 'badFormat': _(
955 955 u'Key name can only consist of letters, '
956 956 u'underscore, dash or numbers'),
957 957 }
958 958
959 959 def validate_python(self, value, state):
960 960 if not re.match('[a-zA-Z0-9_-]+$', value):
961 961 raise formencode.Invalid(self.message('badFormat', state),
962 962 value, state)
963 963 return _validator
964 964
965 965
966 966 def BasePath():
967 967 class _validator(formencode.validators.FancyValidator):
968 968 messages = {
969 969 'badPath': _(u'Filename cannot be inside a directory'),
970 970 }
971 971
972 972 def _to_python(self, value, state):
973 973 return value
974 974
975 975 def validate_python(self, value, state):
976 976 if value != os.path.basename(value):
977 977 raise formencode.Invalid(self.message('badPath', state),
978 978 value, state)
979 979 return _validator
980 980
981 981
982 982 def ValidAuthPlugins():
983 983 class _validator(formencode.validators.FancyValidator):
984 984 messages = {
985 985 'import_duplicate': _(
986 986 u'Plugins %(loaded)s and %(next_to_load)s '
987 987 u'both export the same name'),
988 988 }
989 989
990 990 def _to_python(self, value, state):
991 991 # filter empty values
992 992 return filter(lambda s: s not in [None, ''], value)
993 993
994 994 def validate_python(self, value, state):
995 995 from rhodecode.authentication.base import loadplugin
996 996 module_list = value
997 997 unique_names = {}
998 try:
999 998 for module in module_list:
1000 999 plugin = loadplugin(module)
1000 if plugin is None:
1001 raise formencode.Invalid(
1002 _("Can't find plugin with id '{}'".format(module)),
1003 value, state)
1001 1004 plugin_name = plugin.name
1002 1005 if plugin_name in unique_names:
1003 1006 msg = M(self, 'import_duplicate', state,
1004 1007 loaded=unique_names[plugin_name],
1005 1008 next_to_load=plugin_name)
1006 1009 raise formencode.Invalid(msg, value, state)
1007 1010 unique_names[plugin_name] = plugin
1008 except (KeyError, AttributeError, TypeError) as e:
1009 raise formencode.Invalid(str(e), value, state)
1010 1011
1011 1012 return _validator
1012 1013
1013 1014
1014 1015 def UniqGistId():
1015 1016 class _validator(formencode.validators.FancyValidator):
1016 1017 messages = {
1017 1018 'gistid_taken': _(u'This gistid is already in use')
1018 1019 }
1019 1020
1020 1021 def _to_python(self, value, state):
1021 1022 return repo_name_slug(value.lower())
1022 1023
1023 1024 def validate_python(self, value, state):
1024 1025 existing = Gist.get_by_access_id(value)
1025 1026 if existing:
1026 1027 msg = M(self, 'gistid_taken', state)
1027 1028 raise formencode.Invalid(
1028 1029 msg, value, state, error_dict={'gistid': msg}
1029 1030 )
1030 1031
1031 1032 return _validator
1032 1033
1033 1034
1034 1035 def ValidPattern():
1035 1036
1036 1037 class _Validator(formencode.validators.FancyValidator):
1037 1038
1038 1039 def _to_python(self, value, state):
1039 1040 patterns = []
1040 1041
1041 1042 prefix = 'new_pattern'
1042 1043 for name, v in value.iteritems():
1043 1044 pattern_name = '_'.join((prefix, 'pattern'))
1044 1045 if name.startswith(pattern_name):
1045 1046 new_item_id = name[len(pattern_name)+1:]
1046 1047
1047 1048 def _field(name):
1048 1049 return '%s_%s_%s' % (prefix, name, new_item_id)
1049 1050
1050 1051 values = {
1051 1052 'issuetracker_pat': value.get(_field('pattern')),
1052 1053 'issuetracker_pat': value.get(_field('pattern')),
1053 1054 'issuetracker_url': value.get(_field('url')),
1054 1055 'issuetracker_pref': value.get(_field('prefix')),
1055 1056 'issuetracker_desc': value.get(_field('description'))
1056 1057 }
1057 1058 new_uid = md5(values['issuetracker_pat'])
1058 1059
1059 1060 has_required_fields = (
1060 1061 values['issuetracker_pat']
1061 1062 and values['issuetracker_url'])
1062 1063
1063 1064 if has_required_fields:
1064 1065 settings = [
1065 1066 ('_'.join((key, new_uid)), values[key], 'unicode')
1066 1067 for key in values]
1067 1068 patterns.append(settings)
1068 1069
1069 1070 value['patterns'] = patterns
1070 1071 delete_patterns = value.get('uid') or []
1071 1072 if not isinstance(delete_patterns, (list, tuple)):
1072 1073 delete_patterns = [delete_patterns]
1073 1074 value['delete_patterns'] = delete_patterns
1074 1075 return value
1075 1076 return _Validator
General Comments 0
You need to be logged in to leave comments. Login now