##// END OF EJS Templates
users: add edition of description in admin view for users
marcink -
r4022:8d4c4139 default
parent child Browse files
Show More
@@ -1,760 +1,761 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 logging
22 22 import datetime
23 23 import string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound
29 29 from pyramid.view import view_config
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode import forms
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, \
37 37 HasRepoPermissionAny, HasRepoGroupPermissionAny
38 38 from rhodecode.lib.channelstream import (
39 39 channelstream_request, ChannelstreamException)
40 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 IntegrityError, joinedload,
45 45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 46 PullRequest, UserBookmark, RepoGroup)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.pull_request import PullRequestModel
49 49 from rhodecode.model.scm import RepoList
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.repo import RepoModel
52 52 from rhodecode.model.user_group import UserGroupModel
53 53 from rhodecode.model.validation_schema.schemas import user_schema
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class MyAccountView(BaseAppView, DataGridAppView):
59 59 ALLOW_SCOPED_TOKENS = False
60 60 """
61 61 This view has alternative version inside EE, if modified please take a look
62 62 in there as well.
63 63 """
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.user = c.auth_user.get_instance()
68 68 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
69 69
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @NotAnonymous()
74 74 @view_config(
75 75 route_name='my_account_profile', request_method='GET',
76 76 renderer='rhodecode:templates/admin/my_account/my_account.mako')
77 77 def my_account_profile(self):
78 78 c = self.load_default_context()
79 79 c.active = 'profile'
80 80 return self._get_template_context(c)
81 81
82 82 @LoginRequired()
83 83 @NotAnonymous()
84 84 @view_config(
85 85 route_name='my_account_password', request_method='GET',
86 86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
87 87 def my_account_password(self):
88 88 c = self.load_default_context()
89 89 c.active = 'password'
90 90 c.extern_type = c.user.extern_type
91 91
92 92 schema = user_schema.ChangePasswordSchema().bind(
93 93 username=c.user.username)
94 94
95 95 form = forms.Form(
96 96 schema,
97 97 action=h.route_path('my_account_password_update'),
98 98 buttons=(forms.buttons.save, forms.buttons.reset))
99 99
100 100 c.form = form
101 101 return self._get_template_context(c)
102 102
103 103 @LoginRequired()
104 104 @NotAnonymous()
105 105 @CSRFRequired()
106 106 @view_config(
107 107 route_name='my_account_password_update', request_method='POST',
108 108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
109 109 def my_account_password_update(self):
110 110 _ = self.request.translate
111 111 c = self.load_default_context()
112 112 c.active = 'password'
113 113 c.extern_type = c.user.extern_type
114 114
115 115 schema = user_schema.ChangePasswordSchema().bind(
116 116 username=c.user.username)
117 117
118 118 form = forms.Form(
119 119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
120 120
121 121 if c.extern_type != 'rhodecode':
122 122 raise HTTPFound(self.request.route_path('my_account_password'))
123 123
124 124 controls = self.request.POST.items()
125 125 try:
126 126 valid_data = form.validate(controls)
127 127 UserModel().update_user(c.user.user_id, **valid_data)
128 128 c.user.update_userdata(force_password_change=False)
129 129 Session().commit()
130 130 except forms.ValidationFailure as e:
131 131 c.form = e
132 132 return self._get_template_context(c)
133 133
134 134 except Exception:
135 135 log.exception("Exception updating password")
136 136 h.flash(_('Error occurred during update of user password'),
137 137 category='error')
138 138 else:
139 139 instance = c.auth_user.get_instance()
140 140 self.session.setdefault('rhodecode_user', {}).update(
141 141 {'password': md5(instance.password)})
142 142 self.session.save()
143 143 h.flash(_("Successfully updated password"), category='success')
144 144
145 145 raise HTTPFound(self.request.route_path('my_account_password'))
146 146
147 147 @LoginRequired()
148 148 @NotAnonymous()
149 149 @view_config(
150 150 route_name='my_account_auth_tokens', request_method='GET',
151 151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
152 152 def my_account_auth_tokens(self):
153 153 _ = self.request.translate
154 154
155 155 c = self.load_default_context()
156 156 c.active = 'auth_tokens'
157 157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
158 158 c.role_values = [
159 159 (x, AuthTokenModel.cls._get_role_name(x))
160 160 for x in AuthTokenModel.cls.ROLES]
161 161 c.role_options = [(c.role_values, _("Role"))]
162 162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
163 163 c.user.user_id, show_expired=True)
164 164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
165 165 return self._get_template_context(c)
166 166
167 167 def maybe_attach_token_scope(self, token):
168 168 # implemented in EE edition
169 169 pass
170 170
171 171 @LoginRequired()
172 172 @NotAnonymous()
173 173 @CSRFRequired()
174 174 @view_config(
175 175 route_name='my_account_auth_tokens_add', request_method='POST',)
176 176 def my_account_auth_tokens_add(self):
177 177 _ = self.request.translate
178 178 c = self.load_default_context()
179 179
180 180 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
181 181 description = self.request.POST.get('description')
182 182 role = self.request.POST.get('role')
183 183
184 184 token = UserModel().add_auth_token(
185 185 user=c.user.user_id,
186 186 lifetime_minutes=lifetime, role=role, description=description,
187 187 scope_callback=self.maybe_attach_token_scope)
188 188 token_data = token.get_api_data()
189 189
190 190 audit_logger.store_web(
191 191 'user.edit.token.add', action_data={
192 192 'data': {'token': token_data, 'user': 'self'}},
193 193 user=self._rhodecode_user, )
194 194 Session().commit()
195 195
196 196 h.flash(_("Auth token successfully created"), category='success')
197 197 return HTTPFound(h.route_path('my_account_auth_tokens'))
198 198
199 199 @LoginRequired()
200 200 @NotAnonymous()
201 201 @CSRFRequired()
202 202 @view_config(
203 203 route_name='my_account_auth_tokens_delete', request_method='POST')
204 204 def my_account_auth_tokens_delete(self):
205 205 _ = self.request.translate
206 206 c = self.load_default_context()
207 207
208 208 del_auth_token = self.request.POST.get('del_auth_token')
209 209
210 210 if del_auth_token:
211 211 token = UserApiKeys.get_or_404(del_auth_token)
212 212 token_data = token.get_api_data()
213 213
214 214 AuthTokenModel().delete(del_auth_token, c.user.user_id)
215 215 audit_logger.store_web(
216 216 'user.edit.token.delete', action_data={
217 217 'data': {'token': token_data, 'user': 'self'}},
218 218 user=self._rhodecode_user,)
219 219 Session().commit()
220 220 h.flash(_("Auth token successfully deleted"), category='success')
221 221
222 222 return HTTPFound(h.route_path('my_account_auth_tokens'))
223 223
224 224 @LoginRequired()
225 225 @NotAnonymous()
226 226 @view_config(
227 227 route_name='my_account_emails', request_method='GET',
228 228 renderer='rhodecode:templates/admin/my_account/my_account.mako')
229 229 def my_account_emails(self):
230 230 _ = self.request.translate
231 231
232 232 c = self.load_default_context()
233 233 c.active = 'emails'
234 234
235 235 c.user_email_map = UserEmailMap.query()\
236 236 .filter(UserEmailMap.user == c.user).all()
237 237
238 238 schema = user_schema.AddEmailSchema().bind(
239 239 username=c.user.username, user_emails=c.user.emails)
240 240
241 241 form = forms.RcForm(schema,
242 242 action=h.route_path('my_account_emails_add'),
243 243 buttons=(forms.buttons.save, forms.buttons.reset))
244 244
245 245 c.form = form
246 246 return self._get_template_context(c)
247 247
248 248 @LoginRequired()
249 249 @NotAnonymous()
250 250 @CSRFRequired()
251 251 @view_config(
252 252 route_name='my_account_emails_add', request_method='POST',
253 253 renderer='rhodecode:templates/admin/my_account/my_account.mako')
254 254 def my_account_emails_add(self):
255 255 _ = self.request.translate
256 256 c = self.load_default_context()
257 257 c.active = 'emails'
258 258
259 259 schema = user_schema.AddEmailSchema().bind(
260 260 username=c.user.username, user_emails=c.user.emails)
261 261
262 262 form = forms.RcForm(
263 263 schema, action=h.route_path('my_account_emails_add'),
264 264 buttons=(forms.buttons.save, forms.buttons.reset))
265 265
266 266 controls = self.request.POST.items()
267 267 try:
268 268 valid_data = form.validate(controls)
269 269 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
270 270 audit_logger.store_web(
271 271 'user.edit.email.add', action_data={
272 272 'data': {'email': valid_data['email'], 'user': 'self'}},
273 273 user=self._rhodecode_user,)
274 274 Session().commit()
275 275 except formencode.Invalid as error:
276 276 h.flash(h.escape(error.error_dict['email']), category='error')
277 277 except forms.ValidationFailure as e:
278 278 c.user_email_map = UserEmailMap.query() \
279 279 .filter(UserEmailMap.user == c.user).all()
280 280 c.form = e
281 281 return self._get_template_context(c)
282 282 except Exception:
283 283 log.exception("Exception adding email")
284 284 h.flash(_('Error occurred during adding email'),
285 285 category='error')
286 286 else:
287 287 h.flash(_("Successfully added email"), category='success')
288 288
289 289 raise HTTPFound(self.request.route_path('my_account_emails'))
290 290
291 291 @LoginRequired()
292 292 @NotAnonymous()
293 293 @CSRFRequired()
294 294 @view_config(
295 295 route_name='my_account_emails_delete', request_method='POST')
296 296 def my_account_emails_delete(self):
297 297 _ = self.request.translate
298 298 c = self.load_default_context()
299 299
300 300 del_email_id = self.request.POST.get('del_email_id')
301 301 if del_email_id:
302 302 email = UserEmailMap.get_or_404(del_email_id).email
303 303 UserModel().delete_extra_email(c.user.user_id, del_email_id)
304 304 audit_logger.store_web(
305 305 'user.edit.email.delete', action_data={
306 306 'data': {'email': email, 'user': 'self'}},
307 307 user=self._rhodecode_user,)
308 308 Session().commit()
309 309 h.flash(_("Email successfully deleted"),
310 310 category='success')
311 311 return HTTPFound(h.route_path('my_account_emails'))
312 312
313 313 @LoginRequired()
314 314 @NotAnonymous()
315 315 @CSRFRequired()
316 316 @view_config(
317 317 route_name='my_account_notifications_test_channelstream',
318 318 request_method='POST', renderer='json_ext')
319 319 def my_account_notifications_test_channelstream(self):
320 320 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
321 321 self._rhodecode_user.username, datetime.datetime.now())
322 322 payload = {
323 323 # 'channel': 'broadcast',
324 324 'type': 'message',
325 325 'timestamp': datetime.datetime.utcnow(),
326 326 'user': 'system',
327 327 'pm_users': [self._rhodecode_user.username],
328 328 'message': {
329 329 'message': message,
330 330 'level': 'info',
331 331 'topic': '/notifications'
332 332 }
333 333 }
334 334
335 335 registry = self.request.registry
336 336 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
337 337 channelstream_config = rhodecode_plugins.get('channelstream', {})
338 338
339 339 try:
340 340 channelstream_request(channelstream_config, [payload], '/message')
341 341 except ChannelstreamException as e:
342 342 log.exception('Failed to send channelstream data')
343 343 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
344 344 return {"response": 'Channelstream data sent. '
345 345 'You should see a new live message now.'}
346 346
347 347 def _load_my_repos_data(self, watched=False):
348 348 if watched:
349 349 admin = False
350 350 follows_repos = Session().query(UserFollowing)\
351 351 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
352 352 .options(joinedload(UserFollowing.follows_repository))\
353 353 .all()
354 354 repo_list = [x.follows_repository for x in follows_repos]
355 355 else:
356 356 admin = True
357 357 repo_list = Repository.get_all_repos(
358 358 user_id=self._rhodecode_user.user_id)
359 359 repo_list = RepoList(repo_list, perm_set=[
360 360 'repository.read', 'repository.write', 'repository.admin'])
361 361
362 362 repos_data = RepoModel().get_repos_as_dict(
363 363 repo_list=repo_list, admin=admin, short_name=False)
364 364 # json used to render the grid
365 365 return json.dumps(repos_data)
366 366
367 367 @LoginRequired()
368 368 @NotAnonymous()
369 369 @view_config(
370 370 route_name='my_account_repos', request_method='GET',
371 371 renderer='rhodecode:templates/admin/my_account/my_account.mako')
372 372 def my_account_repos(self):
373 373 c = self.load_default_context()
374 374 c.active = 'repos'
375 375
376 376 # json used to render the grid
377 377 c.data = self._load_my_repos_data()
378 378 return self._get_template_context(c)
379 379
380 380 @LoginRequired()
381 381 @NotAnonymous()
382 382 @view_config(
383 383 route_name='my_account_watched', request_method='GET',
384 384 renderer='rhodecode:templates/admin/my_account/my_account.mako')
385 385 def my_account_watched(self):
386 386 c = self.load_default_context()
387 387 c.active = 'watched'
388 388
389 389 # json used to render the grid
390 390 c.data = self._load_my_repos_data(watched=True)
391 391 return self._get_template_context(c)
392 392
393 393 @LoginRequired()
394 394 @NotAnonymous()
395 395 @view_config(
396 396 route_name='my_account_bookmarks', request_method='GET',
397 397 renderer='rhodecode:templates/admin/my_account/my_account.mako')
398 398 def my_account_bookmarks(self):
399 399 c = self.load_default_context()
400 400 c.active = 'bookmarks'
401 401 return self._get_template_context(c)
402 402
403 403 def _process_bookmark_entry(self, entry, user_id):
404 404 position = safe_int(entry.get('position'))
405 405 cur_position = safe_int(entry.get('cur_position'))
406 406 if position is None or cur_position is None:
407 407 return
408 408
409 409 # check if this is an existing entry
410 410 is_new = False
411 411 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
412 412
413 413 if db_entry and str2bool(entry.get('remove')):
414 414 log.debug('Marked bookmark %s for deletion', db_entry)
415 415 Session().delete(db_entry)
416 416 return
417 417
418 418 if not db_entry:
419 419 # new
420 420 db_entry = UserBookmark()
421 421 is_new = True
422 422
423 423 should_save = False
424 424 default_redirect_url = ''
425 425
426 426 # save repo
427 427 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
428 428 repo = Repository.get(entry['bookmark_repo'])
429 429 perm_check = HasRepoPermissionAny(
430 430 'repository.read', 'repository.write', 'repository.admin')
431 431 if repo and perm_check(repo_name=repo.repo_name):
432 432 db_entry.repository = repo
433 433 should_save = True
434 434 default_redirect_url = '${repo_url}'
435 435 # save repo group
436 436 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
437 437 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
438 438 perm_check = HasRepoGroupPermissionAny(
439 439 'group.read', 'group.write', 'group.admin')
440 440
441 441 if repo_group and perm_check(group_name=repo_group.group_name):
442 442 db_entry.repository_group = repo_group
443 443 should_save = True
444 444 default_redirect_url = '${repo_group_url}'
445 445 # save generic info
446 446 elif entry.get('title') and entry.get('redirect_url'):
447 447 should_save = True
448 448
449 449 if should_save:
450 450 # mark user and position
451 451 db_entry.user_id = user_id
452 452 db_entry.position = position
453 453 db_entry.title = entry.get('title')
454 454 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
455 455 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
456 456
457 457 Session().add(db_entry)
458 458
459 459 @LoginRequired()
460 460 @NotAnonymous()
461 461 @CSRFRequired()
462 462 @view_config(
463 463 route_name='my_account_bookmarks_update', request_method='POST')
464 464 def my_account_bookmarks_update(self):
465 465 _ = self.request.translate
466 466 c = self.load_default_context()
467 467 c.active = 'bookmarks'
468 468
469 469 controls = peppercorn.parse(self.request.POST.items())
470 470 user_id = c.user.user_id
471 471
472 472 # validate positions
473 473 positions = {}
474 474 for entry in controls.get('bookmarks', []):
475 475 position = safe_int(entry['position'])
476 476 if position is None:
477 477 continue
478 478
479 479 if position in positions:
480 480 h.flash(_("Position {} is defined twice. "
481 481 "Please correct this error.").format(position), category='error')
482 482 return HTTPFound(h.route_path('my_account_bookmarks'))
483 483
484 484 entry['position'] = position
485 485 entry['cur_position'] = safe_int(entry.get('cur_position'))
486 486 positions[position] = entry
487 487
488 488 try:
489 489 for entry in positions.values():
490 490 self._process_bookmark_entry(entry, user_id)
491 491
492 492 Session().commit()
493 493 h.flash(_("Update Bookmarks"), category='success')
494 494 except IntegrityError:
495 495 h.flash(_("Failed to update bookmarks. "
496 496 "Make sure an unique position is used."), category='error')
497 497
498 498 return HTTPFound(h.route_path('my_account_bookmarks'))
499 499
500 500 @LoginRequired()
501 501 @NotAnonymous()
502 502 @view_config(
503 503 route_name='my_account_goto_bookmark', request_method='GET',
504 504 renderer='rhodecode:templates/admin/my_account/my_account.mako')
505 505 def my_account_goto_bookmark(self):
506 506
507 507 bookmark_id = self.request.matchdict['bookmark_id']
508 508 user_bookmark = UserBookmark().query()\
509 509 .filter(UserBookmark.user_id == self.request.user.user_id) \
510 510 .filter(UserBookmark.position == bookmark_id).scalar()
511 511
512 512 redirect_url = h.route_path('my_account_bookmarks')
513 513 if not user_bookmark:
514 514 raise HTTPFound(redirect_url)
515 515
516 516 # repository set
517 517 if user_bookmark.repository:
518 518 repo_name = user_bookmark.repository.repo_name
519 519 base_redirect_url = h.route_path(
520 520 'repo_summary', repo_name=repo_name)
521 521 if user_bookmark.redirect_url and \
522 522 '${repo_url}' in user_bookmark.redirect_url:
523 523 redirect_url = string.Template(user_bookmark.redirect_url)\
524 524 .safe_substitute({'repo_url': base_redirect_url})
525 525 else:
526 526 redirect_url = base_redirect_url
527 527 # repository group set
528 528 elif user_bookmark.repository_group:
529 529 repo_group_name = user_bookmark.repository_group.group_name
530 530 base_redirect_url = h.route_path(
531 531 'repo_group_home', repo_group_name=repo_group_name)
532 532 if user_bookmark.redirect_url and \
533 533 '${repo_group_url}' in user_bookmark.redirect_url:
534 534 redirect_url = string.Template(user_bookmark.redirect_url)\
535 535 .safe_substitute({'repo_group_url': base_redirect_url})
536 536 else:
537 537 redirect_url = base_redirect_url
538 538 # custom URL set
539 539 elif user_bookmark.redirect_url:
540 540 server_url = h.route_url('home').rstrip('/')
541 541 redirect_url = string.Template(user_bookmark.redirect_url) \
542 542 .safe_substitute({'server_url': server_url})
543 543
544 544 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
545 545 raise HTTPFound(redirect_url)
546 546
547 547 @LoginRequired()
548 548 @NotAnonymous()
549 549 @view_config(
550 550 route_name='my_account_perms', request_method='GET',
551 551 renderer='rhodecode:templates/admin/my_account/my_account.mako')
552 552 def my_account_perms(self):
553 553 c = self.load_default_context()
554 554 c.active = 'perms'
555 555
556 556 c.perm_user = c.auth_user
557 557 return self._get_template_context(c)
558 558
559 559 @LoginRequired()
560 560 @NotAnonymous()
561 561 @view_config(
562 562 route_name='my_account_notifications', request_method='GET',
563 563 renderer='rhodecode:templates/admin/my_account/my_account.mako')
564 564 def my_notifications(self):
565 565 c = self.load_default_context()
566 566 c.active = 'notifications'
567 567
568 568 return self._get_template_context(c)
569 569
570 570 @LoginRequired()
571 571 @NotAnonymous()
572 572 @CSRFRequired()
573 573 @view_config(
574 574 route_name='my_account_notifications_toggle_visibility',
575 575 request_method='POST', renderer='json_ext')
576 576 def my_notifications_toggle_visibility(self):
577 577 user = self._rhodecode_db_user
578 578 new_status = not user.user_data.get('notification_status', True)
579 579 user.update_userdata(notification_status=new_status)
580 580 Session().commit()
581 581 return user.user_data['notification_status']
582 582
583 583 @LoginRequired()
584 584 @NotAnonymous()
585 585 @view_config(
586 586 route_name='my_account_edit',
587 587 request_method='GET',
588 588 renderer='rhodecode:templates/admin/my_account/my_account.mako')
589 589 def my_account_edit(self):
590 590 c = self.load_default_context()
591 591 c.active = 'profile_edit'
592 592 c.extern_type = c.user.extern_type
593 593 c.extern_name = c.user.extern_name
594 594
595 595 schema = user_schema.UserProfileSchema().bind(
596 596 username=c.user.username, user_emails=c.user.emails)
597 597 appstruct = {
598 598 'username': c.user.username,
599 599 'email': c.user.email,
600 600 'firstname': c.user.firstname,
601 601 'lastname': c.user.lastname,
602 'description': c.user.description,
602 603 }
603 604 c.form = forms.RcForm(
604 605 schema, appstruct=appstruct,
605 606 action=h.route_path('my_account_update'),
606 607 buttons=(forms.buttons.save, forms.buttons.reset))
607 608
608 609 return self._get_template_context(c)
609 610
610 611 @LoginRequired()
611 612 @NotAnonymous()
612 613 @CSRFRequired()
613 614 @view_config(
614 615 route_name='my_account_update',
615 616 request_method='POST',
616 617 renderer='rhodecode:templates/admin/my_account/my_account.mako')
617 618 def my_account_update(self):
618 619 _ = self.request.translate
619 620 c = self.load_default_context()
620 621 c.active = 'profile_edit'
621 622 c.perm_user = c.auth_user
622 623 c.extern_type = c.user.extern_type
623 624 c.extern_name = c.user.extern_name
624 625
625 626 schema = user_schema.UserProfileSchema().bind(
626 627 username=c.user.username, user_emails=c.user.emails)
627 628 form = forms.RcForm(
628 629 schema, buttons=(forms.buttons.save, forms.buttons.reset))
629 630
630 631 controls = self.request.POST.items()
631 632 try:
632 633 valid_data = form.validate(controls)
633 634 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
634 635 'new_password', 'password_confirmation']
635 636 if c.extern_type != "rhodecode":
636 637 # forbid updating username for external accounts
637 638 skip_attrs.append('username')
638 639 old_email = c.user.email
639 640 UserModel().update_user(
640 641 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
641 642 **valid_data)
642 643 if old_email != valid_data['email']:
643 644 old = UserEmailMap.query() \
644 645 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
645 646 old.email = old_email
646 647 h.flash(_('Your account was updated successfully'), category='success')
647 648 Session().commit()
648 649 except forms.ValidationFailure as e:
649 650 c.form = e
650 651 return self._get_template_context(c)
651 652 except Exception:
652 653 log.exception("Exception updating user")
653 654 h.flash(_('Error occurred during update of user'),
654 655 category='error')
655 656 raise HTTPFound(h.route_path('my_account_profile'))
656 657
657 658 def _get_pull_requests_list(self, statuses):
658 659 draw, start, limit = self._extract_chunk(self.request)
659 660 search_q, order_by, order_dir = self._extract_ordering(self.request)
660 661 _render = self.request.get_partial_renderer(
661 662 'rhodecode:templates/data_table/_dt_elements.mako')
662 663
663 664 pull_requests = PullRequestModel().get_im_participating_in(
664 665 user_id=self._rhodecode_user.user_id,
665 666 statuses=statuses,
666 667 offset=start, length=limit, order_by=order_by,
667 668 order_dir=order_dir)
668 669
669 670 pull_requests_total_count = PullRequestModel().count_im_participating_in(
670 671 user_id=self._rhodecode_user.user_id, statuses=statuses)
671 672
672 673 data = []
673 674 comments_model = CommentsModel()
674 675 for pr in pull_requests:
675 676 repo_id = pr.target_repo_id
676 677 comments = comments_model.get_all_comments(
677 678 repo_id, pull_request=pr)
678 679 owned = pr.user_id == self._rhodecode_user.user_id
679 680
680 681 data.append({
681 682 'target_repo': _render('pullrequest_target_repo',
682 683 pr.target_repo.repo_name),
683 684 'name': _render('pullrequest_name',
684 685 pr.pull_request_id, pr.target_repo.repo_name,
685 686 short=True),
686 687 'name_raw': pr.pull_request_id,
687 688 'status': _render('pullrequest_status',
688 689 pr.calculated_review_status()),
689 690 'title': _render('pullrequest_title', pr.title, pr.description),
690 691 'description': h.escape(pr.description),
691 692 'updated_on': _render('pullrequest_updated_on',
692 693 h.datetime_to_time(pr.updated_on)),
693 694 'updated_on_raw': h.datetime_to_time(pr.updated_on),
694 695 'created_on': _render('pullrequest_updated_on',
695 696 h.datetime_to_time(pr.created_on)),
696 697 'created_on_raw': h.datetime_to_time(pr.created_on),
697 698 'state': pr.pull_request_state,
698 699 'author': _render('pullrequest_author',
699 700 pr.author.full_contact, ),
700 701 'author_raw': pr.author.full_name,
701 702 'comments': _render('pullrequest_comments', len(comments)),
702 703 'comments_raw': len(comments),
703 704 'closed': pr.is_closed(),
704 705 'owned': owned
705 706 })
706 707
707 708 # json used to render the grid
708 709 data = ({
709 710 'draw': draw,
710 711 'data': data,
711 712 'recordsTotal': pull_requests_total_count,
712 713 'recordsFiltered': pull_requests_total_count,
713 714 })
714 715 return data
715 716
716 717 @LoginRequired()
717 718 @NotAnonymous()
718 719 @view_config(
719 720 route_name='my_account_pullrequests',
720 721 request_method='GET',
721 722 renderer='rhodecode:templates/admin/my_account/my_account.mako')
722 723 def my_account_pullrequests(self):
723 724 c = self.load_default_context()
724 725 c.active = 'pullrequests'
725 726 req_get = self.request.GET
726 727
727 728 c.closed = str2bool(req_get.get('pr_show_closed'))
728 729
729 730 return self._get_template_context(c)
730 731
731 732 @LoginRequired()
732 733 @NotAnonymous()
733 734 @view_config(
734 735 route_name='my_account_pullrequests_data',
735 736 request_method='GET', renderer='json_ext')
736 737 def my_account_pullrequests_data(self):
737 738 self.load_default_context()
738 739 req_get = self.request.GET
739 740 closed = str2bool(req_get.get('closed'))
740 741
741 742 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
742 743 if closed:
743 744 statuses += [PullRequest.STATUS_CLOSED]
744 745
745 746 data = self._get_pull_requests_list(statuses=statuses)
746 747 return data
747 748
748 749 @LoginRequired()
749 750 @NotAnonymous()
750 751 @view_config(
751 752 route_name='my_account_user_group_membership',
752 753 request_method='GET',
753 754 renderer='rhodecode:templates/admin/my_account/my_account.mako')
754 755 def my_account_user_group_membership(self):
755 756 c = self.load_default_context()
756 757 c.active = 'user_group_membership'
757 758 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
758 759 for group in self._rhodecode_db_user.group_member]
759 760 c.user_groups = json.dumps(groups)
760 761 return self._get_template_context(c)
@@ -1,629 +1,630 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 this is forms validation classes
23 23 http://formencode.org/module-formencode.validators.html
24 24 for list off all availible validators
25 25
26 26 we can create our own validators
27 27
28 28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 29 pre_validators [] These validators will be applied before the schema
30 30 chained_validators [] These validators will be applied after the schema
31 31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35 35
36 36
37 37 <name> = formencode.validators.<name of validator>
38 38 <name> must equal form name
39 39 list=[1,2,3,4,5]
40 40 for SELECT use formencode.All(OneOf(list), Int())
41 41
42 42 """
43 43
44 44 import deform
45 45 import logging
46 46 import formencode
47 47
48 48 from pkg_resources import resource_filename
49 49 from formencode import All, Pipe
50 50
51 51 from pyramid.threadlocal import get_current_request
52 52
53 53 from rhodecode import BACKENDS
54 54 from rhodecode.lib import helpers
55 55 from rhodecode.model import validators as v
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 deform_templates = resource_filename('deform', 'templates')
61 61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 62 search_path = (rhodecode_templates, deform_templates)
63 63
64 64
65 65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 67 def __call__(self, template_name, **kw):
68 68 kw['h'] = helpers
69 69 kw['request'] = get_current_request()
70 70 return self.load(template_name)(**kw)
71 71
72 72
73 73 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
74 74 deform.Form.set_default_renderer(form_renderer)
75 75
76 76
77 77 def LoginForm(localizer):
78 78 _ = localizer
79 79
80 80 class _LoginForm(formencode.Schema):
81 81 allow_extra_fields = True
82 82 filter_extra_fields = True
83 83 username = v.UnicodeString(
84 84 strip=True,
85 85 min=1,
86 86 not_empty=True,
87 87 messages={
88 88 'empty': _(u'Please enter a login'),
89 89 'tooShort': _(u'Enter a value %(min)i characters long or more')
90 90 }
91 91 )
92 92
93 93 password = v.UnicodeString(
94 94 strip=False,
95 95 min=3,
96 96 max=72,
97 97 not_empty=True,
98 98 messages={
99 99 'empty': _(u'Please enter a password'),
100 100 'tooShort': _(u'Enter %(min)i characters or more')}
101 101 )
102 102
103 103 remember = v.StringBoolean(if_missing=False)
104 104
105 105 chained_validators = [v.ValidAuth(localizer)]
106 106 return _LoginForm
107 107
108 108
109 109 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
110 110 old_data = old_data or {}
111 111 available_languages = available_languages or []
112 112 _ = localizer
113 113
114 114 class _UserForm(formencode.Schema):
115 115 allow_extra_fields = True
116 116 filter_extra_fields = True
117 117 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
118 118 v.ValidUsername(localizer, edit, old_data))
119 119 if edit:
120 120 new_password = All(
121 121 v.ValidPassword(localizer),
122 122 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
123 123 )
124 124 password_confirmation = All(
125 125 v.ValidPassword(localizer),
126 126 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
127 127 )
128 128 admin = v.StringBoolean(if_missing=False)
129 129 else:
130 130 password = All(
131 131 v.ValidPassword(localizer),
132 132 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
133 133 )
134 134 password_confirmation = All(
135 135 v.ValidPassword(localizer),
136 136 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
137 137 )
138 138
139 139 password_change = v.StringBoolean(if_missing=False)
140 140 create_repo_group = v.StringBoolean(if_missing=False)
141 141
142 142 active = v.StringBoolean(if_missing=False)
143 143 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
144 144 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
145 145 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
146 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False)
146 147 extern_name = v.UnicodeString(strip=True)
147 148 extern_type = v.UnicodeString(strip=True)
148 149 language = v.OneOf(available_languages, hideList=False,
149 150 testValueList=True, if_missing=None)
150 151 chained_validators = [v.ValidPasswordsMatch(localizer)]
151 152 return _UserForm
152 153
153 154
154 155 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
155 156 old_data = old_data or {}
156 157 _ = localizer
157 158
158 159 class _UserGroupForm(formencode.Schema):
159 160 allow_extra_fields = True
160 161 filter_extra_fields = True
161 162
162 163 users_group_name = All(
163 164 v.UnicodeString(strip=True, min=1, not_empty=True),
164 165 v.ValidUserGroup(localizer, edit, old_data)
165 166 )
166 167 user_group_description = v.UnicodeString(strip=True, min=1,
167 168 not_empty=False)
168 169
169 170 users_group_active = v.StringBoolean(if_missing=False)
170 171
171 172 if edit:
172 173 # this is user group owner
173 174 user = All(
174 175 v.UnicodeString(not_empty=True),
175 176 v.ValidRepoUser(localizer, allow_disabled))
176 177 return _UserGroupForm
177 178
178 179
179 180 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
180 181 can_create_in_root=False, allow_disabled=False):
181 182 _ = localizer
182 183 old_data = old_data or {}
183 184 available_groups = available_groups or []
184 185
185 186 class _RepoGroupForm(formencode.Schema):
186 187 allow_extra_fields = True
187 188 filter_extra_fields = False
188 189
189 190 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
190 191 v.SlugifyName(localizer),)
191 192 group_description = v.UnicodeString(strip=True, min=1,
192 193 not_empty=False)
193 194 group_copy_permissions = v.StringBoolean(if_missing=False)
194 195
195 196 group_parent_id = v.OneOf(available_groups, hideList=False,
196 197 testValueList=True, not_empty=True)
197 198 enable_locking = v.StringBoolean(if_missing=False)
198 199 chained_validators = [
199 200 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
200 201
201 202 if edit:
202 203 # this is repo group owner
203 204 user = All(
204 205 v.UnicodeString(not_empty=True),
205 206 v.ValidRepoUser(localizer, allow_disabled))
206 207 return _RepoGroupForm
207 208
208 209
209 210 def RegisterForm(localizer, edit=False, old_data=None):
210 211 _ = localizer
211 212 old_data = old_data or {}
212 213
213 214 class _RegisterForm(formencode.Schema):
214 215 allow_extra_fields = True
215 216 filter_extra_fields = True
216 217 username = All(
217 218 v.ValidUsername(localizer, edit, old_data),
218 219 v.UnicodeString(strip=True, min=1, not_empty=True)
219 220 )
220 221 password = All(
221 222 v.ValidPassword(localizer),
222 223 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
223 224 )
224 225 password_confirmation = All(
225 226 v.ValidPassword(localizer),
226 227 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
227 228 )
228 229 active = v.StringBoolean(if_missing=False)
229 230 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 231 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
231 232 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
232 233
233 234 chained_validators = [v.ValidPasswordsMatch(localizer)]
234 235 return _RegisterForm
235 236
236 237
237 238 def PasswordResetForm(localizer):
238 239 _ = localizer
239 240
240 241 class _PasswordResetForm(formencode.Schema):
241 242 allow_extra_fields = True
242 243 filter_extra_fields = True
243 244 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
244 245 return _PasswordResetForm
245 246
246 247
247 248 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
248 249 _ = localizer
249 250 old_data = old_data or {}
250 251 repo_groups = repo_groups or []
251 252 supported_backends = BACKENDS.keys()
252 253
253 254 class _RepoForm(formencode.Schema):
254 255 allow_extra_fields = True
255 256 filter_extra_fields = False
256 257 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
257 258 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
258 259 repo_group = All(v.CanWriteGroup(localizer, old_data),
259 260 v.OneOf(repo_groups, hideList=True))
260 261 repo_type = v.OneOf(supported_backends, required=False,
261 262 if_missing=old_data.get('repo_type'))
262 263 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
263 264 repo_private = v.StringBoolean(if_missing=False)
264 265 repo_copy_permissions = v.StringBoolean(if_missing=False)
265 266 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
266 267
267 268 repo_enable_statistics = v.StringBoolean(if_missing=False)
268 269 repo_enable_downloads = v.StringBoolean(if_missing=False)
269 270 repo_enable_locking = v.StringBoolean(if_missing=False)
270 271
271 272 if edit:
272 273 # this is repo owner
273 274 user = All(
274 275 v.UnicodeString(not_empty=True),
275 276 v.ValidRepoUser(localizer, allow_disabled))
276 277 clone_uri_change = v.UnicodeString(
277 278 not_empty=False, if_missing=v.Missing)
278 279
279 280 chained_validators = [v.ValidCloneUri(localizer),
280 281 v.ValidRepoName(localizer, edit, old_data)]
281 282 return _RepoForm
282 283
283 284
284 285 def RepoPermsForm(localizer):
285 286 _ = localizer
286 287
287 288 class _RepoPermsForm(formencode.Schema):
288 289 allow_extra_fields = True
289 290 filter_extra_fields = False
290 291 chained_validators = [v.ValidPerms(localizer, type_='repo')]
291 292 return _RepoPermsForm
292 293
293 294
294 295 def RepoGroupPermsForm(localizer, valid_recursive_choices):
295 296 _ = localizer
296 297
297 298 class _RepoGroupPermsForm(formencode.Schema):
298 299 allow_extra_fields = True
299 300 filter_extra_fields = False
300 301 recursive = v.OneOf(valid_recursive_choices)
301 302 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
302 303 return _RepoGroupPermsForm
303 304
304 305
305 306 def UserGroupPermsForm(localizer):
306 307 _ = localizer
307 308
308 309 class _UserPermsForm(formencode.Schema):
309 310 allow_extra_fields = True
310 311 filter_extra_fields = False
311 312 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
312 313 return _UserPermsForm
313 314
314 315
315 316 def RepoFieldForm(localizer):
316 317 _ = localizer
317 318
318 319 class _RepoFieldForm(formencode.Schema):
319 320 filter_extra_fields = True
320 321 allow_extra_fields = True
321 322
322 323 new_field_key = All(v.FieldKey(localizer),
323 324 v.UnicodeString(strip=True, min=3, not_empty=True))
324 325 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
325 326 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
326 327 if_missing='str')
327 328 new_field_label = v.UnicodeString(not_empty=False)
328 329 new_field_desc = v.UnicodeString(not_empty=False)
329 330 return _RepoFieldForm
330 331
331 332
332 333 def RepoForkForm(localizer, edit=False, old_data=None,
333 334 supported_backends=BACKENDS.keys(), repo_groups=None):
334 335 _ = localizer
335 336 old_data = old_data or {}
336 337 repo_groups = repo_groups or []
337 338
338 339 class _RepoForkForm(formencode.Schema):
339 340 allow_extra_fields = True
340 341 filter_extra_fields = False
341 342 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
342 343 v.SlugifyName(localizer))
343 344 repo_group = All(v.CanWriteGroup(localizer, ),
344 345 v.OneOf(repo_groups, hideList=True))
345 346 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
346 347 description = v.UnicodeString(strip=True, min=1, not_empty=True)
347 348 private = v.StringBoolean(if_missing=False)
348 349 copy_permissions = v.StringBoolean(if_missing=False)
349 350 fork_parent_id = v.UnicodeString()
350 351 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
351 352 return _RepoForkForm
352 353
353 354
354 355 def ApplicationSettingsForm(localizer):
355 356 _ = localizer
356 357
357 358 class _ApplicationSettingsForm(formencode.Schema):
358 359 allow_extra_fields = True
359 360 filter_extra_fields = False
360 361 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
361 362 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
362 363 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
363 364 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
364 365 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
365 366 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
366 367 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
367 368 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
368 369 return _ApplicationSettingsForm
369 370
370 371
371 372 def ApplicationVisualisationForm(localizer):
372 373 from rhodecode.model.db import Repository
373 374 _ = localizer
374 375
375 376 class _ApplicationVisualisationForm(formencode.Schema):
376 377 allow_extra_fields = True
377 378 filter_extra_fields = False
378 379 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
379 380 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
380 381 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
381 382
382 383 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
383 384 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
384 385 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
385 386 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
386 387 rhodecode_show_version = v.StringBoolean(if_missing=False)
387 388 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
388 389 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
389 390 rhodecode_gravatar_url = v.UnicodeString(min=3)
390 391 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
391 392 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
392 393 rhodecode_support_url = v.UnicodeString()
393 394 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
394 395 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
395 396 return _ApplicationVisualisationForm
396 397
397 398
398 399 class _BaseVcsSettingsForm(formencode.Schema):
399 400
400 401 allow_extra_fields = True
401 402 filter_extra_fields = False
402 403 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
403 404 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
404 405 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
405 406
406 407 # PR/Code-review
407 408 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
408 409 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
409 410
410 411 # hg
411 412 extensions_largefiles = v.StringBoolean(if_missing=False)
412 413 extensions_evolve = v.StringBoolean(if_missing=False)
413 414 phases_publish = v.StringBoolean(if_missing=False)
414 415
415 416 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
416 417 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
417 418
418 419 # git
419 420 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
420 421 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
421 422 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
422 423
423 424 # svn
424 425 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
425 426 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
426 427
427 428 # cache
428 429 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
429 430
430 431
431 432 def ApplicationUiSettingsForm(localizer):
432 433 _ = localizer
433 434
434 435 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
435 436 web_push_ssl = v.StringBoolean(if_missing=False)
436 437 paths_root_path = All(
437 438 v.ValidPath(localizer),
438 439 v.UnicodeString(strip=True, min=1, not_empty=True)
439 440 )
440 441 largefiles_usercache = All(
441 442 v.ValidPath(localizer),
442 443 v.UnicodeString(strip=True, min=2, not_empty=True))
443 444 vcs_git_lfs_store_location = All(
444 445 v.ValidPath(localizer),
445 446 v.UnicodeString(strip=True, min=2, not_empty=True))
446 447 extensions_hgsubversion = v.StringBoolean(if_missing=False)
447 448 extensions_hggit = v.StringBoolean(if_missing=False)
448 449 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
449 450 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
450 451 return _ApplicationUiSettingsForm
451 452
452 453
453 454 def RepoVcsSettingsForm(localizer, repo_name):
454 455 _ = localizer
455 456
456 457 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
457 458 inherit_global_settings = v.StringBoolean(if_missing=False)
458 459 new_svn_branch = v.ValidSvnPattern(localizer,
459 460 section='vcs_svn_branch', repo_name=repo_name)
460 461 new_svn_tag = v.ValidSvnPattern(localizer,
461 462 section='vcs_svn_tag', repo_name=repo_name)
462 463 return _RepoVcsSettingsForm
463 464
464 465
465 466 def LabsSettingsForm(localizer):
466 467 _ = localizer
467 468
468 469 class _LabSettingsForm(formencode.Schema):
469 470 allow_extra_fields = True
470 471 filter_extra_fields = False
471 472 return _LabSettingsForm
472 473
473 474
474 475 def ApplicationPermissionsForm(
475 476 localizer, register_choices, password_reset_choices,
476 477 extern_activate_choices):
477 478 _ = localizer
478 479
479 480 class _DefaultPermissionsForm(formencode.Schema):
480 481 allow_extra_fields = True
481 482 filter_extra_fields = True
482 483
483 484 anonymous = v.StringBoolean(if_missing=False)
484 485 default_register = v.OneOf(register_choices)
485 486 default_register_message = v.UnicodeString()
486 487 default_password_reset = v.OneOf(password_reset_choices)
487 488 default_extern_activate = v.OneOf(extern_activate_choices)
488 489 return _DefaultPermissionsForm
489 490
490 491
491 492 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
492 493 user_group_perms_choices):
493 494 _ = localizer
494 495
495 496 class _ObjectPermissionsForm(formencode.Schema):
496 497 allow_extra_fields = True
497 498 filter_extra_fields = True
498 499 overwrite_default_repo = v.StringBoolean(if_missing=False)
499 500 overwrite_default_group = v.StringBoolean(if_missing=False)
500 501 overwrite_default_user_group = v.StringBoolean(if_missing=False)
501 502
502 503 default_repo_perm = v.OneOf(repo_perms_choices)
503 504 default_group_perm = v.OneOf(group_perms_choices)
504 505 default_user_group_perm = v.OneOf(user_group_perms_choices)
505 506
506 507 return _ObjectPermissionsForm
507 508
508 509
509 510 def BranchPermissionsForm(localizer, branch_perms_choices):
510 511 _ = localizer
511 512
512 513 class _BranchPermissionsForm(formencode.Schema):
513 514 allow_extra_fields = True
514 515 filter_extra_fields = True
515 516 overwrite_default_branch = v.StringBoolean(if_missing=False)
516 517 default_branch_perm = v.OneOf(branch_perms_choices)
517 518
518 519 return _BranchPermissionsForm
519 520
520 521
521 522 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
522 523 repo_group_create_choices, user_group_create_choices,
523 524 fork_choices, inherit_default_permissions_choices):
524 525 _ = localizer
525 526
526 527 class _DefaultPermissionsForm(formencode.Schema):
527 528 allow_extra_fields = True
528 529 filter_extra_fields = True
529 530
530 531 anonymous = v.StringBoolean(if_missing=False)
531 532
532 533 default_repo_create = v.OneOf(create_choices)
533 534 default_repo_create_on_write = v.OneOf(create_on_write_choices)
534 535 default_user_group_create = v.OneOf(user_group_create_choices)
535 536 default_repo_group_create = v.OneOf(repo_group_create_choices)
536 537 default_fork_create = v.OneOf(fork_choices)
537 538 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
538 539 return _DefaultPermissionsForm
539 540
540 541
541 542 def UserIndividualPermissionsForm(localizer):
542 543 _ = localizer
543 544
544 545 class _DefaultPermissionsForm(formencode.Schema):
545 546 allow_extra_fields = True
546 547 filter_extra_fields = True
547 548
548 549 inherit_default_permissions = v.StringBoolean(if_missing=False)
549 550 return _DefaultPermissionsForm
550 551
551 552
552 553 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
553 554 _ = localizer
554 555 old_data = old_data or {}
555 556
556 557 class _DefaultsForm(formencode.Schema):
557 558 allow_extra_fields = True
558 559 filter_extra_fields = True
559 560 default_repo_type = v.OneOf(supported_backends)
560 561 default_repo_private = v.StringBoolean(if_missing=False)
561 562 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
562 563 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
563 564 default_repo_enable_locking = v.StringBoolean(if_missing=False)
564 565 return _DefaultsForm
565 566
566 567
567 568 def AuthSettingsForm(localizer):
568 569 _ = localizer
569 570
570 571 class _AuthSettingsForm(formencode.Schema):
571 572 allow_extra_fields = True
572 573 filter_extra_fields = True
573 574 auth_plugins = All(v.ValidAuthPlugins(localizer),
574 575 v.UniqueListFromString(localizer)(not_empty=True))
575 576 return _AuthSettingsForm
576 577
577 578
578 579 def UserExtraEmailForm(localizer):
579 580 _ = localizer
580 581
581 582 class _UserExtraEmailForm(formencode.Schema):
582 583 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
583 584 return _UserExtraEmailForm
584 585
585 586
586 587 def UserExtraIpForm(localizer):
587 588 _ = localizer
588 589
589 590 class _UserExtraIpForm(formencode.Schema):
590 591 ip = v.ValidIp(localizer)(not_empty=True)
591 592 return _UserExtraIpForm
592 593
593 594
594 595 def PullRequestForm(localizer, repo_id):
595 596 _ = localizer
596 597
597 598 class ReviewerForm(formencode.Schema):
598 599 user_id = v.Int(not_empty=True)
599 600 reasons = All()
600 601 rules = All(v.UniqueList(localizer, convert=int)())
601 602 mandatory = v.StringBoolean()
602 603
603 604 class _PullRequestForm(formencode.Schema):
604 605 allow_extra_fields = True
605 606 filter_extra_fields = True
606 607
607 608 common_ancestor = v.UnicodeString(strip=True, required=True)
608 609 source_repo = v.UnicodeString(strip=True, required=True)
609 610 source_ref = v.UnicodeString(strip=True, required=True)
610 611 target_repo = v.UnicodeString(strip=True, required=True)
611 612 target_ref = v.UnicodeString(strip=True, required=True)
612 613 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
613 614 v.UniqueList(localizer)(not_empty=True))
614 615 review_members = formencode.ForEach(ReviewerForm())
615 616 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
616 617 pullrequest_desc = v.UnicodeString(strip=True, required=False)
617 618 description_renderer = v.UnicodeString(strip=True, required=False)
618 619
619 620 return _PullRequestForm
620 621
621 622
622 623 def IssueTrackerPatternsForm(localizer):
623 624 _ = localizer
624 625
625 626 class _IssueTrackerPatternsForm(formencode.Schema):
626 627 allow_extra_fields = True
627 628 filter_extra_fields = False
628 629 chained_validators = [v.ValidPattern(localizer)]
629 630 return _IssueTrackerPatternsForm
@@ -1,1000 +1,1003 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28 import ipaddress
29 29
30 30 from pyramid.threadlocal import get_current_request
31 31 from sqlalchemy.exc import DatabaseError
32 32
33 33 from rhodecode import events
34 34 from rhodecode.lib.user_log_filter import user_log_filter
35 35 from rhodecode.lib.utils2 import (
36 36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 37 AttributeDict, str2bool)
38 38 from rhodecode.lib.exceptions import (
39 39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 40 UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException)
41 41 from rhodecode.lib.caching_query import FromCache
42 42 from rhodecode.model import BaseModel
43 43 from rhodecode.model.auth_token import AuthTokenModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
46 46 UserEmailMap, UserIpMap, UserLog)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(
61 61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def _serialize_user(self, user):
68 68 import rhodecode.lib.helpers as h
69 69
70 70 return {
71 71 'id': user.user_id,
72 72 'first_name': user.first_name,
73 73 'last_name': user.last_name,
74 74 'username': user.username,
75 75 'email': user.email,
76 76 'icon_link': h.gravatar_url(user.email, 30),
77 77 'profile_link': h.link_to_user(user),
78 78 'value_display': h.escape(h.person(user)),
79 79 'value': user.username,
80 80 'value_type': 'user',
81 81 'active': user.active,
82 82 }
83 83
84 84 def get_users(self, name_contains=None, limit=20, only_active=True):
85 85
86 86 query = self.sa.query(User)
87 87 if only_active:
88 88 query = query.filter(User.active == true())
89 89
90 90 if name_contains:
91 91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
92 92 query = query.filter(
93 93 or_(
94 94 User.name.ilike(ilike_expression),
95 95 User.lastname.ilike(ilike_expression),
96 96 User.username.ilike(ilike_expression)
97 97 )
98 98 )
99 99 query = query.limit(limit)
100 100 users = query.all()
101 101
102 102 _users = [
103 103 self._serialize_user(user) for user in users
104 104 ]
105 105 return _users
106 106
107 107 def get_by_username(self, username, cache=False, case_insensitive=False):
108 108
109 109 if case_insensitive:
110 110 user = self.sa.query(User).filter(User.username.ilike(username))
111 111 else:
112 112 user = self.sa.query(User)\
113 113 .filter(User.username == username)
114 114 if cache:
115 115 name_key = _hash_key(username)
116 116 user = user.options(
117 117 FromCache("sql_cache_short", "get_user_%s" % name_key))
118 118 return user.scalar()
119 119
120 120 def get_by_email(self, email, cache=False, case_insensitive=False):
121 121 return User.get_by_email(email, case_insensitive, cache)
122 122
123 123 def get_by_auth_token(self, auth_token, cache=False):
124 124 return User.get_by_auth_token(auth_token, cache)
125 125
126 126 def get_active_user_count(self, cache=False):
127 127 qry = User.query().filter(
128 128 User.active == true()).filter(
129 129 User.username != User.DEFAULT_USER)
130 130 if cache:
131 131 qry = qry.options(
132 132 FromCache("sql_cache_short", "get_active_users"))
133 133 return qry.count()
134 134
135 135 def create(self, form_data, cur_user=None):
136 136 if not cur_user:
137 137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
138 138
139 139 user_data = {
140 140 'username': form_data['username'],
141 141 'password': form_data['password'],
142 142 'email': form_data['email'],
143 143 'firstname': form_data['firstname'],
144 144 'lastname': form_data['lastname'],
145 145 'active': form_data['active'],
146 146 'extern_type': form_data['extern_type'],
147 147 'extern_name': form_data['extern_name'],
148 148 'admin': False,
149 149 'cur_user': cur_user
150 150 }
151 151
152 152 if 'create_repo_group' in form_data:
153 153 user_data['create_repo_group'] = str2bool(
154 154 form_data.get('create_repo_group'))
155 155
156 156 try:
157 157 if form_data.get('password_change'):
158 158 user_data['force_password_change'] = True
159 159 return UserModel().create_or_update(**user_data)
160 160 except Exception:
161 161 log.error(traceback.format_exc())
162 162 raise
163 163
164 164 def update_user(self, user, skip_attrs=None, **kwargs):
165 165 from rhodecode.lib.auth import get_crypt_password
166 166
167 167 user = self._get_user(user)
168 168 if user.username == User.DEFAULT_USER:
169 169 raise DefaultUserException(
170 170 "You can't edit this user (`%(username)s`) since it's "
171 171 "crucial for entire application" % {
172 172 'username': user.username})
173 173
174 174 # first store only defaults
175 175 user_attrs = {
176 176 'updating_user_id': user.user_id,
177 177 'username': user.username,
178 178 'password': user.password,
179 179 'email': user.email,
180 180 'firstname': user.name,
181 181 'lastname': user.lastname,
182 'description': user.description,
182 183 'active': user.active,
183 184 'admin': user.admin,
184 185 'extern_name': user.extern_name,
185 186 'extern_type': user.extern_type,
186 187 'language': user.user_data.get('language')
187 188 }
188 189
189 190 # in case there's new_password, that comes from form, use it to
190 191 # store password
191 192 if kwargs.get('new_password'):
192 193 kwargs['password'] = kwargs['new_password']
193 194
194 195 # cleanups, my_account password change form
195 196 kwargs.pop('current_password', None)
196 197 kwargs.pop('new_password', None)
197 198
198 199 # cleanups, user edit password change form
199 200 kwargs.pop('password_confirmation', None)
200 201 kwargs.pop('password_change', None)
201 202
202 203 # create repo group on user creation
203 204 kwargs.pop('create_repo_group', None)
204 205
205 206 # legacy forms send name, which is the firstname
206 207 firstname = kwargs.pop('name', None)
207 208 if firstname:
208 209 kwargs['firstname'] = firstname
209 210
210 211 for k, v in kwargs.items():
211 212 # skip if we don't want to update this
212 213 if skip_attrs and k in skip_attrs:
213 214 continue
214 215
215 216 user_attrs[k] = v
216 217
217 218 try:
218 219 return self.create_or_update(**user_attrs)
219 220 except Exception:
220 221 log.error(traceback.format_exc())
221 222 raise
222 223
223 224 def create_or_update(
224 225 self, username, password, email, firstname='', lastname='',
225 226 active=True, admin=False, extern_type=None, extern_name=None,
226 227 cur_user=None, plugin=None, force_password_change=False,
227 228 allow_to_create_user=True, create_repo_group=None,
228 updating_user_id=None, language=None, strict_creation_check=True):
229 updating_user_id=None, language=None, description=None,
230 strict_creation_check=True):
229 231 """
230 232 Creates a new instance if not found, or updates current one
231 233
232 234 :param username:
233 235 :param password:
234 236 :param email:
235 237 :param firstname:
236 238 :param lastname:
237 239 :param active:
238 240 :param admin:
239 241 :param extern_type:
240 242 :param extern_name:
241 243 :param cur_user:
242 244 :param plugin: optional plugin this method was called from
243 245 :param force_password_change: toggles new or existing user flag
244 246 for password change
245 247 :param allow_to_create_user: Defines if the method can actually create
246 248 new users
247 249 :param create_repo_group: Defines if the method should also
248 250 create an repo group with user name, and owner
249 251 :param updating_user_id: if we set it up this is the user we want to
250 252 update this allows to editing username.
251 253 :param language: language of user from interface.
252 254
253 255 :returns: new User object with injected `is_new_user` attribute.
254 256 """
255 257
256 258 if not cur_user:
257 259 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
258 260
259 261 from rhodecode.lib.auth import (
260 262 get_crypt_password, check_password, generate_auth_token)
261 263 from rhodecode.lib.hooks_base import (
262 264 log_create_user, check_allowed_create_user)
263 265
264 266 def _password_change(new_user, password):
265 267 old_password = new_user.password or ''
266 268 # empty password
267 269 if not old_password:
268 270 return False
269 271
270 272 # password check is only needed for RhodeCode internal auth calls
271 273 # in case it's a plugin we don't care
272 274 if not plugin:
273 275
274 276 # first check if we gave crypted password back, and if it
275 277 # matches it's not password change
276 278 if new_user.password == password:
277 279 return False
278 280
279 281 password_match = check_password(password, old_password)
280 282 if not password_match:
281 283 return True
282 284
283 285 return False
284 286
285 287 # read settings on default personal repo group creation
286 288 if create_repo_group is None:
287 289 default_create_repo_group = RepoGroupModel()\
288 290 .get_default_create_personal_repo_group()
289 291 create_repo_group = default_create_repo_group
290 292
291 293 user_data = {
292 294 'username': username,
293 295 'password': password,
294 296 'email': email,
295 297 'firstname': firstname,
296 298 'lastname': lastname,
297 299 'active': active,
298 300 'admin': admin
299 301 }
300 302
301 303 if updating_user_id:
302 304 log.debug('Checking for existing account in RhodeCode '
303 305 'database with user_id `%s` ', updating_user_id)
304 306 user = User.get(updating_user_id)
305 307 else:
306 308 log.debug('Checking for existing account in RhodeCode '
307 309 'database with username `%s` ', username)
308 310 user = User.get_by_username(username, case_insensitive=True)
309 311
310 312 if user is None:
311 313 # we check internal flag if this method is actually allowed to
312 314 # create new user
313 315 if not allow_to_create_user:
314 316 msg = ('Method wants to create new user, but it is not '
315 317 'allowed to do so')
316 318 log.warning(msg)
317 319 raise NotAllowedToCreateUserError(msg)
318 320
319 321 log.debug('Creating new user %s', username)
320 322
321 323 # only if we create user that is active
322 324 new_active_user = active
323 325 if new_active_user and strict_creation_check:
324 326 # raises UserCreationError if it's not allowed for any reason to
325 327 # create new active user, this also executes pre-create hooks
326 328 check_allowed_create_user(user_data, cur_user, strict_check=True)
327 329 events.trigger(events.UserPreCreate(user_data))
328 330 new_user = User()
329 331 edit = False
330 332 else:
331 333 log.debug('updating user `%s`', username)
332 334 events.trigger(events.UserPreUpdate(user, user_data))
333 335 new_user = user
334 336 edit = True
335 337
336 338 # we're not allowed to edit default user
337 339 if user.username == User.DEFAULT_USER:
338 340 raise DefaultUserException(
339 341 "You can't edit this user (`%(username)s`) since it's "
340 342 "crucial for entire application"
341 343 % {'username': user.username})
342 344
343 345 # inject special attribute that will tell us if User is new or old
344 346 new_user.is_new_user = not edit
345 347 # for users that didn's specify auth type, we use RhodeCode built in
346 348 from rhodecode.authentication.plugins import auth_rhodecode
347 349 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
348 350 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
349 351
350 352 try:
351 353 new_user.username = username
352 354 new_user.admin = admin
353 355 new_user.email = email
354 356 new_user.active = active
355 357 new_user.extern_name = safe_unicode(extern_name)
356 358 new_user.extern_type = safe_unicode(extern_type)
357 359 new_user.name = firstname
358 360 new_user.lastname = lastname
361 new_user.description = description
359 362
360 363 # set password only if creating an user or password is changed
361 364 if not edit or _password_change(new_user, password):
362 365 reason = 'new password' if edit else 'new user'
363 366 log.debug('Updating password reason=>%s', reason)
364 367 new_user.password = get_crypt_password(password) if password else None
365 368
366 369 if force_password_change:
367 370 new_user.update_userdata(force_password_change=True)
368 371 if language:
369 372 new_user.update_userdata(language=language)
370 373 new_user.update_userdata(notification_status=True)
371 374
372 375 self.sa.add(new_user)
373 376
374 377 if not edit and create_repo_group:
375 378 RepoGroupModel().create_personal_repo_group(
376 379 new_user, commit_early=False)
377 380
378 381 if not edit:
379 382 # add the RSS token
380 383 self.add_auth_token(
381 384 user=username, lifetime_minutes=-1,
382 385 role=self.auth_token_role.ROLE_FEED,
383 386 description=u'Generated feed token')
384 387
385 388 kwargs = new_user.get_dict()
386 389 # backward compat, require api_keys present
387 390 kwargs['api_keys'] = kwargs['auth_tokens']
388 391 log_create_user(created_by=cur_user, **kwargs)
389 392 events.trigger(events.UserPostCreate(user_data))
390 393 return new_user
391 394 except (DatabaseError,):
392 395 log.error(traceback.format_exc())
393 396 raise
394 397
395 398 def create_registration(self, form_data,
396 399 extern_name='rhodecode', extern_type='rhodecode'):
397 400 from rhodecode.model.notification import NotificationModel
398 401 from rhodecode.model.notification import EmailNotificationModel
399 402
400 403 try:
401 404 form_data['admin'] = False
402 405 form_data['extern_name'] = extern_name
403 406 form_data['extern_type'] = extern_type
404 407 new_user = self.create(form_data)
405 408
406 409 self.sa.add(new_user)
407 410 self.sa.flush()
408 411
409 412 user_data = new_user.get_dict()
410 413 kwargs = {
411 414 # use SQLALCHEMY safe dump of user data
412 415 'user': AttributeDict(user_data),
413 416 'date': datetime.datetime.now()
414 417 }
415 418 notification_type = EmailNotificationModel.TYPE_REGISTRATION
416 419 # pre-generate the subject for notification itself
417 420 (subject,
418 421 _h, _e, # we don't care about those
419 422 body_plaintext) = EmailNotificationModel().render_email(
420 423 notification_type, **kwargs)
421 424
422 425 # create notification objects, and emails
423 426 NotificationModel().create(
424 427 created_by=new_user,
425 428 notification_subject=subject,
426 429 notification_body=body_plaintext,
427 430 notification_type=notification_type,
428 431 recipients=None, # all admins
429 432 email_kwargs=kwargs,
430 433 )
431 434
432 435 return new_user
433 436 except Exception:
434 437 log.error(traceback.format_exc())
435 438 raise
436 439
437 440 def _handle_user_repos(self, username, repositories, handle_mode=None):
438 441 _superadmin = self.cls.get_first_super_admin()
439 442 left_overs = True
440 443
441 444 from rhodecode.model.repo import RepoModel
442 445
443 446 if handle_mode == 'detach':
444 447 for obj in repositories:
445 448 obj.user = _superadmin
446 449 # set description we know why we super admin now owns
447 450 # additional repositories that were orphaned !
448 451 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
449 452 self.sa.add(obj)
450 453 left_overs = False
451 454 elif handle_mode == 'delete':
452 455 for obj in repositories:
453 456 RepoModel().delete(obj, forks='detach')
454 457 left_overs = False
455 458
456 459 # if nothing is done we have left overs left
457 460 return left_overs
458 461
459 462 def _handle_user_repo_groups(self, username, repository_groups,
460 463 handle_mode=None):
461 464 _superadmin = self.cls.get_first_super_admin()
462 465 left_overs = True
463 466
464 467 from rhodecode.model.repo_group import RepoGroupModel
465 468
466 469 if handle_mode == 'detach':
467 470 for r in repository_groups:
468 471 r.user = _superadmin
469 472 # set description we know why we super admin now owns
470 473 # additional repositories that were orphaned !
471 474 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
472 475 r.personal = False
473 476 self.sa.add(r)
474 477 left_overs = False
475 478 elif handle_mode == 'delete':
476 479 for r in repository_groups:
477 480 RepoGroupModel().delete(r)
478 481 left_overs = False
479 482
480 483 # if nothing is done we have left overs left
481 484 return left_overs
482 485
483 486 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
484 487 _superadmin = self.cls.get_first_super_admin()
485 488 left_overs = True
486 489
487 490 from rhodecode.model.user_group import UserGroupModel
488 491
489 492 if handle_mode == 'detach':
490 493 for r in user_groups:
491 494 for user_user_group_to_perm in r.user_user_group_to_perm:
492 495 if user_user_group_to_perm.user.username == username:
493 496 user_user_group_to_perm.user = _superadmin
494 497 r.user = _superadmin
495 498 # set description we know why we super admin now owns
496 499 # additional repositories that were orphaned !
497 500 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
498 501 self.sa.add(r)
499 502 left_overs = False
500 503 elif handle_mode == 'delete':
501 504 for r in user_groups:
502 505 UserGroupModel().delete(r)
503 506 left_overs = False
504 507
505 508 # if nothing is done we have left overs left
506 509 return left_overs
507 510
508 511 def _handle_user_artifacts(self, username, artifacts, handle_mode=None):
509 512 _superadmin = self.cls.get_first_super_admin()
510 513 left_overs = True
511 514
512 515 if handle_mode == 'detach':
513 516 for a in artifacts:
514 517 a.upload_user = _superadmin
515 518 # set description we know why we super admin now owns
516 519 # additional artifacts that were orphaned !
517 520 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
518 521 self.sa.add(a)
519 522 left_overs = False
520 523 elif handle_mode == 'delete':
521 524 from rhodecode.apps.file_store import utils as store_utils
522 525 storage = store_utils.get_file_storage(self.request.registry.settings)
523 526 for a in artifacts:
524 527 file_uid = a.file_uid
525 528 storage.delete(file_uid)
526 529 self.sa.delete(a)
527 530
528 531 left_overs = False
529 532
530 533 # if nothing is done we have left overs left
531 534 return left_overs
532 535
533 536 def delete(self, user, cur_user=None, handle_repos=None,
534 537 handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None):
535 538 from rhodecode.lib.hooks_base import log_delete_user
536 539
537 540 if not cur_user:
538 541 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
539 542 user = self._get_user(user)
540 543
541 544 try:
542 545 if user.username == User.DEFAULT_USER:
543 546 raise DefaultUserException(
544 547 u"You can't remove this user since it's"
545 548 u" crucial for entire application")
546 549
547 550 left_overs = self._handle_user_repos(
548 551 user.username, user.repositories, handle_repos)
549 552 if left_overs and user.repositories:
550 553 repos = [x.repo_name for x in user.repositories]
551 554 raise UserOwnsReposException(
552 555 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
553 556 u'removed. Switch owners or remove those repositories:%(list_repos)s'
554 557 % {'username': user.username, 'len_repos': len(repos),
555 558 'list_repos': ', '.join(repos)})
556 559
557 560 left_overs = self._handle_user_repo_groups(
558 561 user.username, user.repository_groups, handle_repo_groups)
559 562 if left_overs and user.repository_groups:
560 563 repo_groups = [x.group_name for x in user.repository_groups]
561 564 raise UserOwnsRepoGroupsException(
562 565 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
563 566 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
564 567 % {'username': user.username, 'len_repo_groups': len(repo_groups),
565 568 'list_repo_groups': ', '.join(repo_groups)})
566 569
567 570 left_overs = self._handle_user_user_groups(
568 571 user.username, user.user_groups, handle_user_groups)
569 572 if left_overs and user.user_groups:
570 573 user_groups = [x.users_group_name for x in user.user_groups]
571 574 raise UserOwnsUserGroupsException(
572 575 u'user "%s" still owns %s user groups and cannot be '
573 576 u'removed. Switch owners or remove those user groups:%s'
574 577 % (user.username, len(user_groups), ', '.join(user_groups)))
575 578
576 579 left_overs = self._handle_user_artifacts(
577 580 user.username, user.artifacts, handle_artifacts)
578 581 if left_overs and user.artifacts:
579 582 artifacts = [x.file_uid for x in user.artifacts]
580 583 raise UserOwnsArtifactsException(
581 584 u'user "%s" still owns %s artifacts and cannot be '
582 585 u'removed. Switch owners or remove those artifacts:%s'
583 586 % (user.username, len(artifacts), ', '.join(artifacts)))
584 587
585 588 user_data = user.get_dict() # fetch user data before expire
586 589
587 590 # we might change the user data with detach/delete, make sure
588 591 # the object is marked as expired before actually deleting !
589 592 self.sa.expire(user)
590 593 self.sa.delete(user)
591 594
592 595 log_delete_user(deleted_by=cur_user, **user_data)
593 596 except Exception:
594 597 log.error(traceback.format_exc())
595 598 raise
596 599
597 600 def reset_password_link(self, data, pwd_reset_url):
598 601 from rhodecode.lib.celerylib import tasks, run_task
599 602 from rhodecode.model.notification import EmailNotificationModel
600 603 user_email = data['email']
601 604 try:
602 605 user = User.get_by_email(user_email)
603 606 if user:
604 607 log.debug('password reset user found %s', user)
605 608
606 609 email_kwargs = {
607 610 'password_reset_url': pwd_reset_url,
608 611 'user': user,
609 612 'email': user_email,
610 613 'date': datetime.datetime.now()
611 614 }
612 615
613 616 (subject, headers, email_body,
614 617 email_body_plaintext) = EmailNotificationModel().render_email(
615 618 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
616 619
617 620 recipients = [user_email]
618 621
619 622 action_logger_generic(
620 623 'sending password reset email to user: {}'.format(
621 624 user), namespace='security.password_reset')
622 625
623 626 run_task(tasks.send_email, recipients, subject,
624 627 email_body_plaintext, email_body)
625 628
626 629 else:
627 630 log.debug("password reset email %s not found", user_email)
628 631 except Exception:
629 632 log.error(traceback.format_exc())
630 633 return False
631 634
632 635 return True
633 636
634 637 def reset_password(self, data):
635 638 from rhodecode.lib.celerylib import tasks, run_task
636 639 from rhodecode.model.notification import EmailNotificationModel
637 640 from rhodecode.lib import auth
638 641 user_email = data['email']
639 642 pre_db = True
640 643 try:
641 644 user = User.get_by_email(user_email)
642 645 new_passwd = auth.PasswordGenerator().gen_password(
643 646 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
644 647 if user:
645 648 user.password = auth.get_crypt_password(new_passwd)
646 649 # also force this user to reset his password !
647 650 user.update_userdata(force_password_change=True)
648 651
649 652 Session().add(user)
650 653
651 654 # now delete the token in question
652 655 UserApiKeys = AuthTokenModel.cls
653 656 UserApiKeys().query().filter(
654 657 UserApiKeys.api_key == data['token']).delete()
655 658
656 659 Session().commit()
657 660 log.info('successfully reset password for `%s`', user_email)
658 661
659 662 if new_passwd is None:
660 663 raise Exception('unable to generate new password')
661 664
662 665 pre_db = False
663 666
664 667 email_kwargs = {
665 668 'new_password': new_passwd,
666 669 'user': user,
667 670 'email': user_email,
668 671 'date': datetime.datetime.now()
669 672 }
670 673
671 674 (subject, headers, email_body,
672 675 email_body_plaintext) = EmailNotificationModel().render_email(
673 676 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
674 677 **email_kwargs)
675 678
676 679 recipients = [user_email]
677 680
678 681 action_logger_generic(
679 682 'sent new password to user: {} with email: {}'.format(
680 683 user, user_email), namespace='security.password_reset')
681 684
682 685 run_task(tasks.send_email, recipients, subject,
683 686 email_body_plaintext, email_body)
684 687
685 688 except Exception:
686 689 log.error('Failed to update user password')
687 690 log.error(traceback.format_exc())
688 691 if pre_db:
689 692 # we rollback only if local db stuff fails. If it goes into
690 693 # run_task, we're pass rollback state this wouldn't work then
691 694 Session().rollback()
692 695
693 696 return True
694 697
695 698 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
696 699 """
697 700 Fetches auth_user by user_id,or api_key if present.
698 701 Fills auth_user attributes with those taken from database.
699 702 Additionally set's is_authenitated if lookup fails
700 703 present in database
701 704
702 705 :param auth_user: instance of user to set attributes
703 706 :param user_id: user id to fetch by
704 707 :param api_key: api key to fetch by
705 708 :param username: username to fetch by
706 709 """
707 710 def token_obfuscate(token):
708 711 if token:
709 712 return token[:4] + "****"
710 713
711 714 if user_id is None and api_key is None and username is None:
712 715 raise Exception('You need to pass user_id, api_key or username')
713 716
714 717 log.debug(
715 718 'AuthUser: fill data execution based on: '
716 719 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
717 720 try:
718 721 dbuser = None
719 722 if user_id:
720 723 dbuser = self.get(user_id)
721 724 elif api_key:
722 725 dbuser = self.get_by_auth_token(api_key)
723 726 elif username:
724 727 dbuser = self.get_by_username(username)
725 728
726 729 if not dbuser:
727 730 log.warning(
728 731 'Unable to lookup user by id:%s api_key:%s username:%s',
729 732 user_id, token_obfuscate(api_key), username)
730 733 return False
731 734 if not dbuser.active:
732 735 log.debug('User `%s:%s` is inactive, skipping fill data',
733 736 username, user_id)
734 737 return False
735 738
736 739 log.debug('AuthUser: filling found user:%s data', dbuser)
737 740
738 741 attrs = {
739 742 'user_id': dbuser.user_id,
740 743 'username': dbuser.username,
741 744 'name': dbuser.name,
742 745 'first_name': dbuser.first_name,
743 746 'firstname': dbuser.firstname,
744 747 'last_name': dbuser.last_name,
745 748 'lastname': dbuser.lastname,
746 749 'admin': dbuser.admin,
747 750 'active': dbuser.active,
748 751
749 752 'email': dbuser.email,
750 753 'emails': dbuser.emails_cached(),
751 754 'short_contact': dbuser.short_contact,
752 755 'full_contact': dbuser.full_contact,
753 756 'full_name': dbuser.full_name,
754 757 'full_name_or_username': dbuser.full_name_or_username,
755 758
756 759 '_api_key': dbuser._api_key,
757 760 '_user_data': dbuser._user_data,
758 761
759 762 'created_on': dbuser.created_on,
760 763 'extern_name': dbuser.extern_name,
761 764 'extern_type': dbuser.extern_type,
762 765
763 766 'inherit_default_permissions': dbuser.inherit_default_permissions,
764 767
765 768 'language': dbuser.language,
766 769 'last_activity': dbuser.last_activity,
767 770 'last_login': dbuser.last_login,
768 771 'password': dbuser.password,
769 772 }
770 773 auth_user.__dict__.update(attrs)
771 774 except Exception:
772 775 log.error(traceback.format_exc())
773 776 auth_user.is_authenticated = False
774 777 return False
775 778
776 779 return True
777 780
778 781 def has_perm(self, user, perm):
779 782 perm = self._get_perm(perm)
780 783 user = self._get_user(user)
781 784
782 785 return UserToPerm.query().filter(UserToPerm.user == user)\
783 786 .filter(UserToPerm.permission == perm).scalar() is not None
784 787
785 788 def grant_perm(self, user, perm):
786 789 """
787 790 Grant user global permissions
788 791
789 792 :param user:
790 793 :param perm:
791 794 """
792 795 user = self._get_user(user)
793 796 perm = self._get_perm(perm)
794 797 # if this permission is already granted skip it
795 798 _perm = UserToPerm.query()\
796 799 .filter(UserToPerm.user == user)\
797 800 .filter(UserToPerm.permission == perm)\
798 801 .scalar()
799 802 if _perm:
800 803 return
801 804 new = UserToPerm()
802 805 new.user = user
803 806 new.permission = perm
804 807 self.sa.add(new)
805 808 return new
806 809
807 810 def revoke_perm(self, user, perm):
808 811 """
809 812 Revoke users global permissions
810 813
811 814 :param user:
812 815 :param perm:
813 816 """
814 817 user = self._get_user(user)
815 818 perm = self._get_perm(perm)
816 819
817 820 obj = UserToPerm.query()\
818 821 .filter(UserToPerm.user == user)\
819 822 .filter(UserToPerm.permission == perm)\
820 823 .scalar()
821 824 if obj:
822 825 self.sa.delete(obj)
823 826
824 827 def add_extra_email(self, user, email):
825 828 """
826 829 Adds email address to UserEmailMap
827 830
828 831 :param user:
829 832 :param email:
830 833 """
831 834
832 835 user = self._get_user(user)
833 836
834 837 obj = UserEmailMap()
835 838 obj.user = user
836 839 obj.email = email
837 840 self.sa.add(obj)
838 841 return obj
839 842
840 843 def delete_extra_email(self, user, email_id):
841 844 """
842 845 Removes email address from UserEmailMap
843 846
844 847 :param user:
845 848 :param email_id:
846 849 """
847 850 user = self._get_user(user)
848 851 obj = UserEmailMap.query().get(email_id)
849 852 if obj and obj.user_id == user.user_id:
850 853 self.sa.delete(obj)
851 854
852 855 def parse_ip_range(self, ip_range):
853 856 ip_list = []
854 857
855 858 def make_unique(value):
856 859 seen = []
857 860 return [c for c in value if not (c in seen or seen.append(c))]
858 861
859 862 # firsts split by commas
860 863 for ip_range in ip_range.split(','):
861 864 if not ip_range:
862 865 continue
863 866 ip_range = ip_range.strip()
864 867 if '-' in ip_range:
865 868 start_ip, end_ip = ip_range.split('-', 1)
866 869 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
867 870 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
868 871 parsed_ip_range = []
869 872
870 873 for index in xrange(int(start_ip), int(end_ip) + 1):
871 874 new_ip = ipaddress.ip_address(index)
872 875 parsed_ip_range.append(str(new_ip))
873 876 ip_list.extend(parsed_ip_range)
874 877 else:
875 878 ip_list.append(ip_range)
876 879
877 880 return make_unique(ip_list)
878 881
879 882 def add_extra_ip(self, user, ip, description=None):
880 883 """
881 884 Adds ip address to UserIpMap
882 885
883 886 :param user:
884 887 :param ip:
885 888 """
886 889
887 890 user = self._get_user(user)
888 891 obj = UserIpMap()
889 892 obj.user = user
890 893 obj.ip_addr = ip
891 894 obj.description = description
892 895 self.sa.add(obj)
893 896 return obj
894 897
895 898 auth_token_role = AuthTokenModel.cls
896 899
897 900 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
898 901 scope_callback=None):
899 902 """
900 903 Add AuthToken for user.
901 904
902 905 :param user: username/user_id
903 906 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
904 907 :param role: one of AuthTokenModel.cls.ROLE_*
905 908 :param description: optional string description
906 909 """
907 910
908 911 token = AuthTokenModel().create(
909 912 user, description, lifetime_minutes, role)
910 913 if scope_callback and callable(scope_callback):
911 914 # call the callback if we provide, used to attach scope for EE edition
912 915 scope_callback(token)
913 916 return token
914 917
915 918 def delete_extra_ip(self, user, ip_id):
916 919 """
917 920 Removes ip address from UserIpMap
918 921
919 922 :param user:
920 923 :param ip_id:
921 924 """
922 925 user = self._get_user(user)
923 926 obj = UserIpMap.query().get(ip_id)
924 927 if obj and obj.user_id == user.user_id:
925 928 self.sa.delete(obj)
926 929
927 930 def get_accounts_in_creation_order(self, current_user=None):
928 931 """
929 932 Get accounts in order of creation for deactivation for license limits
930 933
931 934 pick currently logged in user, and append to the list in position 0
932 935 pick all super-admins in order of creation date and add it to the list
933 936 pick all other accounts in order of creation and add it to the list.
934 937
935 938 Based on that list, the last accounts can be disabled as they are
936 939 created at the end and don't include any of the super admins as well
937 940 as the current user.
938 941
939 942 :param current_user: optionally current user running this operation
940 943 """
941 944
942 945 if not current_user:
943 946 current_user = get_current_rhodecode_user()
944 947 active_super_admins = [
945 948 x.user_id for x in User.query()
946 949 .filter(User.user_id != current_user.user_id)
947 950 .filter(User.active == true())
948 951 .filter(User.admin == true())
949 952 .order_by(User.created_on.asc())]
950 953
951 954 active_regular_users = [
952 955 x.user_id for x in User.query()
953 956 .filter(User.user_id != current_user.user_id)
954 957 .filter(User.active == true())
955 958 .filter(User.admin == false())
956 959 .order_by(User.created_on.asc())]
957 960
958 961 list_of_accounts = [current_user.user_id]
959 962 list_of_accounts += active_super_admins
960 963 list_of_accounts += active_regular_users
961 964
962 965 return list_of_accounts
963 966
964 967 def deactivate_last_users(self, expected_users, current_user=None):
965 968 """
966 969 Deactivate accounts that are over the license limits.
967 970 Algorithm of which accounts to disabled is based on the formula:
968 971
969 972 Get current user, then super admins in creation order, then regular
970 973 active users in creation order.
971 974
972 975 Using that list we mark all accounts from the end of it as inactive.
973 976 This way we block only latest created accounts.
974 977
975 978 :param expected_users: list of users in special order, we deactivate
976 979 the end N amount of users from that list
977 980 """
978 981
979 982 list_of_accounts = self.get_accounts_in_creation_order(
980 983 current_user=current_user)
981 984
982 985 for acc_id in list_of_accounts[expected_users + 1:]:
983 986 user = User.get(acc_id)
984 987 log.info('Deactivating account %s for license unlock', user)
985 988 user.active = False
986 989 Session().add(user)
987 990 Session().commit()
988 991
989 992 return
990 993
991 994 def get_user_log(self, user, filter_term):
992 995 user_log = UserLog.query()\
993 996 .filter(or_(UserLog.user_id == user.user_id,
994 997 UserLog.username == user.username))\
995 998 .options(joinedload(UserLog.user))\
996 999 .options(joinedload(UserLog.repository))\
997 1000 .order_by(UserLog.action_date.desc())
998 1001
999 1002 user_log = user_log_filter(user_log, filter_term)
1000 1003 return user_log
@@ -1,188 +1,195 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 re
22 22 import colander
23 23
24 24 from rhodecode import forms
25 25 from rhodecode.model.db import User, UserEmailMap
26 26 from rhodecode.model.validation_schema import types, validators
27 27 from rhodecode.translation import _
28 28 from rhodecode.lib.auth import check_password
29 29 from rhodecode.lib import helpers as h
30 30
31 31
32 32 @colander.deferred
33 33 def deferred_user_password_validator(node, kw):
34 34 username = kw.get('username')
35 35 user = User.get_by_username(username)
36 36
37 37 def _user_password_validator(node, value):
38 38 if not check_password(value, user.password):
39 39 msg = _('Password is incorrect')
40 40 raise colander.Invalid(node, msg)
41 41 return _user_password_validator
42 42
43 43
44 44
45 45 class ChangePasswordSchema(colander.Schema):
46 46
47 47 current_password = colander.SchemaNode(
48 48 colander.String(),
49 49 missing=colander.required,
50 50 widget=forms.widget.PasswordWidget(redisplay=True),
51 51 validator=deferred_user_password_validator)
52 52
53 53 new_password = colander.SchemaNode(
54 54 colander.String(),
55 55 missing=colander.required,
56 56 widget=forms.widget.CheckedPasswordWidget(redisplay=True),
57 57 validator=colander.Length(min=6))
58 58
59 59 def validator(self, form, values):
60 60 if values['current_password'] == values['new_password']:
61 61 exc = colander.Invalid(form)
62 62 exc['new_password'] = _('New password must be different '
63 63 'to old password')
64 64 raise exc
65 65
66 66
67 67 @colander.deferred
68 68 def deferred_username_validator(node, kw):
69 69
70 70 def name_validator(node, value):
71 71 msg = _(
72 72 u'Username may only contain alphanumeric characters '
73 73 u'underscores, periods or dashes and must begin with '
74 74 u'alphanumeric character or underscore')
75 75
76 76 if not re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value):
77 77 raise colander.Invalid(node, msg)
78 78
79 79 return name_validator
80 80
81 81
82 82 @colander.deferred
83 83 def deferred_email_validator(node, kw):
84 84 # NOTE(marcink): we might provide uniqueness validation later here...
85 85 return colander.Email()
86 86
87 87
88 88 class UserSchema(colander.Schema):
89 89 username = colander.SchemaNode(
90 90 colander.String(),
91 91 validator=deferred_username_validator)
92 92
93 93 email = colander.SchemaNode(
94 94 colander.String(),
95 95 validator=deferred_email_validator)
96 96
97 97 password = colander.SchemaNode(
98 98 colander.String(), missing='')
99 99
100 100 first_name = colander.SchemaNode(
101 101 colander.String(), missing='')
102 102
103 103 last_name = colander.SchemaNode(
104 104 colander.String(), missing='')
105 105
106 106 active = colander.SchemaNode(
107 107 types.StringBooleanType(),
108 108 missing=False)
109 109
110 110 admin = colander.SchemaNode(
111 111 types.StringBooleanType(),
112 112 missing=False)
113 113
114 114 extern_name = colander.SchemaNode(
115 115 colander.String(), missing='')
116 116
117 117 extern_type = colander.SchemaNode(
118 118 colander.String(), missing='')
119 119
120 120 def deserialize(self, cstruct):
121 121 """
122 122 Custom deserialize that allows to chain validation, and verify
123 123 permissions, and as last step uniqueness
124 124 """
125 125
126 126 appstruct = super(UserSchema, self).deserialize(cstruct)
127 127 return appstruct
128 128
129 129
130 130 @colander.deferred
131 131 def deferred_user_email_in_emails_validator(node, kw):
132 132 return colander.OneOf(kw.get('user_emails'))
133 133
134 134
135 135 @colander.deferred
136 136 def deferred_additional_email_validator(node, kw):
137 137 emails = kw.get('user_emails')
138 138
139 139 def name_validator(node, value):
140 140 if value in emails:
141 141 msg = _('This e-mail address is already taken')
142 142 raise colander.Invalid(node, msg)
143 143 user = User.get_by_email(value, case_insensitive=True)
144 144 if user:
145 145 msg = _(u'This e-mail address is already taken')
146 146 raise colander.Invalid(node, msg)
147 147 c = colander.Email()
148 148 return c(node, value)
149 149 return name_validator
150 150
151 151
152 152 @colander.deferred
153 153 def deferred_user_email_in_emails_widget(node, kw):
154 154 import deform.widget
155 155 emails = [(email, email) for email in kw.get('user_emails')]
156 156 return deform.widget.Select2Widget(values=emails)
157 157
158 158
159 159 class UserProfileSchema(colander.Schema):
160 160 username = colander.SchemaNode(
161 161 colander.String(),
162 162 validator=deferred_username_validator)
163 163
164 164 firstname = colander.SchemaNode(
165 165 colander.String(), missing='', title='First name')
166 166
167 167 lastname = colander.SchemaNode(
168 168 colander.String(), missing='', title='Last name')
169 169
170 description = colander.SchemaNode(
171 colander.String(), missing='', title='Personal Description',
172 widget=forms.widget.TextAreaWidget(),
173 validator=colander.Length(max=250)
174 )
175
170 176 email = colander.SchemaNode(
171 177 colander.String(), widget=deferred_user_email_in_emails_widget,
172 178 validator=deferred_user_email_in_emails_validator,
173 179 description=h.literal(
174 180 _('Additional emails can be specified at <a href="{}">extra emails</a> page.').format(
175 181 '/_admin/my_account/emails')),
176 182 )
177 183
178 184
185
179 186 class AddEmailSchema(colander.Schema):
180 187 current_password = colander.SchemaNode(
181 188 colander.String(),
182 189 missing=colander.required,
183 190 widget=forms.widget.PasswordWidget(redisplay=True),
184 191 validator=deferred_user_password_validator)
185 192
186 193 email = colander.SchemaNode(
187 194 colander.String(), title='New Email',
188 195 validator=deferred_additional_email_validator)
@@ -1,147 +1,155 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default user-profile">
4 4 <div class="panel-heading">
5 5 <h3 class="panel-title">${_('User Profile')}</h3>
6 6 </div>
7 7 <div class="panel-body">
8 8 <div class="user-profile-content">
9 9 ${h.secure_form(h.route_path('user_update', user_id=c.user.user_id), class_='form', request=request)}
10 10 <% readonly = None %>
11 11 <% disabled = "" %>
12 12 %if c.extern_type != 'rhodecode':
13 13 <% readonly = "readonly" %>
14 14 <% disabled = " disabled" %>
15 15 <div class="alert-warning" style="margin:0px 0px 20px 0px; padding: 10px">
16 16 <strong>${_('This user was created from external source (%s). Editing some of the settings is limited.' % c.extern_type)}</strong>
17 17 </div>
18 18 %endif
19 19 <div class="form">
20 20 <div class="fields">
21 21 <div class="field">
22 22 <div class="label photo">
23 23 ${_('Photo')}:
24 24 </div>
25 25 <div class="input profile">
26 26 %if c.visual.use_gravatar:
27 27 ${base.gravatar(c.user.email, 100)}
28 28 <p class="help-block">${_('Change the avatar at')} <a href="http://gravatar.com">gravatar.com</a>.</p>
29 29 %else:
30 30 ${base.gravatar(c.user.email, 100)}
31 31 %endif
32 32 </div>
33 33 </div>
34 34 <div class="field">
35 35 <div class="label">
36 36 ${_('Username')}:
37 37 </div>
38 38 <div class="input">
39 39 ${h.text('username', class_='%s medium' % disabled, readonly=readonly)}
40 40 </div>
41 41 </div>
42 42 <div class="field">
43 43 <div class="label">
44 44 <label for="name">${_('First Name')}:</label>
45 45 </div>
46 46 <div class="input">
47 47 ${h.text('firstname', class_="medium")}
48 48 </div>
49 49 </div>
50 50
51 51 <div class="field">
52 52 <div class="label">
53 53 <label for="lastname">${_('Last Name')}:</label>
54 54 </div>
55 55 <div class="input">
56 56 ${h.text('lastname', class_="medium")}
57 57 </div>
58 58 </div>
59 59
60 60 <div class="field">
61 61 <div class="label">
62 62 <label for="email">${_('Email')}:</label>
63 63 </div>
64 64 <div class="input">
65 65 ## we should be able to edit email !
66 66 ${h.text('email', class_="medium")}
67 67 </div>
68 68 </div>
69 69 <div class="field">
70 70 <div class="label">
71 <label for="description">${_('Description')}:</label>
72 </div>
73 <div class="input textarea editor">
74 ${h.textarea('description', class_="medium")}
75 </div>
76 </div>
77 <div class="field">
78 <div class="label">
71 79 ${_('New Password')}:
72 80 </div>
73 81 <div class="input">
74 82 ${h.password('new_password',class_='%s medium' % disabled,autocomplete="off",readonly=readonly)}
75 83 </div>
76 84 </div>
77 85 <div class="field">
78 86 <div class="label">
79 87 ${_('New Password Confirmation')}:
80 88 </div>
81 89 <div class="input">
82 90 ${h.password('password_confirmation',class_="%s medium" % disabled,autocomplete="off",readonly=readonly)}
83 91 </div>
84 92 </div>
85 93 <div class="field">
86 94 <div class="label-text">
87 95 ${_('Active')}:
88 96 </div>
89 97 <div class="input user-checkbox">
90 98 ${h.checkbox('active',value=True)}
91 99 </div>
92 100 </div>
93 101 <div class="field">
94 102 <div class="label-text">
95 103 ${_('Super Admin')}:
96 104 </div>
97 105 <div class="input user-checkbox">
98 106 ${h.checkbox('admin',value=True)}
99 107 </div>
100 108 </div>
101 109 <div class="field">
102 110 <div class="label-text">
103 111 ${_('Authentication type')}:
104 112 </div>
105 113 <div class="input">
106 114 ${h.select('extern_type', c.extern_type, c.allowed_extern_types)}
107 115 <p class="help-block">${_('When user was created using an external source. He is bound to authentication using this method.')}</p>
108 116 </div>
109 117 </div>
110 118 <div class="field">
111 119 <div class="label-text">
112 120 ${_('Name in Source of Record')}:
113 121 </div>
114 122 <div class="input">
115 123 <p>${c.extern_name}</p>
116 124 ${h.hidden('extern_name', readonly="readonly")}
117 125 </div>
118 126 </div>
119 127 <div class="field">
120 128 <div class="label">
121 129 ${_('Language')}:
122 130 </div>
123 131 <div class="input">
124 132 ## allowed_languages is defined in the users.py
125 133 ## c.language comes from base.py as a default language
126 134 ${h.select('language', c.language, c.allowed_languages)}
127 135 <p class="help-block">${h.literal(_('User interface language. Help translate %(rc_link)s into your language.') % {'rc_link': h.link_to('RhodeCode Enterprise', h.route_url('rhodecode_translations'))})}</p>
128 136 </div>
129 137 </div>
130 138 <div class="buttons">
131 139 ${h.submit('save', _('Save'), class_="btn")}
132 140 ${h.reset('reset', _('Reset'), class_="btn")}
133 141 </div>
134 142 </div>
135 143 </div>
136 144 ${h.end_form()}
137 145 </div>
138 146 </div>
139 147 </div>
140 148
141 149 <script>
142 150 $('#language').select2({
143 151 'containerCssClass': "drop-menu",
144 152 'dropdownCssClass': "drop-menu-dropdown",
145 153 'dropdownAutoWidth': true
146 154 });
147 155 </script>
General Comments 0
You need to be logged in to leave comments. Login now