##// END OF EJS Templates
app: improve logging, and remove DB calls on app startup.
milka -
r4548:2f66e04c default
parent child Browse files
Show More
@@ -1,1414 +1,1418 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
33 33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 34 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
35 35 from rhodecode.authentication.plugins import auth_rhodecode
36 36 from rhodecode.events import trigger
37 37 from rhodecode.model.db import true, UserNotice
38 38
39 from rhodecode.lib import audit_logger, rc_cache
39 from rhodecode.lib import audit_logger, rc_cache, auth
40 40 from rhodecode.lib.exceptions import (
41 41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
42 42 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
43 43 UserOwnsArtifactsException, DefaultUserException)
44 44 from rhodecode.lib.ext_json import json
45 45 from rhodecode.lib.auth import (
46 46 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
47 47 from rhodecode.lib import helpers as h
48 48 from rhodecode.lib.helpers import SqlPage
49 49 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
50 50 from rhodecode.model.auth_token import AuthTokenModel
51 51 from rhodecode.model.forms import (
52 52 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
53 53 UserExtraEmailForm, UserExtraIpForm)
54 54 from rhodecode.model.permission import PermissionModel
55 55 from rhodecode.model.repo_group import RepoGroupModel
56 56 from rhodecode.model.ssh_key import SshKeyModel
57 57 from rhodecode.model.user import UserModel
58 58 from rhodecode.model.user_group import UserGroupModel
59 59 from rhodecode.model.db import (
60 60 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
61 61 UserApiKeys, UserSshKeys, RepoGroup)
62 62 from rhodecode.model.meta import Session
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 class AdminUsersView(BaseAppView, DataGridAppView):
68 68
69 69 def load_default_context(self):
70 70 c = self._get_local_tmpl_context()
71 71 return c
72 72
73 73 @LoginRequired()
74 74 @HasPermissionAllDecorator('hg.admin')
75 75 @view_config(
76 76 route_name='users', request_method='GET',
77 77 renderer='rhodecode:templates/admin/users/users.mako')
78 78 def users_list(self):
79 79 c = self.load_default_context()
80 80 return self._get_template_context(c)
81 81
82 82 @LoginRequired()
83 83 @HasPermissionAllDecorator('hg.admin')
84 84 @view_config(
85 85 # renderer defined below
86 86 route_name='users_data', request_method='GET',
87 87 renderer='json_ext', xhr=True)
88 88 def users_list_data(self):
89 89 self.load_default_context()
90 90 column_map = {
91 91 'first_name': 'name',
92 92 'last_name': 'lastname',
93 93 }
94 94 draw, start, limit = self._extract_chunk(self.request)
95 95 search_q, order_by, order_dir = self._extract_ordering(
96 96 self.request, column_map=column_map)
97 97 _render = self.request.get_partial_renderer(
98 98 'rhodecode:templates/data_table/_dt_elements.mako')
99 99
100 100 def user_actions(user_id, username):
101 101 return _render("user_actions", user_id, username)
102 102
103 103 users_data_total_count = User.query()\
104 104 .filter(User.username != User.DEFAULT_USER) \
105 105 .count()
106 106
107 107 users_data_total_inactive_count = User.query()\
108 108 .filter(User.username != User.DEFAULT_USER) \
109 109 .filter(User.active != true())\
110 110 .count()
111 111
112 112 # json generate
113 113 base_q = User.query().filter(User.username != User.DEFAULT_USER)
114 114 base_inactive_q = base_q.filter(User.active != true())
115 115
116 116 if search_q:
117 117 like_expression = u'%{}%'.format(safe_unicode(search_q))
118 118 base_q = base_q.filter(or_(
119 119 User.username.ilike(like_expression),
120 120 User._email.ilike(like_expression),
121 121 User.name.ilike(like_expression),
122 122 User.lastname.ilike(like_expression),
123 123 ))
124 124 base_inactive_q = base_q.filter(User.active != true())
125 125
126 126 users_data_total_filtered_count = base_q.count()
127 127 users_data_total_filtered_inactive_count = base_inactive_q.count()
128 128
129 129 sort_col = getattr(User, order_by, None)
130 130 if sort_col:
131 131 if order_dir == 'asc':
132 132 # handle null values properly to order by NULL last
133 133 if order_by in ['last_activity']:
134 134 sort_col = coalesce(sort_col, datetime.date.max)
135 135 sort_col = sort_col.asc()
136 136 else:
137 137 # handle null values properly to order by NULL last
138 138 if order_by in ['last_activity']:
139 139 sort_col = coalesce(sort_col, datetime.date.min)
140 140 sort_col = sort_col.desc()
141 141
142 142 base_q = base_q.order_by(sort_col)
143 143 base_q = base_q.offset(start).limit(limit)
144 144
145 145 users_list = base_q.all()
146 146
147 147 users_data = []
148 148 for user in users_list:
149 149 users_data.append({
150 150 "username": h.gravatar_with_user(self.request, user.username),
151 151 "email": user.email,
152 152 "first_name": user.first_name,
153 153 "last_name": user.last_name,
154 154 "last_login": h.format_date(user.last_login),
155 155 "last_activity": h.format_date(user.last_activity),
156 156 "active": h.bool2icon(user.active),
157 157 "active_raw": user.active,
158 158 "admin": h.bool2icon(user.admin),
159 159 "extern_type": user.extern_type,
160 160 "extern_name": user.extern_name,
161 161 "action": user_actions(user.user_id, user.username),
162 162 })
163 163 data = ({
164 164 'draw': draw,
165 165 'data': users_data,
166 166 'recordsTotal': users_data_total_count,
167 167 'recordsFiltered': users_data_total_filtered_count,
168 168 'recordsTotalInactive': users_data_total_inactive_count,
169 169 'recordsFilteredInactive': users_data_total_filtered_inactive_count
170 170 })
171 171
172 172 return data
173 173
174 174 def _set_personal_repo_group_template_vars(self, c_obj):
175 175 DummyUser = AttributeDict({
176 176 'username': '${username}',
177 177 'user_id': '${user_id}',
178 178 })
179 179 c_obj.default_create_repo_group = RepoGroupModel() \
180 180 .get_default_create_personal_repo_group()
181 181 c_obj.personal_repo_group_name = RepoGroupModel() \
182 182 .get_personal_group_name(DummyUser)
183 183
184 184 @LoginRequired()
185 185 @HasPermissionAllDecorator('hg.admin')
186 186 @view_config(
187 187 route_name='users_new', request_method='GET',
188 188 renderer='rhodecode:templates/admin/users/user_add.mako')
189 189 def users_new(self):
190 190 _ = self.request.translate
191 191 c = self.load_default_context()
192 192 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
193 193 self._set_personal_repo_group_template_vars(c)
194 194 return self._get_template_context(c)
195 195
196 196 @LoginRequired()
197 197 @HasPermissionAllDecorator('hg.admin')
198 198 @CSRFRequired()
199 199 @view_config(
200 200 route_name='users_create', request_method='POST',
201 201 renderer='rhodecode:templates/admin/users/user_add.mako')
202 202 def users_create(self):
203 203 _ = self.request.translate
204 204 c = self.load_default_context()
205 205 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
206 206 user_model = UserModel()
207 207 user_form = UserForm(self.request.translate)()
208 208 try:
209 209 form_result = user_form.to_python(dict(self.request.POST))
210 210 user = user_model.create(form_result)
211 211 Session().flush()
212 212 creation_data = user.get_api_data()
213 213 username = form_result['username']
214 214
215 215 audit_logger.store_web(
216 216 'user.create', action_data={'data': creation_data},
217 217 user=c.rhodecode_user)
218 218
219 219 user_link = h.link_to(
220 220 h.escape(username),
221 221 h.route_path('user_edit', user_id=user.user_id))
222 222 h.flash(h.literal(_('Created user %(user_link)s')
223 223 % {'user_link': user_link}), category='success')
224 224 Session().commit()
225 225 except formencode.Invalid as errors:
226 226 self._set_personal_repo_group_template_vars(c)
227 227 data = render(
228 228 'rhodecode:templates/admin/users/user_add.mako',
229 229 self._get_template_context(c), self.request)
230 230 html = formencode.htmlfill.render(
231 231 data,
232 232 defaults=errors.value,
233 233 errors=errors.error_dict or {},
234 234 prefix_error=False,
235 235 encoding="UTF-8",
236 236 force_defaults=False
237 237 )
238 238 return Response(html)
239 239 except UserCreationError as e:
240 240 h.flash(e, 'error')
241 241 except Exception:
242 242 log.exception("Exception creation of user")
243 243 h.flash(_('Error occurred during creation of user %s')
244 244 % self.request.POST.get('username'), category='error')
245 245 raise HTTPFound(h.route_path('users'))
246 246
247 247
248 248 class UsersView(UserAppView):
249 249 ALLOW_SCOPED_TOKENS = False
250 250 """
251 251 This view has alternative version inside EE, if modified please take a look
252 252 in there as well.
253 253 """
254 254
255 255 def get_auth_plugins(self):
256 256 valid_plugins = []
257 257 authn_registry = get_authn_registry(self.request.registry)
258 258 for plugin in authn_registry.get_plugins_for_authentication():
259 259 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
260 260 valid_plugins.append(plugin)
261 261 elif plugin.name == 'rhodecode':
262 262 valid_plugins.append(plugin)
263 263
264 264 # extend our choices if user has set a bound plugin which isn't enabled at the
265 265 # moment
266 266 extern_type = self.db_user.extern_type
267 267 if extern_type not in [x.uid for x in valid_plugins]:
268 268 try:
269 269 plugin = authn_registry.get_plugin_by_uid(extern_type)
270 270 if plugin:
271 271 valid_plugins.append(plugin)
272 272
273 273 except Exception:
274 274 log.exception(
275 275 'Could not extend user plugins with `{}`'.format(extern_type))
276 276 return valid_plugins
277 277
278 278 def load_default_context(self):
279 279 req = self.request
280 280
281 281 c = self._get_local_tmpl_context()
282 282 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
283 283 c.allowed_languages = [
284 284 ('en', 'English (en)'),
285 285 ('de', 'German (de)'),
286 286 ('fr', 'French (fr)'),
287 287 ('it', 'Italian (it)'),
288 288 ('ja', 'Japanese (ja)'),
289 289 ('pl', 'Polish (pl)'),
290 290 ('pt', 'Portuguese (pt)'),
291 291 ('ru', 'Russian (ru)'),
292 292 ('zh', 'Chinese (zh)'),
293 293 ]
294 294
295 295 c.allowed_extern_types = [
296 296 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
297 297 ]
298 perms = req.registry.settings.get('available_permissions')
299 if not perms:
300 # inject info about available permissions
301 auth.set_available_permissions(req.registry.settings)
298 302
299 303 c.available_permissions = req.registry.settings['available_permissions']
300 304 PermissionModel().set_global_permission_choices(
301 305 c, gettext_translator=req.translate)
302 306
303 307 return c
304 308
305 309 @LoginRequired()
306 310 @HasPermissionAllDecorator('hg.admin')
307 311 @CSRFRequired()
308 312 @view_config(
309 313 route_name='user_update', request_method='POST',
310 314 renderer='rhodecode:templates/admin/users/user_edit.mako')
311 315 def user_update(self):
312 316 _ = self.request.translate
313 317 c = self.load_default_context()
314 318
315 319 user_id = self.db_user_id
316 320 c.user = self.db_user
317 321
318 322 c.active = 'profile'
319 323 c.extern_type = c.user.extern_type
320 324 c.extern_name = c.user.extern_name
321 325 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
322 326 available_languages = [x[0] for x in c.allowed_languages]
323 327 _form = UserForm(self.request.translate, edit=True,
324 328 available_languages=available_languages,
325 329 old_data={'user_id': user_id,
326 330 'email': c.user.email})()
327 331 form_result = {}
328 332 old_values = c.user.get_api_data()
329 333 try:
330 334 form_result = _form.to_python(dict(self.request.POST))
331 335 skip_attrs = ['extern_name']
332 336 # TODO: plugin should define if username can be updated
333 337 if c.extern_type != "rhodecode":
334 338 # forbid updating username for external accounts
335 339 skip_attrs.append('username')
336 340
337 341 UserModel().update_user(
338 342 user_id, skip_attrs=skip_attrs, **form_result)
339 343
340 344 audit_logger.store_web(
341 345 'user.edit', action_data={'old_data': old_values},
342 346 user=c.rhodecode_user)
343 347
344 348 Session().commit()
345 349 h.flash(_('User updated successfully'), category='success')
346 350 except formencode.Invalid as errors:
347 351 data = render(
348 352 'rhodecode:templates/admin/users/user_edit.mako',
349 353 self._get_template_context(c), self.request)
350 354 html = formencode.htmlfill.render(
351 355 data,
352 356 defaults=errors.value,
353 357 errors=errors.error_dict or {},
354 358 prefix_error=False,
355 359 encoding="UTF-8",
356 360 force_defaults=False
357 361 )
358 362 return Response(html)
359 363 except UserCreationError as e:
360 364 h.flash(e, 'error')
361 365 except Exception:
362 366 log.exception("Exception updating user")
363 367 h.flash(_('Error occurred during update of user %s')
364 368 % form_result.get('username'), category='error')
365 369 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
366 370
367 371 @LoginRequired()
368 372 @HasPermissionAllDecorator('hg.admin')
369 373 @CSRFRequired()
370 374 @view_config(
371 375 route_name='user_delete', request_method='POST',
372 376 renderer='rhodecode:templates/admin/users/user_edit.mako')
373 377 def user_delete(self):
374 378 _ = self.request.translate
375 379 c = self.load_default_context()
376 380 c.user = self.db_user
377 381
378 382 _repos = c.user.repositories
379 383 _repo_groups = c.user.repository_groups
380 384 _user_groups = c.user.user_groups
381 385 _pull_requests = c.user.user_pull_requests
382 386 _artifacts = c.user.artifacts
383 387
384 388 handle_repos = None
385 389 handle_repo_groups = None
386 390 handle_user_groups = None
387 391 handle_pull_requests = None
388 392 handle_artifacts = None
389 393
390 394 # calls for flash of handle based on handle case detach or delete
391 395 def set_handle_flash_repos():
392 396 handle = handle_repos
393 397 if handle == 'detach':
394 398 h.flash(_('Detached %s repositories') % len(_repos),
395 399 category='success')
396 400 elif handle == 'delete':
397 401 h.flash(_('Deleted %s repositories') % len(_repos),
398 402 category='success')
399 403
400 404 def set_handle_flash_repo_groups():
401 405 handle = handle_repo_groups
402 406 if handle == 'detach':
403 407 h.flash(_('Detached %s repository groups') % len(_repo_groups),
404 408 category='success')
405 409 elif handle == 'delete':
406 410 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
407 411 category='success')
408 412
409 413 def set_handle_flash_user_groups():
410 414 handle = handle_user_groups
411 415 if handle == 'detach':
412 416 h.flash(_('Detached %s user groups') % len(_user_groups),
413 417 category='success')
414 418 elif handle == 'delete':
415 419 h.flash(_('Deleted %s user groups') % len(_user_groups),
416 420 category='success')
417 421
418 422 def set_handle_flash_pull_requests():
419 423 handle = handle_pull_requests
420 424 if handle == 'detach':
421 425 h.flash(_('Detached %s pull requests') % len(_pull_requests),
422 426 category='success')
423 427 elif handle == 'delete':
424 428 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
425 429 category='success')
426 430
427 431 def set_handle_flash_artifacts():
428 432 handle = handle_artifacts
429 433 if handle == 'detach':
430 434 h.flash(_('Detached %s artifacts') % len(_artifacts),
431 435 category='success')
432 436 elif handle == 'delete':
433 437 h.flash(_('Deleted %s artifacts') % len(_artifacts),
434 438 category='success')
435 439
436 440 handle_user = User.get_first_super_admin()
437 441 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
438 442 if handle_user_id:
439 443 # NOTE(marcink): we get new owner for objects...
440 444 handle_user = User.get_or_404(handle_user_id)
441 445
442 446 if _repos and self.request.POST.get('user_repos'):
443 447 handle_repos = self.request.POST['user_repos']
444 448
445 449 if _repo_groups and self.request.POST.get('user_repo_groups'):
446 450 handle_repo_groups = self.request.POST['user_repo_groups']
447 451
448 452 if _user_groups and self.request.POST.get('user_user_groups'):
449 453 handle_user_groups = self.request.POST['user_user_groups']
450 454
451 455 if _pull_requests and self.request.POST.get('user_pull_requests'):
452 456 handle_pull_requests = self.request.POST['user_pull_requests']
453 457
454 458 if _artifacts and self.request.POST.get('user_artifacts'):
455 459 handle_artifacts = self.request.POST['user_artifacts']
456 460
457 461 old_values = c.user.get_api_data()
458 462
459 463 try:
460 464
461 465 UserModel().delete(
462 466 c.user,
463 467 handle_repos=handle_repos,
464 468 handle_repo_groups=handle_repo_groups,
465 469 handle_user_groups=handle_user_groups,
466 470 handle_pull_requests=handle_pull_requests,
467 471 handle_artifacts=handle_artifacts,
468 472 handle_new_owner=handle_user
469 473 )
470 474
471 475 audit_logger.store_web(
472 476 'user.delete', action_data={'old_data': old_values},
473 477 user=c.rhodecode_user)
474 478
475 479 Session().commit()
476 480 set_handle_flash_repos()
477 481 set_handle_flash_repo_groups()
478 482 set_handle_flash_user_groups()
479 483 set_handle_flash_pull_requests()
480 484 set_handle_flash_artifacts()
481 485 username = h.escape(old_values['username'])
482 486 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
483 487 except (UserOwnsReposException, UserOwnsRepoGroupsException,
484 488 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
485 489 UserOwnsArtifactsException, DefaultUserException) as e:
486 490 h.flash(e, category='warning')
487 491 except Exception:
488 492 log.exception("Exception during deletion of user")
489 493 h.flash(_('An error occurred during deletion of user'),
490 494 category='error')
491 495 raise HTTPFound(h.route_path('users'))
492 496
493 497 @LoginRequired()
494 498 @HasPermissionAllDecorator('hg.admin')
495 499 @view_config(
496 500 route_name='user_edit', request_method='GET',
497 501 renderer='rhodecode:templates/admin/users/user_edit.mako')
498 502 def user_edit(self):
499 503 _ = self.request.translate
500 504 c = self.load_default_context()
501 505 c.user = self.db_user
502 506
503 507 c.active = 'profile'
504 508 c.extern_type = c.user.extern_type
505 509 c.extern_name = c.user.extern_name
506 510 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
507 511
508 512 defaults = c.user.get_dict()
509 513 defaults.update({'language': c.user.user_data.get('language')})
510 514
511 515 data = render(
512 516 'rhodecode:templates/admin/users/user_edit.mako',
513 517 self._get_template_context(c), self.request)
514 518 html = formencode.htmlfill.render(
515 519 data,
516 520 defaults=defaults,
517 521 encoding="UTF-8",
518 522 force_defaults=False
519 523 )
520 524 return Response(html)
521 525
522 526 @LoginRequired()
523 527 @HasPermissionAllDecorator('hg.admin')
524 528 @view_config(
525 529 route_name='user_edit_advanced', request_method='GET',
526 530 renderer='rhodecode:templates/admin/users/user_edit.mako')
527 531 def user_edit_advanced(self):
528 532 _ = self.request.translate
529 533 c = self.load_default_context()
530 534
531 535 user_id = self.db_user_id
532 536 c.user = self.db_user
533 537
534 538 c.detach_user = User.get_first_super_admin()
535 539 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
536 540 if detach_user_id:
537 541 c.detach_user = User.get_or_404(detach_user_id)
538 542
539 543 c.active = 'advanced'
540 544 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
541 545 c.personal_repo_group_name = RepoGroupModel()\
542 546 .get_personal_group_name(c.user)
543 547
544 548 c.user_to_review_rules = sorted(
545 549 (x.user for x in c.user.user_review_rules),
546 550 key=lambda u: u.username.lower())
547 551
548 552 defaults = c.user.get_dict()
549 553
550 554 # Interim workaround if the user participated on any pull requests as a
551 555 # reviewer.
552 556 has_review = len(c.user.reviewer_pull_requests)
553 557 c.can_delete_user = not has_review
554 558 c.can_delete_user_message = ''
555 559 inactive_link = h.link_to(
556 560 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
557 561 if has_review == 1:
558 562 c.can_delete_user_message = h.literal(_(
559 563 'The user participates as reviewer in {} pull request and '
560 564 'cannot be deleted. \nYou can set the user to '
561 565 '"{}" instead of deleting it.').format(
562 566 has_review, inactive_link))
563 567 elif has_review:
564 568 c.can_delete_user_message = h.literal(_(
565 569 'The user participates as reviewer in {} pull requests and '
566 570 'cannot be deleted. \nYou can set the user to '
567 571 '"{}" instead of deleting it.').format(
568 572 has_review, inactive_link))
569 573
570 574 data = render(
571 575 'rhodecode:templates/admin/users/user_edit.mako',
572 576 self._get_template_context(c), self.request)
573 577 html = formencode.htmlfill.render(
574 578 data,
575 579 defaults=defaults,
576 580 encoding="UTF-8",
577 581 force_defaults=False
578 582 )
579 583 return Response(html)
580 584
581 585 @LoginRequired()
582 586 @HasPermissionAllDecorator('hg.admin')
583 587 @view_config(
584 588 route_name='user_edit_global_perms', request_method='GET',
585 589 renderer='rhodecode:templates/admin/users/user_edit.mako')
586 590 def user_edit_global_perms(self):
587 591 _ = self.request.translate
588 592 c = self.load_default_context()
589 593 c.user = self.db_user
590 594
591 595 c.active = 'global_perms'
592 596
593 597 c.default_user = User.get_default_user()
594 598 defaults = c.user.get_dict()
595 599 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
596 600 defaults.update(c.default_user.get_default_perms())
597 601 defaults.update(c.user.get_default_perms())
598 602
599 603 data = render(
600 604 'rhodecode:templates/admin/users/user_edit.mako',
601 605 self._get_template_context(c), self.request)
602 606 html = formencode.htmlfill.render(
603 607 data,
604 608 defaults=defaults,
605 609 encoding="UTF-8",
606 610 force_defaults=False
607 611 )
608 612 return Response(html)
609 613
610 614 @LoginRequired()
611 615 @HasPermissionAllDecorator('hg.admin')
612 616 @CSRFRequired()
613 617 @view_config(
614 618 route_name='user_edit_global_perms_update', request_method='POST',
615 619 renderer='rhodecode:templates/admin/users/user_edit.mako')
616 620 def user_edit_global_perms_update(self):
617 621 _ = self.request.translate
618 622 c = self.load_default_context()
619 623
620 624 user_id = self.db_user_id
621 625 c.user = self.db_user
622 626
623 627 c.active = 'global_perms'
624 628 try:
625 629 # first stage that verifies the checkbox
626 630 _form = UserIndividualPermissionsForm(self.request.translate)
627 631 form_result = _form.to_python(dict(self.request.POST))
628 632 inherit_perms = form_result['inherit_default_permissions']
629 633 c.user.inherit_default_permissions = inherit_perms
630 634 Session().add(c.user)
631 635
632 636 if not inherit_perms:
633 637 # only update the individual ones if we un check the flag
634 638 _form = UserPermissionsForm(
635 639 self.request.translate,
636 640 [x[0] for x in c.repo_create_choices],
637 641 [x[0] for x in c.repo_create_on_write_choices],
638 642 [x[0] for x in c.repo_group_create_choices],
639 643 [x[0] for x in c.user_group_create_choices],
640 644 [x[0] for x in c.fork_choices],
641 645 [x[0] for x in c.inherit_default_permission_choices])()
642 646
643 647 form_result = _form.to_python(dict(self.request.POST))
644 648 form_result.update({'perm_user_id': c.user.user_id})
645 649
646 650 PermissionModel().update_user_permissions(form_result)
647 651
648 652 # TODO(marcink): implement global permissions
649 653 # audit_log.store_web('user.edit.permissions')
650 654
651 655 Session().commit()
652 656
653 657 h.flash(_('User global permissions updated successfully'),
654 658 category='success')
655 659
656 660 except formencode.Invalid as errors:
657 661 data = render(
658 662 'rhodecode:templates/admin/users/user_edit.mako',
659 663 self._get_template_context(c), self.request)
660 664 html = formencode.htmlfill.render(
661 665 data,
662 666 defaults=errors.value,
663 667 errors=errors.error_dict or {},
664 668 prefix_error=False,
665 669 encoding="UTF-8",
666 670 force_defaults=False
667 671 )
668 672 return Response(html)
669 673 except Exception:
670 674 log.exception("Exception during permissions saving")
671 675 h.flash(_('An error occurred during permissions saving'),
672 676 category='error')
673 677
674 678 affected_user_ids = [user_id]
675 679 PermissionModel().trigger_permission_flush(affected_user_ids)
676 680 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
677 681
678 682 @LoginRequired()
679 683 @HasPermissionAllDecorator('hg.admin')
680 684 @CSRFRequired()
681 685 @view_config(
682 686 route_name='user_enable_force_password_reset', request_method='POST',
683 687 renderer='rhodecode:templates/admin/users/user_edit.mako')
684 688 def user_enable_force_password_reset(self):
685 689 _ = self.request.translate
686 690 c = self.load_default_context()
687 691
688 692 user_id = self.db_user_id
689 693 c.user = self.db_user
690 694
691 695 try:
692 696 c.user.update_userdata(force_password_change=True)
693 697
694 698 msg = _('Force password change enabled for user')
695 699 audit_logger.store_web('user.edit.password_reset.enabled',
696 700 user=c.rhodecode_user)
697 701
698 702 Session().commit()
699 703 h.flash(msg, category='success')
700 704 except Exception:
701 705 log.exception("Exception during password reset for user")
702 706 h.flash(_('An error occurred during password reset for user'),
703 707 category='error')
704 708
705 709 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
706 710
707 711 @LoginRequired()
708 712 @HasPermissionAllDecorator('hg.admin')
709 713 @CSRFRequired()
710 714 @view_config(
711 715 route_name='user_disable_force_password_reset', request_method='POST',
712 716 renderer='rhodecode:templates/admin/users/user_edit.mako')
713 717 def user_disable_force_password_reset(self):
714 718 _ = self.request.translate
715 719 c = self.load_default_context()
716 720
717 721 user_id = self.db_user_id
718 722 c.user = self.db_user
719 723
720 724 try:
721 725 c.user.update_userdata(force_password_change=False)
722 726
723 727 msg = _('Force password change disabled for user')
724 728 audit_logger.store_web(
725 729 'user.edit.password_reset.disabled',
726 730 user=c.rhodecode_user)
727 731
728 732 Session().commit()
729 733 h.flash(msg, category='success')
730 734 except Exception:
731 735 log.exception("Exception during password reset for user")
732 736 h.flash(_('An error occurred during password reset for user'),
733 737 category='error')
734 738
735 739 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
736 740
737 741 @LoginRequired()
738 742 @HasPermissionAllDecorator('hg.admin')
739 743 @CSRFRequired()
740 744 @view_config(
741 745 route_name='user_notice_dismiss', request_method='POST',
742 746 renderer='json_ext', xhr=True)
743 747 def user_notice_dismiss(self):
744 748 _ = self.request.translate
745 749 c = self.load_default_context()
746 750
747 751 user_id = self.db_user_id
748 752 c.user = self.db_user
749 753 user_notice_id = safe_int(self.request.POST.get('notice_id'))
750 754 notice = UserNotice().query()\
751 755 .filter(UserNotice.user_id == user_id)\
752 756 .filter(UserNotice.user_notice_id == user_notice_id)\
753 757 .scalar()
754 758 read = False
755 759 if notice:
756 760 notice.notice_read = True
757 761 Session().add(notice)
758 762 Session().commit()
759 763 read = True
760 764
761 765 return {'notice': user_notice_id, 'read': read}
762 766
763 767 @LoginRequired()
764 768 @HasPermissionAllDecorator('hg.admin')
765 769 @CSRFRequired()
766 770 @view_config(
767 771 route_name='user_create_personal_repo_group', request_method='POST',
768 772 renderer='rhodecode:templates/admin/users/user_edit.mako')
769 773 def user_create_personal_repo_group(self):
770 774 """
771 775 Create personal repository group for this user
772 776 """
773 777 from rhodecode.model.repo_group import RepoGroupModel
774 778
775 779 _ = self.request.translate
776 780 c = self.load_default_context()
777 781
778 782 user_id = self.db_user_id
779 783 c.user = self.db_user
780 784
781 785 personal_repo_group = RepoGroup.get_user_personal_repo_group(
782 786 c.user.user_id)
783 787 if personal_repo_group:
784 788 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
785 789
786 790 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
787 791 named_personal_group = RepoGroup.get_by_group_name(
788 792 personal_repo_group_name)
789 793 try:
790 794
791 795 if named_personal_group and named_personal_group.user_id == c.user.user_id:
792 796 # migrate the same named group, and mark it as personal
793 797 named_personal_group.personal = True
794 798 Session().add(named_personal_group)
795 799 Session().commit()
796 800 msg = _('Linked repository group `%s` as personal' % (
797 801 personal_repo_group_name,))
798 802 h.flash(msg, category='success')
799 803 elif not named_personal_group:
800 804 RepoGroupModel().create_personal_repo_group(c.user)
801 805
802 806 msg = _('Created repository group `%s`' % (
803 807 personal_repo_group_name,))
804 808 h.flash(msg, category='success')
805 809 else:
806 810 msg = _('Repository group `%s` is already taken' % (
807 811 personal_repo_group_name,))
808 812 h.flash(msg, category='warning')
809 813 except Exception:
810 814 log.exception("Exception during repository group creation")
811 815 msg = _(
812 816 'An error occurred during repository group creation for user')
813 817 h.flash(msg, category='error')
814 818 Session().rollback()
815 819
816 820 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
817 821
818 822 @LoginRequired()
819 823 @HasPermissionAllDecorator('hg.admin')
820 824 @view_config(
821 825 route_name='edit_user_auth_tokens', request_method='GET',
822 826 renderer='rhodecode:templates/admin/users/user_edit.mako')
823 827 def auth_tokens(self):
824 828 _ = self.request.translate
825 829 c = self.load_default_context()
826 830 c.user = self.db_user
827 831
828 832 c.active = 'auth_tokens'
829 833
830 834 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
831 835 c.role_values = [
832 836 (x, AuthTokenModel.cls._get_role_name(x))
833 837 for x in AuthTokenModel.cls.ROLES]
834 838 c.role_options = [(c.role_values, _("Role"))]
835 839 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
836 840 c.user.user_id, show_expired=True)
837 841 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
838 842 return self._get_template_context(c)
839 843
840 844 @LoginRequired()
841 845 @HasPermissionAllDecorator('hg.admin')
842 846 @view_config(
843 847 route_name='edit_user_auth_tokens_view', request_method='POST',
844 848 renderer='json_ext', xhr=True)
845 849 def auth_tokens_view(self):
846 850 _ = self.request.translate
847 851 c = self.load_default_context()
848 852 c.user = self.db_user
849 853
850 854 auth_token_id = self.request.POST.get('auth_token_id')
851 855
852 856 if auth_token_id:
853 857 token = UserApiKeys.get_or_404(auth_token_id)
854 858
855 859 return {
856 860 'auth_token': token.api_key
857 861 }
858 862
859 863 def maybe_attach_token_scope(self, token):
860 864 # implemented in EE edition
861 865 pass
862 866
863 867 @LoginRequired()
864 868 @HasPermissionAllDecorator('hg.admin')
865 869 @CSRFRequired()
866 870 @view_config(
867 871 route_name='edit_user_auth_tokens_add', request_method='POST')
868 872 def auth_tokens_add(self):
869 873 _ = self.request.translate
870 874 c = self.load_default_context()
871 875
872 876 user_id = self.db_user_id
873 877 c.user = self.db_user
874 878
875 879 user_data = c.user.get_api_data()
876 880 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
877 881 description = self.request.POST.get('description')
878 882 role = self.request.POST.get('role')
879 883
880 884 token = UserModel().add_auth_token(
881 885 user=c.user.user_id,
882 886 lifetime_minutes=lifetime, role=role, description=description,
883 887 scope_callback=self.maybe_attach_token_scope)
884 888 token_data = token.get_api_data()
885 889
886 890 audit_logger.store_web(
887 891 'user.edit.token.add', action_data={
888 892 'data': {'token': token_data, 'user': user_data}},
889 893 user=self._rhodecode_user, )
890 894 Session().commit()
891 895
892 896 h.flash(_("Auth token successfully created"), category='success')
893 897 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
894 898
895 899 @LoginRequired()
896 900 @HasPermissionAllDecorator('hg.admin')
897 901 @CSRFRequired()
898 902 @view_config(
899 903 route_name='edit_user_auth_tokens_delete', request_method='POST')
900 904 def auth_tokens_delete(self):
901 905 _ = self.request.translate
902 906 c = self.load_default_context()
903 907
904 908 user_id = self.db_user_id
905 909 c.user = self.db_user
906 910
907 911 user_data = c.user.get_api_data()
908 912
909 913 del_auth_token = self.request.POST.get('del_auth_token')
910 914
911 915 if del_auth_token:
912 916 token = UserApiKeys.get_or_404(del_auth_token)
913 917 token_data = token.get_api_data()
914 918
915 919 AuthTokenModel().delete(del_auth_token, c.user.user_id)
916 920 audit_logger.store_web(
917 921 'user.edit.token.delete', action_data={
918 922 'data': {'token': token_data, 'user': user_data}},
919 923 user=self._rhodecode_user,)
920 924 Session().commit()
921 925 h.flash(_("Auth token successfully deleted"), category='success')
922 926
923 927 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
924 928
925 929 @LoginRequired()
926 930 @HasPermissionAllDecorator('hg.admin')
927 931 @view_config(
928 932 route_name='edit_user_ssh_keys', request_method='GET',
929 933 renderer='rhodecode:templates/admin/users/user_edit.mako')
930 934 def ssh_keys(self):
931 935 _ = self.request.translate
932 936 c = self.load_default_context()
933 937 c.user = self.db_user
934 938
935 939 c.active = 'ssh_keys'
936 940 c.default_key = self.request.GET.get('default_key')
937 941 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
938 942 return self._get_template_context(c)
939 943
940 944 @LoginRequired()
941 945 @HasPermissionAllDecorator('hg.admin')
942 946 @view_config(
943 947 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
944 948 renderer='rhodecode:templates/admin/users/user_edit.mako')
945 949 def ssh_keys_generate_keypair(self):
946 950 _ = self.request.translate
947 951 c = self.load_default_context()
948 952
949 953 c.user = self.db_user
950 954
951 955 c.active = 'ssh_keys_generate'
952 956 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
953 957 private_format = self.request.GET.get('private_format') \
954 958 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
955 959 c.private, c.public = SshKeyModel().generate_keypair(
956 960 comment=comment, private_format=private_format)
957 961
958 962 return self._get_template_context(c)
959 963
960 964 @LoginRequired()
961 965 @HasPermissionAllDecorator('hg.admin')
962 966 @CSRFRequired()
963 967 @view_config(
964 968 route_name='edit_user_ssh_keys_add', request_method='POST')
965 969 def ssh_keys_add(self):
966 970 _ = self.request.translate
967 971 c = self.load_default_context()
968 972
969 973 user_id = self.db_user_id
970 974 c.user = self.db_user
971 975
972 976 user_data = c.user.get_api_data()
973 977 key_data = self.request.POST.get('key_data')
974 978 description = self.request.POST.get('description')
975 979
976 980 fingerprint = 'unknown'
977 981 try:
978 982 if not key_data:
979 983 raise ValueError('Please add a valid public key')
980 984
981 985 key = SshKeyModel().parse_key(key_data.strip())
982 986 fingerprint = key.hash_md5()
983 987
984 988 ssh_key = SshKeyModel().create(
985 989 c.user.user_id, fingerprint, key.keydata, description)
986 990 ssh_key_data = ssh_key.get_api_data()
987 991
988 992 audit_logger.store_web(
989 993 'user.edit.ssh_key.add', action_data={
990 994 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
991 995 user=self._rhodecode_user, )
992 996 Session().commit()
993 997
994 998 # Trigger an event on change of keys.
995 999 trigger(SshKeyFileChangeEvent(), self.request.registry)
996 1000
997 1001 h.flash(_("Ssh Key successfully created"), category='success')
998 1002
999 1003 except IntegrityError:
1000 1004 log.exception("Exception during ssh key saving")
1001 1005 err = 'Such key with fingerprint `{}` already exists, ' \
1002 1006 'please use a different one'.format(fingerprint)
1003 1007 h.flash(_('An error occurred during ssh key saving: {}').format(err),
1004 1008 category='error')
1005 1009 except Exception as e:
1006 1010 log.exception("Exception during ssh key saving")
1007 1011 h.flash(_('An error occurred during ssh key saving: {}').format(e),
1008 1012 category='error')
1009 1013
1010 1014 return HTTPFound(
1011 1015 h.route_path('edit_user_ssh_keys', user_id=user_id))
1012 1016
1013 1017 @LoginRequired()
1014 1018 @HasPermissionAllDecorator('hg.admin')
1015 1019 @CSRFRequired()
1016 1020 @view_config(
1017 1021 route_name='edit_user_ssh_keys_delete', request_method='POST')
1018 1022 def ssh_keys_delete(self):
1019 1023 _ = self.request.translate
1020 1024 c = self.load_default_context()
1021 1025
1022 1026 user_id = self.db_user_id
1023 1027 c.user = self.db_user
1024 1028
1025 1029 user_data = c.user.get_api_data()
1026 1030
1027 1031 del_ssh_key = self.request.POST.get('del_ssh_key')
1028 1032
1029 1033 if del_ssh_key:
1030 1034 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
1031 1035 ssh_key_data = ssh_key.get_api_data()
1032 1036
1033 1037 SshKeyModel().delete(del_ssh_key, c.user.user_id)
1034 1038 audit_logger.store_web(
1035 1039 'user.edit.ssh_key.delete', action_data={
1036 1040 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
1037 1041 user=self._rhodecode_user,)
1038 1042 Session().commit()
1039 1043 # Trigger an event on change of keys.
1040 1044 trigger(SshKeyFileChangeEvent(), self.request.registry)
1041 1045 h.flash(_("Ssh key successfully deleted"), category='success')
1042 1046
1043 1047 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
1044 1048
1045 1049 @LoginRequired()
1046 1050 @HasPermissionAllDecorator('hg.admin')
1047 1051 @view_config(
1048 1052 route_name='edit_user_emails', request_method='GET',
1049 1053 renderer='rhodecode:templates/admin/users/user_edit.mako')
1050 1054 def emails(self):
1051 1055 _ = self.request.translate
1052 1056 c = self.load_default_context()
1053 1057 c.user = self.db_user
1054 1058
1055 1059 c.active = 'emails'
1056 1060 c.user_email_map = UserEmailMap.query() \
1057 1061 .filter(UserEmailMap.user == c.user).all()
1058 1062
1059 1063 return self._get_template_context(c)
1060 1064
1061 1065 @LoginRequired()
1062 1066 @HasPermissionAllDecorator('hg.admin')
1063 1067 @CSRFRequired()
1064 1068 @view_config(
1065 1069 route_name='edit_user_emails_add', request_method='POST')
1066 1070 def emails_add(self):
1067 1071 _ = self.request.translate
1068 1072 c = self.load_default_context()
1069 1073
1070 1074 user_id = self.db_user_id
1071 1075 c.user = self.db_user
1072 1076
1073 1077 email = self.request.POST.get('new_email')
1074 1078 user_data = c.user.get_api_data()
1075 1079 try:
1076 1080
1077 1081 form = UserExtraEmailForm(self.request.translate)()
1078 1082 data = form.to_python({'email': email})
1079 1083 email = data['email']
1080 1084
1081 1085 UserModel().add_extra_email(c.user.user_id, email)
1082 1086 audit_logger.store_web(
1083 1087 'user.edit.email.add',
1084 1088 action_data={'email': email, 'user': user_data},
1085 1089 user=self._rhodecode_user)
1086 1090 Session().commit()
1087 1091 h.flash(_("Added new email address `%s` for user account") % email,
1088 1092 category='success')
1089 1093 except formencode.Invalid as error:
1090 1094 h.flash(h.escape(error.error_dict['email']), category='error')
1091 1095 except IntegrityError:
1092 1096 log.warning("Email %s already exists", email)
1093 1097 h.flash(_('Email `{}` is already registered for another user.').format(email),
1094 1098 category='error')
1095 1099 except Exception:
1096 1100 log.exception("Exception during email saving")
1097 1101 h.flash(_('An error occurred during email saving'),
1098 1102 category='error')
1099 1103 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1100 1104
1101 1105 @LoginRequired()
1102 1106 @HasPermissionAllDecorator('hg.admin')
1103 1107 @CSRFRequired()
1104 1108 @view_config(
1105 1109 route_name='edit_user_emails_delete', request_method='POST')
1106 1110 def emails_delete(self):
1107 1111 _ = self.request.translate
1108 1112 c = self.load_default_context()
1109 1113
1110 1114 user_id = self.db_user_id
1111 1115 c.user = self.db_user
1112 1116
1113 1117 email_id = self.request.POST.get('del_email_id')
1114 1118 user_model = UserModel()
1115 1119
1116 1120 email = UserEmailMap.query().get(email_id).email
1117 1121 user_data = c.user.get_api_data()
1118 1122 user_model.delete_extra_email(c.user.user_id, email_id)
1119 1123 audit_logger.store_web(
1120 1124 'user.edit.email.delete',
1121 1125 action_data={'email': email, 'user': user_data},
1122 1126 user=self._rhodecode_user)
1123 1127 Session().commit()
1124 1128 h.flash(_("Removed email address from user account"),
1125 1129 category='success')
1126 1130 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1127 1131
1128 1132 @LoginRequired()
1129 1133 @HasPermissionAllDecorator('hg.admin')
1130 1134 @view_config(
1131 1135 route_name='edit_user_ips', request_method='GET',
1132 1136 renderer='rhodecode:templates/admin/users/user_edit.mako')
1133 1137 def ips(self):
1134 1138 _ = self.request.translate
1135 1139 c = self.load_default_context()
1136 1140 c.user = self.db_user
1137 1141
1138 1142 c.active = 'ips'
1139 1143 c.user_ip_map = UserIpMap.query() \
1140 1144 .filter(UserIpMap.user == c.user).all()
1141 1145
1142 1146 c.inherit_default_ips = c.user.inherit_default_permissions
1143 1147 c.default_user_ip_map = UserIpMap.query() \
1144 1148 .filter(UserIpMap.user == User.get_default_user()).all()
1145 1149
1146 1150 return self._get_template_context(c)
1147 1151
1148 1152 @LoginRequired()
1149 1153 @HasPermissionAllDecorator('hg.admin')
1150 1154 @CSRFRequired()
1151 1155 @view_config(
1152 1156 route_name='edit_user_ips_add', request_method='POST')
1153 1157 # NOTE(marcink): this view is allowed for default users, as we can
1154 1158 # edit their IP white list
1155 1159 def ips_add(self):
1156 1160 _ = self.request.translate
1157 1161 c = self.load_default_context()
1158 1162
1159 1163 user_id = self.db_user_id
1160 1164 c.user = self.db_user
1161 1165
1162 1166 user_model = UserModel()
1163 1167 desc = self.request.POST.get('description')
1164 1168 try:
1165 1169 ip_list = user_model.parse_ip_range(
1166 1170 self.request.POST.get('new_ip'))
1167 1171 except Exception as e:
1168 1172 ip_list = []
1169 1173 log.exception("Exception during ip saving")
1170 1174 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1171 1175 category='error')
1172 1176 added = []
1173 1177 user_data = c.user.get_api_data()
1174 1178 for ip in ip_list:
1175 1179 try:
1176 1180 form = UserExtraIpForm(self.request.translate)()
1177 1181 data = form.to_python({'ip': ip})
1178 1182 ip = data['ip']
1179 1183
1180 1184 user_model.add_extra_ip(c.user.user_id, ip, desc)
1181 1185 audit_logger.store_web(
1182 1186 'user.edit.ip.add',
1183 1187 action_data={'ip': ip, 'user': user_data},
1184 1188 user=self._rhodecode_user)
1185 1189 Session().commit()
1186 1190 added.append(ip)
1187 1191 except formencode.Invalid as error:
1188 1192 msg = error.error_dict['ip']
1189 1193 h.flash(msg, category='error')
1190 1194 except Exception:
1191 1195 log.exception("Exception during ip saving")
1192 1196 h.flash(_('An error occurred during ip saving'),
1193 1197 category='error')
1194 1198 if added:
1195 1199 h.flash(
1196 1200 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1197 1201 category='success')
1198 1202 if 'default_user' in self.request.POST:
1199 1203 # case for editing global IP list we do it for 'DEFAULT' user
1200 1204 raise HTTPFound(h.route_path('admin_permissions_ips'))
1201 1205 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1202 1206
1203 1207 @LoginRequired()
1204 1208 @HasPermissionAllDecorator('hg.admin')
1205 1209 @CSRFRequired()
1206 1210 @view_config(
1207 1211 route_name='edit_user_ips_delete', request_method='POST')
1208 1212 # NOTE(marcink): this view is allowed for default users, as we can
1209 1213 # edit their IP white list
1210 1214 def ips_delete(self):
1211 1215 _ = self.request.translate
1212 1216 c = self.load_default_context()
1213 1217
1214 1218 user_id = self.db_user_id
1215 1219 c.user = self.db_user
1216 1220
1217 1221 ip_id = self.request.POST.get('del_ip_id')
1218 1222 user_model = UserModel()
1219 1223 user_data = c.user.get_api_data()
1220 1224 ip = UserIpMap.query().get(ip_id).ip_addr
1221 1225 user_model.delete_extra_ip(c.user.user_id, ip_id)
1222 1226 audit_logger.store_web(
1223 1227 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1224 1228 user=self._rhodecode_user)
1225 1229 Session().commit()
1226 1230 h.flash(_("Removed ip address from user whitelist"), category='success')
1227 1231
1228 1232 if 'default_user' in self.request.POST:
1229 1233 # case for editing global IP list we do it for 'DEFAULT' user
1230 1234 raise HTTPFound(h.route_path('admin_permissions_ips'))
1231 1235 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1232 1236
1233 1237 @LoginRequired()
1234 1238 @HasPermissionAllDecorator('hg.admin')
1235 1239 @view_config(
1236 1240 route_name='edit_user_groups_management', request_method='GET',
1237 1241 renderer='rhodecode:templates/admin/users/user_edit.mako')
1238 1242 def groups_management(self):
1239 1243 c = self.load_default_context()
1240 1244 c.user = self.db_user
1241 1245 c.data = c.user.group_member
1242 1246
1243 1247 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1244 1248 for group in c.user.group_member]
1245 1249 c.groups = json.dumps(groups)
1246 1250 c.active = 'groups'
1247 1251
1248 1252 return self._get_template_context(c)
1249 1253
1250 1254 @LoginRequired()
1251 1255 @HasPermissionAllDecorator('hg.admin')
1252 1256 @CSRFRequired()
1253 1257 @view_config(
1254 1258 route_name='edit_user_groups_management_updates', request_method='POST')
1255 1259 def groups_management_updates(self):
1256 1260 _ = self.request.translate
1257 1261 c = self.load_default_context()
1258 1262
1259 1263 user_id = self.db_user_id
1260 1264 c.user = self.db_user
1261 1265
1262 1266 user_groups = set(self.request.POST.getall('users_group_id'))
1263 1267 user_groups_objects = []
1264 1268
1265 1269 for ugid in user_groups:
1266 1270 user_groups_objects.append(
1267 1271 UserGroupModel().get_group(safe_int(ugid)))
1268 1272 user_group_model = UserGroupModel()
1269 1273 added_to_groups, removed_from_groups = \
1270 1274 user_group_model.change_groups(c.user, user_groups_objects)
1271 1275
1272 1276 user_data = c.user.get_api_data()
1273 1277 for user_group_id in added_to_groups:
1274 1278 user_group = UserGroup.get(user_group_id)
1275 1279 old_values = user_group.get_api_data()
1276 1280 audit_logger.store_web(
1277 1281 'user_group.edit.member.add',
1278 1282 action_data={'user': user_data, 'old_data': old_values},
1279 1283 user=self._rhodecode_user)
1280 1284
1281 1285 for user_group_id in removed_from_groups:
1282 1286 user_group = UserGroup.get(user_group_id)
1283 1287 old_values = user_group.get_api_data()
1284 1288 audit_logger.store_web(
1285 1289 'user_group.edit.member.delete',
1286 1290 action_data={'user': user_data, 'old_data': old_values},
1287 1291 user=self._rhodecode_user)
1288 1292
1289 1293 Session().commit()
1290 1294 c.active = 'user_groups_management'
1291 1295 h.flash(_("Groups successfully changed"), category='success')
1292 1296
1293 1297 return HTTPFound(h.route_path(
1294 1298 'edit_user_groups_management', user_id=user_id))
1295 1299
1296 1300 @LoginRequired()
1297 1301 @HasPermissionAllDecorator('hg.admin')
1298 1302 @view_config(
1299 1303 route_name='edit_user_audit_logs', request_method='GET',
1300 1304 renderer='rhodecode:templates/admin/users/user_edit.mako')
1301 1305 def user_audit_logs(self):
1302 1306 _ = self.request.translate
1303 1307 c = self.load_default_context()
1304 1308 c.user = self.db_user
1305 1309
1306 1310 c.active = 'audit'
1307 1311
1308 1312 p = safe_int(self.request.GET.get('page', 1), 1)
1309 1313
1310 1314 filter_term = self.request.GET.get('filter')
1311 1315 user_log = UserModel().get_user_log(c.user, filter_term)
1312 1316
1313 1317 def url_generator(page_num):
1314 1318 query_params = {
1315 1319 'page': page_num
1316 1320 }
1317 1321 if filter_term:
1318 1322 query_params['filter'] = filter_term
1319 1323 return self.request.current_route_path(_query=query_params)
1320 1324
1321 1325 c.audit_logs = SqlPage(
1322 1326 user_log, page=p, items_per_page=10, url_maker=url_generator)
1323 1327 c.filter_term = filter_term
1324 1328 return self._get_template_context(c)
1325 1329
1326 1330 @LoginRequired()
1327 1331 @HasPermissionAllDecorator('hg.admin')
1328 1332 @view_config(
1329 1333 route_name='edit_user_audit_logs_download', request_method='GET',
1330 1334 renderer='string')
1331 1335 def user_audit_logs_download(self):
1332 1336 _ = self.request.translate
1333 1337 c = self.load_default_context()
1334 1338 c.user = self.db_user
1335 1339
1336 1340 user_log = UserModel().get_user_log(c.user, filter_term=None)
1337 1341
1338 1342 audit_log_data = {}
1339 1343 for entry in user_log:
1340 1344 audit_log_data[entry.user_log_id] = entry.get_dict()
1341 1345
1342 1346 response = Response(json.dumps(audit_log_data, indent=4))
1343 1347 response.content_disposition = str(
1344 1348 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1345 1349 response.content_type = 'application/json'
1346 1350
1347 1351 return response
1348 1352
1349 1353 @LoginRequired()
1350 1354 @HasPermissionAllDecorator('hg.admin')
1351 1355 @view_config(
1352 1356 route_name='edit_user_perms_summary', request_method='GET',
1353 1357 renderer='rhodecode:templates/admin/users/user_edit.mako')
1354 1358 def user_perms_summary(self):
1355 1359 _ = self.request.translate
1356 1360 c = self.load_default_context()
1357 1361 c.user = self.db_user
1358 1362
1359 1363 c.active = 'perms_summary'
1360 1364 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1361 1365
1362 1366 return self._get_template_context(c)
1363 1367
1364 1368 @LoginRequired()
1365 1369 @HasPermissionAllDecorator('hg.admin')
1366 1370 @view_config(
1367 1371 route_name='edit_user_perms_summary_json', request_method='GET',
1368 1372 renderer='json_ext')
1369 1373 def user_perms_summary_json(self):
1370 1374 self.load_default_context()
1371 1375 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1372 1376
1373 1377 return perm_user.permissions
1374 1378
1375 1379 @LoginRequired()
1376 1380 @HasPermissionAllDecorator('hg.admin')
1377 1381 @view_config(
1378 1382 route_name='edit_user_caches', request_method='GET',
1379 1383 renderer='rhodecode:templates/admin/users/user_edit.mako')
1380 1384 def user_caches(self):
1381 1385 _ = self.request.translate
1382 1386 c = self.load_default_context()
1383 1387 c.user = self.db_user
1384 1388
1385 1389 c.active = 'caches'
1386 1390 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1387 1391
1388 1392 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1389 1393 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1390 1394 c.backend = c.region.backend
1391 1395 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1392 1396
1393 1397 return self._get_template_context(c)
1394 1398
1395 1399 @LoginRequired()
1396 1400 @HasPermissionAllDecorator('hg.admin')
1397 1401 @CSRFRequired()
1398 1402 @view_config(
1399 1403 route_name='edit_user_caches_update', request_method='POST')
1400 1404 def user_caches_update(self):
1401 1405 _ = self.request.translate
1402 1406 c = self.load_default_context()
1403 1407 c.user = self.db_user
1404 1408
1405 1409 c.active = 'caches'
1406 1410 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1407 1411
1408 1412 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1409 1413 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1410 1414
1411 1415 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1412 1416
1413 1417 return HTTPFound(h.route_path(
1414 1418 'edit_user_caches', user_id=c.user.user_id))
@@ -1,768 +1,766 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import tempfile
26 26 import time
27 27
28 28 from paste.gzipper import make_gzip_middleware
29 29 import pyramid.events
30 30 from pyramid.wsgi import wsgiapp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 36 from pyramid.renderers import render_to_response
37 37
38 38 from rhodecode.model import meta
39 39 from rhodecode.config import patches
40 40 from rhodecode.config import utils as config_utils
41 41 from rhodecode.config.environment import load_pyramid_environment
42 42
43 43 import rhodecode.events
44 44 from rhodecode.lib.middleware.vcs import VCSMiddleware
45 45 from rhodecode.lib.request import Request
46 46 from rhodecode.lib.vcs import VCSCommunicationError
47 47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 50 from rhodecode.lib.celerylib.loader import configure_celery
51 51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 52 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
53 53 from rhodecode.lib.exc_tracking import store_exception
54 54 from rhodecode.subscribers import (
55 55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 write_metadata_if_needed, write_usage_data, inject_app_settings)
56 write_metadata_if_needed, write_usage_data)
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 def is_http_error(response):
63 63 # error which should have traceback
64 64 return response.status_code > 499
65 65
66 66
67 67 def should_load_all():
68 68 """
69 69 Returns if all application components should be loaded. In some cases it's
70 70 desired to skip apps loading for faster shell script execution
71 71 """
72 72 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
73 73 if ssh_cmd:
74 74 return False
75 75
76 76 return True
77 77
78 78
79 79 def make_pyramid_app(global_config, **settings):
80 80 """
81 81 Constructs the WSGI application based on Pyramid.
82 82
83 83 Specials:
84 84
85 85 * The application can also be integrated like a plugin via the call to
86 86 `includeme`. This is accompanied with the other utility functions which
87 87 are called. Changing this should be done with great care to not break
88 88 cases when these fragments are assembled from another place.
89 89
90 90 """
91 91
92 92 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
93 93 # will be replaced by the value of the environment variable "NAME" in this case.
94 94 start_time = time.time()
95 95
96 96 debug = asbool(global_config.get('debug'))
97 97 if debug:
98 98 enable_debug()
99 99
100 100 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
101 101
102 102 global_config = _substitute_values(global_config, environ)
103 103 settings = _substitute_values(settings, environ)
104 104
105 105 sanitize_settings_and_apply_defaults(global_config, settings)
106 106
107 107 config = Configurator(settings=settings)
108 108
109 109 # Apply compatibility patches
110 110 patches.inspect_getargspec()
111 111
112 112 load_pyramid_environment(global_config, settings)
113 113
114 114 # Static file view comes first
115 115 includeme_first(config)
116 116
117 117 includeme(config)
118 118
119 119 pyramid_app = config.make_wsgi_app()
120 120 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
121 121 pyramid_app.config = config
122 122
123 123 config.configure_celery(global_config['__file__'])
124 124 # creating the app uses a connection - return it after we are done
125 125 meta.Session.remove()
126 126 total_time = time.time() - start_time
127 127 log.info('Pyramid app `%s` created and configured in %.2fs',
128 128 pyramid_app.func_name, total_time)
129 129
130 130 return pyramid_app
131 131
132 132
133 133 def not_found_view(request):
134 134 """
135 135 This creates the view which should be registered as not-found-view to
136 136 pyramid.
137 137 """
138 138
139 139 if not getattr(request, 'vcs_call', None):
140 140 # handle like regular case with our error_handler
141 141 return error_handler(HTTPNotFound(), request)
142 142
143 143 # handle not found view as a vcs call
144 144 settings = request.registry.settings
145 145 ae_client = getattr(request, 'ae_client', None)
146 146 vcs_app = VCSMiddleware(
147 147 HTTPNotFound(), request.registry, settings,
148 148 appenlight_client=ae_client)
149 149
150 150 return wsgiapp(vcs_app)(None, request)
151 151
152 152
153 153 def error_handler(exception, request):
154 154 import rhodecode
155 155 from rhodecode.lib import helpers
156 156
157 157 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
158 158
159 159 base_response = HTTPInternalServerError()
160 160 # prefer original exception for the response since it may have headers set
161 161 if isinstance(exception, HTTPException):
162 162 base_response = exception
163 163 elif isinstance(exception, VCSCommunicationError):
164 164 base_response = VCSServerUnavailable()
165 165
166 166 if is_http_error(base_response):
167 167 log.exception(
168 168 'error occurred handling this request for path: %s', request.path)
169 169
170 170 error_explanation = base_response.explanation or str(base_response)
171 171 if base_response.status_code == 404:
172 172 error_explanation += " Optionally you don't have permission to access this page."
173 173 c = AttributeDict()
174 174 c.error_message = base_response.status
175 175 c.error_explanation = error_explanation
176 176 c.visual = AttributeDict()
177 177
178 178 c.visual.rhodecode_support_url = (
179 179 request.registry.settings.get('rhodecode_support_url') or
180 180 request.route_url('rhodecode_support')
181 181 )
182 182 c.redirect_time = 0
183 183 c.rhodecode_name = rhodecode_title
184 184 if not c.rhodecode_name:
185 185 c.rhodecode_name = 'Rhodecode'
186 186
187 187 c.causes = []
188 188 if is_http_error(base_response):
189 189 c.causes.append('Server is overloaded.')
190 190 c.causes.append('Server database connection is lost.')
191 191 c.causes.append('Server expected unhandled error.')
192 192
193 193 if hasattr(base_response, 'causes'):
194 194 c.causes = base_response.causes
195 195
196 196 c.messages = helpers.flash.pop_messages(request=request)
197 197
198 198 exc_info = sys.exc_info()
199 199 c.exception_id = id(exc_info)
200 200 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
201 201 or base_response.status_code > 499
202 202 c.exception_id_url = request.route_url(
203 203 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
204 204
205 205 if c.show_exception_id:
206 206 store_exception(c.exception_id, exc_info)
207 207
208 208 response = render_to_response(
209 209 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
210 210 response=base_response)
211 211
212 212 return response
213 213
214 214
215 215 def includeme_first(config):
216 216 # redirect automatic browser favicon.ico requests to correct place
217 217 def favicon_redirect(context, request):
218 218 return HTTPFound(
219 219 request.static_path('rhodecode:public/images/favicon.ico'))
220 220
221 221 config.add_view(favicon_redirect, route_name='favicon')
222 222 config.add_route('favicon', '/favicon.ico')
223 223
224 224 def robots_redirect(context, request):
225 225 return HTTPFound(
226 226 request.static_path('rhodecode:public/robots.txt'))
227 227
228 228 config.add_view(robots_redirect, route_name='robots')
229 229 config.add_route('robots', '/robots.txt')
230 230
231 231 config.add_static_view(
232 232 '_static/deform', 'deform:static')
233 233 config.add_static_view(
234 234 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
235 235
236 236
237 237 def includeme(config):
238 238 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
239 239 settings = config.registry.settings
240 240 config.set_request_factory(Request)
241 241
242 242 # plugin information
243 243 config.registry.rhodecode_plugins = collections.OrderedDict()
244 244
245 245 config.add_directive(
246 246 'register_rhodecode_plugin', register_rhodecode_plugin)
247 247
248 248 config.add_directive('configure_celery', configure_celery)
249 249
250 250 if asbool(settings.get('appenlight', 'false')):
251 251 config.include('appenlight_client.ext.pyramid_tween')
252 252
253 253 load_all = should_load_all()
254 254
255 255 # Includes which are required. The application would fail without them.
256 256 config.include('pyramid_mako')
257 257 config.include('rhodecode.lib.rc_beaker')
258 258 config.include('rhodecode.lib.rc_cache')
259 259
260 260 config.include('rhodecode.apps._base.navigation')
261 261 config.include('rhodecode.apps._base.subscribers')
262 262 config.include('rhodecode.tweens')
263 263 config.include('rhodecode.authentication')
264 264
265 265 if load_all:
266 266 config.include('rhodecode.integrations')
267 267
268 268 if load_all:
269 269 # load CE authentication plugins
270 270 config.include('rhodecode.authentication.plugins.auth_crowd')
271 271 config.include('rhodecode.authentication.plugins.auth_headers')
272 272 config.include('rhodecode.authentication.plugins.auth_jasig_cas')
273 273 config.include('rhodecode.authentication.plugins.auth_ldap')
274 274 config.include('rhodecode.authentication.plugins.auth_pam')
275 275 config.include('rhodecode.authentication.plugins.auth_rhodecode')
276 276 config.include('rhodecode.authentication.plugins.auth_token')
277 277
278 278 # Auto discover authentication plugins and include their configuration.
279 279 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
280 280 from rhodecode.authentication import discover_legacy_plugins
281 281 discover_legacy_plugins(config)
282 282
283 283 # apps
284 284 if load_all:
285 285 config.include('rhodecode.apps._base')
286 286 config.include('rhodecode.apps.hovercards')
287 287 config.include('rhodecode.apps.ops')
288 288 config.include('rhodecode.apps.admin')
289 289 config.include('rhodecode.apps.channelstream')
290 290 config.include('rhodecode.apps.file_store')
291 291 config.include('rhodecode.apps.login')
292 292 config.include('rhodecode.apps.home')
293 293 config.include('rhodecode.apps.journal')
294 294 config.include('rhodecode.apps.repository')
295 295 config.include('rhodecode.apps.repo_group')
296 296 config.include('rhodecode.apps.user_group')
297 297 config.include('rhodecode.apps.search')
298 298 config.include('rhodecode.apps.user_profile')
299 299 config.include('rhodecode.apps.user_group_profile')
300 300 config.include('rhodecode.apps.my_account')
301 301 config.include('rhodecode.apps.svn_support')
302 302 config.include('rhodecode.apps.ssh_support')
303 303 config.include('rhodecode.apps.gist')
304 304 config.include('rhodecode.apps.debug_style')
305 305 config.include('rhodecode.api')
306 306
307 307 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
308 308 config.add_translation_dirs('rhodecode:i18n/')
309 309 settings['default_locale_name'] = settings.get('lang', 'en')
310 310
311 311 # Add subscribers.
312 312 if load_all:
313 config.add_subscriber(inject_app_settings,
314 pyramid.events.ApplicationCreated)
315 313 config.add_subscriber(scan_repositories_if_enabled,
316 314 pyramid.events.ApplicationCreated)
317 315 config.add_subscriber(write_metadata_if_needed,
318 316 pyramid.events.ApplicationCreated)
319 317 config.add_subscriber(write_usage_data,
320 318 pyramid.events.ApplicationCreated)
321 319 config.add_subscriber(write_js_routes_if_enabled,
322 320 pyramid.events.ApplicationCreated)
323 321
324 322 # request custom methods
325 323 config.add_request_method(
326 324 'rhodecode.lib.partial_renderer.get_partial_renderer',
327 325 'get_partial_renderer')
328 326
329 327 config.add_request_method(
330 328 'rhodecode.lib.request_counter.get_request_counter',
331 329 'request_count')
332 330
333 331 # Set the authorization policy.
334 332 authz_policy = ACLAuthorizationPolicy()
335 333 config.set_authorization_policy(authz_policy)
336 334
337 335 # Set the default renderer for HTML templates to mako.
338 336 config.add_mako_renderer('.html')
339 337
340 338 config.add_renderer(
341 339 name='json_ext',
342 340 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
343 341
344 342 config.add_renderer(
345 343 name='string_html',
346 344 factory='rhodecode.lib.string_renderer.html')
347 345
348 346 # include RhodeCode plugins
349 347 includes = aslist(settings.get('rhodecode.includes', []))
350 348 for inc in includes:
351 349 config.include(inc)
352 350
353 351 # custom not found view, if our pyramid app doesn't know how to handle
354 352 # the request pass it to potential VCS handling ap
355 353 config.add_notfound_view(not_found_view)
356 354 if not settings.get('debugtoolbar.enabled', False):
357 355 # disabled debugtoolbar handle all exceptions via the error_handlers
358 356 config.add_view(error_handler, context=Exception)
359 357
360 358 # all errors including 403/404/50X
361 359 config.add_view(error_handler, context=HTTPError)
362 360
363 361
364 362 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
365 363 """
366 364 Apply outer WSGI middlewares around the application.
367 365 """
368 366 registry = config.registry
369 367 settings = registry.settings
370 368
371 369 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
372 370 pyramid_app = HttpsFixup(pyramid_app, settings)
373 371
374 372 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
375 373 pyramid_app, settings)
376 374 registry.ae_client = _ae_client
377 375
378 376 if settings['gzip_responses']:
379 377 pyramid_app = make_gzip_middleware(
380 378 pyramid_app, settings, compress_level=1)
381 379
382 380 # this should be the outer most middleware in the wsgi stack since
383 381 # middleware like Routes make database calls
384 382 def pyramid_app_with_cleanup(environ, start_response):
385 383 try:
386 384 return pyramid_app(environ, start_response)
387 385 finally:
388 386 # Dispose current database session and rollback uncommitted
389 387 # transactions.
390 388 meta.Session.remove()
391 389
392 390 # In a single threaded mode server, on non sqlite db we should have
393 391 # '0 Current Checked out connections' at the end of a request,
394 392 # if not, then something, somewhere is leaving a connection open
395 393 pool = meta.Base.metadata.bind.engine.pool
396 394 log.debug('sa pool status: %s', pool.status())
397 395 log.debug('Request processing finalized')
398 396
399 397 return pyramid_app_with_cleanup
400 398
401 399
402 400 def sanitize_settings_and_apply_defaults(global_config, settings):
403 401 """
404 402 Applies settings defaults and does all type conversion.
405 403
406 404 We would move all settings parsing and preparation into this place, so that
407 405 we have only one place left which deals with this part. The remaining parts
408 406 of the application would start to rely fully on well prepared settings.
409 407
410 408 This piece would later be split up per topic to avoid a big fat monster
411 409 function.
412 410 """
413 411
414 412 settings.setdefault('rhodecode.edition', 'Community Edition')
415 413 settings.setdefault('rhodecode.edition_id', 'CE')
416 414
417 415 if 'mako.default_filters' not in settings:
418 416 # set custom default filters if we don't have it defined
419 417 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
420 418 settings['mako.default_filters'] = 'h_filter'
421 419
422 420 if 'mako.directories' not in settings:
423 421 mako_directories = settings.setdefault('mako.directories', [
424 422 # Base templates of the original application
425 423 'rhodecode:templates',
426 424 ])
427 425 log.debug(
428 426 "Using the following Mako template directories: %s",
429 427 mako_directories)
430 428
431 429 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
432 430 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
433 431 raw_url = settings['beaker.session.url']
434 432 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
435 433 settings['beaker.session.url'] = 'redis://' + raw_url
436 434
437 435 # Default includes, possible to change as a user
438 436 pyramid_includes = settings.setdefault('pyramid.includes', [])
439 437 log.debug(
440 438 "Using the following pyramid.includes: %s",
441 439 pyramid_includes)
442 440
443 441 # TODO: johbo: Re-think this, usually the call to config.include
444 442 # should allow to pass in a prefix.
445 443 settings.setdefault('rhodecode.api.url', '/_admin/api')
446 444 settings.setdefault('__file__', global_config.get('__file__'))
447 445
448 446 # Sanitize generic settings.
449 447 _list_setting(settings, 'default_encoding', 'UTF-8')
450 448 _bool_setting(settings, 'is_test', 'false')
451 449 _bool_setting(settings, 'gzip_responses', 'false')
452 450
453 451 # Call split out functions that sanitize settings for each topic.
454 452 _sanitize_appenlight_settings(settings)
455 453 _sanitize_vcs_settings(settings)
456 454 _sanitize_cache_settings(settings)
457 455
458 456 # configure instance id
459 457 config_utils.set_instance_id(settings)
460 458
461 459 return settings
462 460
463 461
464 462 def enable_debug():
465 463 """
466 464 Helper to enable debug on running instance
467 465 :return:
468 466 """
469 467 import tempfile
470 468 import textwrap
471 469 import logging.config
472 470
473 471 ini_template = textwrap.dedent("""
474 472 #####################################
475 473 ### DEBUG LOGGING CONFIGURATION ####
476 474 #####################################
477 475 [loggers]
478 476 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
479 477
480 478 [handlers]
481 479 keys = console, console_sql
482 480
483 481 [formatters]
484 482 keys = generic, color_formatter, color_formatter_sql
485 483
486 484 #############
487 485 ## LOGGERS ##
488 486 #############
489 487 [logger_root]
490 488 level = NOTSET
491 489 handlers = console
492 490
493 491 [logger_sqlalchemy]
494 492 level = INFO
495 493 handlers = console_sql
496 494 qualname = sqlalchemy.engine
497 495 propagate = 0
498 496
499 497 [logger_beaker]
500 498 level = DEBUG
501 499 handlers =
502 500 qualname = beaker.container
503 501 propagate = 1
504 502
505 503 [logger_rhodecode]
506 504 level = DEBUG
507 505 handlers =
508 506 qualname = rhodecode
509 507 propagate = 1
510 508
511 509 [logger_ssh_wrapper]
512 510 level = DEBUG
513 511 handlers =
514 512 qualname = ssh_wrapper
515 513 propagate = 1
516 514
517 515 [logger_celery]
518 516 level = DEBUG
519 517 handlers =
520 518 qualname = celery
521 519
522 520
523 521 ##############
524 522 ## HANDLERS ##
525 523 ##############
526 524
527 525 [handler_console]
528 526 class = StreamHandler
529 527 args = (sys.stderr, )
530 528 level = DEBUG
531 529 formatter = color_formatter
532 530
533 531 [handler_console_sql]
534 532 # "level = DEBUG" logs SQL queries and results.
535 533 # "level = INFO" logs SQL queries.
536 534 # "level = WARN" logs neither. (Recommended for production systems.)
537 535 class = StreamHandler
538 536 args = (sys.stderr, )
539 537 level = WARN
540 538 formatter = color_formatter_sql
541 539
542 540 ################
543 541 ## FORMATTERS ##
544 542 ################
545 543
546 544 [formatter_generic]
547 545 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
548 546 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
549 547 datefmt = %Y-%m-%d %H:%M:%S
550 548
551 549 [formatter_color_formatter]
552 550 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
553 551 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
554 552 datefmt = %Y-%m-%d %H:%M:%S
555 553
556 554 [formatter_color_formatter_sql]
557 555 class = rhodecode.lib.logging_formatter.ColorFormatterSql
558 556 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
559 557 datefmt = %Y-%m-%d %H:%M:%S
560 558 """)
561 559
562 560 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
563 561 delete=False) as f:
564 562 log.info('Saved Temporary DEBUG config at %s', f.name)
565 563 f.write(ini_template)
566 564
567 565 logging.config.fileConfig(f.name)
568 566 log.debug('DEBUG MODE ON')
569 567 os.remove(f.name)
570 568
571 569
572 570 def _sanitize_appenlight_settings(settings):
573 571 _bool_setting(settings, 'appenlight', 'false')
574 572
575 573
576 574 def _sanitize_vcs_settings(settings):
577 575 """
578 576 Applies settings defaults and does type conversion for all VCS related
579 577 settings.
580 578 """
581 579 _string_setting(settings, 'vcs.svn.compatible_version', '')
582 580 _string_setting(settings, 'vcs.hooks.protocol', 'http')
583 581 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
584 582 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
585 583 _string_setting(settings, 'vcs.server', '')
586 584 _string_setting(settings, 'vcs.server.protocol', 'http')
587 585 _bool_setting(settings, 'startup.import_repos', 'false')
588 586 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
589 587 _bool_setting(settings, 'vcs.server.enable', 'true')
590 588 _bool_setting(settings, 'vcs.start_server', 'false')
591 589 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
592 590 _int_setting(settings, 'vcs.connection_timeout', 3600)
593 591
594 592 # Support legacy values of vcs.scm_app_implementation. Legacy
595 593 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
596 594 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
597 595 scm_app_impl = settings['vcs.scm_app_implementation']
598 596 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
599 597 settings['vcs.scm_app_implementation'] = 'http'
600 598
601 599
602 600 def _sanitize_cache_settings(settings):
603 601 temp_store = tempfile.gettempdir()
604 602 default_cache_dir = os.path.join(temp_store, 'rc_cache')
605 603
606 604 # save default, cache dir, and use it for all backends later.
607 605 default_cache_dir = _string_setting(
608 606 settings,
609 607 'cache_dir',
610 608 default_cache_dir, lower=False, default_when_empty=True)
611 609
612 610 # ensure we have our dir created
613 611 if not os.path.isdir(default_cache_dir):
614 612 os.makedirs(default_cache_dir, mode=0o755)
615 613
616 614 # exception store cache
617 615 _string_setting(
618 616 settings,
619 617 'exception_tracker.store_path',
620 618 temp_store, lower=False, default_when_empty=True)
621 619 _bool_setting(
622 620 settings,
623 621 'exception_tracker.send_email',
624 622 'false')
625 623 _string_setting(
626 624 settings,
627 625 'exception_tracker.email_prefix',
628 626 '[RHODECODE ERROR]', lower=False, default_when_empty=True)
629 627
630 628 # cache_perms
631 629 _string_setting(
632 630 settings,
633 631 'rc_cache.cache_perms.backend',
634 632 'dogpile.cache.rc.file_namespace', lower=False)
635 633 _int_setting(
636 634 settings,
637 635 'rc_cache.cache_perms.expiration_time',
638 636 60)
639 637 _string_setting(
640 638 settings,
641 639 'rc_cache.cache_perms.arguments.filename',
642 640 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
643 641
644 642 # cache_repo
645 643 _string_setting(
646 644 settings,
647 645 'rc_cache.cache_repo.backend',
648 646 'dogpile.cache.rc.file_namespace', lower=False)
649 647 _int_setting(
650 648 settings,
651 649 'rc_cache.cache_repo.expiration_time',
652 650 60)
653 651 _string_setting(
654 652 settings,
655 653 'rc_cache.cache_repo.arguments.filename',
656 654 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
657 655
658 656 # cache_license
659 657 _string_setting(
660 658 settings,
661 659 'rc_cache.cache_license.backend',
662 660 'dogpile.cache.rc.file_namespace', lower=False)
663 661 _int_setting(
664 662 settings,
665 663 'rc_cache.cache_license.expiration_time',
666 664 5*60)
667 665 _string_setting(
668 666 settings,
669 667 'rc_cache.cache_license.arguments.filename',
670 668 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
671 669
672 670 # cache_repo_longterm memory, 96H
673 671 _string_setting(
674 672 settings,
675 673 'rc_cache.cache_repo_longterm.backend',
676 674 'dogpile.cache.rc.memory_lru', lower=False)
677 675 _int_setting(
678 676 settings,
679 677 'rc_cache.cache_repo_longterm.expiration_time',
680 678 345600)
681 679 _int_setting(
682 680 settings,
683 681 'rc_cache.cache_repo_longterm.max_size',
684 682 10000)
685 683
686 684 # sql_cache_short
687 685 _string_setting(
688 686 settings,
689 687 'rc_cache.sql_cache_short.backend',
690 688 'dogpile.cache.rc.memory_lru', lower=False)
691 689 _int_setting(
692 690 settings,
693 691 'rc_cache.sql_cache_short.expiration_time',
694 692 30)
695 693 _int_setting(
696 694 settings,
697 695 'rc_cache.sql_cache_short.max_size',
698 696 10000)
699 697
700 698
701 699 def _int_setting(settings, name, default):
702 700 settings[name] = int(settings.get(name, default))
703 701 return settings[name]
704 702
705 703
706 704 def _bool_setting(settings, name, default):
707 705 input_val = settings.get(name, default)
708 706 if isinstance(input_val, unicode):
709 707 input_val = input_val.encode('utf8')
710 708 settings[name] = asbool(input_val)
711 709 return settings[name]
712 710
713 711
714 712 def _list_setting(settings, name, default):
715 713 raw_value = settings.get(name, default)
716 714
717 715 old_separator = ','
718 716 if old_separator in raw_value:
719 717 # If we get a comma separated list, pass it to our own function.
720 718 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
721 719 else:
722 720 # Otherwise we assume it uses pyramids space/newline separation.
723 721 settings[name] = aslist(raw_value)
724 722 return settings[name]
725 723
726 724
727 725 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
728 726 value = settings.get(name, default)
729 727
730 728 if default_when_empty and not value:
731 729 # use default value when value is empty
732 730 value = default
733 731
734 732 if lower:
735 733 value = value.lower()
736 734 settings[name] = value
737 735 return settings[name]
738 736
739 737
740 738 def _substitute_values(mapping, substitutions):
741 739 result = {}
742 740
743 741 try:
744 742 for key, value in mapping.items():
745 743 # initialize without substitution first
746 744 result[key] = value
747 745
748 746 # Note: Cannot use regular replacements, since they would clash
749 747 # with the implementation of ConfigParser. Using "format" instead.
750 748 try:
751 749 result[key] = value.format(**substitutions)
752 750 except KeyError as e:
753 751 env_var = '{}'.format(e.args[0])
754 752
755 753 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
756 754 'Make sure your environment has {var} set, or remove this ' \
757 755 'variable from config file'.format(key=key, var=env_var)
758 756
759 757 if env_var.startswith('ENV_'):
760 758 raise ValueError(msg)
761 759 else:
762 760 log.warning(msg)
763 761
764 762 except ValueError as e:
765 763 log.warning('Failed to substitute ENV variable: %s', e)
766 764 result = mapping
767 765
768 766 return result
@@ -1,82 +1,86 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2015-2020 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 from dogpile.cache import register_backend
23 23
24 24 register_backend(
25 25 "dogpile.cache.rc.memory_lru", "rhodecode.lib.rc_cache.backends",
26 26 "LRUMemoryBackend")
27 27
28 28 register_backend(
29 29 "dogpile.cache.rc.file_namespace", "rhodecode.lib.rc_cache.backends",
30 30 "FileNamespaceBackend")
31 31
32 32 register_backend(
33 33 "dogpile.cache.rc.redis", "rhodecode.lib.rc_cache.backends",
34 34 "RedisPickleBackend")
35 35
36 36 register_backend(
37 37 "dogpile.cache.rc.redis_msgpack", "rhodecode.lib.rc_cache.backends",
38 38 "RedisMsgPackBackend")
39 39
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43 from . import region_meta
44 44 from .utils import (
45 45 get_default_cache_settings, backend_key_generator, get_or_create_region,
46 46 clear_cache_namespace, make_region, InvalidationContext,
47 47 FreshRegionCache, ActiveRegionCache)
48 48
49 49
50 50 FILE_TREE_CACHE_VER = 'v4'
51 51 LICENSE_CACHE_VER = 'v2'
52 52
53 53
54 54 def configure_dogpile_cache(settings):
55 55 cache_dir = settings.get('cache_dir')
56 56 if cache_dir:
57 57 region_meta.dogpile_config_defaults['cache_dir'] = cache_dir
58 58
59 59 rc_cache_data = get_default_cache_settings(settings, prefixes=['rc_cache.'])
60 60
61 61 # inspect available namespaces
62 62 avail_regions = set()
63 63 for key in rc_cache_data.keys():
64 64 namespace_name = key.split('.', 1)[0]
65 65 avail_regions.add(namespace_name)
66 66 log.debug('dogpile: found following cache regions: %s', avail_regions)
67 67
68 68 # register them into namespace
69 69 for region_name in avail_regions:
70 70 new_region = make_region(
71 71 name=region_name,
72 72 function_key_generator=None
73 73 )
74 74
75 75 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
76 76 new_region.function_key_generator = backend_key_generator(new_region.actual_backend)
77 log.debug('dogpile: registering a new region %s[%s]', region_name, new_region.__dict__)
77 if log.isEnabledFor(logging.DEBUG):
78 region_args = dict(backend=new_region.actual_backend.__class__,
79 region_invalidator=new_region.region_invalidator.__class__)
80 log.debug('dogpile: registering a new region `%s` %s', region_name, region_args)
81
78 82 region_meta.dogpile_cache_regions[region_name] = new_region
79 83
80 84
81 85 def includeme(config):
82 86 configure_dogpile_cache(config.registry.settings)
@@ -1,395 +1,389 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 import io
21 21 import math
22 22 import re
23 23 import os
24 24 import datetime
25 25 import logging
26 26 import Queue
27 27 import subprocess32
28 28
29 29
30 30 from dateutil.parser import parse
31 31 from pyramid.threadlocal import get_current_request
32 32 from pyramid.interfaces import IRoutesMapper
33 33 from pyramid.settings import asbool
34 34 from pyramid.path import AssetResolver
35 35 from threading import Thread
36 36
37 37 from rhodecode.translation import _ as tsf
38 38 from rhodecode.config.jsroutes import generate_jsroutes_content
39 39 from rhodecode.lib import auth
40 40 from rhodecode.lib.base import get_auth_user
41 41
42 42 import rhodecode
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 def add_renderer_globals(event):
49 49 from rhodecode.lib import helpers
50 50
51 51 # TODO: When executed in pyramid view context the request is not available
52 52 # in the event. Find a better solution to get the request.
53 53 request = event['request'] or get_current_request()
54 54
55 55 # Add Pyramid translation as '_' to context
56 56 event['_'] = request.translate
57 57 event['_ungettext'] = request.plularize
58 58 event['h'] = helpers
59 59
60 60
61 61 def add_localizer(event):
62 62 request = event.request
63 63 localizer = request.localizer
64 64
65 65 def auto_translate(*args, **kwargs):
66 66 return localizer.translate(tsf(*args, **kwargs))
67 67
68 68 request.translate = auto_translate
69 69 request.plularize = localizer.pluralize
70 70
71 71
72 72 def set_user_lang(event):
73 73 request = event.request
74 74 cur_user = getattr(request, 'user', None)
75 75
76 76 if cur_user:
77 77 user_lang = cur_user.get_instance().user_data.get('language')
78 78 if user_lang:
79 79 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
80 80 event.request._LOCALE_ = user_lang
81 81
82 82
83 83 def add_request_user_context(event):
84 84 """
85 85 Adds auth user into request context
86 86 """
87 87 request = event.request
88 88 # access req_id as soon as possible
89 89 req_id = request.req_id
90 90
91 91 if hasattr(request, 'vcs_call'):
92 92 # skip vcs calls
93 93 return
94 94
95 95 if hasattr(request, 'rpc_method'):
96 96 # skip api calls
97 97 return
98 98
99 99 auth_user, auth_token = get_auth_user(request)
100 100 request.user = auth_user
101 101 request.user_auth_token = auth_token
102 102 request.environ['rc_auth_user'] = auth_user
103 103 request.environ['rc_auth_user_id'] = auth_user.user_id
104 104 request.environ['rc_req_id'] = req_id
105 105
106 106
107 def inject_app_settings(event):
108 settings = event.app.registry.settings
109 # inject info about available permissions
110 auth.set_available_permissions(settings)
111
112
113 107 def scan_repositories_if_enabled(event):
114 108 """
115 109 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
116 110 does a repository scan if enabled in the settings.
117 111 """
118 112 settings = event.app.registry.settings
119 113 vcs_server_enabled = settings['vcs.server.enable']
120 114 import_on_startup = settings['startup.import_repos']
121 115 if vcs_server_enabled and import_on_startup:
122 116 from rhodecode.model.scm import ScmModel
123 117 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
124 118 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
125 119 repo2db_mapper(repositories, remove_obsolete=False)
126 120
127 121
128 122 def write_metadata_if_needed(event):
129 123 """
130 124 Writes upgrade metadata
131 125 """
132 126 import rhodecode
133 127 from rhodecode.lib import system_info
134 128 from rhodecode.lib import ext_json
135 129
136 130 fname = '.rcmetadata.json'
137 131 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
138 132 metadata_destination = os.path.join(ini_loc, fname)
139 133
140 134 def get_update_age():
141 135 now = datetime.datetime.utcnow()
142 136
143 137 with open(metadata_destination, 'rb') as f:
144 138 data = ext_json.json.loads(f.read())
145 139 if 'created_on' in data:
146 140 update_date = parse(data['created_on'])
147 141 diff = now - update_date
148 142 return diff.total_seconds() / 60.0
149 143
150 144 return 0
151 145
152 146 def write():
153 147 configuration = system_info.SysInfo(
154 148 system_info.rhodecode_config)()['value']
155 149 license_token = configuration['config']['license_token']
156 150
157 151 setup = dict(
158 152 workers=configuration['config']['server:main'].get(
159 153 'workers', '?'),
160 154 worker_type=configuration['config']['server:main'].get(
161 155 'worker_class', 'sync'),
162 156 )
163 157 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
164 158 del dbinfo['url']
165 159
166 160 metadata = dict(
167 161 desc='upgrade metadata info',
168 162 license_token=license_token,
169 163 created_on=datetime.datetime.utcnow().isoformat(),
170 164 usage=system_info.SysInfo(system_info.usage_info)()['value'],
171 165 platform=system_info.SysInfo(system_info.platform_type)()['value'],
172 166 database=dbinfo,
173 167 cpu=system_info.SysInfo(system_info.cpu)()['value'],
174 168 memory=system_info.SysInfo(system_info.memory)()['value'],
175 169 setup=setup
176 170 )
177 171
178 172 with open(metadata_destination, 'wb') as f:
179 173 f.write(ext_json.json.dumps(metadata))
180 174
181 175 settings = event.app.registry.settings
182 176 if settings.get('metadata.skip'):
183 177 return
184 178
185 179 # only write this every 24h, workers restart caused unwanted delays
186 180 try:
187 181 age_in_min = get_update_age()
188 182 except Exception:
189 183 age_in_min = 0
190 184
191 185 if age_in_min > 60 * 60 * 24:
192 186 return
193 187
194 188 try:
195 189 write()
196 190 except Exception:
197 191 pass
198 192
199 193
200 194 def write_usage_data(event):
201 195 import rhodecode
202 196 from rhodecode.lib import system_info
203 197 from rhodecode.lib import ext_json
204 198
205 199 settings = event.app.registry.settings
206 200 instance_tag = settings.get('metadata.write_usage_tag')
207 201 if not settings.get('metadata.write_usage'):
208 202 return
209 203
210 204 def get_update_age(dest_file):
211 205 now = datetime.datetime.utcnow()
212 206
213 207 with open(dest_file, 'rb') as f:
214 208 data = ext_json.json.loads(f.read())
215 209 if 'created_on' in data:
216 210 update_date = parse(data['created_on'])
217 211 diff = now - update_date
218 212 return math.ceil(diff.total_seconds() / 60.0)
219 213
220 214 return 0
221 215
222 216 utc_date = datetime.datetime.utcnow()
223 217 hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
224 218 fname = '.rc_usage_{date.year}{date.month:02d}{date.day:02d}_{hour}.json'.format(
225 219 date=utc_date, hour=hour_quarter)
226 220 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
227 221
228 222 usage_dir = os.path.join(ini_loc, '.rcusage')
229 223 if not os.path.isdir(usage_dir):
230 224 os.makedirs(usage_dir)
231 225 usage_metadata_destination = os.path.join(usage_dir, fname)
232 226
233 227 try:
234 228 age_in_min = get_update_age(usage_metadata_destination)
235 229 except Exception:
236 230 age_in_min = 0
237 231
238 232 # write every 6th hour
239 233 if age_in_min and age_in_min < 60 * 6:
240 234 log.debug('Usage file created %s minutes ago, skipping (threashold: %s)...',
241 235 age_in_min, 60 * 6)
242 236 return
243 237
244 238 def write(dest_file):
245 239 configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
246 240 license_token = configuration['config']['license_token']
247 241
248 242 metadata = dict(
249 243 desc='Usage data',
250 244 instance_tag=instance_tag,
251 245 license_token=license_token,
252 246 created_on=datetime.datetime.utcnow().isoformat(),
253 247 usage=system_info.SysInfo(system_info.usage_info)()['value'],
254 248 )
255 249
256 250 with open(dest_file, 'wb') as f:
257 251 f.write(ext_json.json.dumps(metadata, indent=2, sort_keys=True))
258 252
259 253 try:
260 254 log.debug('Writing usage file at: %s', usage_metadata_destination)
261 255 write(usage_metadata_destination)
262 256 except Exception:
263 257 pass
264 258
265 259
266 260 def write_js_routes_if_enabled(event):
267 261 registry = event.app.registry
268 262
269 263 mapper = registry.queryUtility(IRoutesMapper)
270 264 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
271 265
272 266 def _extract_route_information(route):
273 267 """
274 268 Convert a route into tuple(name, path, args), eg:
275 269 ('show_user', '/profile/%(username)s', ['username'])
276 270 """
277 271
278 272 routepath = route.pattern
279 273 pattern = route.pattern
280 274
281 275 def replace(matchobj):
282 276 if matchobj.group(1):
283 277 return "%%(%s)s" % matchobj.group(1).split(':')[0]
284 278 else:
285 279 return "%%(%s)s" % matchobj.group(2)
286 280
287 281 routepath = _argument_prog.sub(replace, routepath)
288 282
289 283 if not routepath.startswith('/'):
290 284 routepath = '/'+routepath
291 285
292 286 return (
293 287 route.name,
294 288 routepath,
295 289 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
296 290 for arg in _argument_prog.findall(pattern)]
297 291 )
298 292
299 293 def get_routes():
300 294 # pyramid routes
301 295 for route in mapper.get_routes():
302 296 if not route.name.startswith('__'):
303 297 yield _extract_route_information(route)
304 298
305 299 if asbool(registry.settings.get('generate_js_files', 'false')):
306 300 static_path = AssetResolver().resolve('rhodecode:public').abspath()
307 301 jsroutes = get_routes()
308 302 jsroutes_file_content = generate_jsroutes_content(jsroutes)
309 303 jsroutes_file_path = os.path.join(
310 304 static_path, 'js', 'rhodecode', 'routes.js')
311 305
312 306 try:
313 307 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
314 308 f.write(jsroutes_file_content)
315 309 except Exception:
316 310 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
317 311
318 312
319 313 class Subscriber(object):
320 314 """
321 315 Base class for subscribers to the pyramid event system.
322 316 """
323 317 def __call__(self, event):
324 318 self.run(event)
325 319
326 320 def run(self, event):
327 321 raise NotImplementedError('Subclass has to implement this.')
328 322
329 323
330 324 class AsyncSubscriber(Subscriber):
331 325 """
332 326 Subscriber that handles the execution of events in a separate task to not
333 327 block the execution of the code which triggers the event. It puts the
334 328 received events into a queue from which the worker process takes them in
335 329 order.
336 330 """
337 331 def __init__(self):
338 332 self._stop = False
339 333 self._eventq = Queue.Queue()
340 334 self._worker = self.create_worker()
341 335 self._worker.start()
342 336
343 337 def __call__(self, event):
344 338 self._eventq.put(event)
345 339
346 340 def create_worker(self):
347 341 worker = Thread(target=self.do_work)
348 342 worker.daemon = True
349 343 return worker
350 344
351 345 def stop_worker(self):
352 346 self._stop = False
353 347 self._eventq.put(None)
354 348 self._worker.join()
355 349
356 350 def do_work(self):
357 351 while not self._stop:
358 352 event = self._eventq.get()
359 353 if event is not None:
360 354 self.run(event)
361 355
362 356
363 357 class AsyncSubprocessSubscriber(AsyncSubscriber):
364 358 """
365 359 Subscriber that uses the subprocess32 module to execute a command if an
366 360 event is received. Events are handled asynchronously.
367 361 """
368 362
369 363 def __init__(self, cmd, timeout=None):
370 364 super(AsyncSubprocessSubscriber, self).__init__()
371 365 self._cmd = cmd
372 366 self._timeout = timeout
373 367
374 368 def run(self, event):
375 369 cmd = self._cmd
376 370 timeout = self._timeout
377 371 log.debug('Executing command %s.', cmd)
378 372
379 373 try:
380 374 output = subprocess32.check_output(
381 375 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
382 376 log.debug('Command finished %s', cmd)
383 377 if output:
384 378 log.debug('Command output: %s', output)
385 379 except subprocess32.TimeoutExpired as e:
386 380 log.exception('Timeout while executing command.')
387 381 if e.output:
388 382 log.error('Command output: %s', e.output)
389 383 except subprocess32.CalledProcessError as e:
390 384 log.exception('Error while executing command.')
391 385 if e.output:
392 386 log.error('Command output: %s', e.output)
393 387 except:
394 388 log.exception(
395 389 'Exception while executing command %s.', cmd)
General Comments 0
You need to be logged in to leave comments. Login now