##// END OF EJS Templates
fix(user-models): added extra protection against model username changes that would create duplicates
super-admin -
r5352:77661e7b default
parent child Browse files
Show More
@@ -1,783 +1,784 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import datetime
21 21 import string
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25 import peppercorn
26 26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
27 27
28 28 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 29 from rhodecode import forms
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib import ext_json
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, NotAnonymous, CSRFRequired,
35 35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
36 36 from rhodecode.lib.channelstream import (
37 37 channelstream_request, ChannelstreamException)
38 38 from rhodecode.lib.hash_utils import md5_safe
39 39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 40 from rhodecode.model.auth_token import AuthTokenModel
41 41 from rhodecode.model.comment import CommentsModel
42 42 from rhodecode.model.db import (
43 43 IntegrityError, or_, in_filter_generator,
44 44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.pull_request import PullRequestModel
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.user_group import UserGroupModel
50 50 from rhodecode.model.validation_schema.schemas import user_schema
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MyAccountView(BaseAppView, DataGridAppView):
56 56 ALLOW_SCOPED_TOKENS = False
57 57 """
58 58 This view has alternative version inside EE, if modified please take a look
59 59 in there as well.
60 60 """
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context()
64 64 c.user = c.auth_user.get_instance()
65 65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 66 return c
67 67
68 68 @LoginRequired()
69 69 @NotAnonymous()
70 70 def my_account_profile(self):
71 71 c = self.load_default_context()
72 72 c.active = 'profile'
73 73 c.extern_type = c.user.extern_type
74 74 return self._get_template_context(c)
75 75
76 76 @LoginRequired()
77 77 @NotAnonymous()
78 78 def my_account_edit(self):
79 79 c = self.load_default_context()
80 80 c.active = 'profile_edit'
81 81 c.extern_type = c.user.extern_type
82 82 c.extern_name = c.user.extern_name
83 83
84 84 schema = user_schema.UserProfileSchema().bind(
85 85 username=c.user.username, user_emails=c.user.emails)
86 86 appstruct = {
87 87 'username': c.user.username,
88 88 'email': c.user.email,
89 89 'firstname': c.user.firstname,
90 90 'lastname': c.user.lastname,
91 91 'description': c.user.description,
92 92 }
93 93 c.form = forms.RcForm(
94 94 schema, appstruct=appstruct,
95 95 action=h.route_path('my_account_update'),
96 96 buttons=(forms.buttons.save, forms.buttons.reset))
97 97
98 98 return self._get_template_context(c)
99 99
100 100 @LoginRequired()
101 101 @NotAnonymous()
102 102 @CSRFRequired()
103 103 def my_account_update(self):
104 104 _ = self.request.translate
105 105 c = self.load_default_context()
106 106 c.active = 'profile_edit'
107 107 c.perm_user = c.auth_user
108 108 c.extern_type = c.user.extern_type
109 109 c.extern_name = c.user.extern_name
110 110
111 111 schema = user_schema.UserProfileSchema().bind(
112 112 username=c.user.username, user_emails=c.user.emails)
113 113 form = forms.RcForm(
114 114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115 115
116 116 controls = list(self.request.POST.items())
117 117 try:
118 118 valid_data = form.validate(controls)
119 119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 120 'new_password', 'password_confirmation']
121 121 if c.extern_type != "rhodecode":
122 122 # forbid updating username for external accounts
123 123 skip_attrs.append('username')
124 124 old_email = c.user.email
125 125 UserModel().update_user(
126 126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
127 127 **valid_data)
128 128 if old_email != valid_data['email']:
129 129 old = UserEmailMap.query() \
130 130 .filter(UserEmailMap.user == c.user)\
131 131 .filter(UserEmailMap.email == valid_data['email'])\
132 132 .first()
133 133 old.email = old_email
134 134 h.flash(_('Your account was updated successfully'), category='success')
135 135 Session().commit()
136 136 except forms.ValidationFailure as e:
137 137 c.form = e
138 138 return self._get_template_context(c)
139
139 140 except Exception:
140 141 log.exception("Exception updating user")
141 142 h.flash(_('Error occurred during update of user'),
142 143 category='error')
143 144 raise HTTPFound(h.route_path('my_account_profile'))
144 145
145 146 @LoginRequired()
146 147 @NotAnonymous()
147 148 def my_account_password(self):
148 149 c = self.load_default_context()
149 150 c.active = 'password'
150 151 c.extern_type = c.user.extern_type
151 152
152 153 schema = user_schema.ChangePasswordSchema().bind(
153 154 username=c.user.username)
154 155
155 156 form = forms.Form(
156 157 schema,
157 158 action=h.route_path('my_account_password_update'),
158 159 buttons=(forms.buttons.save, forms.buttons.reset))
159 160
160 161 c.form = form
161 162 return self._get_template_context(c)
162 163
163 164 @LoginRequired()
164 165 @NotAnonymous()
165 166 @CSRFRequired()
166 167 def my_account_password_update(self):
167 168 _ = self.request.translate
168 169 c = self.load_default_context()
169 170 c.active = 'password'
170 171 c.extern_type = c.user.extern_type
171 172
172 173 schema = user_schema.ChangePasswordSchema().bind(
173 174 username=c.user.username)
174 175
175 176 form = forms.Form(
176 177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
177 178
178 179 if c.extern_type != 'rhodecode':
179 180 raise HTTPFound(self.request.route_path('my_account_password'))
180 181
181 182 controls = list(self.request.POST.items())
182 183 try:
183 184 valid_data = form.validate(controls)
184 185 UserModel().update_user(c.user.user_id, **valid_data)
185 186 c.user.update_userdata(force_password_change=False)
186 187 Session().commit()
187 188 except forms.ValidationFailure as e:
188 189 c.form = e
189 190 return self._get_template_context(c)
190 191
191 192 except Exception:
192 193 log.exception("Exception updating password")
193 194 h.flash(_('Error occurred during update of user password'),
194 195 category='error')
195 196 else:
196 197 instance = c.auth_user.get_instance()
197 198 self.session.setdefault('rhodecode_user', {}).update(
198 199 {'password': md5_safe(instance.password)})
199 200 self.session.save()
200 201 h.flash(_("Successfully updated password"), category='success')
201 202
202 203 raise HTTPFound(self.request.route_path('my_account_password'))
203 204
204 205 @LoginRequired()
205 206 @NotAnonymous()
206 207 def my_account_auth_tokens(self):
207 208 _ = self.request.translate
208 209
209 210 c = self.load_default_context()
210 211 c.active = 'auth_tokens'
211 212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
212 213 c.role_values = [
213 214 (x, AuthTokenModel.cls._get_role_name(x))
214 215 for x in AuthTokenModel.cls.ROLES]
215 216 c.role_options = [(c.role_values, _("Role"))]
216 217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
217 218 c.user.user_id, show_expired=True)
218 219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
219 220 return self._get_template_context(c)
220 221
221 222 @LoginRequired()
222 223 @NotAnonymous()
223 224 @CSRFRequired()
224 225 def my_account_auth_tokens_view(self):
225 226 _ = self.request.translate
226 227 c = self.load_default_context()
227 228
228 229 auth_token_id = self.request.POST.get('auth_token_id')
229 230
230 231 if auth_token_id:
231 232 token = UserApiKeys.get_or_404(auth_token_id)
232 233 if token.user.user_id != c.user.user_id:
233 234 raise HTTPNotFound()
234 235
235 236 return {
236 237 'auth_token': token.api_key
237 238 }
238 239
239 240 def maybe_attach_token_scope(self, token):
240 241 # implemented in EE edition
241 242 pass
242 243
243 244 @LoginRequired()
244 245 @NotAnonymous()
245 246 @CSRFRequired()
246 247 def my_account_auth_tokens_add(self):
247 248 _ = self.request.translate
248 249 c = self.load_default_context()
249 250
250 251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
251 252 description = self.request.POST.get('description')
252 253 role = self.request.POST.get('role')
253 254
254 255 token = UserModel().add_auth_token(
255 256 user=c.user.user_id,
256 257 lifetime_minutes=lifetime, role=role, description=description,
257 258 scope_callback=self.maybe_attach_token_scope)
258 259 token_data = token.get_api_data()
259 260
260 261 audit_logger.store_web(
261 262 'user.edit.token.add', action_data={
262 263 'data': {'token': token_data, 'user': 'self'}},
263 264 user=self._rhodecode_user, )
264 265 Session().commit()
265 266
266 267 h.flash(_("Auth token successfully created"), category='success')
267 268 return HTTPFound(h.route_path('my_account_auth_tokens'))
268 269
269 270 @LoginRequired()
270 271 @NotAnonymous()
271 272 @CSRFRequired()
272 273 def my_account_auth_tokens_delete(self):
273 274 _ = self.request.translate
274 275 c = self.load_default_context()
275 276
276 277 del_auth_token = self.request.POST.get('del_auth_token')
277 278
278 279 if del_auth_token:
279 280 token = UserApiKeys.get_or_404(del_auth_token)
280 281 token_data = token.get_api_data()
281 282
282 283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
283 284 audit_logger.store_web(
284 285 'user.edit.token.delete', action_data={
285 286 'data': {'token': token_data, 'user': 'self'}},
286 287 user=self._rhodecode_user,)
287 288 Session().commit()
288 289 h.flash(_("Auth token successfully deleted"), category='success')
289 290
290 291 return HTTPFound(h.route_path('my_account_auth_tokens'))
291 292
292 293 @LoginRequired()
293 294 @NotAnonymous()
294 295 def my_account_emails(self):
295 296 _ = self.request.translate
296 297
297 298 c = self.load_default_context()
298 299 c.active = 'emails'
299 300
300 301 c.user_email_map = UserEmailMap.query()\
301 302 .filter(UserEmailMap.user == c.user).all()
302 303
303 304 schema = user_schema.AddEmailSchema().bind(
304 305 username=c.user.username, user_emails=c.user.emails)
305 306
306 307 form = forms.RcForm(schema,
307 308 action=h.route_path('my_account_emails_add'),
308 309 buttons=(forms.buttons.save, forms.buttons.reset))
309 310
310 311 c.form = form
311 312 return self._get_template_context(c)
312 313
313 314 @LoginRequired()
314 315 @NotAnonymous()
315 316 @CSRFRequired()
316 317 def my_account_emails_add(self):
317 318 _ = self.request.translate
318 319 c = self.load_default_context()
319 320 c.active = 'emails'
320 321
321 322 schema = user_schema.AddEmailSchema().bind(
322 323 username=c.user.username, user_emails=c.user.emails)
323 324
324 325 form = forms.RcForm(
325 326 schema, action=h.route_path('my_account_emails_add'),
326 327 buttons=(forms.buttons.save, forms.buttons.reset))
327 328
328 329 controls = list(self.request.POST.items())
329 330 try:
330 331 valid_data = form.validate(controls)
331 332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
332 333 audit_logger.store_web(
333 334 'user.edit.email.add', action_data={
334 335 'data': {'email': valid_data['email'], 'user': 'self'}},
335 336 user=self._rhodecode_user,)
336 337 Session().commit()
337 338 except formencode.Invalid as error:
338 339 h.flash(h.escape(error.error_dict['email']), category='error')
339 340 except forms.ValidationFailure as e:
340 341 c.user_email_map = UserEmailMap.query() \
341 342 .filter(UserEmailMap.user == c.user).all()
342 343 c.form = e
343 344 return self._get_template_context(c)
344 345 except Exception:
345 346 log.exception("Exception adding email")
346 347 h.flash(_('Error occurred during adding email'),
347 348 category='error')
348 349 else:
349 350 h.flash(_("Successfully added email"), category='success')
350 351
351 352 raise HTTPFound(self.request.route_path('my_account_emails'))
352 353
353 354 @LoginRequired()
354 355 @NotAnonymous()
355 356 @CSRFRequired()
356 357 def my_account_emails_delete(self):
357 358 _ = self.request.translate
358 359 c = self.load_default_context()
359 360
360 361 del_email_id = self.request.POST.get('del_email_id')
361 362 if del_email_id:
362 363 email = UserEmailMap.get_or_404(del_email_id).email
363 364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
364 365 audit_logger.store_web(
365 366 'user.edit.email.delete', action_data={
366 367 'data': {'email': email, 'user': 'self'}},
367 368 user=self._rhodecode_user,)
368 369 Session().commit()
369 370 h.flash(_("Email successfully deleted"),
370 371 category='success')
371 372 return HTTPFound(h.route_path('my_account_emails'))
372 373
373 374 @LoginRequired()
374 375 @NotAnonymous()
375 376 @CSRFRequired()
376 377 def my_account_notifications_test_channelstream(self):
377 378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
378 379 self._rhodecode_user.username, datetime.datetime.now())
379 380 payload = {
380 381 # 'channel': 'broadcast',
381 382 'type': 'message',
382 383 'timestamp': datetime.datetime.utcnow(),
383 384 'user': 'system',
384 385 'pm_users': [self._rhodecode_user.username],
385 386 'message': {
386 387 'message': message,
387 388 'level': 'info',
388 389 'topic': '/notifications'
389 390 }
390 391 }
391 392
392 393 registry = self.request.registry
393 394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
394 395 channelstream_config = rhodecode_plugins.get('channelstream', {})
395 396
396 397 try:
397 398 channelstream_request(channelstream_config, [payload], '/message')
398 399 except ChannelstreamException as e:
399 400 log.exception('Failed to send channelstream data')
400 401 return {"response": f'ERROR: {e.__class__.__name__}'}
401 402 return {"response": 'Channelstream data sent. '
402 403 'You should see a new live message now.'}
403 404
404 405 def _load_my_repos_data(self, watched=False):
405 406
406 407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
407 408
408 409 if watched:
409 410 # repos user watch
410 411 repo_list = Session().query(
411 412 Repository
412 413 ) \
413 414 .join(
414 415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
415 416 ) \
416 417 .filter(
417 418 UserFollowing.user_id == self._rhodecode_user.user_id
418 419 ) \
419 420 .filter(or_(
420 421 # generate multiple IN to fix limitation problems
421 422 *in_filter_generator(Repository.repo_id, allowed_ids))
422 423 ) \
423 424 .order_by(Repository.repo_name) \
424 425 .all()
425 426
426 427 else:
427 428 # repos user is owner of
428 429 repo_list = Session().query(
429 430 Repository
430 431 ) \
431 432 .filter(
432 433 Repository.user_id == self._rhodecode_user.user_id
433 434 ) \
434 435 .filter(or_(
435 436 # generate multiple IN to fix limitation problems
436 437 *in_filter_generator(Repository.repo_id, allowed_ids))
437 438 ) \
438 439 .order_by(Repository.repo_name) \
439 440 .all()
440 441
441 442 _render = self.request.get_partial_renderer(
442 443 'rhodecode:templates/data_table/_dt_elements.mako')
443 444
444 445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
445 446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
446 447 short_name=False, admin=False)
447 448
448 449 repos_data = []
449 450 for repo in repo_list:
450 451 row = {
451 452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
452 453 repo.private, repo.archived, repo.fork),
453 454 "name_raw": repo.repo_name.lower(),
454 455 }
455 456
456 457 repos_data.append(row)
457 458
458 459 # json used to render the grid
459 460 return ext_json.str_json(repos_data)
460 461
461 462 @LoginRequired()
462 463 @NotAnonymous()
463 464 def my_account_repos(self):
464 465 c = self.load_default_context()
465 466 c.active = 'repos'
466 467
467 468 # json used to render the grid
468 469 c.data = self._load_my_repos_data()
469 470 return self._get_template_context(c)
470 471
471 472 @LoginRequired()
472 473 @NotAnonymous()
473 474 def my_account_watched(self):
474 475 c = self.load_default_context()
475 476 c.active = 'watched'
476 477
477 478 # json used to render the grid
478 479 c.data = self._load_my_repos_data(watched=True)
479 480 return self._get_template_context(c)
480 481
481 482 @LoginRequired()
482 483 @NotAnonymous()
483 484 def my_account_bookmarks(self):
484 485 c = self.load_default_context()
485 486 c.active = 'bookmarks'
486 487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
487 488 self._rhodecode_db_user.user_id, cache=False)
488 489 return self._get_template_context(c)
489 490
490 491 def _process_bookmark_entry(self, entry, user_id):
491 492 position = safe_int(entry.get('position'))
492 493 cur_position = safe_int(entry.get('cur_position'))
493 494 if position is None:
494 495 return
495 496
496 497 # check if this is an existing entry
497 498 is_new = False
498 499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
499 500
500 501 if db_entry and str2bool(entry.get('remove')):
501 502 log.debug('Marked bookmark %s for deletion', db_entry)
502 503 Session().delete(db_entry)
503 504 return
504 505
505 506 if not db_entry:
506 507 # new
507 508 db_entry = UserBookmark()
508 509 is_new = True
509 510
510 511 should_save = False
511 512 default_redirect_url = ''
512 513
513 514 # save repo
514 515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
515 516 repo = Repository.get(entry['bookmark_repo'])
516 517 perm_check = HasRepoPermissionAny(
517 518 'repository.read', 'repository.write', 'repository.admin')
518 519 if repo and perm_check(repo_name=repo.repo_name):
519 520 db_entry.repository = repo
520 521 should_save = True
521 522 default_redirect_url = '${repo_url}'
522 523 # save repo group
523 524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
524 525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
525 526 perm_check = HasRepoGroupPermissionAny(
526 527 'group.read', 'group.write', 'group.admin')
527 528
528 529 if repo_group and perm_check(group_name=repo_group.group_name):
529 530 db_entry.repository_group = repo_group
530 531 should_save = True
531 532 default_redirect_url = '${repo_group_url}'
532 533 # save generic info
533 534 elif entry.get('title') and entry.get('redirect_url'):
534 535 should_save = True
535 536
536 537 if should_save:
537 538 # mark user and position
538 539 db_entry.user_id = user_id
539 540 db_entry.position = position
540 541 db_entry.title = entry.get('title')
541 542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
542 543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
543 544
544 545 Session().add(db_entry)
545 546
546 547 @LoginRequired()
547 548 @NotAnonymous()
548 549 @CSRFRequired()
549 550 def my_account_bookmarks_update(self):
550 551 _ = self.request.translate
551 552 c = self.load_default_context()
552 553 c.active = 'bookmarks'
553 554
554 555 controls = peppercorn.parse(self.request.POST.items())
555 556 user_id = c.user.user_id
556 557
557 558 # validate positions
558 559 positions = {}
559 560 for entry in controls.get('bookmarks', []):
560 561 position = safe_int(entry['position'])
561 562 if position is None:
562 563 continue
563 564
564 565 if position in positions:
565 566 h.flash(_("Position {} is defined twice. "
566 567 "Please correct this error.").format(position), category='error')
567 568 return HTTPFound(h.route_path('my_account_bookmarks'))
568 569
569 570 entry['position'] = position
570 571 entry['cur_position'] = safe_int(entry.get('cur_position'))
571 572 positions[position] = entry
572 573
573 574 try:
574 575 for entry in positions.values():
575 576 self._process_bookmark_entry(entry, user_id)
576 577
577 578 Session().commit()
578 579 h.flash(_("Update Bookmarks"), category='success')
579 580 except IntegrityError:
580 581 h.flash(_("Failed to update bookmarks. "
581 582 "Make sure an unique position is used."), category='error')
582 583
583 584 return HTTPFound(h.route_path('my_account_bookmarks'))
584 585
585 586 @LoginRequired()
586 587 @NotAnonymous()
587 588 def my_account_goto_bookmark(self):
588 589
589 590 bookmark_id = self.request.matchdict['bookmark_id']
590 591 user_bookmark = UserBookmark().query()\
591 592 .filter(UserBookmark.user_id == self.request.user.user_id) \
592 593 .filter(UserBookmark.position == bookmark_id).scalar()
593 594
594 595 redirect_url = h.route_path('my_account_bookmarks')
595 596 if not user_bookmark:
596 597 raise HTTPFound(redirect_url)
597 598
598 599 # repository set
599 600 if user_bookmark.repository:
600 601 repo_name = user_bookmark.repository.repo_name
601 602 base_redirect_url = h.route_path(
602 603 'repo_summary', repo_name=repo_name)
603 604 if user_bookmark.redirect_url and \
604 605 '${repo_url}' in user_bookmark.redirect_url:
605 606 redirect_url = string.Template(user_bookmark.redirect_url)\
606 607 .safe_substitute({'repo_url': base_redirect_url})
607 608 else:
608 609 redirect_url = base_redirect_url
609 610 # repository group set
610 611 elif user_bookmark.repository_group:
611 612 repo_group_name = user_bookmark.repository_group.group_name
612 613 base_redirect_url = h.route_path(
613 614 'repo_group_home', repo_group_name=repo_group_name)
614 615 if user_bookmark.redirect_url and \
615 616 '${repo_group_url}' in user_bookmark.redirect_url:
616 617 redirect_url = string.Template(user_bookmark.redirect_url)\
617 618 .safe_substitute({'repo_group_url': base_redirect_url})
618 619 else:
619 620 redirect_url = base_redirect_url
620 621 # custom URL set
621 622 elif user_bookmark.redirect_url:
622 623 server_url = h.route_url('home').rstrip('/')
623 624 redirect_url = string.Template(user_bookmark.redirect_url) \
624 625 .safe_substitute({'server_url': server_url})
625 626
626 627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
627 628 raise HTTPFound(redirect_url)
628 629
629 630 @LoginRequired()
630 631 @NotAnonymous()
631 632 def my_account_perms(self):
632 633 c = self.load_default_context()
633 634 c.active = 'perms'
634 635
635 636 c.perm_user = c.auth_user
636 637 return self._get_template_context(c)
637 638
638 639 @LoginRequired()
639 640 @NotAnonymous()
640 641 def my_notifications(self):
641 642 c = self.load_default_context()
642 643 c.active = 'notifications'
643 644
644 645 return self._get_template_context(c)
645 646
646 647 @LoginRequired()
647 648 @NotAnonymous()
648 649 @CSRFRequired()
649 650 def my_notifications_toggle_visibility(self):
650 651 user = self._rhodecode_db_user
651 652 new_status = not user.user_data.get('notification_status', True)
652 653 user.update_userdata(notification_status=new_status)
653 654 Session().commit()
654 655 return user.user_data['notification_status']
655 656
656 657 def _get_pull_requests_list(self, statuses, filter_type=None):
657 658 draw, start, limit = self._extract_chunk(self.request)
658 659 search_q, order_by, order_dir = self._extract_ordering(self.request)
659 660
660 661 _render = self.request.get_partial_renderer(
661 662 'rhodecode:templates/data_table/_dt_elements.mako')
662 663
663 664 if filter_type == 'awaiting_my_review':
664 665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
665 666 user_id=self._rhodecode_user.user_id,
666 667 statuses=statuses, query=search_q,
667 668 offset=start, length=limit, order_by=order_by,
668 669 order_dir=order_dir)
669 670
670 671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
671 672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
672 673 else:
673 674 pull_requests = PullRequestModel().get_im_participating_in(
674 675 user_id=self._rhodecode_user.user_id,
675 676 statuses=statuses, query=search_q,
676 677 offset=start, length=limit, order_by=order_by,
677 678 order_dir=order_dir)
678 679
679 680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
680 681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
681 682
682 683 data = []
683 684 comments_model = CommentsModel()
684 685 for pr in pull_requests:
685 686 repo_id = pr.target_repo_id
686 687 comments_count = comments_model.get_all_comments(
687 688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
688 689 owned = pr.user_id == self._rhodecode_user.user_id
689 690
690 691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
691 692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
692 693 if review_statuses and review_statuses[4]:
693 694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
694 695 my_review_status = statuses[0][1].status
695 696
696 697 data.append({
697 698 'target_repo': _render('pullrequest_target_repo',
698 699 pr.target_repo.repo_name),
699 700 'name': _render('pullrequest_name',
700 701 pr.pull_request_id, pr.pull_request_state,
701 702 pr.work_in_progress, pr.target_repo.repo_name,
702 703 short=True),
703 704 'name_raw': pr.pull_request_id,
704 705 'status': _render('pullrequest_status',
705 706 pr.calculated_review_status()),
706 707 'my_status': _render('pullrequest_status',
707 708 my_review_status),
708 709 'title': _render('pullrequest_title', pr.title, pr.description),
709 710 'pr_flow': _render('pullrequest_commit_flow', pr),
710 711 'description': h.escape(pr.description),
711 712 'updated_on': _render('pullrequest_updated_on',
712 713 h.datetime_to_time(pr.updated_on),
713 714 pr.versions_count),
714 715 'updated_on_raw': h.datetime_to_time(pr.updated_on),
715 716 'created_on': _render('pullrequest_updated_on',
716 717 h.datetime_to_time(pr.created_on)),
717 718 'created_on_raw': h.datetime_to_time(pr.created_on),
718 719 'state': pr.pull_request_state,
719 720 'author': _render('pullrequest_author',
720 721 pr.author.full_contact, ),
721 722 'author_raw': pr.author.full_name,
722 723 'comments': _render('pullrequest_comments', comments_count),
723 724 'comments_raw': comments_count,
724 725 'closed': pr.is_closed(),
725 726 'owned': owned
726 727 })
727 728
728 729 # json used to render the grid
729 730 data = ({
730 731 'draw': draw,
731 732 'data': data,
732 733 'recordsTotal': pull_requests_total_count,
733 734 'recordsFiltered': pull_requests_total_count,
734 735 })
735 736 return data
736 737
737 738 @LoginRequired()
738 739 @NotAnonymous()
739 740 def my_account_pullrequests(self):
740 741 c = self.load_default_context()
741 742 c.active = 'pullrequests'
742 743 req_get = self.request.GET
743 744
744 745 c.closed = str2bool(req_get.get('closed'))
745 746 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746 747
747 748 c.selected_filter = 'all'
748 749 if c.closed:
749 750 c.selected_filter = 'all_closed'
750 751 if c.awaiting_my_review:
751 752 c.selected_filter = 'awaiting_my_review'
752 753
753 754 return self._get_template_context(c)
754 755
755 756 @LoginRequired()
756 757 @NotAnonymous()
757 758 def my_account_pullrequests_data(self):
758 759 self.load_default_context()
759 760 req_get = self.request.GET
760 761
761 762 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
762 763 closed = str2bool(req_get.get('closed'))
763 764
764 765 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
765 766 if closed:
766 767 statuses += [PullRequest.STATUS_CLOSED]
767 768
768 769 filter_type = \
769 770 'awaiting_my_review' if awaiting_my_review \
770 771 else None
771 772
772 773 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
773 774 return data
774 775
775 776 @LoginRequired()
776 777 @NotAnonymous()
777 778 def my_account_user_group_membership(self):
778 779 c = self.load_default_context()
779 780 c.active = 'user_group_membership'
780 781 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
781 782 for group in self._rhodecode_db_user.group_member]
782 783 c.user_groups = ext_json.str_json(groups)
783 784 return self._get_template_context(c)
@@ -1,201 +1,205 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Set of custom exceptions used in RhodeCode
21 21 """
22 22
23 23 from webob.exc import HTTPClientError
24 24 from pyramid.httpexceptions import HTTPBadGateway
25 25
26 26
27 27 class LdapUsernameError(Exception):
28 28 pass
29 29
30 30
31 31 class LdapPasswordError(Exception):
32 32 pass
33 33
34 34
35 35 class LdapConnectionError(Exception):
36 36 pass
37 37
38 38
39 39 class LdapImportError(Exception):
40 40 pass
41 41
42 42
43 43 class DefaultUserException(Exception):
44 44 pass
45 45
46 46
47 47 class UserOwnsReposException(Exception):
48 48 pass
49 49
50 50
51 51 class UserOwnsRepoGroupsException(Exception):
52 52 pass
53 53
54 54
55 55 class UserOwnsUserGroupsException(Exception):
56 56 pass
57 57
58 58
59 59 class UserOwnsPullRequestsException(Exception):
60 60 pass
61 61
62 62
63 63 class UserOwnsArtifactsException(Exception):
64 64 pass
65 65
66 66
67 67 class UserGroupAssignedException(Exception):
68 68 pass
69 69
70 70
71 71 class StatusChangeOnClosedPullRequestError(Exception):
72 72 pass
73 73
74 74
75 75 class AttachedForksError(Exception):
76 76 pass
77 77
78 78
79 79 class AttachedPullRequestsError(Exception):
80 80 pass
81 81
82 82
83 83 class RepoGroupAssignmentError(Exception):
84 84 pass
85 85
86 86
87 87 class NonRelativePathError(Exception):
88 88 pass
89 89
90 90
91 91 class HTTPRequirementError(HTTPClientError):
92 92 title = explanation = 'Repository Requirement Missing'
93 93 reason = None
94 94
95 95 def __init__(self, message, *args, **kwargs):
96 96 self.title = self.explanation = message
97 97 super().__init__(*args, **kwargs)
98 98 self.args = (message, )
99 99
100 100
101 101 class HTTPLockedRC(HTTPClientError):
102 102 """
103 103 Special Exception For locked Repos in RhodeCode, the return code can
104 104 be overwritten by _code keyword argument passed into constructors
105 105 """
106 106 code = 423
107 107 title = explanation = 'Repository Locked'
108 108 reason = None
109 109
110 110 def __init__(self, message, *args, **kwargs):
111 111 import rhodecode
112 112
113 113 self.code = rhodecode.ConfigGet().get_int('lock_ret_code', missing=self.code)
114 114
115 115 self.title = self.explanation = message
116 116 super().__init__(*args, **kwargs)
117 117 self.args = (message, )
118 118
119 119
120 120 class HTTPBranchProtected(HTTPClientError):
121 121 """
122 122 Special Exception For Indicating that branch is protected in RhodeCode, the
123 123 return code can be overwritten by _code keyword argument passed into constructors
124 124 """
125 125 code = 403
126 126 title = explanation = 'Branch Protected'
127 127 reason = None
128 128
129 129 def __init__(self, message, *args, **kwargs):
130 130 self.title = self.explanation = message
131 131 super().__init__(*args, **kwargs)
132 132 self.args = (message, )
133 133
134 134
135 135 class IMCCommitError(Exception):
136 136 pass
137 137
138 138
139 139 class UserCreationError(Exception):
140 140 pass
141 141
142 142
143 143 class NotAllowedToCreateUserError(Exception):
144 144 pass
145 145
146 146
147 class DuplicateUpdateUserError(Exception):
148 pass
149
150
147 151 class RepositoryCreationError(Exception):
148 152 pass
149 153
150 154
151 155 class VCSServerUnavailable(HTTPBadGateway):
152 156 """ HTTP Exception class for VCS Server errors """
153 157 code = 502
154 158 title = 'VCS Server Error'
155 159 causes = [
156 160 'VCS Server is not running',
157 161 'Incorrect vcs.server=host:port',
158 162 'Incorrect vcs.server.protocol',
159 163 ]
160 164
161 165 def __init__(self, message=''):
162 166 self.explanation = 'Could not connect to VCS Server'
163 167 if message:
164 168 self.explanation += ': ' + message
165 169 super().__init__()
166 170
167 171
168 172 class ArtifactMetadataDuplicate(ValueError):
169 173
170 174 def __init__(self, *args, **kwargs):
171 175 self.err_section = kwargs.pop('err_section', None)
172 176 self.err_key = kwargs.pop('err_key', None)
173 177 super().__init__(*args, **kwargs)
174 178
175 179
176 180 class ArtifactMetadataBadValueType(ValueError):
177 181 pass
178 182
179 183
180 184 class CommentVersionMismatch(ValueError):
181 185 pass
182 186
183 187
184 188 class SignatureVerificationError(ValueError):
185 189 pass
186 190
187 191
188 192 def signature_verification_error(msg):
189 193 details = """
190 194 Encryption signature verification failed.
191 195 Please check your value of secret key, and/or encrypted value stored.
192 196 Secret key stored inside .ini file:
193 197 `rhodecode.encrypted_values.secret` or defaults to
194 198 `beaker.session.secret`
195 199
196 200 Probably the stored values were encrypted using a different secret then currently set in .ini file
197 201 """
198 202
199 203 final_msg = f'{msg}\n{details}'
200 204 return SignatureVerificationError(final_msg)
201 205
@@ -1,1046 +1,1050 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 users model for RhodeCode
21 21 """
22 22
23 23 import logging
24 24 import traceback
25 25 import datetime
26 26 import ipaddress
27 27
28 28 from pyramid.threadlocal import get_current_request
29 29 from sqlalchemy.exc import DatabaseError
30 30
31 31 from rhodecode import events
32 32 from rhodecode.lib.user_log_filter import user_log_filter
33 33 from rhodecode.lib.utils2 import (
34 34 get_current_rhodecode_user, action_logger_generic,
35 35 AttributeDict, str2bool)
36 36 from rhodecode.lib.str_utils import safe_str
37 37 from rhodecode.lib.exceptions import (
38 38 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 39 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
40 UserOwnsPullRequestsException, UserOwnsArtifactsException)
40 UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError)
41 41 from rhodecode.lib.caching_query import FromCache
42 42 from rhodecode.model import BaseModel
43 43 from rhodecode.model.db import (
44 44 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
45 45 UserEmailMap, UserIpMap, UserLog)
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.auth_token import AuthTokenModel
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class UserModel(BaseModel):
54 54 cls = User
55 55
56 56 def get(self, user_id, cache=False):
57 57 user = self.sa.query(User)
58 58 if cache:
59 59 user = user.options(
60 60 FromCache("sql_cache_short", f"get_user_{user_id}"))
61 61 return user.get(user_id)
62 62
63 63 def get_user(self, user):
64 64 return self._get_user(user)
65 65
66 66 def _serialize_user(self, user):
67 67 import rhodecode.lib.helpers as h
68 68
69 69 return {
70 70 'id': user.user_id,
71 71 'first_name': user.first_name,
72 72 'last_name': user.last_name,
73 73 'username': user.username,
74 74 'email': user.email,
75 75 'icon_link': h.gravatar_url(user.email, 30),
76 76 'profile_link': h.link_to_user(user),
77 77 'value_display': h.escape(h.person(user)),
78 78 'value': user.username,
79 79 'value_type': 'user',
80 80 'active': user.active,
81 81 }
82 82
83 83 def get_users(self, name_contains=None, limit=20, only_active=True):
84 84
85 85 query = self.sa.query(User)
86 86 if only_active:
87 87 query = query.filter(User.active == true())
88 88
89 89 if name_contains:
90 90 ilike_expression = f'%{safe_str(name_contains)}%'
91 91 query = query.filter(
92 92 or_(
93 93 User.name.ilike(ilike_expression),
94 94 User.lastname.ilike(ilike_expression),
95 95 User.username.ilike(ilike_expression)
96 96 )
97 97 )
98 98 # sort by len to have top most matches first
99 99 query = query.order_by(func.length(User.username))\
100 100 .order_by(User.username)
101 101 query = query.limit(limit)
102 102
103 103 users = query.all()
104 104
105 105 _users = [
106 106 self._serialize_user(user) for user in users
107 107 ]
108 108 return _users
109 109
110 110 def get_by_username(self, username, cache=False, case_insensitive=False):
111 111
112 112 if case_insensitive:
113 113 user = self.sa.query(User).filter(User.username.ilike(username))
114 114 else:
115 115 user = self.sa.query(User)\
116 116 .filter(User.username == username)
117 117 if cache:
118 118 name_key = _hash_key(username)
119 119 user = user.options(
120 120 FromCache("sql_cache_short", f"get_user_{name_key}"))
121 121 return user.scalar()
122 122
123 123 def get_by_email(self, email, cache=False, case_insensitive=False):
124 124 return User.get_by_email(email, case_insensitive, cache)
125 125
126 126 def get_by_auth_token(self, auth_token, cache=False):
127 127 return User.get_by_auth_token(auth_token, cache)
128 128
129 129 def get_active_user_count(self, cache=False):
130 130 qry = User.query().filter(
131 131 User.active == true()).filter(
132 132 User.username != User.DEFAULT_USER)
133 133 if cache:
134 134 qry = qry.options(
135 135 FromCache("sql_cache_short", "get_active_users"))
136 136 return qry.count()
137 137
138 138 def create(self, form_data, cur_user=None):
139 139 if not cur_user:
140 140 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
141 141
142 142 user_data = {
143 143 'username': form_data['username'],
144 144 'password': form_data['password'],
145 145 'email': form_data['email'],
146 146 'firstname': form_data['firstname'],
147 147 'lastname': form_data['lastname'],
148 148 'active': form_data['active'],
149 149 'extern_type': form_data['extern_type'],
150 150 'extern_name': form_data['extern_name'],
151 151 'admin': False,
152 152 'cur_user': cur_user
153 153 }
154 154
155 155 if 'create_repo_group' in form_data:
156 156 user_data['create_repo_group'] = str2bool(
157 157 form_data.get('create_repo_group'))
158 158
159 159 try:
160 160 if form_data.get('password_change'):
161 161 user_data['force_password_change'] = True
162 162 return UserModel().create_or_update(**user_data)
163 163 except Exception:
164 164 log.error(traceback.format_exc())
165 165 raise
166 166
167 167 def update_user(self, user, skip_attrs=None, **kwargs):
168 168 from rhodecode.lib.auth import get_crypt_password
169 169
170 170 user = self._get_user(user)
171 171 if user.username == User.DEFAULT_USER:
172 172 raise DefaultUserException(
173 173 "You can't edit this user (`%(username)s`) since it's "
174 174 "crucial for entire application" % {
175 175 'username': user.username})
176 176
177 177 # first store only defaults
178 178 user_attrs = {
179 179 'updating_user_id': user.user_id,
180 180 'username': user.username,
181 181 'password': user.password,
182 182 'email': user.email,
183 183 'firstname': user.name,
184 184 'lastname': user.lastname,
185 185 'description': user.description,
186 186 'active': user.active,
187 187 'admin': user.admin,
188 188 'extern_name': user.extern_name,
189 189 'extern_type': user.extern_type,
190 190 'language': user.user_data.get('language')
191 191 }
192 192
193 193 # in case there's new_password, that comes from form, use it to
194 194 # store password
195 195 if kwargs.get('new_password'):
196 196 kwargs['password'] = kwargs['new_password']
197 197
198 198 # cleanups, my_account password change form
199 199 kwargs.pop('current_password', None)
200 200 kwargs.pop('new_password', None)
201 201
202 202 # cleanups, user edit password change form
203 203 kwargs.pop('password_confirmation', None)
204 204 kwargs.pop('password_change', None)
205 205
206 206 # create repo group on user creation
207 207 kwargs.pop('create_repo_group', None)
208 208
209 209 # legacy forms send name, which is the firstname
210 210 firstname = kwargs.pop('name', None)
211 211 if firstname:
212 212 kwargs['firstname'] = firstname
213 213
214 214 for k, v in kwargs.items():
215 215 # skip if we don't want to update this
216 216 if skip_attrs and k in skip_attrs:
217 217 continue
218 218
219 219 user_attrs[k] = v
220 220
221 221 try:
222 222 return self.create_or_update(**user_attrs)
223 223 except Exception:
224 224 log.error(traceback.format_exc())
225 225 raise
226 226
227 227 def create_or_update(
228 228 self, username, password, email, firstname='', lastname='',
229 229 active=True, admin=False, extern_type=None, extern_name=None,
230 230 cur_user=None, plugin=None, force_password_change=False,
231 231 allow_to_create_user=True, create_repo_group=None,
232 232 updating_user_id=None, language=None, description='',
233 233 strict_creation_check=True):
234 234 """
235 235 Creates a new instance if not found, or updates current one
236 236
237 237 :param username:
238 238 :param password:
239 239 :param email:
240 240 :param firstname:
241 241 :param lastname:
242 242 :param active:
243 243 :param admin:
244 244 :param extern_type:
245 245 :param extern_name:
246 246 :param cur_user:
247 247 :param plugin: optional plugin this method was called from
248 248 :param force_password_change: toggles new or existing user flag
249 249 for password change
250 250 :param allow_to_create_user: Defines if the method can actually create
251 251 new users
252 252 :param create_repo_group: Defines if the method should also
253 253 create an repo group with user name, and owner
254 254 :param updating_user_id: if we set it up this is the user we want to
255 255 update this allows to editing username.
256 256 :param language: language of user from interface.
257 257 :param description: user description
258 258 :param strict_creation_check: checks for allowed creation license wise etc.
259 259
260 260 :returns: new User object with injected `is_new_user` attribute.
261 261 """
262 262
263 263 if not cur_user:
264 264 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
265 265
266 266 from rhodecode.lib.auth import (
267 267 get_crypt_password, check_password)
268 268 from rhodecode.lib import hooks_base
269 269
270 270 def _password_change(new_user, password):
271 271 old_password = new_user.password or ''
272 272 # empty password
273 273 if not old_password:
274 274 return False
275 275
276 276 # password check is only needed for RhodeCode internal auth calls
277 277 # in case it's a plugin we don't care
278 278 if not plugin:
279 279
280 280 # first check if we gave crypted password back, and if it
281 281 # matches it's not password change
282 282 if new_user.password == password:
283 283 return False
284 284
285 285 password_match = check_password(password, old_password)
286 286 if not password_match:
287 287 return True
288 288
289 289 return False
290 290
291 291 # read settings on default personal repo group creation
292 292 if create_repo_group is None:
293 293 default_create_repo_group = RepoGroupModel()\
294 294 .get_default_create_personal_repo_group()
295 295 create_repo_group = default_create_repo_group
296 296
297 297 user_data = {
298 298 'username': username,
299 299 'password': password,
300 300 'email': email,
301 301 'firstname': firstname,
302 302 'lastname': lastname,
303 303 'active': active,
304 304 'admin': admin
305 305 }
306 306
307 307 if updating_user_id:
308 308 log.debug('Checking for existing account in RhodeCode '
309 309 'database with user_id `%s` ', updating_user_id)
310 310 user = User.get(updating_user_id)
311 # now also validate if USERNAME belongs to potentially other user
312 maybe_other_user = User.get_by_username(username, case_insensitive=True)
313 if maybe_other_user and maybe_other_user.user_id != updating_user_id:
314 raise DuplicateUpdateUserError(f'different user exists with the {username} username')
311 315 else:
312 316 log.debug('Checking for existing account in RhodeCode '
313 317 'database with username `%s` ', username)
314 318 user = User.get_by_username(username, case_insensitive=True)
315 319
316 320 if user is None:
317 321 # we check internal flag if this method is actually allowed to
318 322 # create new user
319 323 if not allow_to_create_user:
320 324 msg = ('Method wants to create new user, but it is not '
321 325 'allowed to do so')
322 326 log.warning(msg)
323 327 raise NotAllowedToCreateUserError(msg)
324 328
325 329 log.debug('Creating new user %s', username)
326 330
327 331 # only if we create user that is active
328 332 new_active_user = active
329 333 if new_active_user and strict_creation_check:
330 334 # raises UserCreationError if it's not allowed for any reason to
331 335 # create new active user, this also executes pre-create hooks
332 336 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
333 337 events.trigger(events.UserPreCreate(user_data))
334 338 new_user = User()
335 339 edit = False
336 340 else:
337 341 log.debug('updating user `%s`', username)
338 342 events.trigger(events.UserPreUpdate(user, user_data))
339 343 new_user = user
340 344 edit = True
341 345
342 346 # we're not allowed to edit default user
343 347 if user.username == User.DEFAULT_USER:
344 348 raise DefaultUserException(
345 349 "You can't edit this user (`%(username)s`) since it's "
346 350 "crucial for entire application"
347 351 % {'username': user.username})
348 352
349 353 # inject special attribute that will tell us if User is new or old
350 354 new_user.is_new_user = not edit
351 355 # for users that didn's specify auth type, we use RhodeCode built in
352 356 from rhodecode.authentication.plugins import auth_rhodecode
353 357 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
354 358 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
355 359
356 360 try:
357 361 new_user.username = username
358 362 new_user.admin = admin
359 363 new_user.email = email
360 364 new_user.active = active
361 365 new_user.extern_name = safe_str(extern_name)
362 366 new_user.extern_type = safe_str(extern_type)
363 367 new_user.name = firstname
364 368 new_user.lastname = lastname
365 369 new_user.description = description
366 370
367 371 # set password only if creating an user or password is changed
368 372 if not edit or _password_change(new_user, password):
369 373 reason = 'new password' if edit else 'new user'
370 374 log.debug('Updating password reason=>%s', reason)
371 375 new_user.password = get_crypt_password(password) if password else None
372 376
373 377 if force_password_change:
374 378 new_user.update_userdata(force_password_change=True)
375 379 if language:
376 380 new_user.update_userdata(language=language)
377 381 new_user.update_userdata(notification_status=True)
378 382
379 383 self.sa.add(new_user)
380 384
381 385 if not edit and create_repo_group:
382 386 RepoGroupModel().create_personal_repo_group(
383 387 new_user, commit_early=False)
384 388
385 389 if not edit:
386 390 # add the RSS token
387 391 self.add_auth_token(
388 392 user=username, lifetime_minutes=-1,
389 393 role=self.auth_token_role.ROLE_FEED,
390 394 description='Generated feed token')
391 395
392 396 kwargs = new_user.get_dict()
393 397 # backward compat, require api_keys present
394 398 kwargs['api_keys'] = kwargs['auth_tokens']
395 399 hooks_base.create_user(created_by=cur_user, **kwargs)
396 400 events.trigger(events.UserPostCreate(user_data))
397 401 return new_user
398 402 except (DatabaseError,):
399 403 log.error(traceback.format_exc())
400 404 raise
401 405
402 406 def create_registration(self, form_data,
403 407 extern_name='rhodecode', extern_type='rhodecode'):
404 408 from rhodecode.model.notification import NotificationModel
405 409 from rhodecode.model.notification import EmailNotificationModel
406 410
407 411 try:
408 412 form_data['admin'] = False
409 413 form_data['extern_name'] = extern_name
410 414 form_data['extern_type'] = extern_type
411 415 new_user = self.create(form_data)
412 416
413 417 self.sa.add(new_user)
414 418 self.sa.flush()
415 419
416 420 user_data = new_user.get_dict()
417 421 user_data.update({
418 422 'first_name': user_data.get('firstname'),
419 423 'last_name': user_data.get('lastname'),
420 424 })
421 425 kwargs = {
422 426 # use SQLALCHEMY safe dump of user data
423 427 'user': AttributeDict(user_data),
424 428 'date': datetime.datetime.now()
425 429 }
426 430 notification_type = EmailNotificationModel.TYPE_REGISTRATION
427 431
428 432 # create notification objects, and emails
429 433 NotificationModel().create(
430 434 created_by=new_user,
431 435 notification_subject='', # Filled in based on the notification_type
432 436 notification_body='', # Filled in based on the notification_type
433 437 notification_type=notification_type,
434 438 recipients=None, # all admins
435 439 email_kwargs=kwargs,
436 440 )
437 441
438 442 return new_user
439 443 except Exception:
440 444 log.error(traceback.format_exc())
441 445 raise
442 446
443 447 def _handle_user_repos(self, username, repositories, handle_user,
444 448 handle_mode=None):
445 449
446 450 left_overs = True
447 451
448 452 from rhodecode.model.repo import RepoModel
449 453
450 454 if handle_mode == 'detach':
451 455 for obj in repositories:
452 456 obj.user = handle_user
453 457 # set description we know why we super admin now owns
454 458 # additional repositories that were orphaned !
455 459 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
456 460 self.sa.add(obj)
457 461 left_overs = False
458 462 elif handle_mode == 'delete':
459 463 for obj in repositories:
460 464 RepoModel().delete(obj, forks='detach')
461 465 left_overs = False
462 466
463 467 # if nothing is done we have left overs left
464 468 return left_overs
465 469
466 470 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
467 471 handle_mode=None):
468 472
469 473 left_overs = True
470 474
471 475 from rhodecode.model.repo_group import RepoGroupModel
472 476
473 477 if handle_mode == 'detach':
474 478 for r in repository_groups:
475 479 r.user = handle_user
476 480 # set description we know why we super admin now owns
477 481 # additional repositories that were orphaned !
478 482 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
479 483 r.personal = False
480 484 self.sa.add(r)
481 485 left_overs = False
482 486 elif handle_mode == 'delete':
483 487 for r in repository_groups:
484 488 RepoGroupModel().delete(r)
485 489 left_overs = False
486 490
487 491 # if nothing is done we have left overs left
488 492 return left_overs
489 493
490 494 def _handle_user_user_groups(self, username, user_groups, handle_user,
491 495 handle_mode=None):
492 496
493 497 left_overs = True
494 498
495 499 from rhodecode.model.user_group import UserGroupModel
496 500
497 501 if handle_mode == 'detach':
498 502 for r in user_groups:
499 503 for user_user_group_to_perm in r.user_user_group_to_perm:
500 504 if user_user_group_to_perm.user.username == username:
501 505 user_user_group_to_perm.user = handle_user
502 506 r.user = handle_user
503 507 # set description we know why we super admin now owns
504 508 # additional repositories that were orphaned !
505 509 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
506 510 self.sa.add(r)
507 511 left_overs = False
508 512 elif handle_mode == 'delete':
509 513 for r in user_groups:
510 514 UserGroupModel().delete(r)
511 515 left_overs = False
512 516
513 517 # if nothing is done we have left overs left
514 518 return left_overs
515 519
516 520 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
517 521 handle_mode=None):
518 522 left_overs = True
519 523
520 524 from rhodecode.model.pull_request import PullRequestModel
521 525
522 526 if handle_mode == 'detach':
523 527 for pr in pull_requests:
524 528 pr.user_id = handle_user.user_id
525 529 # set description we know why we super admin now owns
526 530 # additional repositories that were orphaned !
527 531 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
528 532 self.sa.add(pr)
529 533 left_overs = False
530 534 elif handle_mode == 'delete':
531 535 for pr in pull_requests:
532 536 PullRequestModel().delete(pr)
533 537
534 538 left_overs = False
535 539
536 540 # if nothing is done we have leftovers left
537 541 return left_overs
538 542
539 543 def _handle_user_artifacts(self, username, artifacts, handle_user,
540 544 handle_mode=None):
541 545
542 546 left_overs = True
543 547
544 548 if handle_mode == 'detach':
545 549 for a in artifacts:
546 550 a.upload_user = handle_user
547 551 # set description we know why we super admin now owns
548 552 # additional artifacts that were orphaned !
549 553 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
550 554 self.sa.add(a)
551 555 left_overs = False
552 556 elif handle_mode == 'delete':
553 557 from rhodecode.apps.file_store import utils as store_utils
554 558 request = get_current_request()
555 559 storage = store_utils.get_file_storage(request.registry.settings)
556 560 for a in artifacts:
557 561 file_uid = a.file_uid
558 562 storage.delete(file_uid)
559 563 self.sa.delete(a)
560 564
561 565 left_overs = False
562 566
563 567 # if nothing is done we have left overs left
564 568 return left_overs
565 569
566 570 def delete(self, user, cur_user=None, handle_repos=None,
567 571 handle_repo_groups=None, handle_user_groups=None,
568 572 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
569 573 from rhodecode.lib import hooks_base
570 574
571 575 if not cur_user:
572 576 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
573 577
574 578 user = self._get_user(user)
575 579
576 580 try:
577 581 if user.username == User.DEFAULT_USER:
578 582 raise DefaultUserException(
579 583 "You can't remove this user since it's"
580 584 " crucial for entire application")
581 585 handle_user = handle_new_owner or self.cls.get_first_super_admin()
582 586 log.debug('New detached objects owner %s', handle_user)
583 587
584 588 left_overs = self._handle_user_repos(
585 589 user.username, user.repositories, handle_user, handle_repos)
586 590 if left_overs and user.repositories:
587 591 repos = [x.repo_name for x in user.repositories]
588 592 raise UserOwnsReposException(
589 593 'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
590 594 'removed. Switch owners or remove those repositories:%(list_repos)s'
591 595 % {'username': user.username, 'len_repos': len(repos),
592 596 'list_repos': ', '.join(repos)})
593 597
594 598 left_overs = self._handle_user_repo_groups(
595 599 user.username, user.repository_groups, handle_user, handle_repo_groups)
596 600 if left_overs and user.repository_groups:
597 601 repo_groups = [x.group_name for x in user.repository_groups]
598 602 raise UserOwnsRepoGroupsException(
599 603 'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
600 604 'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
601 605 % {'username': user.username, 'len_repo_groups': len(repo_groups),
602 606 'list_repo_groups': ', '.join(repo_groups)})
603 607
604 608 left_overs = self._handle_user_user_groups(
605 609 user.username, user.user_groups, handle_user, handle_user_groups)
606 610 if left_overs and user.user_groups:
607 611 user_groups = [x.users_group_name for x in user.user_groups]
608 612 raise UserOwnsUserGroupsException(
609 613 'user "%s" still owns %s user groups and cannot be '
610 614 'removed. Switch owners or remove those user groups:%s'
611 615 % (user.username, len(user_groups), ', '.join(user_groups)))
612 616
613 617 left_overs = self._handle_user_pull_requests(
614 618 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
615 619 if left_overs and user.user_pull_requests:
616 620 pull_requests = [f'!{x.pull_request_id}' for x in user.user_pull_requests]
617 621 raise UserOwnsPullRequestsException(
618 622 'user "%s" still owns %s pull requests and cannot be '
619 623 'removed. Switch owners or remove those pull requests:%s'
620 624 % (user.username, len(pull_requests), ', '.join(pull_requests)))
621 625
622 626 left_overs = self._handle_user_artifacts(
623 627 user.username, user.artifacts, handle_user, handle_artifacts)
624 628 if left_overs and user.artifacts:
625 629 artifacts = [x.file_uid for x in user.artifacts]
626 630 raise UserOwnsArtifactsException(
627 631 'user "%s" still owns %s artifacts and cannot be '
628 632 'removed. Switch owners or remove those artifacts:%s'
629 633 % (user.username, len(artifacts), ', '.join(artifacts)))
630 634
631 635 user_data = user.get_dict() # fetch user data before expire
632 636
633 637 # we might change the user data with detach/delete, make sure
634 638 # the object is marked as expired before actually deleting !
635 639 self.sa.expire(user)
636 640 self.sa.delete(user)
637 641
638 642 hooks_base.delete_user(deleted_by=cur_user, **user_data)
639 643 except Exception:
640 644 log.error(traceback.format_exc())
641 645 raise
642 646
643 647 def reset_password_link(self, data, pwd_reset_url):
644 648 from rhodecode.lib.celerylib import tasks, run_task
645 649 from rhodecode.model.notification import EmailNotificationModel
646 650 user_email = data['email']
647 651 try:
648 652 user = User.get_by_email(user_email)
649 653 if user:
650 654 log.debug('password reset user found %s', user)
651 655
652 656 email_kwargs = {
653 657 'password_reset_url': pwd_reset_url,
654 658 'user': user,
655 659 'email': user_email,
656 660 'date': datetime.datetime.now(),
657 661 'first_admin_email': User.get_first_super_admin().email
658 662 }
659 663
660 664 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
661 665 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
662 666
663 667 recipients = [user_email]
664 668
665 669 action_logger_generic(
666 670 'sending password reset email to user: {}'.format(
667 671 user), namespace='security.password_reset')
668 672
669 673 run_task(tasks.send_email, recipients, subject,
670 674 email_body_plaintext, email_body)
671 675
672 676 else:
673 677 log.debug("password reset email %s not found", user_email)
674 678 except Exception:
675 679 log.error(traceback.format_exc())
676 680 return False
677 681
678 682 return True
679 683
680 684 def reset_password(self, data):
681 685 from rhodecode.lib.celerylib import tasks, run_task
682 686 from rhodecode.model.notification import EmailNotificationModel
683 687 from rhodecode.lib import auth
684 688 user_email = data['email']
685 689 pre_db = True
686 690 try:
687 691 user = User.get_by_email(user_email)
688 692 new_passwd = auth.PasswordGenerator().gen_password(
689 693 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
690 694 if user:
691 695 user.password = auth.get_crypt_password(new_passwd)
692 696 # also force this user to reset his password !
693 697 user.update_userdata(force_password_change=True)
694 698
695 699 Session().add(user)
696 700
697 701 # now delete the token in question
698 702 UserApiKeys = AuthTokenModel.cls
699 703 UserApiKeys().query().filter(
700 704 UserApiKeys.api_key == data['token']).delete()
701 705
702 706 Session().commit()
703 707 log.info('successfully reset password for `%s`', user_email)
704 708
705 709 if new_passwd is None:
706 710 raise Exception('unable to generate new password')
707 711
708 712 pre_db = False
709 713
710 714 email_kwargs = {
711 715 'new_password': new_passwd,
712 716 'user': user,
713 717 'email': user_email,
714 718 'date': datetime.datetime.now(),
715 719 'first_admin_email': User.get_first_super_admin().email
716 720 }
717 721
718 722 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
719 723 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
720 724 **email_kwargs)
721 725
722 726 recipients = [user_email]
723 727
724 728 action_logger_generic(
725 729 'sent new password to user: {} with email: {}'.format(
726 730 user, user_email), namespace='security.password_reset')
727 731
728 732 run_task(tasks.send_email, recipients, subject,
729 733 email_body_plaintext, email_body)
730 734
731 735 except Exception:
732 736 log.error('Failed to update user password')
733 737 log.error(traceback.format_exc())
734 738 if pre_db:
735 739 # we rollback only if local db stuff fails. If it goes into
736 740 # run_task, we're pass rollback state this wouldn't work then
737 741 Session().rollback()
738 742
739 743 return True
740 744
741 745 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
742 746 """
743 747 Fetches auth_user by user_id,or api_key if present.
744 748 Fills auth_user attributes with those taken from database.
745 749 Additionally set's is_authenitated if lookup fails
746 750 present in database
747 751
748 752 :param auth_user: instance of user to set attributes
749 753 :param user_id: user id to fetch by
750 754 :param api_key: api key to fetch by
751 755 :param username: username to fetch by
752 756 """
753 757 def token_obfuscate(token):
754 758 if token:
755 759 return token[:4] + "****"
756 760
757 761 if user_id is None and api_key is None and username is None:
758 762 raise Exception('You need to pass user_id, api_key or username')
759 763
760 764 log.debug(
761 765 'AuthUser: fill data execution based on: '
762 766 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
763 767 try:
764 768 dbuser = None
765 769 if user_id:
766 770 dbuser = self.get(user_id)
767 771 elif api_key:
768 772 dbuser = self.get_by_auth_token(api_key)
769 773 elif username:
770 774 dbuser = self.get_by_username(username)
771 775
772 776 if not dbuser:
773 777 log.warning(
774 778 'Unable to lookup user by id:%s api_key:%s username:%s',
775 779 user_id, token_obfuscate(api_key), username)
776 780 return False
777 781 if not dbuser.active:
778 782 log.debug('User `%s:%s` is inactive, skipping fill data',
779 783 username, user_id)
780 784 return False
781 785
782 786 log.debug('AuthUser: filling found user:%s data', dbuser)
783 787
784 788 attrs = {
785 789 'user_id': dbuser.user_id,
786 790 'username': dbuser.username,
787 791 'name': dbuser.name,
788 792 'first_name': dbuser.first_name,
789 793 'firstname': dbuser.firstname,
790 794 'last_name': dbuser.last_name,
791 795 'lastname': dbuser.lastname,
792 796 'admin': dbuser.admin,
793 797 'active': dbuser.active,
794 798
795 799 'email': dbuser.email,
796 800 'emails': dbuser.emails_cached(),
797 801 'short_contact': dbuser.short_contact,
798 802 'full_contact': dbuser.full_contact,
799 803 'full_name': dbuser.full_name,
800 804 'full_name_or_username': dbuser.full_name_or_username,
801 805
802 806 '_api_key': dbuser._api_key,
803 807 '_user_data': dbuser._user_data,
804 808
805 809 'created_on': dbuser.created_on,
806 810 'extern_name': dbuser.extern_name,
807 811 'extern_type': dbuser.extern_type,
808 812
809 813 'inherit_default_permissions': dbuser.inherit_default_permissions,
810 814
811 815 'language': dbuser.language,
812 816 'last_activity': dbuser.last_activity,
813 817 'last_login': dbuser.last_login,
814 818 'password': dbuser.password,
815 819 }
816 820 auth_user.__dict__.update(attrs)
817 821 except Exception:
818 822 log.error(traceback.format_exc())
819 823 auth_user.is_authenticated = False
820 824 return False
821 825
822 826 return True
823 827
824 828 def has_perm(self, user, perm):
825 829 perm = self._get_perm(perm)
826 830 user = self._get_user(user)
827 831
828 832 return UserToPerm.query().filter(UserToPerm.user == user)\
829 833 .filter(UserToPerm.permission == perm).scalar() is not None
830 834
831 835 def grant_perm(self, user, perm):
832 836 """
833 837 Grant user global permissions
834 838
835 839 :param user:
836 840 :param perm:
837 841 """
838 842 user = self._get_user(user)
839 843 perm = self._get_perm(perm)
840 844 # if this permission is already granted skip it
841 845 _perm = UserToPerm.query()\
842 846 .filter(UserToPerm.user == user)\
843 847 .filter(UserToPerm.permission == perm)\
844 848 .scalar()
845 849 if _perm:
846 850 return
847 851 new = UserToPerm()
848 852 new.user = user
849 853 new.permission = perm
850 854 self.sa.add(new)
851 855 return new
852 856
853 857 def revoke_perm(self, user, perm):
854 858 """
855 859 Revoke users global permissions
856 860
857 861 :param user:
858 862 :param perm:
859 863 """
860 864 user = self._get_user(user)
861 865 perm = self._get_perm(perm)
862 866
863 867 obj = UserToPerm.query()\
864 868 .filter(UserToPerm.user == user)\
865 869 .filter(UserToPerm.permission == perm)\
866 870 .scalar()
867 871 if obj:
868 872 self.sa.delete(obj)
869 873
870 874 def add_extra_email(self, user, email):
871 875 """
872 876 Adds email address to UserEmailMap
873 877
874 878 :param user:
875 879 :param email:
876 880 """
877 881
878 882 user = self._get_user(user)
879 883
880 884 obj = UserEmailMap()
881 885 obj.user = user
882 886 obj.email = email
883 887 self.sa.add(obj)
884 888 return obj
885 889
886 890 def delete_extra_email(self, user, email_id):
887 891 """
888 892 Removes email address from UserEmailMap
889 893
890 894 :param user:
891 895 :param email_id:
892 896 """
893 897 user = self._get_user(user)
894 898 obj = UserEmailMap.query().get(email_id)
895 899 if obj and obj.user_id == user.user_id:
896 900 self.sa.delete(obj)
897 901
898 902 def parse_ip_range(self, ip_range):
899 903 ip_list = []
900 904
901 905 def make_unique(value):
902 906 seen = []
903 907 return [c for c in value if not (c in seen or seen.append(c))]
904 908
905 909 # firsts split by commas
906 910 for ip_range in ip_range.split(','):
907 911 if not ip_range:
908 912 continue
909 913 ip_range = ip_range.strip()
910 914 if '-' in ip_range:
911 915 start_ip, end_ip = ip_range.split('-', 1)
912 916 start_ip = ipaddress.ip_address(safe_str(start_ip.strip()))
913 917 end_ip = ipaddress.ip_address(safe_str(end_ip.strip()))
914 918 parsed_ip_range = []
915 919
916 920 for index in range(int(start_ip), int(end_ip) + 1):
917 921 new_ip = ipaddress.ip_address(index)
918 922 parsed_ip_range.append(str(new_ip))
919 923 ip_list.extend(parsed_ip_range)
920 924 else:
921 925 ip_list.append(ip_range)
922 926
923 927 return make_unique(ip_list)
924 928
925 929 def add_extra_ip(self, user, ip, description=None):
926 930 """
927 931 Adds ip address to UserIpMap
928 932
929 933 :param user:
930 934 :param ip:
931 935 """
932 936
933 937 user = self._get_user(user)
934 938 obj = UserIpMap()
935 939 obj.user = user
936 940 obj.ip_addr = ip
937 941 obj.description = description
938 942 self.sa.add(obj)
939 943 return obj
940 944
941 945 auth_token_role = AuthTokenModel.cls
942 946
943 947 def add_auth_token(self, user, lifetime_minutes, role, description='',
944 948 scope_callback=None):
945 949 """
946 950 Add AuthToken for user.
947 951
948 952 :param user: username/user_id
949 953 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
950 954 :param role: one of AuthTokenModel.cls.ROLE_*
951 955 :param description: optional string description
952 956 """
953 957
954 958 token = AuthTokenModel().create(
955 959 user, description, lifetime_minutes, role)
956 960 if scope_callback and callable(scope_callback):
957 961 # call the callback if we provide, used to attach scope for EE edition
958 962 scope_callback(token)
959 963 return token
960 964
961 965 def delete_extra_ip(self, user, ip_id):
962 966 """
963 967 Removes ip address from UserIpMap
964 968
965 969 :param user:
966 970 :param ip_id:
967 971 """
968 972 user = self._get_user(user)
969 973 obj = UserIpMap.query().get(ip_id)
970 974 if obj and obj.user_id == user.user_id:
971 975 self.sa.delete(obj)
972 976
973 977 def get_accounts_in_creation_order(self, current_user=None):
974 978 """
975 979 Get accounts in order of creation for deactivation for license limits
976 980
977 981 pick currently logged in user, and append to the list in position 0
978 982 pick all super-admins in order of creation date and add it to the list
979 983 pick all other accounts in order of creation and add it to the list.
980 984
981 985 Based on that list, the last accounts can be disabled as they are
982 986 created at the end and don't include any of the super admins as well
983 987 as the current user.
984 988
985 989 :param current_user: optionally current user running this operation
986 990 """
987 991
988 992 if not current_user:
989 993 current_user = get_current_rhodecode_user()
990 994 active_super_admins = [
991 995 x.user_id for x in User.query()
992 996 .filter(User.user_id != current_user.user_id)
993 997 .filter(User.active == true())
994 998 .filter(User.admin == true())
995 999 .order_by(User.created_on.asc())]
996 1000
997 1001 active_regular_users = [
998 1002 x.user_id for x in User.query()
999 1003 .filter(User.user_id != current_user.user_id)
1000 1004 .filter(User.active == true())
1001 1005 .filter(User.admin == false())
1002 1006 .order_by(User.created_on.asc())]
1003 1007
1004 1008 list_of_accounts = [current_user.user_id]
1005 1009 list_of_accounts += active_super_admins
1006 1010 list_of_accounts += active_regular_users
1007 1011
1008 1012 return list_of_accounts
1009 1013
1010 1014 def deactivate_last_users(self, expected_users, current_user=None):
1011 1015 """
1012 1016 Deactivate accounts that are over the license limits.
1013 1017 Algorithm of which accounts to disabled is based on the formula:
1014 1018
1015 1019 Get current user, then super admins in creation order, then regular
1016 1020 active users in creation order.
1017 1021
1018 1022 Using that list we mark all accounts from the end of it as inactive.
1019 1023 This way we block only latest created accounts.
1020 1024
1021 1025 :param expected_users: list of users in special order, we deactivate
1022 1026 the end N amount of users from that list
1023 1027 """
1024 1028
1025 1029 list_of_accounts = self.get_accounts_in_creation_order(
1026 1030 current_user=current_user)
1027 1031
1028 1032 for acc_id in list_of_accounts[expected_users + 1:]:
1029 1033 user = User.get(acc_id)
1030 1034 log.info('Deactivating account %s for license unlock', user)
1031 1035 user.active = False
1032 1036 Session().add(user)
1033 1037 Session().commit()
1034 1038
1035 1039 return
1036 1040
1037 1041 def get_user_log(self, user, filter_term):
1038 1042 user_log = UserLog.query()\
1039 1043 .filter(or_(UserLog.user_id == user.user_id,
1040 1044 UserLog.username == user.username))\
1041 1045 .options(joinedload(UserLog.user))\
1042 1046 .options(joinedload(UserLog.repository))\
1043 1047 .order_by(UserLog.action_date.desc())
1044 1048
1045 1049 user_log = user_log_filter(user_log, filter_term)
1046 1050 return user_log
General Comments 0
You need to be logged in to leave comments. Login now