##// END OF EJS Templates
feat(vcs): dropped validate SSL for vcs-operations....
super-admin -
r5531:8639dbc5 default
parent child Browse files
Show More
@@ -1,708 +1,707 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 import logging
21 21 import collections
22 22
23 23 import datetime
24 24 import formencode
25 25 import formencode.htmlfill
26 26
27 27 import rhodecode
28 28
29 29 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import BaseAppView
34 34 from rhodecode.apps._base.navigation import navigation_list
35 35 from rhodecode.apps.svn_support import config_keys
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 39 from rhodecode.lib.celerylib import tasks, run_task
40 40 from rhodecode.lib.str_utils import safe_str
41 41 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path
42 42 from rhodecode.lib.utils2 import str2bool, AttributeDict
43 43 from rhodecode.lib.index import searcher_from_config
44 44
45 45 from rhodecode.model.db import RhodeCodeUi, Repository
46 46 from rhodecode.model.forms import (ApplicationSettingsForm,
47 47 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 48 LabsSettingsForm, IssueTrackerPatternsForm)
49 49 from rhodecode.model.permission import PermissionModel
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.notification import EmailNotificationModel
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.settings import (
56 56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 57 SettingsModel)
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class AdminSettingsView(BaseAppView):
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.labs_active = str2bool(
68 68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 69 c.navlist = navigation_list(self.request)
70 70 return c
71 71
72 72 @classmethod
73 73 def _get_ui_settings(cls):
74 74 ret = RhodeCodeUi.query().all()
75 75
76 76 if not ret:
77 77 raise Exception('Could not get application ui settings !')
78 78 settings = {}
79 79 for each in ret:
80 80 k = each.ui_key
81 81 v = each.ui_value
82 82 if k == '/':
83 83 k = 'root_path'
84 84
85 if k in ['push_ssl', 'publish', 'enabled']:
85 if k in ['publish', 'enabled']:
86 86 v = str2bool(v)
87 87
88 88 if k.find('.') != -1:
89 89 k = k.replace('.', '_')
90 90
91 91 if each.ui_section in ['hooks', 'extensions']:
92 92 v = each.ui_active
93 93
94 94 settings[each.ui_section + '_' + k] = v
95 95 return settings
96 96
97 97 @classmethod
98 98 def _form_defaults(cls):
99 99 defaults = SettingsModel().get_all_settings()
100 100 defaults.update(cls._get_ui_settings())
101 101
102 102 defaults.update({
103 103 'new_svn_branch': '',
104 104 'new_svn_tag': '',
105 105 })
106 106 return defaults
107 107
108 108 @LoginRequired()
109 109 @HasPermissionAllDecorator('hg.admin')
110 110 def settings_vcs(self):
111 111 c = self.load_default_context()
112 112 c.active = 'vcs'
113 113 model = VcsSettingsModel()
114 114 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
115 115 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
116 116 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
117 117 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
118 118 defaults = self._form_defaults()
119 119
120 120 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
121 121
122 122 data = render('rhodecode:templates/admin/settings/settings.mako',
123 123 self._get_template_context(c), self.request)
124 124 html = formencode.htmlfill.render(
125 125 data,
126 126 defaults=defaults,
127 127 encoding="UTF-8",
128 128 force_defaults=False
129 129 )
130 130 return Response(html)
131 131
132 132 @LoginRequired()
133 133 @HasPermissionAllDecorator('hg.admin')
134 134 @CSRFRequired()
135 135 def settings_vcs_update(self):
136 136 _ = self.request.translate
137 137 c = self.load_default_context()
138 138 c.active = 'vcs'
139 139
140 140 model = VcsSettingsModel()
141 141 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
142 142 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
143 143
144 144 c.svn_generate_config = rhodecode.ConfigGet().get_bool(config_keys.generate_config)
145 145 c.svn_config_path = rhodecode.ConfigGet().get_str(config_keys.config_file_path)
146 146 application_form = ApplicationUiSettingsForm(self.request.translate)()
147 147
148 148 try:
149 149 form_result = application_form.to_python(dict(self.request.POST))
150 150 except formencode.Invalid as errors:
151 151 h.flash(
152 152 _("Some form inputs contain invalid data."),
153 153 category='error')
154 154 data = render('rhodecode:templates/admin/settings/settings.mako',
155 155 self._get_template_context(c), self.request)
156 156 html = formencode.htmlfill.render(
157 157 data,
158 158 defaults=errors.value,
159 159 errors=errors.unpack_errors() or {},
160 160 prefix_error=False,
161 161 encoding="UTF-8",
162 162 force_defaults=False
163 163 )
164 164 return Response(html)
165 165
166 166 try:
167 model.update_global_ssl_setting(form_result['web_push_ssl'])
168 167 model.update_global_hook_settings(form_result)
169 168
170 169 model.create_or_update_global_svn_settings(form_result)
171 170 model.create_or_update_global_hg_settings(form_result)
172 171 model.create_or_update_global_git_settings(form_result)
173 172 model.create_or_update_global_pr_settings(form_result)
174 173 except Exception:
175 174 log.exception("Exception while updating settings")
176 175 h.flash(_('Error occurred during updating '
177 176 'application settings'), category='error')
178 177 else:
179 178 Session().commit()
180 179 h.flash(_('Updated VCS settings'), category='success')
181 180 raise HTTPFound(h.route_path('admin_settings_vcs'))
182 181
183 182 data = render('rhodecode:templates/admin/settings/settings.mako',
184 183 self._get_template_context(c), self.request)
185 184 html = formencode.htmlfill.render(
186 185 data,
187 186 defaults=self._form_defaults(),
188 187 encoding="UTF-8",
189 188 force_defaults=False
190 189 )
191 190 return Response(html)
192 191
193 192 @LoginRequired()
194 193 @HasPermissionAllDecorator('hg.admin')
195 194 @CSRFRequired()
196 195 def settings_vcs_delete_svn_pattern(self):
197 196 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
198 197 model = VcsSettingsModel()
199 198 try:
200 199 model.delete_global_svn_pattern(delete_pattern_id)
201 200 except SettingNotFound:
202 201 log.exception(
203 202 'Failed to delete svn_pattern with id %s', delete_pattern_id)
204 203 raise HTTPNotFound()
205 204
206 205 Session().commit()
207 206 return True
208 207
209 208 @LoginRequired()
210 209 @HasPermissionAllDecorator('hg.admin')
211 210 def settings_mapping(self):
212 211 c = self.load_default_context()
213 212 c.active = 'mapping'
214 213 c.storage_path = get_rhodecode_repo_store_path()
215 214 data = render('rhodecode:templates/admin/settings/settings.mako',
216 215 self._get_template_context(c), self.request)
217 216 html = formencode.htmlfill.render(
218 217 data,
219 218 defaults=self._form_defaults(),
220 219 encoding="UTF-8",
221 220 force_defaults=False
222 221 )
223 222 return Response(html)
224 223
225 224 @LoginRequired()
226 225 @HasPermissionAllDecorator('hg.admin')
227 226 @CSRFRequired()
228 227 def settings_mapping_update(self):
229 228 _ = self.request.translate
230 229 c = self.load_default_context()
231 230 c.active = 'mapping'
232 231 rm_obsolete = self.request.POST.get('destroy', False)
233 232 invalidate_cache = self.request.POST.get('invalidate', False)
234 233 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
235 234
236 235 if invalidate_cache:
237 236 log.debug('invalidating all repositories cache')
238 237 for repo in Repository.get_all():
239 238 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
240 239
241 240 filesystem_repos = ScmModel().repo_scan()
242 241 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete, force_hooks_rebuild=True)
243 242 PermissionModel().trigger_permission_flush()
244 243
245 244 def _repr(rm_repo):
246 245 return ', '.join(map(safe_str, rm_repo)) or '-'
247 246
248 247 h.flash(_('Repositories successfully '
249 248 'rescanned added: %s ; removed: %s') %
250 249 (_repr(added), _repr(removed)),
251 250 category='success')
252 251 raise HTTPFound(h.route_path('admin_settings_mapping'))
253 252
254 253 @LoginRequired()
255 254 @HasPermissionAllDecorator('hg.admin')
256 255 def settings_global(self):
257 256 c = self.load_default_context()
258 257 c.active = 'global'
259 258 c.personal_repo_group_default_pattern = RepoGroupModel()\
260 259 .get_personal_group_name_pattern()
261 260
262 261 data = render('rhodecode:templates/admin/settings/settings.mako',
263 262 self._get_template_context(c), self.request)
264 263 html = formencode.htmlfill.render(
265 264 data,
266 265 defaults=self._form_defaults(),
267 266 encoding="UTF-8",
268 267 force_defaults=False
269 268 )
270 269 return Response(html)
271 270
272 271 @LoginRequired()
273 272 @HasPermissionAllDecorator('hg.admin')
274 273 @CSRFRequired()
275 274 def settings_global_update(self):
276 275 _ = self.request.translate
277 276 c = self.load_default_context()
278 277 c.active = 'global'
279 278 c.personal_repo_group_default_pattern = RepoGroupModel()\
280 279 .get_personal_group_name_pattern()
281 280 application_form = ApplicationSettingsForm(self.request.translate)()
282 281 try:
283 282 form_result = application_form.to_python(dict(self.request.POST))
284 283 except formencode.Invalid as errors:
285 284 h.flash(
286 285 _("Some form inputs contain invalid data."),
287 286 category='error')
288 287 data = render('rhodecode:templates/admin/settings/settings.mako',
289 288 self._get_template_context(c), self.request)
290 289 html = formencode.htmlfill.render(
291 290 data,
292 291 defaults=errors.value,
293 292 errors=errors.unpack_errors() or {},
294 293 prefix_error=False,
295 294 encoding="UTF-8",
296 295 force_defaults=False
297 296 )
298 297 return Response(html)
299 298
300 299 settings = [
301 300 ('title', 'rhodecode_title', 'unicode'),
302 301 ('realm', 'rhodecode_realm', 'unicode'),
303 302 ('pre_code', 'rhodecode_pre_code', 'unicode'),
304 303 ('post_code', 'rhodecode_post_code', 'unicode'),
305 304 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
306 305 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
307 306 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
308 307 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
309 308 ]
310 309
311 310 try:
312 311 for setting, form_key, type_ in settings:
313 312 sett = SettingsModel().create_or_update_setting(
314 313 setting, form_result[form_key], type_)
315 314 Session().add(sett)
316 315
317 316 Session().commit()
318 317 SettingsModel().invalidate_settings_cache()
319 318 h.flash(_('Updated application settings'), category='success')
320 319 except Exception:
321 320 log.exception("Exception while updating application settings")
322 321 h.flash(
323 322 _('Error occurred during updating application settings'),
324 323 category='error')
325 324
326 325 raise HTTPFound(h.route_path('admin_settings_global'))
327 326
328 327 @LoginRequired()
329 328 @HasPermissionAllDecorator('hg.admin')
330 329 def settings_visual(self):
331 330 c = self.load_default_context()
332 331 c.active = 'visual'
333 332
334 333 data = render('rhodecode:templates/admin/settings/settings.mako',
335 334 self._get_template_context(c), self.request)
336 335 html = formencode.htmlfill.render(
337 336 data,
338 337 defaults=self._form_defaults(),
339 338 encoding="UTF-8",
340 339 force_defaults=False
341 340 )
342 341 return Response(html)
343 342
344 343 @LoginRequired()
345 344 @HasPermissionAllDecorator('hg.admin')
346 345 @CSRFRequired()
347 346 def settings_visual_update(self):
348 347 _ = self.request.translate
349 348 c = self.load_default_context()
350 349 c.active = 'visual'
351 350 application_form = ApplicationVisualisationForm(self.request.translate)()
352 351 try:
353 352 form_result = application_form.to_python(dict(self.request.POST))
354 353 except formencode.Invalid as errors:
355 354 h.flash(
356 355 _("Some form inputs contain invalid data."),
357 356 category='error')
358 357 data = render('rhodecode:templates/admin/settings/settings.mako',
359 358 self._get_template_context(c), self.request)
360 359 html = formencode.htmlfill.render(
361 360 data,
362 361 defaults=errors.value,
363 362 errors=errors.unpack_errors() or {},
364 363 prefix_error=False,
365 364 encoding="UTF-8",
366 365 force_defaults=False
367 366 )
368 367 return Response(html)
369 368
370 369 try:
371 370 settings = [
372 371 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
373 372 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
374 373 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
375 374 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
376 375 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
377 376 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
378 377 ('show_version', 'rhodecode_show_version', 'bool'),
379 378 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
380 379 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
381 380 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
382 381 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
383 382 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
384 383 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
385 384 ('support_url', 'rhodecode_support_url', 'unicode'),
386 385 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
387 386 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
388 387 ]
389 388 for setting, form_key, type_ in settings:
390 389 sett = SettingsModel().create_or_update_setting(
391 390 setting, form_result[form_key], type_)
392 391 Session().add(sett)
393 392
394 393 Session().commit()
395 394 SettingsModel().invalidate_settings_cache()
396 395 h.flash(_('Updated visualisation settings'), category='success')
397 396 except Exception:
398 397 log.exception("Exception updating visualization settings")
399 398 h.flash(_('Error occurred during updating '
400 399 'visualisation settings'),
401 400 category='error')
402 401
403 402 raise HTTPFound(h.route_path('admin_settings_visual'))
404 403
405 404 @LoginRequired()
406 405 @HasPermissionAllDecorator('hg.admin')
407 406 def settings_issuetracker(self):
408 407 c = self.load_default_context()
409 408 c.active = 'issuetracker'
410 409 defaults = c.rc_config
411 410
412 411 entry_key = 'rhodecode_issuetracker_pat_'
413 412
414 413 c.issuetracker_entries = {}
415 414 for k, v in defaults.items():
416 415 if k.startswith(entry_key):
417 416 uid = k[len(entry_key):]
418 417 c.issuetracker_entries[uid] = None
419 418
420 419 for uid in c.issuetracker_entries:
421 420 c.issuetracker_entries[uid] = AttributeDict({
422 421 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
423 422 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
424 423 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
425 424 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
426 425 })
427 426
428 427 return self._get_template_context(c)
429 428
430 429 @LoginRequired()
431 430 @HasPermissionAllDecorator('hg.admin')
432 431 @CSRFRequired()
433 432 def settings_issuetracker_test(self):
434 433 error_container = []
435 434
436 435 urlified_commit = h.urlify_commit_message(
437 436 self.request.POST.get('test_text', ''),
438 437 'repo_group/test_repo1', error_container=error_container)
439 438 if error_container:
440 439 def converter(inp):
441 440 return h.html_escape(inp)
442 441
443 442 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
444 443
445 444 return urlified_commit
446 445
447 446 @LoginRequired()
448 447 @HasPermissionAllDecorator('hg.admin')
449 448 @CSRFRequired()
450 449 def settings_issuetracker_update(self):
451 450 _ = self.request.translate
452 451 self.load_default_context()
453 452 settings_model = IssueTrackerSettingsModel()
454 453
455 454 try:
456 455 form = IssueTrackerPatternsForm(self.request.translate)()
457 456 data = form.to_python(self.request.POST)
458 457 except formencode.Invalid as errors:
459 458 log.exception('Failed to add new pattern')
460 459 error = errors
461 460 h.flash(_(f'Invalid issue tracker pattern: {error}'),
462 461 category='error')
463 462 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
464 463
465 464 if data:
466 465 for uid in data.get('delete_patterns', []):
467 466 settings_model.delete_entries(uid)
468 467
469 468 for pattern in data.get('patterns', []):
470 469 for setting, value, type_ in pattern:
471 470 sett = settings_model.create_or_update_setting(
472 471 setting, value, type_)
473 472 Session().add(sett)
474 473
475 474 Session().commit()
476 475
477 476 SettingsModel().invalidate_settings_cache()
478 477 h.flash(_('Updated issue tracker entries'), category='success')
479 478 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
480 479
481 480 @LoginRequired()
482 481 @HasPermissionAllDecorator('hg.admin')
483 482 @CSRFRequired()
484 483 def settings_issuetracker_delete(self):
485 484 _ = self.request.translate
486 485 self.load_default_context()
487 486 uid = self.request.POST.get('uid')
488 487 try:
489 488 IssueTrackerSettingsModel().delete_entries(uid)
490 489 except Exception:
491 490 log.exception('Failed to delete issue tracker setting %s', uid)
492 491 raise HTTPNotFound()
493 492
494 493 SettingsModel().invalidate_settings_cache()
495 494 h.flash(_('Removed issue tracker entry.'), category='success')
496 495
497 496 return {'deleted': uid}
498 497
499 498 @LoginRequired()
500 499 @HasPermissionAllDecorator('hg.admin')
501 500 def settings_email(self):
502 501 c = self.load_default_context()
503 502 c.active = 'email'
504 503 c.rhodecode_ini = rhodecode.CONFIG
505 504
506 505 data = render('rhodecode:templates/admin/settings/settings.mako',
507 506 self._get_template_context(c), self.request)
508 507 html = formencode.htmlfill.render(
509 508 data,
510 509 defaults=self._form_defaults(),
511 510 encoding="UTF-8",
512 511 force_defaults=False
513 512 )
514 513 return Response(html)
515 514
516 515 @LoginRequired()
517 516 @HasPermissionAllDecorator('hg.admin')
518 517 @CSRFRequired()
519 518 def settings_email_update(self):
520 519 _ = self.request.translate
521 520 c = self.load_default_context()
522 521 c.active = 'email'
523 522
524 523 test_email = self.request.POST.get('test_email')
525 524
526 525 if not test_email:
527 526 h.flash(_('Please enter email address'), category='error')
528 527 raise HTTPFound(h.route_path('admin_settings_email'))
529 528
530 529 email_kwargs = {
531 530 'date': datetime.datetime.now(),
532 531 'user': self._rhodecode_db_user
533 532 }
534 533
535 534 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
536 535 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
537 536
538 537 recipients = [test_email] if test_email else None
539 538
540 539 run_task(tasks.send_email, recipients, subject,
541 540 email_body_plaintext, email_body)
542 541
543 542 h.flash(_('Send email task created'), category='success')
544 543 raise HTTPFound(h.route_path('admin_settings_email'))
545 544
546 545 @LoginRequired()
547 546 @HasPermissionAllDecorator('hg.admin')
548 547 def settings_hooks(self):
549 548 c = self.load_default_context()
550 549 c.active = 'hooks'
551 550
552 551 model = SettingsModel()
553 552 c.hooks = model.get_builtin_hooks()
554 553 c.custom_hooks = model.get_custom_hooks()
555 554
556 555 data = render('rhodecode:templates/admin/settings/settings.mako',
557 556 self._get_template_context(c), self.request)
558 557 html = formencode.htmlfill.render(
559 558 data,
560 559 defaults=self._form_defaults(),
561 560 encoding="UTF-8",
562 561 force_defaults=False
563 562 )
564 563 return Response(html)
565 564
566 565 @LoginRequired()
567 566 @HasPermissionAllDecorator('hg.admin')
568 567 @CSRFRequired()
569 568 def settings_hooks_update(self):
570 569 _ = self.request.translate
571 570 c = self.load_default_context()
572 571 c.active = 'hooks'
573 572 if c.visual.allow_custom_hooks_settings:
574 573 ui_key = self.request.POST.get('new_hook_ui_key')
575 574 ui_value = self.request.POST.get('new_hook_ui_value')
576 575
577 576 hook_id = self.request.POST.get('hook_id')
578 577 new_hook = False
579 578
580 579 model = SettingsModel()
581 580 try:
582 581 if ui_value and ui_key:
583 582 model.create_or_update_hook(ui_key, ui_value)
584 583 h.flash(_('Added new hook'), category='success')
585 584 new_hook = True
586 585 elif hook_id:
587 586 RhodeCodeUi.delete(hook_id)
588 587 Session().commit()
589 588
590 589 # check for edits
591 590 update = False
592 591 _d = self.request.POST.dict_of_lists()
593 592 for k, v in zip(_d.get('hook_ui_key', []),
594 593 _d.get('hook_ui_value_new', [])):
595 594 model.create_or_update_hook(k, v)
596 595 update = True
597 596
598 597 if update and not new_hook:
599 598 h.flash(_('Updated hooks'), category='success')
600 599 Session().commit()
601 600 except Exception:
602 601 log.exception("Exception during hook creation")
603 602 h.flash(_('Error occurred during hook creation'),
604 603 category='error')
605 604
606 605 raise HTTPFound(h.route_path('admin_settings_hooks'))
607 606
608 607 @LoginRequired()
609 608 @HasPermissionAllDecorator('hg.admin')
610 609 def settings_search(self):
611 610 c = self.load_default_context()
612 611 c.active = 'search'
613 612
614 613 c.searcher = searcher_from_config(self.request.registry.settings)
615 614 c.statistics = c.searcher.statistics(self.request.translate)
616 615
617 616 return self._get_template_context(c)
618 617
619 618 @LoginRequired()
620 619 @HasPermissionAllDecorator('hg.admin')
621 620 def settings_labs(self):
622 621 c = self.load_default_context()
623 622 if not c.labs_active:
624 623 raise HTTPFound(h.route_path('admin_settings'))
625 624
626 625 c.active = 'labs'
627 626 c.lab_settings = _LAB_SETTINGS
628 627
629 628 data = render('rhodecode:templates/admin/settings/settings.mako',
630 629 self._get_template_context(c), self.request)
631 630 html = formencode.htmlfill.render(
632 631 data,
633 632 defaults=self._form_defaults(),
634 633 encoding="UTF-8",
635 634 force_defaults=False
636 635 )
637 636 return Response(html)
638 637
639 638 @LoginRequired()
640 639 @HasPermissionAllDecorator('hg.admin')
641 640 @CSRFRequired()
642 641 def settings_labs_update(self):
643 642 _ = self.request.translate
644 643 c = self.load_default_context()
645 644 c.active = 'labs'
646 645
647 646 application_form = LabsSettingsForm(self.request.translate)()
648 647 try:
649 648 form_result = application_form.to_python(dict(self.request.POST))
650 649 except formencode.Invalid as errors:
651 650 h.flash(
652 651 _("Some form inputs contain invalid data."),
653 652 category='error')
654 653 data = render('rhodecode:templates/admin/settings/settings.mako',
655 654 self._get_template_context(c), self.request)
656 655 html = formencode.htmlfill.render(
657 656 data,
658 657 defaults=errors.value,
659 658 errors=errors.unpack_errors() or {},
660 659 prefix_error=False,
661 660 encoding="UTF-8",
662 661 force_defaults=False
663 662 )
664 663 return Response(html)
665 664
666 665 try:
667 666 session = Session()
668 667 for setting in _LAB_SETTINGS:
669 668 setting_name = setting.key[len('rhodecode_'):]
670 669 sett = SettingsModel().create_or_update_setting(
671 670 setting_name, form_result[setting.key], setting.type)
672 671 session.add(sett)
673 672
674 673 except Exception:
675 674 log.exception('Exception while updating lab settings')
676 675 h.flash(_('Error occurred during updating labs settings'),
677 676 category='error')
678 677 else:
679 678 Session().commit()
680 679 SettingsModel().invalidate_settings_cache()
681 680 h.flash(_('Updated Labs settings'), category='success')
682 681 raise HTTPFound(h.route_path('admin_settings_labs'))
683 682
684 683 data = render('rhodecode:templates/admin/settings/settings.mako',
685 684 self._get_template_context(c), self.request)
686 685 html = formencode.htmlfill.render(
687 686 data,
688 687 defaults=self._form_defaults(),
689 688 encoding="UTF-8",
690 689 force_defaults=False
691 690 )
692 691 return Response(html)
693 692
694 693
695 694 # :param key: name of the setting including the 'rhodecode_' prefix
696 695 # :param type: the RhodeCodeSetting type to use.
697 696 # :param group: the i18ned group in which we should dispaly this setting
698 697 # :param label: the i18ned label we should display for this setting
699 698 # :param help: the i18ned help we should dispaly for this setting
700 699 LabSetting = collections.namedtuple(
701 700 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
702 701
703 702
704 703 # This list has to be kept in sync with the form
705 704 # rhodecode.model.forms.LabsSettingsForm.
706 705 _LAB_SETTINGS = [
707 706
708 707 ]
@@ -1,470 +1,471 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import sys
21 21 import collections
22 22
23 23 import time
24 24 import logging.config
25 25
26 26 from paste.gzipper import make_gzip_middleware
27 27 import pyramid.events
28 28 from pyramid.wsgi import wsgiapp
29 29 from pyramid.config import Configurator
30 30 from pyramid.settings import asbool, aslist
31 31 from pyramid.httpexceptions import (
32 32 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
33 33 from pyramid.renderers import render_to_response
34 34
35 35 from rhodecode.model import meta
36 36 from rhodecode.config import patches
37 37
38 38 from rhodecode.config.environment import load_pyramid_environment, propagate_rhodecode_config
39 39
40 40 import rhodecode.events
41 41 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
42 42 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 43 from rhodecode.lib.request import Request
44 44 from rhodecode.lib.vcs import VCSCommunicationError
45 45 from rhodecode.lib.exceptions import VCSServerUnavailable
46 46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 48 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
49 49 from rhodecode.lib.utils2 import AttributeDict
50 50 from rhodecode.lib.exc_tracking import store_exception, format_exc
51 51 from rhodecode.subscribers import (
52 52 scan_repositories_if_enabled, write_js_routes_if_enabled,
53 write_metadata_if_needed, write_usage_data)
53 write_metadata_if_needed, write_usage_data, import_license_if_present)
54 54 from rhodecode.lib.statsd_client import StatsdClient
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def is_http_error(response):
60 60 # error which should have traceback
61 61 return response.status_code > 499
62 62
63 63
64 64 def should_load_all():
65 65 """
66 66 Returns if all application components should be loaded. In some cases it's
67 67 desired to skip apps loading for faster shell script execution
68 68 """
69 69 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
70 70 if ssh_cmd:
71 71 return False
72 72
73 73 return True
74 74
75 75
76 76 def make_pyramid_app(global_config, **settings):
77 77 """
78 78 Constructs the WSGI application based on Pyramid.
79 79
80 80 Specials:
81 81
82 82 * The application can also be integrated like a plugin via the call to
83 83 `includeme`. This is accompanied with the other utility functions which
84 84 are called. Changing this should be done with great care to not break
85 85 cases when these fragments are assembled from another place.
86 86
87 87 """
88 88 start_time = time.time()
89 89 log.info('Pyramid app config starting')
90 90
91 91 sanitize_settings_and_apply_defaults(global_config, settings)
92 92
93 93 # init and bootstrap StatsdClient
94 94 StatsdClient.setup(settings)
95 95
96 96 config = Configurator(settings=settings)
97 97 # Init our statsd at very start
98 98 config.registry.statsd = StatsdClient.statsd
99 99
100 100 # Apply compatibility patches
101 101 patches.inspect_getargspec()
102 102 patches.repoze_sendmail_lf_fix()
103 103
104 104 load_pyramid_environment(global_config, settings)
105 105
106 106 # Static file view comes first
107 107 includeme_first(config)
108 108
109 109 includeme(config)
110 110
111 111 pyramid_app = config.make_wsgi_app()
112 112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
113 113 pyramid_app.config = config
114 114
115 115 celery_settings = get_celery_config(settings)
116 116 config.configure_celery(celery_settings)
117 117
118 118 # final config set...
119 119 propagate_rhodecode_config(global_config, settings, config.registry.settings)
120 120
121 121 # creating the app uses a connection - return it after we are done
122 122 meta.Session.remove()
123 123
124 124 total_time = time.time() - start_time
125 125 log.info('Pyramid app created and configured in %.2fs', total_time)
126 126 return pyramid_app
127 127
128 128
129 129 def get_celery_config(settings):
130 130 """
131 131 Converts basic ini configuration into celery 4.X options
132 132 """
133 133
134 134 def key_converter(key_name):
135 135 pref = 'celery.'
136 136 if key_name.startswith(pref):
137 137 return key_name[len(pref):].replace('.', '_').lower()
138 138
139 139 def type_converter(parsed_key, value):
140 140 # cast to int
141 141 if value.isdigit():
142 142 return int(value)
143 143
144 144 # cast to bool
145 145 if value.lower() in ['true', 'false', 'True', 'False']:
146 146 return value.lower() == 'true'
147 147 return value
148 148
149 149 celery_config = {}
150 150 for k, v in settings.items():
151 151 pref = 'celery.'
152 152 if k.startswith(pref):
153 153 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
154 154
155 155 # TODO:rethink if we want to support celerybeat based file config, probably NOT
156 156 # beat_config = {}
157 157 # for section in parser.sections():
158 158 # if section.startswith('celerybeat:'):
159 159 # name = section.split(':', 1)[1]
160 160 # beat_config[name] = get_beat_config(parser, section)
161 161
162 162 # final compose of settings
163 163 celery_settings = {}
164 164
165 165 if celery_config:
166 166 celery_settings.update(celery_config)
167 167 # if beat_config:
168 168 # celery_settings.update({'beat_schedule': beat_config})
169 169
170 170 return celery_settings
171 171
172 172
173 173 def not_found_view(request):
174 174 """
175 175 This creates the view which should be registered as not-found-view to
176 176 pyramid.
177 177 """
178 178
179 179 if not getattr(request, 'vcs_call', None):
180 180 # handle like regular case with our error_handler
181 181 return error_handler(HTTPNotFound(), request)
182 182
183 183 # handle not found view as a vcs call
184 184 settings = request.registry.settings
185 185 ae_client = getattr(request, 'ae_client', None)
186 186 vcs_app = VCSMiddleware(
187 187 HTTPNotFound(), request.registry, settings,
188 188 appenlight_client=ae_client)
189 189
190 190 return wsgiapp(vcs_app)(None, request)
191 191
192 192
193 193 def error_handler(exception, request):
194 194 import rhodecode
195 195 from rhodecode.lib import helpers
196 196
197 197 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
198 198
199 199 base_response = HTTPInternalServerError()
200 200 # prefer original exception for the response since it may have headers set
201 201 if isinstance(exception, HTTPException):
202 202 base_response = exception
203 203 elif isinstance(exception, VCSCommunicationError):
204 204 base_response = VCSServerUnavailable()
205 205
206 206 if is_http_error(base_response):
207 207 traceback_info = format_exc(request.exc_info)
208 208 log.error(
209 209 'error occurred handling this request for path: %s, \n%s',
210 210 request.path, traceback_info)
211 211
212 212 error_explanation = base_response.explanation or str(base_response)
213 213 if base_response.status_code == 404:
214 214 error_explanation += " Optionally you don't have permission to access this page."
215 215 c = AttributeDict()
216 216 c.error_message = base_response.status
217 217 c.error_explanation = error_explanation
218 218 c.visual = AttributeDict()
219 219
220 220 c.visual.rhodecode_support_url = (
221 221 request.registry.settings.get('rhodecode_support_url') or
222 222 request.route_url('rhodecode_support')
223 223 )
224 224 c.redirect_time = 0
225 225 c.rhodecode_name = rhodecode_title
226 226 if not c.rhodecode_name:
227 227 c.rhodecode_name = 'Rhodecode'
228 228
229 229 c.causes = []
230 230 if is_http_error(base_response):
231 231 c.causes.append('Server is overloaded.')
232 232 c.causes.append('Server database connection is lost.')
233 233 c.causes.append('Server expected unhandled error.')
234 234
235 235 if hasattr(base_response, 'causes'):
236 236 c.causes = base_response.causes
237 237
238 238 c.messages = helpers.flash.pop_messages(request=request)
239 239 exc_info = sys.exc_info()
240 240 c.exception_id = id(exc_info)
241 241 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
242 242 or base_response.status_code > 499
243 243 c.exception_id_url = request.route_url(
244 244 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
245 245
246 246 debug_mode = rhodecode.ConfigGet().get_bool('debug')
247 247 if c.show_exception_id:
248 248 store_exception(c.exception_id, exc_info)
249 249 c.exception_debug = debug_mode
250 250 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
251 251
252 252 if debug_mode:
253 253 try:
254 254 from rich.traceback import install
255 255 install(show_locals=True)
256 256 log.debug('Installing rich tracebacks...')
257 257 except ImportError:
258 258 pass
259 259
260 260 response = render_to_response(
261 261 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
262 262 response=base_response)
263 263
264 264 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
265 265
266 266 statsd = request.registry.statsd
267 267 if statsd and base_response.status_code > 499:
268 268 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
269 269 statsd.incr('rhodecode_exception_total',
270 270 tags=["exc_source:web",
271 271 f"http_code:{base_response.status_code}",
272 272 f"type:{exc_type}"])
273 273
274 274 return response
275 275
276 276
277 277 def includeme_first(config):
278 278 # redirect automatic browser favicon.ico requests to correct place
279 279 def favicon_redirect(context, request):
280 280 return HTTPFound(
281 281 request.static_path('rhodecode:public/images/favicon.ico'))
282 282
283 283 config.add_view(favicon_redirect, route_name='favicon')
284 284 config.add_route('favicon', '/favicon.ico')
285 285
286 286 def robots_redirect(context, request):
287 287 return HTTPFound(
288 288 request.static_path('rhodecode:public/robots.txt'))
289 289
290 290 config.add_view(robots_redirect, route_name='robots')
291 291 config.add_route('robots', '/robots.txt')
292 292
293 293 config.add_static_view(
294 294 '_static/deform', 'deform:static')
295 295 config.add_static_view(
296 296 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
297 297
298 298
299 299 ce_auth_resources = [
300 300 'rhodecode.authentication.plugins.auth_crowd',
301 301 'rhodecode.authentication.plugins.auth_headers',
302 302 'rhodecode.authentication.plugins.auth_jasig_cas',
303 303 'rhodecode.authentication.plugins.auth_ldap',
304 304 'rhodecode.authentication.plugins.auth_pam',
305 305 'rhodecode.authentication.plugins.auth_rhodecode',
306 306 'rhodecode.authentication.plugins.auth_token',
307 307 ]
308 308
309 309
310 310 def includeme(config, auth_resources=None):
311 311 from rhodecode.lib.celerylib.loader import configure_celery
312 312 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
313 313 settings = config.registry.settings
314 314 config.set_request_factory(Request)
315 315
316 316 # plugin information
317 317 config.registry.rhodecode_plugins = collections.OrderedDict()
318 318
319 319 config.add_directive(
320 320 'register_rhodecode_plugin', register_rhodecode_plugin)
321 321
322 322 config.add_directive('configure_celery', configure_celery)
323 323
324 324 if settings.get('appenlight', False):
325 325 config.include('appenlight_client.ext.pyramid_tween')
326 326
327 327 load_all = should_load_all()
328 328
329 329 # Includes which are required. The application would fail without them.
330 330 config.include('pyramid_mako')
331 331 config.include('rhodecode.lib.rc_beaker')
332 332 config.include('rhodecode.lib.rc_cache')
333 333 config.include('rhodecode.lib.archive_cache')
334 334
335 335 config.include('rhodecode.apps._base.navigation')
336 336 config.include('rhodecode.apps._base.subscribers')
337 337 config.include('rhodecode.tweens')
338 338 config.include('rhodecode.authentication')
339 339
340 340 if load_all:
341 341
342 342 # load CE authentication plugins
343 343
344 344 if auth_resources:
345 345 ce_auth_resources.extend(auth_resources)
346 346
347 347 for resource in ce_auth_resources:
348 348 config.include(resource)
349 349
350 350 # Auto discover authentication plugins and include their configuration.
351 351 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
352 352 from rhodecode.authentication import discover_legacy_plugins
353 353 discover_legacy_plugins(config)
354 354
355 355 # apps
356 356 if load_all:
357 357 log.debug('Starting config.include() calls')
358 358 config.include('rhodecode.api.includeme')
359 359 config.include('rhodecode.apps._base.includeme')
360 360 config.include('rhodecode.apps._base.navigation.includeme')
361 361 config.include('rhodecode.apps._base.subscribers.includeme')
362 362 config.include('rhodecode.apps.hovercards.includeme')
363 363 config.include('rhodecode.apps.ops.includeme')
364 364 config.include('rhodecode.apps.channelstream.includeme')
365 365 config.include('rhodecode.apps.file_store.includeme')
366 366 config.include('rhodecode.apps.admin.includeme')
367 367 config.include('rhodecode.apps.login.includeme')
368 368 config.include('rhodecode.apps.home.includeme')
369 369 config.include('rhodecode.apps.journal.includeme')
370 370
371 371 config.include('rhodecode.apps.repository.includeme')
372 372 config.include('rhodecode.apps.repo_group.includeme')
373 373 config.include('rhodecode.apps.user_group.includeme')
374 374 config.include('rhodecode.apps.search.includeme')
375 375 config.include('rhodecode.apps.user_profile.includeme')
376 376 config.include('rhodecode.apps.user_group_profile.includeme')
377 377 config.include('rhodecode.apps.my_account.includeme')
378 378 config.include('rhodecode.apps.gist.includeme')
379 379
380 380 config.include('rhodecode.apps.svn_support.includeme')
381 381 config.include('rhodecode.apps.ssh_support.includeme')
382 382 config.include('rhodecode.apps.debug_style')
383 383
384 384 if load_all:
385 385 config.include('rhodecode.integrations.includeme')
386 386 config.include('rhodecode.integrations.routes.includeme')
387 387
388 388 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
389 389 settings['default_locale_name'] = settings.get('lang', 'en')
390 390 config.add_translation_dirs('rhodecode:i18n/')
391 391
392 392 # Add subscribers.
393 393 if load_all:
394 394 log.debug('Adding subscribers...')
395 395 config.add_subscriber(scan_repositories_if_enabled,
396 396 pyramid.events.ApplicationCreated)
397 397 config.add_subscriber(write_metadata_if_needed,
398 398 pyramid.events.ApplicationCreated)
399 399 config.add_subscriber(write_usage_data,
400 400 pyramid.events.ApplicationCreated)
401 401 config.add_subscriber(write_js_routes_if_enabled,
402 402 pyramid.events.ApplicationCreated)
403
403 config.add_subscriber(import_license_if_present,
404 pyramid.events.ApplicationCreated)
404 405
405 406 # Set the default renderer for HTML templates to mako.
406 407 config.add_mako_renderer('.html')
407 408
408 409 config.add_renderer(
409 410 name='json_ext',
410 411 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
411 412
412 413 config.add_renderer(
413 414 name='string_html',
414 415 factory='rhodecode.lib.string_renderer.html')
415 416
416 417 # include RhodeCode plugins
417 418 includes = aslist(settings.get('rhodecode.includes', []))
418 419 log.debug('processing rhodecode.includes data...')
419 420 for inc in includes:
420 421 config.include(inc)
421 422
422 423 # custom not found view, if our pyramid app doesn't know how to handle
423 424 # the request pass it to potential VCS handling ap
424 425 config.add_notfound_view(not_found_view)
425 426 if not settings.get('debugtoolbar.enabled', False):
426 427 # disabled debugtoolbar handle all exceptions via the error_handlers
427 428 config.add_view(error_handler, context=Exception)
428 429
429 430 # all errors including 403/404/50X
430 431 config.add_view(error_handler, context=HTTPError)
431 432
432 433
433 434 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
434 435 """
435 436 Apply outer WSGI middlewares around the application.
436 437 """
437 438 registry = config.registry
438 439 settings = registry.settings
439 440
440 441 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
441 442 pyramid_app = HttpsFixup(pyramid_app, settings)
442 443
443 444 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
444 445 pyramid_app, settings)
445 446 registry.ae_client = _ae_client
446 447
447 448 if settings['gzip_responses']:
448 449 pyramid_app = make_gzip_middleware(
449 450 pyramid_app, settings, compress_level=1)
450 451
451 452 # this should be the outer most middleware in the wsgi stack since
452 453 # middleware like Routes make database calls
453 454 def pyramid_app_with_cleanup(environ, start_response):
454 455 start = time.time()
455 456 try:
456 457 return pyramid_app(environ, start_response)
457 458 finally:
458 459 # Dispose current database session and rollback uncommitted
459 460 # transactions.
460 461 meta.Session.remove()
461 462
462 463 # In a single threaded mode server, on non sqlite db we should have
463 464 # '0 Current Checked out connections' at the end of a request,
464 465 # if not, then something, somewhere is leaving a connection open
465 466 pool = meta.get_engine().pool
466 467 log.debug('sa pool status: %s', pool.status())
467 468 total = time.time() - start
468 469 log.debug('Request processing finalized: %.4fs', total)
469 470
470 471 return pyramid_app_with_cleanup
@@ -1,679 +1,678 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Database creation, and setup module for RhodeCode Enterprise. Used for creation
21 21 of database as well as for migration operations
22 22 """
23 23
24 24 import os
25 25 import sys
26 26 import time
27 27 import uuid
28 28 import logging
29 29 import getpass
30 30 from os.path import dirname as dn, join as jn
31 31
32 32 from sqlalchemy.engine import create_engine
33 33
34 34 from rhodecode import __dbversion__
35 35 from rhodecode.model import init_model
36 36 from rhodecode.model.user import UserModel
37 37 from rhodecode.model.db import (
38 38 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
39 39 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
40 40 from rhodecode.model.meta import Session, Base
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.repo_group import RepoGroupModel
44 44 from rhodecode.model.settings import SettingsModel
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 def notify(msg):
51 51 """
52 52 Notification for migrations messages
53 53 """
54 54 ml = len(msg) + (4 * 2)
55 55 print((('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper()))
56 56
57 57
58 58 class DbManage(object):
59 59
60 60 def __init__(self, log_sql, dbconf, root, tests=False,
61 61 SESSION=None, cli_args=None, enc_key=b''):
62 62
63 63 self.dbname = dbconf.split('/')[-1]
64 64 self.tests = tests
65 65 self.root = root
66 66 self.dburi = dbconf
67 67 self.log_sql = log_sql
68 68 self.cli_args = cli_args or {}
69 69 self.sa = None
70 70 self.engine = None
71 71 self.enc_key = enc_key
72 72 # sets .sa .engine
73 73 self.init_db(SESSION=SESSION)
74 74
75 75 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
76 76
77 77 def db_exists(self):
78 78 if not self.sa:
79 79 self.init_db()
80 80 try:
81 81 self.sa.query(RhodeCodeUi)\
82 82 .filter(RhodeCodeUi.ui_key == '/')\
83 83 .scalar()
84 84 return True
85 85 except Exception:
86 86 return False
87 87 finally:
88 88 self.sa.rollback()
89 89
90 90 def get_ask_ok_func(self, param):
91 91 if param not in [None]:
92 92 # return a function lambda that has a default set to param
93 93 return lambda *args, **kwargs: param
94 94 else:
95 95 from rhodecode.lib.utils import ask_ok
96 96 return ask_ok
97 97
98 98 def init_db(self, SESSION=None):
99 99
100 100 if SESSION:
101 101 self.sa = SESSION
102 102 self.engine = SESSION.bind
103 103 else:
104 104 # init new sessions
105 105 engine = create_engine(self.dburi, echo=self.log_sql)
106 106 init_model(engine, encryption_key=self.enc_key)
107 107 self.sa = Session()
108 108 self.engine = engine
109 109
110 110 def create_tables(self, override=False):
111 111 """
112 112 Create a auth database
113 113 """
114 114
115 115 log.info("Existing database with the same name is going to be destroyed.")
116 116 log.info("Setup command will run DROP ALL command on that database.")
117 117 engine = self.engine
118 118
119 119 if self.tests:
120 120 destroy = True
121 121 else:
122 122 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
123 123 if not destroy:
124 124 log.info('db tables bootstrap: Nothing done.')
125 125 sys.exit(0)
126 126 if destroy:
127 127 Base.metadata.drop_all(bind=engine)
128 128
129 129 checkfirst = not override
130 130 Base.metadata.create_all(bind=engine, checkfirst=checkfirst)
131 131 log.info('Created tables for %s', self.dbname)
132 132
133 133 def set_db_version(self):
134 134 ver = DbMigrateVersion()
135 135 ver.version = __dbversion__
136 136 ver.repository_id = 'rhodecode_db_migrations'
137 137 ver.repository_path = 'versions'
138 138 self.sa.add(ver)
139 139 log.info('db version set to: %s', __dbversion__)
140 140
141 141 def run_post_migration_tasks(self):
142 142 """
143 143 Run various tasks before actually doing migrations
144 144 """
145 145 # delete cache keys on each upgrade
146 146 total = CacheKey.query().count()
147 147 log.info("Deleting (%s) cache keys now...", total)
148 148 CacheKey.delete_all_cache()
149 149
150 150 def upgrade(self, version=None):
151 151 """
152 152 Upgrades given database schema to given revision following
153 153 all needed steps, to perform the upgrade
154 154
155 155 """
156 156
157 157 from rhodecode.lib.dbmigrate.migrate.versioning import api
158 158 from rhodecode.lib.dbmigrate.migrate.exceptions import DatabaseNotControlledError
159 159
160 160 if 'sqlite' in self.dburi:
161 161 print(
162 162 '********************** WARNING **********************\n'
163 163 'Make sure your version of sqlite is at least 3.7.X. \n'
164 164 'Earlier versions are known to fail on some migrations\n'
165 165 '*****************************************************\n')
166 166
167 167 upgrade = self.ask_ok(
168 168 'You are about to perform a database upgrade. Make '
169 169 'sure you have backed up your database. '
170 170 'Continue ? [y/n]')
171 171 if not upgrade:
172 172 log.info('No upgrade performed')
173 173 sys.exit(0)
174 174
175 175 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
176 176 'rhodecode/lib/dbmigrate')
177 177 db_uri = self.dburi
178 178
179 179 if version:
180 180 DbMigrateVersion.set_version(version)
181 181
182 182 try:
183 183 curr_version = api.db_version(db_uri, repository_path)
184 184 msg = (f'Found current database db_uri under version '
185 185 f'control with version {curr_version}')
186 186
187 187 except (RuntimeError, DatabaseNotControlledError):
188 188 curr_version = 1
189 189 msg = f'Current database is not under version control. ' \
190 190 f'Setting as version {curr_version}'
191 191 api.version_control(db_uri, repository_path, curr_version)
192 192
193 193 notify(msg)
194 194
195 195 if curr_version == __dbversion__:
196 196 log.info('This database is already at the newest version')
197 197 sys.exit(0)
198 198
199 199 upgrade_steps = list(range(curr_version + 1, __dbversion__ + 1))
200 200 notify(f'attempting to upgrade database from '
201 201 f'version {curr_version} to version {__dbversion__}')
202 202
203 203 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
204 204 final_step = 'latest'
205 205 for step in upgrade_steps:
206 206 notify(f'performing upgrade step {step}')
207 207 time.sleep(0.5)
208 208
209 209 api.upgrade(db_uri, repository_path, step)
210 210 self.sa.rollback()
211 211 notify(f'schema upgrade for step {step} completed')
212 212
213 213 final_step = step
214 214
215 215 self.run_post_migration_tasks()
216 216 notify(f'upgrade to version {final_step} successful')
217 217
218 218 def fix_repo_paths(self):
219 219 """
220 220 Fixes an old RhodeCode version path into new one without a '*'
221 221 """
222 222
223 223 paths = self.sa.query(RhodeCodeUi)\
224 224 .filter(RhodeCodeUi.ui_key == '/')\
225 225 .scalar()
226 226
227 227 paths.ui_value = paths.ui_value.replace('*', '')
228 228
229 229 try:
230 230 self.sa.add(paths)
231 231 self.sa.commit()
232 232 except Exception:
233 233 self.sa.rollback()
234 234 raise
235 235
236 236 def fix_default_user(self):
237 237 """
238 238 Fixes an old default user with some 'nicer' default values,
239 239 used mostly for anonymous access
240 240 """
241 241 def_user = self.sa.query(User)\
242 242 .filter(User.username == User.DEFAULT_USER)\
243 243 .one()
244 244
245 245 def_user.name = 'Anonymous'
246 246 def_user.lastname = 'User'
247 247 def_user.email = User.DEFAULT_USER_EMAIL
248 248
249 249 try:
250 250 self.sa.add(def_user)
251 251 self.sa.commit()
252 252 except Exception:
253 253 self.sa.rollback()
254 254 raise
255 255
256 256 def fix_settings(self):
257 257 """
258 258 Fixes rhodecode settings and adds ga_code key for google analytics
259 259 """
260 260
261 261 hgsettings3 = RhodeCodeSetting('ga_code', '')
262 262
263 263 try:
264 264 self.sa.add(hgsettings3)
265 265 self.sa.commit()
266 266 except Exception:
267 267 self.sa.rollback()
268 268 raise
269 269
270 270 def create_admin_and_prompt(self):
271 271
272 272 # defaults
273 273 defaults = self.cli_args
274 274 username = defaults.get('username')
275 275 password = defaults.get('password')
276 276 email = defaults.get('email')
277 277
278 278 if username is None:
279 279 username = input('Specify admin username:')
280 280 if password is None:
281 281 password = self._get_admin_password()
282 282 if not password:
283 283 # second try
284 284 password = self._get_admin_password()
285 285 if not password:
286 286 sys.exit()
287 287 if email is None:
288 288 email = input('Specify admin email:')
289 289 api_key = self.cli_args.get('api_key')
290 290 self.create_user(username, password, email, True,
291 291 strict_creation_check=False,
292 292 api_key=api_key)
293 293
294 294 def _get_admin_password(self):
295 295 password = getpass.getpass('Specify admin password '
296 296 '(min 6 chars):')
297 297 confirm = getpass.getpass('Confirm password:')
298 298
299 299 if password != confirm:
300 300 log.error('passwords mismatch')
301 301 return False
302 302 if len(password) < 6:
303 303 log.error('password is too short - use at least 6 characters')
304 304 return False
305 305
306 306 return password
307 307
308 308 def create_test_admin_and_users(self):
309 309 log.info('creating admin and regular test users')
310 310 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
311 311 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
312 312 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
313 313 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
314 314 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
315 315
316 316 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
317 317 TEST_USER_ADMIN_EMAIL, True, api_key=True)
318 318
319 319 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
320 320 TEST_USER_REGULAR_EMAIL, False, api_key=True)
321 321
322 322 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
323 323 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
324 324
325 325 def create_ui_settings(self, repo_store_path):
326 326 """
327 327 Creates ui settings, fills out hooks
328 328 and disables dotencode
329 329 """
330 330 settings_model = SettingsModel(sa=self.sa)
331 331 from rhodecode.lib.vcs.backends.hg import largefiles_store
332 332 from rhodecode.lib.vcs.backends.git import lfs_store
333 333
334 334 # Build HOOKS
335 335 hooks = [
336 336 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
337 337
338 338 # HG
339 339 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
340 340 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
341 341 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
342 342 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
343 343 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
344 344 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
345 345
346 346 ]
347 347
348 348 for key, value in hooks:
349 349 hook_obj = settings_model.get_ui_by_key(key)
350 350 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
351 351 hooks2.ui_section = 'hooks'
352 352 hooks2.ui_key = key
353 353 hooks2.ui_value = value
354 354 self.sa.add(hooks2)
355 355
356 356 # enable largefiles
357 357 largefiles = RhodeCodeUi()
358 358 largefiles.ui_section = 'extensions'
359 359 largefiles.ui_key = 'largefiles'
360 360 largefiles.ui_value = ''
361 361 self.sa.add(largefiles)
362 362
363 363 # set default largefiles cache dir, defaults to
364 364 # /repo_store_location/.cache/largefiles
365 365 largefiles = RhodeCodeUi()
366 366 largefiles.ui_section = 'largefiles'
367 367 largefiles.ui_key = 'usercache'
368 368 largefiles.ui_value = largefiles_store(repo_store_path)
369 369
370 370 self.sa.add(largefiles)
371 371
372 372 # set default lfs cache dir, defaults to
373 373 # /repo_store_location/.cache/lfs_store
374 374 lfsstore = RhodeCodeUi()
375 375 lfsstore.ui_section = 'vcs_git_lfs'
376 376 lfsstore.ui_key = 'store_location'
377 377 lfsstore.ui_value = lfs_store(repo_store_path)
378 378
379 379 self.sa.add(lfsstore)
380 380
381 381 # enable hgevolve disabled by default
382 382 hgevolve = RhodeCodeUi()
383 383 hgevolve.ui_section = 'extensions'
384 384 hgevolve.ui_key = 'evolve'
385 385 hgevolve.ui_value = ''
386 386 hgevolve.ui_active = False
387 387 self.sa.add(hgevolve)
388 388
389 389 hgevolve = RhodeCodeUi()
390 390 hgevolve.ui_section = 'experimental'
391 391 hgevolve.ui_key = 'evolution'
392 392 hgevolve.ui_value = ''
393 393 hgevolve.ui_active = False
394 394 self.sa.add(hgevolve)
395 395
396 396 hgevolve = RhodeCodeUi()
397 397 hgevolve.ui_section = 'experimental'
398 398 hgevolve.ui_key = 'evolution.exchange'
399 399 hgevolve.ui_value = ''
400 400 hgevolve.ui_active = False
401 401 self.sa.add(hgevolve)
402 402
403 403 hgevolve = RhodeCodeUi()
404 404 hgevolve.ui_section = 'extensions'
405 405 hgevolve.ui_key = 'topic'
406 406 hgevolve.ui_value = ''
407 407 hgevolve.ui_active = False
408 408 self.sa.add(hgevolve)
409 409
410 410 # enable hggit disabled by default
411 411 hggit = RhodeCodeUi()
412 412 hggit.ui_section = 'extensions'
413 413 hggit.ui_key = 'hggit'
414 414 hggit.ui_value = ''
415 415 hggit.ui_active = False
416 416 self.sa.add(hggit)
417 417
418 418 # set svn branch defaults
419 419 branches = ["/branches/*", "/trunk"]
420 420 tags = ["/tags/*"]
421 421
422 422 for branch in branches:
423 423 settings_model.create_ui_section_value(
424 424 RhodeCodeUi.SVN_BRANCH_ID, branch)
425 425
426 426 for tag in tags:
427 427 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
428 428
429 429 def create_auth_plugin_options(self, skip_existing=False):
430 430 """
431 431 Create default auth plugin settings, and make it active
432 432
433 433 :param skip_existing:
434 434 """
435 435 defaults = [
436 436 ('auth_plugins',
437 437 'egg:rhodecode-enterprise-ce#token,egg:rhodecode-enterprise-ce#rhodecode',
438 438 'list'),
439 439
440 440 ('auth_authtoken_enabled',
441 441 'True',
442 442 'bool'),
443 443
444 444 ('auth_rhodecode_enabled',
445 445 'True',
446 446 'bool'),
447 447 ]
448 448 for k, v, t in defaults:
449 449 if (skip_existing and
450 450 SettingsModel().get_setting_by_name(k) is not None):
451 451 log.debug('Skipping option %s', k)
452 452 continue
453 453 setting = RhodeCodeSetting(k, v, t)
454 454 self.sa.add(setting)
455 455
456 456 def create_default_options(self, skip_existing=False):
457 457 """Creates default settings"""
458 458
459 459 for k, v, t in [
460 460 ('default_repo_enable_locking', False, 'bool'),
461 461 ('default_repo_enable_downloads', False, 'bool'),
462 462 ('default_repo_enable_statistics', False, 'bool'),
463 463 ('default_repo_private', False, 'bool'),
464 464 ('default_repo_type', 'hg', 'unicode')]:
465 465
466 466 if (skip_existing and
467 467 SettingsModel().get_setting_by_name(k) is not None):
468 468 log.debug('Skipping option %s', k)
469 469 continue
470 470 setting = RhodeCodeSetting(k, v, t)
471 471 self.sa.add(setting)
472 472
473 473 def fixup_groups(self):
474 474 def_usr = User.get_default_user()
475 475 for g in RepoGroup.query().all():
476 476 g.group_name = g.get_new_name(g.name)
477 477 self.sa.add(g)
478 478 # get default perm
479 479 default = UserRepoGroupToPerm.query()\
480 480 .filter(UserRepoGroupToPerm.group == g)\
481 481 .filter(UserRepoGroupToPerm.user == def_usr)\
482 482 .scalar()
483 483
484 484 if default is None:
485 485 log.debug('missing default permission for group %s adding', g)
486 486 perm_obj = RepoGroupModel()._create_default_perms(g)
487 487 self.sa.add(perm_obj)
488 488
489 489 def reset_permissions(self, username):
490 490 """
491 491 Resets permissions to default state, useful when old systems had
492 492 bad permissions, we must clean them up
493 493
494 494 :param username:
495 495 """
496 496 default_user = User.get_by_username(username)
497 497 if not default_user:
498 498 return
499 499
500 500 u2p = UserToPerm.query()\
501 501 .filter(UserToPerm.user == default_user).all()
502 502 fixed = False
503 503 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
504 504 for p in u2p:
505 505 Session().delete(p)
506 506 fixed = True
507 507 self.populate_default_permissions()
508 508 return fixed
509 509
510 510 def config_prompt(self, test_repo_path='', retries=3):
511 511 defaults = self.cli_args
512 512 _path = defaults.get('repos_location')
513 513 if retries == 3:
514 514 log.info('Setting up repositories config')
515 515
516 516 if _path is not None:
517 517 path = _path
518 518 elif not self.tests and not test_repo_path:
519 519 path = input(
520 520 'Enter a valid absolute path to store repositories. '
521 521 'All repositories in that path will be added automatically:'
522 522 )
523 523 else:
524 524 path = test_repo_path
525 525 path_ok = True
526 526
527 527 # check proper dir
528 528 if not os.path.isdir(path):
529 529 path_ok = False
530 530 log.error('Given path %s is not a valid directory', path)
531 531
532 532 elif not os.path.isabs(path):
533 533 path_ok = False
534 534 log.error('Given path %s is not an absolute path', path)
535 535
536 536 # check if path is at least readable.
537 537 if not os.access(path, os.R_OK):
538 538 path_ok = False
539 539 log.error('Given path %s is not readable', path)
540 540
541 541 # check write access, warn user about non writeable paths
542 542 elif not os.access(path, os.W_OK) and path_ok:
543 543 log.warning('No write permission to given path %s', path)
544 544
545 545 q = (f'Given path {path} is not writeable, do you want to '
546 546 f'continue with read only mode ? [y/n]')
547 547 if not self.ask_ok(q):
548 548 log.error('Canceled by user')
549 549 sys.exit(-1)
550 550
551 551 if retries == 0:
552 552 sys.exit('max retries reached')
553 553 if not path_ok:
554 554 retries -= 1
555 555 return self.config_prompt(test_repo_path, retries)
556 556
557 557 real_path = os.path.normpath(os.path.realpath(path))
558 558
559 559 if real_path != os.path.normpath(path):
560 560 q = (f'Path looks like a symlink, RhodeCode Enterprise will store '
561 561 f'given path as {real_path} ? [y/n]')
562 562 if not self.ask_ok(q):
563 563 log.error('Canceled by user')
564 564 sys.exit(-1)
565 565
566 566 return real_path
567 567
568 568 def create_settings(self, path):
569 569
570 570 self.create_ui_settings(path)
571 571
572 572 ui_config = [
573 ('web', 'push_ssl', 'False'),
574 573 ('web', 'allow_archive', 'gz zip bz2'),
575 574 ('web', 'allow_push', '*'),
576 575 ('web', 'baseurl', '/'),
577 576 ('paths', '/', path),
578 577 ('phases', 'publish', 'True')
579 578 ]
580 579 for section, key, value in ui_config:
581 580 ui_conf = RhodeCodeUi()
582 581 setattr(ui_conf, 'ui_section', section)
583 582 setattr(ui_conf, 'ui_key', key)
584 583 setattr(ui_conf, 'ui_value', value)
585 584 self.sa.add(ui_conf)
586 585
587 586 # rhodecode app settings
588 587 settings = [
589 588 ('realm', 'RhodeCode', 'unicode'),
590 589 ('title', '', 'unicode'),
591 590 ('pre_code', '', 'unicode'),
592 591 ('post_code', '', 'unicode'),
593 592
594 593 # Visual
595 594 ('show_public_icon', True, 'bool'),
596 595 ('show_private_icon', True, 'bool'),
597 596 ('stylify_metatags', True, 'bool'),
598 597 ('dashboard_items', 100, 'int'),
599 598 ('admin_grid_items', 25, 'int'),
600 599
601 600 ('markup_renderer', 'markdown', 'unicode'),
602 601
603 602 ('repository_fields', True, 'bool'),
604 603 ('show_version', True, 'bool'),
605 604 ('show_revision_number', True, 'bool'),
606 605 ('show_sha_length', 12, 'int'),
607 606
608 607 ('use_gravatar', False, 'bool'),
609 608 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
610 609
611 610 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
612 611 ('clone_uri_id_tmpl', Repository.DEFAULT_CLONE_URI_ID, 'unicode'),
613 612 ('clone_uri_ssh_tmpl', Repository.DEFAULT_CLONE_URI_SSH, 'unicode'),
614 613 ('support_url', '', 'unicode'),
615 614 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
616 615
617 616 # VCS Settings
618 617 ('pr_merge_enabled', True, 'bool'),
619 618 ('use_outdated_comments', True, 'bool'),
620 619 ('diff_cache', True, 'bool'),
621 620 ]
622 621
623 622 for key, val, type_ in settings:
624 623 sett = RhodeCodeSetting(key, val, type_)
625 624 self.sa.add(sett)
626 625
627 626 self.create_auth_plugin_options()
628 627 self.create_default_options()
629 628
630 629 log.info('created ui config')
631 630
632 631 def create_user(self, username, password, email='', admin=False,
633 632 strict_creation_check=True, api_key=None):
634 633 log.info('creating user `%s`', username)
635 634 user = UserModel().create_or_update(
636 635 username, password, email, firstname='RhodeCode', lastname='Admin',
637 636 active=True, admin=admin, extern_type="rhodecode",
638 637 strict_creation_check=strict_creation_check)
639 638
640 639 if api_key:
641 640 log.info('setting a new default auth token for user `%s`', username)
642 641 UserModel().add_auth_token(
643 642 user=user, lifetime_minutes=-1,
644 643 role=UserModel.auth_token_role.ROLE_ALL,
645 644 description='BUILTIN TOKEN')
646 645
647 646 def create_default_user(self):
648 647 log.info('creating default user')
649 648 # create default user for handling default permissions.
650 649 user = UserModel().create_or_update(username=User.DEFAULT_USER,
651 650 password=str(uuid.uuid1())[:20],
652 651 email=User.DEFAULT_USER_EMAIL,
653 652 firstname='Anonymous',
654 653 lastname='User',
655 654 strict_creation_check=False)
656 655 # based on configuration options activate/de-activate this user which
657 656 # controls anonymous access
658 657 if self.cli_args.get('public_access') is False:
659 658 log.info('Public access disabled')
660 659 user.active = False
661 660 Session().add(user)
662 661 Session().commit()
663 662
664 663 def create_permissions(self):
665 664 """
666 665 Creates all permissions defined in the system
667 666 """
668 667 # module.(access|create|change|delete)_[name]
669 668 # module.(none|read|write|admin)
670 669 log.info('creating permissions')
671 670 PermissionModel(self.sa).create_permissions()
672 671
673 672 def populate_default_permissions(self):
674 673 """
675 674 Populate default permissions. It will create only the default
676 675 permissions that are missing, and not alter already defined ones
677 676 """
678 677 log.info('creating default user permissions')
679 678 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
@@ -1,683 +1,662 b''
1 1
2 2
3 3 # Copyright (C) 2014-2023 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31
32 32 import time
33 33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 34
35 35 from pyramid.httpexceptions import (
36 36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 37 from zope.cachedescriptors.property import Lazy as LazyProperty
38 38
39 39 import rhodecode
40 40 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
41 41 from rhodecode.lib import rc_cache
42 42 from rhodecode.lib.svn_txn_utils import store_txn_id_data
43 43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 44 from rhodecode.lib.base import (
45 45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 46 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
47 47 from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
48 48 from rhodecode.lib.middleware import appenlight
49 49 from rhodecode.lib.middleware.utils import scm_app_http
50 50 from rhodecode.lib.str_utils import safe_bytes, safe_int
51 51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
53 53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 54 from rhodecode.lib.vcs.backends import base
55 55
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def initialize_generator(factory):
66 66 """
67 67 Initializes the returned generator by draining its first element.
68 68
69 69 This can be used to give a generator an initializer, which is the code
70 70 up to the first yield statement. This decorator enforces that the first
71 71 produced element has the value ``"__init__"`` to make its special
72 72 purpose very explicit in the using code.
73 73 """
74 74
75 75 @wraps(factory)
76 76 def wrapper(*args, **kwargs):
77 77 gen = factory(*args, **kwargs)
78 78 try:
79 79 init = next(gen)
80 80 except StopIteration:
81 81 raise ValueError('Generator must yield at least one element.')
82 82 if init != "__init__":
83 83 raise ValueError('First yielded element must be "__init__".')
84 84 return gen
85 85 return wrapper
86 86
87 87
88 88 class SimpleVCS(object):
89 89 """Common functionality for SCM HTTP handlers."""
90 90
91 91 SCM = 'unknown'
92 92
93 93 acl_repo_name = None
94 94 url_repo_name = None
95 95 vcs_repo_name = None
96 96 rc_extras = {}
97 97
98 98 # We have to handle requests to shadow repositories different than requests
99 99 # to normal repositories. Therefore we have to distinguish them. To do this
100 100 # we use this regex which will match only on URLs pointing to shadow
101 101 # repositories.
102 102 shadow_repo_re = re.compile(
103 103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
104 104 '(?P<target>{slug_pat})/' # target repo
105 105 'pull-request/(?P<pr_id>\\d+)/' # pull request
106 106 'repository$' # shadow repo
107 107 .format(slug_pat=SLUG_RE.pattern))
108 108
109 109 def __init__(self, config, registry):
110 110 self.registry = registry
111 111 self.config = config
112 112 # re-populated by specialized middleware
113 113 self.repo_vcs_config = base.Config()
114 114
115 115 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
116 116 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
117 117
118 118 # authenticate this VCS request using authfunc
119 119 auth_ret_code_detection = \
120 120 str2bool(self.config.get('auth_ret_code_detection', False))
121 121 self.authenticate = BasicAuth(
122 122 '', authenticate, registry, config.get('auth_ret_code'),
123 123 auth_ret_code_detection, rc_realm=realm)
124 124 self.ip_addr = '0.0.0.0'
125 125
126 126 @LazyProperty
127 127 def global_vcs_config(self):
128 128 try:
129 129 return VcsSettingsModel().get_ui_settings_as_config_obj()
130 130 except Exception:
131 131 return base.Config()
132 132
133 133 @property
134 134 def base_path(self):
135 135 settings_path = self.config.get('repo_store.path')
136 136
137 137 if not settings_path:
138 138 raise ValueError('FATAL: repo_store.path is empty')
139 139 return settings_path
140 140
141 141 def set_repo_names(self, environ):
142 142 """
143 143 This will populate the attributes acl_repo_name, url_repo_name,
144 144 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
145 145 shadow) repositories all names are equal. In case of requests to a
146 146 shadow repository the acl-name points to the target repo of the pull
147 147 request and the vcs-name points to the shadow repo file system path.
148 148 The url-name is always the URL used by the vcs client program.
149 149
150 150 Example in case of a shadow repo:
151 151 acl_repo_name = RepoGroup/MyRepo
152 152 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
153 153 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
154 154 """
155 155 # First we set the repo name from URL for all attributes. This is the
156 156 # default if handling normal (non shadow) repo requests.
157 157 self.url_repo_name = self._get_repository_name(environ)
158 158 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
159 159 self.is_shadow_repo = False
160 160
161 161 # Check if this is a request to a shadow repository.
162 162 match = self.shadow_repo_re.match(self.url_repo_name)
163 163 if match:
164 164 match_dict = match.groupdict()
165 165
166 166 # Build acl repo name from regex match.
167 167 acl_repo_name = safe_str('{groups}{target}'.format(
168 168 groups=match_dict['groups'] or '',
169 169 target=match_dict['target']))
170 170
171 171 # Retrieve pull request instance by ID from regex match.
172 172 pull_request = PullRequest.get(match_dict['pr_id'])
173 173
174 174 # Only proceed if we got a pull request and if acl repo name from
175 175 # URL equals the target repo name of the pull request.
176 176 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
177 177
178 178 # Get file system path to shadow repository.
179 179 workspace_id = PullRequestModel()._workspace_id(pull_request)
180 180 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
181 181
182 182 # Store names for later usage.
183 183 self.vcs_repo_name = vcs_repo_name
184 184 self.acl_repo_name = acl_repo_name
185 185 self.is_shadow_repo = True
186 186
187 187 log.debug('Setting all VCS repository names: %s', {
188 188 'acl_repo_name': self.acl_repo_name,
189 189 'url_repo_name': self.url_repo_name,
190 190 'vcs_repo_name': self.vcs_repo_name,
191 191 })
192 192
193 193 @property
194 194 def scm_app(self):
195 195 custom_implementation = self.config['vcs.scm_app_implementation']
196 196 if custom_implementation == 'http':
197 197 log.debug('Using HTTP implementation of scm app.')
198 198 scm_app_impl = scm_app_http
199 199 else:
200 200 log.debug('Using custom implementation of scm_app: "{}"'.format(
201 201 custom_implementation))
202 202 scm_app_impl = importlib.import_module(custom_implementation)
203 203 return scm_app_impl
204 204
205 205 def _get_by_id(self, repo_name):
206 206 """
207 207 Gets a special pattern _<ID> from clone url and tries to replace it
208 208 with a repository_name for support of _<ID> non changeable urls
209 209 """
210 210
211 211 data = repo_name.split('/')
212 212 if len(data) >= 2:
213 213 from rhodecode.model.repo import RepoModel
214 214 by_id_match = RepoModel().get_repo_by_id(repo_name)
215 215 if by_id_match:
216 216 data[1] = by_id_match.repo_name
217 217
218 218 # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
219 219 # and we use this data
220 220 maybe_new_path = '/'.join(data)
221 221 return safe_bytes(maybe_new_path).decode('latin1')
222 222
223 223 def _invalidate_cache(self, repo_name):
224 224 """
225 225 Set's cache for this repository for invalidation on next access
226 226
227 227 :param repo_name: full repo name, also a cache key
228 228 """
229 229 ScmModel().mark_for_invalidation(repo_name)
230 230
231 231 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
232 232 db_repo = Repository.get_by_repo_name(repo_name)
233 233 if not db_repo:
234 234 log.debug('Repository `%s` not found inside the database.',
235 235 repo_name)
236 236 return False
237 237
238 238 if db_repo.repo_type != scm_type:
239 239 log.warning(
240 240 'Repository `%s` have incorrect scm_type, expected %s got %s',
241 241 repo_name, db_repo.repo_type, scm_type)
242 242 return False
243 243
244 244 config = db_repo._config
245 245 config.set('extensions', 'largefiles', '')
246 246 return is_valid_repo(
247 247 repo_name, base_path,
248 248 explicit_scm=scm_type, expect_scm=scm_type, config=config)
249 249
250 250 def valid_and_active_user(self, user):
251 251 """
252 252 Checks if that user is not empty, and if it's actually object it checks
253 253 if he's active.
254 254
255 255 :param user: user object or None
256 256 :return: boolean
257 257 """
258 258 if user is None:
259 259 return False
260 260
261 261 elif user.active:
262 262 return True
263 263
264 264 return False
265 265
266 266 @property
267 267 def is_shadow_repo_dir(self):
268 268 return os.path.isdir(self.vcs_repo_name)
269 269
270 270 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
271 271 plugin_id='', plugin_cache_active=False, cache_ttl=0):
272 272 """
273 273 Checks permissions using action (push/pull) user and repository
274 274 name. If plugin_cache and ttl is set it will use the plugin which
275 275 authenticated the user to store the cached permissions result for N
276 276 amount of seconds as in cache_ttl
277 277
278 278 :param action: push or pull action
279 279 :param user: user instance
280 280 :param repo_name: repository name
281 281 """
282 282
283 283 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
284 284 plugin_id, plugin_cache_active, cache_ttl)
285 285
286 286 user_id = user.user_id
287 287 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
288 288 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
289 289
290 290 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
291 291 expiration_time=cache_ttl,
292 292 condition=plugin_cache_active)
293 293 def compute_perm_vcs(
294 294 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
295 295
296 296 log.debug('auth: calculating permission access now...')
297 297 # check IP
298 298 inherit = user.inherit_default_permissions
299 299 ip_allowed = AuthUser.check_ip_allowed(
300 300 user_id, ip_addr, inherit_from_default=inherit)
301 301 if ip_allowed:
302 302 log.info('Access for IP:%s allowed', ip_addr)
303 303 else:
304 304 return False
305 305
306 306 if action == 'push':
307 307 perms = ('repository.write', 'repository.admin')
308 308 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
309 309 return False
310 310
311 311 else:
312 312 # any other action need at least read permission
313 313 perms = (
314 314 'repository.read', 'repository.write', 'repository.admin')
315 315 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
316 316 return False
317 317
318 318 return True
319 319
320 320 start = time.time()
321 321 log.debug('Running plugin `%s` permissions check', plugin_id)
322 322
323 323 # for environ based auth, password can be empty, but then the validation is
324 324 # on the server that fills in the env data needed for authentication
325 325 perm_result = compute_perm_vcs(
326 326 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
327 327
328 328 auth_time = time.time() - start
329 329 log.debug('Permissions for plugin `%s` completed in %.4fs, '
330 330 'expiration time of fetched cache %.1fs.',
331 331 plugin_id, auth_time, cache_ttl)
332 332
333 333 return perm_result
334 334
335 335 def _get_http_scheme(self, environ):
336 336 try:
337 337 return environ['wsgi.url_scheme']
338 338 except Exception:
339 339 log.exception('Failed to read http scheme')
340 340 return 'http'
341 341
342 def _check_ssl(self, environ, start_response):
343 """
344 Checks the SSL check flag and returns False if SSL is not present
345 and required True otherwise
346 """
347 org_proto = environ['wsgi._org_proto']
348 # check if we have SSL required ! if not it's a bad request !
349 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
350 if require_ssl and org_proto == 'http':
351 log.debug(
352 'Bad request: detected protocol is `%s` and '
353 'SSL/HTTPS is required.', org_proto)
354 return False
355 return True
356
357 342 def _get_default_cache_ttl(self):
358 343 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
359 344 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
360 345 plugin_settings = plugin.get_settings()
361 346 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
362 347 plugin_settings) or (False, 0)
363 348 return plugin_cache_active, cache_ttl
364 349
365 350 def __call__(self, environ, start_response):
366 351 try:
367 352 return self._handle_request(environ, start_response)
368 353 except Exception:
369 354 log.exception("Exception while handling request")
370 355 appenlight.track_exception(environ)
371 356 return HTTPInternalServerError()(environ, start_response)
372 357 finally:
373 358 meta.Session.remove()
374 359
375 360 def _handle_request(self, environ, start_response):
376 if not self._check_ssl(environ, start_response):
377 reason = ('SSL required, while RhodeCode was unable '
378 'to detect this as SSL request')
379 log.debug('User not allowed to proceed, %s', reason)
380 return HTTPNotAcceptable(reason)(environ, start_response)
381
382 361 if not self.url_repo_name:
383 362 log.warning('Repository name is empty: %s', self.url_repo_name)
384 363 # failed to get repo name, we fail now
385 364 return HTTPNotFound()(environ, start_response)
386 365 log.debug('Extracted repo name is %s', self.url_repo_name)
387 366
388 367 ip_addr = get_ip_addr(environ)
389 368 user_agent = get_user_agent(environ)
390 369 username = None
391 370
392 371 # skip passing error to error controller
393 372 environ['pylons.status_code_redirect'] = True
394 373
395 374 # ======================================================================
396 375 # GET ACTION PULL or PUSH
397 376 # ======================================================================
398 377 action = self._get_action(environ)
399 378
400 379 # ======================================================================
401 380 # Check if this is a request to a shadow repository of a pull request.
402 381 # In this case only pull action is allowed.
403 382 # ======================================================================
404 383 if self.is_shadow_repo and action != 'pull':
405 384 reason = 'Only pull action is allowed for shadow repositories.'
406 385 log.debug('User not allowed to proceed, %s', reason)
407 386 return HTTPNotAcceptable(reason)(environ, start_response)
408 387
409 388 # Check if the shadow repo actually exists, in case someone refers
410 389 # to it, and it has been deleted because of successful merge.
411 390 if self.is_shadow_repo and not self.is_shadow_repo_dir:
412 391 log.debug(
413 392 'Shadow repo detected, and shadow repo dir `%s` is missing',
414 393 self.is_shadow_repo_dir)
415 394 return HTTPNotFound()(environ, start_response)
416 395
417 396 # ======================================================================
418 397 # CHECK ANONYMOUS PERMISSION
419 398 # ======================================================================
420 399 detect_force_push = False
421 400 check_branch_perms = False
422 401 if action in ['pull', 'push']:
423 402 user_obj = anonymous_user = User.get_default_user()
424 403 auth_user = user_obj.AuthUser()
425 404 username = anonymous_user.username
426 405 if anonymous_user.active:
427 406 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
428 407 # ONLY check permissions if the user is activated
429 408 anonymous_perm = self._check_permission(
430 409 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
431 410 plugin_id='anonymous_access',
432 411 plugin_cache_active=plugin_cache_active,
433 412 cache_ttl=cache_ttl,
434 413 )
435 414 else:
436 415 anonymous_perm = False
437 416
438 417 if not anonymous_user.active or not anonymous_perm:
439 418 if not anonymous_user.active:
440 419 log.debug('Anonymous access is disabled, running '
441 420 'authentication')
442 421
443 422 if not anonymous_perm:
444 423 log.debug('Not enough credentials to access repo: `%s` '
445 424 'repository as anonymous user', self.acl_repo_name)
446 425
447 426 username = None
448 427 # ==============================================================
449 428 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
450 429 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
451 430 # ==============================================================
452 431
453 432 # try to auth based on environ, container auth methods
454 433 log.debug('Running PRE-AUTH for container|headers based authentication')
455 434
456 435 # headers auth, by just reading special headers and bypass the auth with user/passwd
457 436 pre_auth = authenticate(
458 437 '', '', environ, VCS_TYPE, registry=self.registry,
459 438 acl_repo_name=self.acl_repo_name)
460 439
461 440 if pre_auth and pre_auth.get('username'):
462 441 username = pre_auth['username']
463 442 log.debug('PRE-AUTH got `%s` as username', username)
464 443 if pre_auth:
465 444 log.debug('PRE-AUTH successful from %s',
466 445 pre_auth.get('auth_data', {}).get('_plugin'))
467 446
468 447 # If not authenticated by the container, running basic auth
469 448 # before inject the calling repo_name for special scope checks
470 449 self.authenticate.acl_repo_name = self.acl_repo_name
471 450
472 451 plugin_cache_active, cache_ttl = False, 0
473 452 plugin = None
474 453
475 454 # regular auth chain
476 455 if not username:
477 456 self.authenticate.realm = self.authenticate.get_rc_realm()
478 457
479 458 try:
480 459 auth_result = self.authenticate(environ)
481 460 except (UserCreationError, NotAllowedToCreateUserError) as e:
482 461 log.error(e)
483 462 reason = safe_str(e)
484 463 return HTTPNotAcceptable(reason)(environ, start_response)
485 464
486 465 if isinstance(auth_result, dict):
487 466 AUTH_TYPE.update(environ, 'basic')
488 467 REMOTE_USER.update(environ, auth_result['username'])
489 468 username = auth_result['username']
490 469 plugin = auth_result.get('auth_data', {}).get('_plugin')
491 470 log.info(
492 471 'MAIN-AUTH successful for user `%s` from %s plugin',
493 472 username, plugin)
494 473
495 474 plugin_cache_active, cache_ttl = auth_result.get(
496 475 'auth_data', {}).get('_ttl_cache') or (False, 0)
497 476 else:
498 477 return auth_result.wsgi_application(environ, start_response)
499 478
500 479 # ==============================================================
501 480 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
502 481 # ==============================================================
503 482 user = User.get_by_username(username)
504 483 if not self.valid_and_active_user(user):
505 484 return HTTPForbidden()(environ, start_response)
506 485 username = user.username
507 486 user_id = user.user_id
508 487
509 488 # check user attributes for password change flag
510 489 user_obj = user
511 490 auth_user = user_obj.AuthUser()
512 491 if user_obj and user_obj.username != User.DEFAULT_USER and \
513 492 user_obj.user_data.get('force_password_change'):
514 493 reason = 'password change required'
515 494 log.debug('User not allowed to authenticate, %s', reason)
516 495 return HTTPNotAcceptable(reason)(environ, start_response)
517 496
518 497 # check permissions for this repository
519 498 perm = self._check_permission(
520 499 action, user, auth_user, self.acl_repo_name, ip_addr,
521 500 plugin, plugin_cache_active, cache_ttl)
522 501 if not perm:
523 502 return HTTPForbidden()(environ, start_response)
524 503 environ['rc_auth_user_id'] = str(user_id)
525 504
526 505 if action == 'push':
527 506 perms = auth_user.get_branch_permissions(self.acl_repo_name)
528 507 if perms:
529 508 check_branch_perms = True
530 509 detect_force_push = True
531 510
532 511 # extras are injected into UI object and later available
533 512 # in hooks executed by RhodeCode
534 513 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
535 514
536 515 extras = vcs_operation_context(
537 516 environ, repo_name=self.acl_repo_name, username=username,
538 517 action=action, scm=self.SCM, check_locking=check_locking,
539 518 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
540 519 detect_force_push=detect_force_push
541 520 )
542 521
543 522 # ======================================================================
544 523 # REQUEST HANDLING
545 524 # ======================================================================
546 525 repo_path = os.path.join(
547 526 safe_str(self.base_path), safe_str(self.vcs_repo_name))
548 527 log.debug('Repository path is %s', repo_path)
549 528
550 529 fix_PATH()
551 530
552 531 log.info(
553 532 '%s action on %s repo "%s" by "%s" from %s %s',
554 533 action, self.SCM, safe_str(self.url_repo_name),
555 534 safe_str(username), ip_addr, user_agent)
556 535
557 536 return self._generate_vcs_response(
558 537 environ, start_response, repo_path, extras, action)
559 538
560 539 def _get_txn_id(self, environ):
561 540
562 541 for k in ['RAW_URI', 'HTTP_DESTINATION']:
563 542 url = environ.get(k)
564 543 if not url:
565 544 continue
566 545
567 546 # regex to search for svn-txn-id
568 547 pattern = r'/!svn/txr/([^/]+)/'
569 548
570 549 # Search for the pattern in the URL
571 550 match = re.search(pattern, url)
572 551
573 552 # Check if a match is found and extract the captured group
574 553 if match:
575 554 txn_id = match.group(1)
576 555 return txn_id
577 556
578 557 @initialize_generator
579 558 def _generate_vcs_response(
580 559 self, environ, start_response, repo_path, extras, action):
581 560 """
582 561 Returns a generator for the response content.
583 562
584 563 This method is implemented as a generator, so that it can trigger
585 564 the cache validation after all content sent back to the client. It
586 565 also handles the locking exceptions which will be triggered when
587 566 the first chunk is produced by the underlying WSGI application.
588 567 """
589 568 svn_txn_id = ''
590 569 if action == 'push':
591 570 svn_txn_id = self._get_txn_id(environ)
592 571
593 572 callback_daemon, extras = self._prepare_callback_daemon(
594 573 extras, environ, action, txn_id=svn_txn_id)
595 574
596 575 if svn_txn_id:
597 576
598 577 port = safe_int(extras['hooks_uri'].split(':')[-1])
599 578 txn_id_data = extras.copy()
600 579 txn_id_data.update({'port': port})
601 580 txn_id_data.update({'req_method': environ['REQUEST_METHOD']})
602 581
603 582 full_repo_path = repo_path
604 583 store_txn_id_data(full_repo_path, svn_txn_id, txn_id_data)
605 584
606 585 log.debug('HOOKS extras is %s', extras)
607 586
608 587 http_scheme = self._get_http_scheme(environ)
609 588
610 589 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
611 590 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
612 591 with callback_daemon:
613 592 app.rc_extras = extras
614 593
615 594 try:
616 595 response = app(environ, start_response)
617 596 finally:
618 597 # This statement works together with the decorator
619 598 # "initialize_generator" above. The decorator ensures that
620 599 # we hit the first yield statement before the generator is
621 600 # returned back to the WSGI server. This is needed to
622 601 # ensure that the call to "app" above triggers the
623 602 # needed callback to "start_response" before the
624 603 # generator is actually used.
625 604 yield "__init__"
626 605
627 606 # iter content
628 607 for chunk in response:
629 608 yield chunk
630 609
631 610 try:
632 611 # invalidate cache on push
633 612 if action == 'push':
634 613 self._invalidate_cache(self.url_repo_name)
635 614 finally:
636 615 meta.Session.remove()
637 616
638 617 def _get_repository_name(self, environ):
639 618 """Get repository name out of the environmnent
640 619
641 620 :param environ: WSGI environment
642 621 """
643 622 raise NotImplementedError()
644 623
645 624 def _get_action(self, environ):
646 625 """Map request commands into a pull or push command.
647 626
648 627 :param environ: WSGI environment
649 628 """
650 629 raise NotImplementedError()
651 630
652 631 def _create_wsgi_app(self, repo_path, repo_name, config):
653 632 """Return the WSGI app that will finally handle the request."""
654 633 raise NotImplementedError()
655 634
656 635 def _create_config(self, extras, repo_name, scheme='http'):
657 636 """Create a safe config representation."""
658 637 raise NotImplementedError()
659 638
660 639 def _should_use_callback_daemon(self, extras, environ, action):
661 640 if extras.get('is_shadow_repo'):
662 641 # we don't want to execute hooks, and callback daemon for shadow repos
663 642 return False
664 643 return True
665 644
666 645 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
667 646 protocol = vcs_settings.HOOKS_PROTOCOL
668 647
669 648 if not self._should_use_callback_daemon(extras, environ, action):
670 649 # disable callback daemon for actions that don't require it
671 650 protocol = 'local'
672 651
673 652 return prepare_callback_daemon(
674 653 extras, protocol=protocol,
675 654 host=vcs_settings.HOOKS_HOST, txn_id=txn_id)
676 655
677 656
678 657 def _should_check_locking(query_string):
679 658 # this is kind of hacky, but due to how mercurial handles client-server
680 659 # server see all operation on commit; bookmarks, phases and
681 660 # obsolescence marker in different transaction, we don't want to check
682 661 # locking on those
683 662 return query_string not in ['cmd=listkeys']
@@ -1,827 +1,826 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Utilities library for RhodeCode
21 21 """
22 22
23 23 import datetime
24 24
25 25 import decorator
26 26 import logging
27 27 import os
28 28 import re
29 29 import sys
30 30 import shutil
31 31 import socket
32 32 import tempfile
33 33 import traceback
34 34 import tarfile
35 35
36 36 from functools import wraps
37 37 from os.path import join as jn
38 38
39 39 import paste
40 40 import pkg_resources
41 41 from webhelpers2.text import collapse, strip_tags, convert_accented_entities, convert_misc_entities
42 42
43 43 from mako import exceptions
44 44
45 45 from rhodecode.lib.hash_utils import sha256_safe, md5, sha1
46 46 from rhodecode.lib.type_utils import AttributeDict
47 47 from rhodecode.lib.str_utils import safe_bytes, safe_str
48 48 from rhodecode.lib.vcs.backends.base import Config
49 49 from rhodecode.lib.vcs.exceptions import VCSError
50 50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 51 from rhodecode.lib.ext_json import sjson as json
52 52 from rhodecode.model import meta
53 53 from rhodecode.model.db import (
54 54 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
55 55 from rhodecode.model.meta import Session
56 56
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
61 61
62 62 # String which contains characters that are not allowed in slug names for
63 63 # repositories or repository groups. It is properly escaped to use it in
64 64 # regular expressions.
65 65 SLUG_BAD_CHARS = re.escape(r'`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
66 66
67 67 # Regex that matches forbidden characters in repo/group slugs.
68 68 SLUG_BAD_CHAR_RE = re.compile(r'[{}\x00-\x08\x0b-\x0c\x0e-\x1f]'.format(SLUG_BAD_CHARS))
69 69
70 70 # Regex that matches allowed characters in repo/group slugs.
71 71 SLUG_GOOD_CHAR_RE = re.compile(r'[^{}]'.format(SLUG_BAD_CHARS))
72 72
73 73 # Regex that matches whole repo/group slugs.
74 74 SLUG_RE = re.compile(r'[^{}]+'.format(SLUG_BAD_CHARS))
75 75
76 76 _license_cache = None
77 77
78 78
79 79 def adopt_for_celery(func):
80 80 """
81 81 Decorator designed to adopt hooks (from rhodecode.lib.hooks_base)
82 82 for further usage as a celery tasks.
83 83 """
84 84 @wraps(func)
85 85 def wrapper(extras):
86 86 extras = AttributeDict(extras)
87 87 try:
88 88 # HooksResponse implements to_json method which must be used there.
89 89 return func(extras).to_json()
90 90 except Exception as e:
91 91 return {'status': 128, 'exception': type(e).__name__, 'exception_args': e.args}
92 92 return wrapper
93 93
94 94
95 95 def repo_name_slug(value):
96 96 """
97 97 Return slug of name of repository
98 98 This function is called on each creation/modification
99 99 of repository to prevent bad names in repo
100 100 """
101 101
102 102 replacement_char = '-'
103 103
104 104 slug = strip_tags(value)
105 105 slug = convert_accented_entities(slug)
106 106 slug = convert_misc_entities(slug)
107 107
108 108 slug = SLUG_BAD_CHAR_RE.sub('', slug)
109 109 slug = re.sub(r'[\s]+', '-', slug)
110 110 slug = collapse(slug, replacement_char)
111 111
112 112 return slug
113 113
114 114
115 115 #==============================================================================
116 116 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
117 117 #==============================================================================
118 118 def get_repo_slug(request):
119 119 _repo = ''
120 120
121 121 if hasattr(request, 'db_repo_name'):
122 122 # if our requests has set db reference use it for name, this
123 123 # translates the example.com/_<id> into proper repo names
124 124 _repo = request.db_repo_name
125 125 elif getattr(request, 'matchdict', None):
126 126 # pyramid
127 127 _repo = request.matchdict.get('repo_name')
128 128
129 129 if _repo:
130 130 _repo = _repo.rstrip('/')
131 131 return _repo
132 132
133 133
134 134 def get_repo_group_slug(request):
135 135 _group = ''
136 136 if hasattr(request, 'db_repo_group'):
137 137 # if our requests has set db reference use it for name, this
138 138 # translates the example.com/_<id> into proper repo group names
139 139 _group = request.db_repo_group.group_name
140 140 elif getattr(request, 'matchdict', None):
141 141 # pyramid
142 142 _group = request.matchdict.get('repo_group_name')
143 143
144 144 if _group:
145 145 _group = _group.rstrip('/')
146 146 return _group
147 147
148 148
149 149 def get_user_group_slug(request):
150 150 _user_group = ''
151 151
152 152 if hasattr(request, 'db_user_group'):
153 153 _user_group = request.db_user_group.users_group_name
154 154 elif getattr(request, 'matchdict', None):
155 155 # pyramid
156 156 _user_group = request.matchdict.get('user_group_id')
157 157 _user_group_name = request.matchdict.get('user_group_name')
158 158 try:
159 159 if _user_group:
160 160 _user_group = UserGroup.get(_user_group)
161 161 elif _user_group_name:
162 162 _user_group = UserGroup.get_by_group_name(_user_group_name)
163 163
164 164 if _user_group:
165 165 _user_group = _user_group.users_group_name
166 166 except Exception:
167 167 log.exception('Failed to get user group by id and name')
168 168 # catch all failures here
169 169 return None
170 170
171 171 return _user_group
172 172
173 173
174 174 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
175 175 """
176 176 Scans given path for repos and return (name,(type,path)) tuple
177 177
178 178 :param path: path to scan for repositories
179 179 :param recursive: recursive search and return names with subdirs in front
180 180 """
181 181
182 182 # remove ending slash for better results
183 183 path = path.rstrip(os.sep)
184 184 log.debug('now scanning in %s location recursive:%s...', path, recursive)
185 185
186 186 def _get_repos(p):
187 187 dirpaths = get_dirpaths(p)
188 188 if not _is_dir_writable(p):
189 189 log.warning('repo path without write access: %s', p)
190 190
191 191 for dirpath in dirpaths:
192 192 if os.path.isfile(os.path.join(p, dirpath)):
193 193 continue
194 194 cur_path = os.path.join(p, dirpath)
195 195
196 196 # skip removed repos
197 197 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
198 198 continue
199 199
200 200 #skip .<somethin> dirs
201 201 if dirpath.startswith('.'):
202 202 continue
203 203
204 204 try:
205 205 scm_info = get_scm(cur_path)
206 206 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
207 207 except VCSError:
208 208 if not recursive:
209 209 continue
210 210 #check if this dir containts other repos for recursive scan
211 211 rec_path = os.path.join(p, dirpath)
212 212 if os.path.isdir(rec_path):
213 213 yield from _get_repos(rec_path)
214 214
215 215 return _get_repos(path)
216 216
217 217
218 218 def get_dirpaths(p: str) -> list:
219 219 try:
220 220 # OS-independable way of checking if we have at least read-only
221 221 # access or not.
222 222 dirpaths = os.listdir(p)
223 223 except OSError:
224 224 log.warning('ignoring repo path without read access: %s', p)
225 225 return []
226 226
227 227 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
228 228 # decode paths and suddenly returns unicode objects itself. The items it
229 229 # cannot decode are returned as strings and cause issues.
230 230 #
231 231 # Those paths are ignored here until a solid solution for path handling has
232 232 # been built.
233 233 expected_type = type(p)
234 234
235 235 def _has_correct_type(item):
236 236 if type(item) is not expected_type:
237 237 log.error(
238 238 "Ignoring path %s since it cannot be decoded into str.",
239 239 # Using "repr" to make sure that we see the byte value in case
240 240 # of support.
241 241 repr(item))
242 242 return False
243 243 return True
244 244
245 245 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
246 246
247 247 return dirpaths
248 248
249 249
250 250 def _is_dir_writable(path):
251 251 """
252 252 Probe if `path` is writable.
253 253
254 254 Due to trouble on Cygwin / Windows, this is actually probing if it is
255 255 possible to create a file inside of `path`, stat does not produce reliable
256 256 results in this case.
257 257 """
258 258 try:
259 259 with tempfile.TemporaryFile(dir=path):
260 260 pass
261 261 except OSError:
262 262 return False
263 263 return True
264 264
265 265
266 266 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None, config=None):
267 267 """
268 268 Returns True if given path is a valid repository False otherwise.
269 269 If expect_scm param is given also, compare if given scm is the same
270 270 as expected from scm parameter. If explicit_scm is given don't try to
271 271 detect the scm, just use the given one to check if repo is valid
272 272
273 273 :param repo_name:
274 274 :param base_path:
275 275 :param expect_scm:
276 276 :param explicit_scm:
277 277 :param config:
278 278
279 279 :return True: if given path is a valid repository
280 280 """
281 281 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
282 282 log.debug('Checking if `%s` is a valid path for repository. '
283 283 'Explicit type: %s', repo_name, explicit_scm)
284 284
285 285 try:
286 286 if explicit_scm:
287 287 detected_scms = [get_scm_backend(explicit_scm)(
288 288 full_path, config=config).alias]
289 289 else:
290 290 detected_scms = get_scm(full_path)
291 291
292 292 if expect_scm:
293 293 return detected_scms[0] == expect_scm
294 294 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
295 295 return True
296 296 except VCSError:
297 297 log.debug('path: %s is not a valid repo !', full_path)
298 298 return False
299 299
300 300
301 301 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
302 302 """
303 303 Returns True if a given path is a repository group, False otherwise
304 304
305 305 :param repo_group_name:
306 306 :param base_path:
307 307 """
308 308 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
309 309 log.debug('Checking if `%s` is a valid path for repository group',
310 310 repo_group_name)
311 311
312 312 # check if it's not a repo
313 313 if is_valid_repo(repo_group_name, base_path):
314 314 log.debug('Repo called %s exist, it is not a valid repo group', repo_group_name)
315 315 return False
316 316
317 317 try:
318 318 # we need to check bare git repos at higher level
319 319 # since we might match branches/hooks/info/objects or possible
320 320 # other things inside bare git repo
321 321 maybe_repo = os.path.dirname(full_path)
322 322 if maybe_repo == base_path:
323 323 # skip root level repo check; we know root location CANNOT BE a repo group
324 324 return False
325 325
326 326 scm_ = get_scm(maybe_repo)
327 327 log.debug('path: %s is a vcs object:%s, not valid repo group', full_path, scm_)
328 328 return False
329 329 except VCSError:
330 330 pass
331 331
332 332 # check if it's a valid path
333 333 if skip_path_check or os.path.isdir(full_path):
334 334 log.debug('path: %s is a valid repo group !', full_path)
335 335 return True
336 336
337 337 log.debug('path: %s is not a valid repo group !', full_path)
338 338 return False
339 339
340 340
341 341 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
342 342 while True:
343 343 ok = input(prompt)
344 344 if ok.lower() in ('y', 'ye', 'yes'):
345 345 return True
346 346 if ok.lower() in ('n', 'no', 'nop', 'nope'):
347 347 return False
348 348 retries = retries - 1
349 349 if retries < 0:
350 350 raise OSError
351 351 print(complaint)
352 352
353 353 # propagated from mercurial documentation
354 354 ui_sections = [
355 355 'alias', 'auth',
356 356 'decode/encode', 'defaults',
357 357 'diff', 'email',
358 358 'extensions', 'format',
359 359 'merge-patterns', 'merge-tools',
360 360 'hooks', 'http_proxy',
361 361 'smtp', 'patch',
362 362 'paths', 'profiling',
363 363 'server', 'trusted',
364 364 'ui', 'web', ]
365 365
366 366
367 367 def config_data_from_db(clear_session=True, repo=None):
368 368 """
369 369 Read the configuration data from the database and return configuration
370 370 tuples.
371 371 """
372 372 from rhodecode.model.settings import VcsSettingsModel
373 373
374 374 config = []
375 375
376 376 sa = meta.Session()
377 377 settings_model = VcsSettingsModel(repo=repo, sa=sa)
378 378
379 379 ui_settings = settings_model.get_ui_settings()
380 380
381 381 ui_data = []
382 382 for setting in ui_settings:
383 383 if setting.active:
384 384 ui_data.append((setting.section, setting.key, setting.value))
385 385 config.append((
386 386 safe_str(setting.section), safe_str(setting.key),
387 387 safe_str(setting.value)))
388 388 if setting.key == 'push_ssl':
389 # force set push_ssl requirement to False, rhodecode
390 # handles that
389 # force set push_ssl requirement to False this is deprecated, and we must force it to False
391 390 config.append((
392 391 safe_str(setting.section), safe_str(setting.key), False))
393 392 log.debug(
394 393 'settings ui from db@repo[%s]: %s',
395 394 repo,
396 395 ','.join(['[{}] {}={}'.format(*s) for s in ui_data]))
397 396 if clear_session:
398 397 meta.Session.remove()
399 398
400 399 # TODO: mikhail: probably it makes no sense to re-read hooks information.
401 400 # It's already there and activated/deactivated
402 401 skip_entries = []
403 402 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
404 403 if 'pull' not in enabled_hook_classes:
405 404 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
406 405 if 'push' not in enabled_hook_classes:
407 406 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
408 407 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
409 408 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
410 409
411 410 config = [entry for entry in config if entry[:2] not in skip_entries]
412 411
413 412 return config
414 413
415 414
416 415 def make_db_config(clear_session=True, repo=None):
417 416 """
418 417 Create a :class:`Config` instance based on the values in the database.
419 418 """
420 419 config = Config()
421 420 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
422 421 for section, option, value in config_data:
423 422 config.set(section, option, value)
424 423 return config
425 424
426 425
427 426 def get_enabled_hook_classes(ui_settings):
428 427 """
429 428 Return the enabled hook classes.
430 429
431 430 :param ui_settings: List of ui_settings as returned
432 431 by :meth:`VcsSettingsModel.get_ui_settings`
433 432
434 433 :return: a list with the enabled hook classes. The order is not guaranteed.
435 434 :rtype: list
436 435 """
437 436 enabled_hooks = []
438 437 active_hook_keys = [
439 438 key for section, key, value, active in ui_settings
440 439 if section == 'hooks' and active]
441 440
442 441 hook_names = {
443 442 RhodeCodeUi.HOOK_PUSH: 'push',
444 443 RhodeCodeUi.HOOK_PULL: 'pull',
445 444 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
446 445 }
447 446
448 447 for key in active_hook_keys:
449 448 hook = hook_names.get(key)
450 449 if hook:
451 450 enabled_hooks.append(hook)
452 451
453 452 return enabled_hooks
454 453
455 454
456 455 def set_rhodecode_config(config):
457 456 """
458 457 Updates pyramid config with new settings from database
459 458
460 459 :param config:
461 460 """
462 461 from rhodecode.model.settings import SettingsModel
463 462 app_settings = SettingsModel().get_all_settings()
464 463
465 464 for k, v in list(app_settings.items()):
466 465 config[k] = v
467 466
468 467
469 468 def get_rhodecode_realm():
470 469 """
471 470 Return the rhodecode realm from database.
472 471 """
473 472 from rhodecode.model.settings import SettingsModel
474 473 realm = SettingsModel().get_setting_by_name('realm')
475 474 return safe_str(realm.app_settings_value)
476 475
477 476
478 477 def get_rhodecode_repo_store_path():
479 478 """
480 479 Returns the base path. The base path is the filesystem path which points
481 480 to the repository store.
482 481 """
483 482
484 483 import rhodecode
485 484 return rhodecode.CONFIG['repo_store.path']
486 485
487 486
488 487 def map_groups(path):
489 488 """
490 489 Given a full path to a repository, create all nested groups that this
491 490 repo is inside. This function creates parent-child relationships between
492 491 groups and creates default perms for all new groups.
493 492
494 493 :param paths: full path to repository
495 494 """
496 495 from rhodecode.model.repo_group import RepoGroupModel
497 496 sa = meta.Session()
498 497 groups = path.split(Repository.NAME_SEP)
499 498 parent = None
500 499 group = None
501 500
502 501 # last element is repo in nested groups structure
503 502 groups = groups[:-1]
504 503 rgm = RepoGroupModel(sa)
505 504 owner = User.get_first_super_admin()
506 505 for lvl, group_name in enumerate(groups):
507 506 group_name = '/'.join(groups[:lvl] + [group_name])
508 507 group = RepoGroup.get_by_group_name(group_name)
509 508 desc = '%s group' % group_name
510 509
511 510 # skip folders that are now removed repos
512 511 if REMOVED_REPO_PAT.match(group_name):
513 512 break
514 513
515 514 if group is None:
516 515 log.debug('creating group level: %s group_name: %s',
517 516 lvl, group_name)
518 517 group = RepoGroup(group_name, parent)
519 518 group.group_description = desc
520 519 group.user = owner
521 520 sa.add(group)
522 521 perm_obj = rgm._create_default_perms(group)
523 522 sa.add(perm_obj)
524 523 sa.flush()
525 524
526 525 parent = group
527 526 return group
528 527
529 528
530 529 def repo2db_mapper(initial_repo_list, remove_obsolete=False, force_hooks_rebuild=False):
531 530 """
532 531 maps all repos given in initial_repo_list, non existing repositories
533 532 are created, if remove_obsolete is True it also checks for db entries
534 533 that are not in initial_repo_list and removes them.
535 534
536 535 :param initial_repo_list: list of repositories found by scanning methods
537 536 :param remove_obsolete: check for obsolete entries in database
538 537 """
539 538 from rhodecode.model.repo import RepoModel
540 539 from rhodecode.model.repo_group import RepoGroupModel
541 540 from rhodecode.model.settings import SettingsModel
542 541
543 542 sa = meta.Session()
544 543 repo_model = RepoModel()
545 544 user = User.get_first_super_admin()
546 545 added = []
547 546
548 547 # creation defaults
549 548 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
550 549 enable_statistics = defs.get('repo_enable_statistics')
551 550 enable_locking = defs.get('repo_enable_locking')
552 551 enable_downloads = defs.get('repo_enable_downloads')
553 552 private = defs.get('repo_private')
554 553
555 554 for name, repo in list(initial_repo_list.items()):
556 555 group = map_groups(name)
557 556 str_name = safe_str(name)
558 557 db_repo = repo_model.get_by_repo_name(str_name)
559 558
560 559 # found repo that is on filesystem not in RhodeCode database
561 560 if not db_repo:
562 561 log.info('repository `%s` not found in the database, creating now', name)
563 562 added.append(name)
564 563 desc = (repo.description
565 564 if repo.description != 'unknown'
566 565 else '%s repository' % name)
567 566
568 567 db_repo = repo_model._create_repo(
569 568 repo_name=name,
570 569 repo_type=repo.alias,
571 570 description=desc,
572 571 repo_group=getattr(group, 'group_id', None),
573 572 owner=user,
574 573 enable_locking=enable_locking,
575 574 enable_downloads=enable_downloads,
576 575 enable_statistics=enable_statistics,
577 576 private=private,
578 577 state=Repository.STATE_CREATED
579 578 )
580 579 sa.commit()
581 580 # we added that repo just now, and make sure we updated server info
582 581 if db_repo.repo_type == 'git':
583 582 git_repo = db_repo.scm_instance()
584 583 # update repository server-info
585 584 log.debug('Running update server info')
586 585 git_repo._update_server_info(force=True)
587 586
588 587 db_repo.update_commit_cache(recursive=False)
589 588
590 589 config = db_repo._config
591 590 config.set('extensions', 'largefiles', '')
592 591 repo = db_repo.scm_instance(config=config)
593 592 repo.install_hooks(force=force_hooks_rebuild)
594 593
595 594 removed = []
596 595 if remove_obsolete:
597 596 # remove from database those repositories that are not in the filesystem
598 597 for repo in sa.query(Repository).all():
599 598 if repo.repo_name not in list(initial_repo_list.keys()):
600 599 log.debug("Removing non-existing repository found in db `%s`",
601 600 repo.repo_name)
602 601 try:
603 602 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
604 603 sa.commit()
605 604 removed.append(repo.repo_name)
606 605 except Exception:
607 606 # don't hold further removals on error
608 607 log.error(traceback.format_exc())
609 608 sa.rollback()
610 609
611 610 def splitter(full_repo_name):
612 611 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
613 612 gr_name = None
614 613 if len(_parts) == 2:
615 614 gr_name = _parts[0]
616 615 return gr_name
617 616
618 617 initial_repo_group_list = [splitter(x) for x in
619 618 list(initial_repo_list.keys()) if splitter(x)]
620 619
621 620 # remove from database those repository groups that are not in the
622 621 # filesystem due to parent child relationships we need to delete them
623 622 # in a specific order of most nested first
624 623 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
625 624 def nested_sort(gr):
626 625 return len(gr.split('/'))
627 626 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
628 627 if group_name not in initial_repo_group_list:
629 628 repo_group = RepoGroup.get_by_group_name(group_name)
630 629 if (repo_group.children.all() or
631 630 not RepoGroupModel().check_exist_filesystem(
632 631 group_name=group_name, exc_on_failure=False)):
633 632 continue
634 633
635 634 log.info(
636 635 'Removing non-existing repository group found in db `%s`',
637 636 group_name)
638 637 try:
639 638 RepoGroupModel(sa).delete(group_name, fs_remove=False)
640 639 sa.commit()
641 640 removed.append(group_name)
642 641 except Exception:
643 642 # don't hold further removals on error
644 643 log.exception(
645 644 'Unable to remove repository group `%s`',
646 645 group_name)
647 646 sa.rollback()
648 647 raise
649 648
650 649 return added, removed
651 650
652 651
653 652 def load_rcextensions(root_path):
654 653 import rhodecode
655 654 from rhodecode.config import conf
656 655
657 656 path = os.path.join(root_path)
658 657 sys.path.append(path)
659 658
660 659 try:
661 660 rcextensions = __import__('rcextensions')
662 661 except ImportError:
663 662 if os.path.isdir(os.path.join(path, 'rcextensions')):
664 663 log.warning('Unable to load rcextensions from %s', path)
665 664 rcextensions = None
666 665
667 666 if rcextensions:
668 667 log.info('Loaded rcextensions from %s...', rcextensions)
669 668 rhodecode.EXTENSIONS = rcextensions
670 669
671 670 # Additional mappings that are not present in the pygments lexers
672 671 conf.LANGUAGES_EXTENSIONS_MAP.update(
673 672 getattr(rhodecode.EXTENSIONS, 'EXTRA_MAPPINGS', {}))
674 673
675 674
676 675 def get_custom_lexer(extension):
677 676 """
678 677 returns a custom lexer if it is defined in rcextensions module, or None
679 678 if there's no custom lexer defined
680 679 """
681 680 import rhodecode
682 681 from pygments import lexers
683 682
684 683 # custom override made by RhodeCode
685 684 if extension in ['mako']:
686 685 return lexers.get_lexer_by_name('html+mako')
687 686
688 687 # check if we didn't define this extension as other lexer
689 688 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
690 689 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
691 690 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
692 691 return lexers.get_lexer_by_name(_lexer_name)
693 692
694 693
695 694 #==============================================================================
696 695 # TEST FUNCTIONS AND CREATORS
697 696 #==============================================================================
698 697 def create_test_index(repo_location, config):
699 698 """
700 699 Makes default test index.
701 700 """
702 701 try:
703 702 import rc_testdata
704 703 except ImportError:
705 704 raise ImportError('Failed to import rc_testdata, '
706 705 'please make sure this package is installed from requirements_test.txt')
707 706 rc_testdata.extract_search_index(
708 707 'vcs_search_index', os.path.dirname(config['search.location']))
709 708
710 709
711 710 def create_test_directory(test_path):
712 711 """
713 712 Create test directory if it doesn't exist.
714 713 """
715 714 if not os.path.isdir(test_path):
716 715 log.debug('Creating testdir %s', test_path)
717 716 os.makedirs(test_path)
718 717
719 718
720 719 def create_test_database(test_path, config):
721 720 """
722 721 Makes a fresh database.
723 722 """
724 723 from rhodecode.lib.db_manage import DbManage
725 724 from rhodecode.lib.utils2 import get_encryption_key
726 725
727 726 # PART ONE create db
728 727 dbconf = config['sqlalchemy.db1.url']
729 728 enc_key = get_encryption_key(config)
730 729
731 730 log.debug('making test db %s', dbconf)
732 731
733 732 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
734 733 tests=True, cli_args={'force_ask': True}, enc_key=enc_key)
735 734 dbmanage.create_tables(override=True)
736 735 dbmanage.set_db_version()
737 736 # for tests dynamically set new root paths based on generated content
738 737 dbmanage.create_settings(dbmanage.config_prompt(test_path))
739 738 dbmanage.create_default_user()
740 739 dbmanage.create_test_admin_and_users()
741 740 dbmanage.create_permissions()
742 741 dbmanage.populate_default_permissions()
743 742 Session().commit()
744 743
745 744
746 745 def create_test_repositories(test_path, config):
747 746 """
748 747 Creates test repositories in the temporary directory. Repositories are
749 748 extracted from archives within the rc_testdata package.
750 749 """
751 750 import rc_testdata
752 751 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
753 752
754 753 log.debug('making test vcs repositories')
755 754
756 755 idx_path = config['search.location']
757 756 data_path = config['cache_dir']
758 757
759 758 # clean index and data
760 759 if idx_path and os.path.exists(idx_path):
761 760 log.debug('remove %s', idx_path)
762 761 shutil.rmtree(idx_path)
763 762
764 763 if data_path and os.path.exists(data_path):
765 764 log.debug('remove %s', data_path)
766 765 shutil.rmtree(data_path)
767 766
768 767 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
769 768 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
770 769
771 770 # Note: Subversion is in the process of being integrated with the system,
772 771 # until we have a properly packed version of the test svn repository, this
773 772 # tries to copy over the repo from a package "rc_testdata"
774 773 svn_repo_path = rc_testdata.get_svn_repo_archive()
775 774 with tarfile.open(svn_repo_path) as tar:
776 775 tar.extractall(jn(test_path, SVN_REPO))
777 776
778 777
779 778 def password_changed(auth_user, session):
780 779 # Never report password change in case of default user or anonymous user.
781 780 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
782 781 return False
783 782
784 783 password_hash = md5(safe_bytes(auth_user.password)) if auth_user.password else None
785 784 rhodecode_user = session.get('rhodecode_user', {})
786 785 session_password_hash = rhodecode_user.get('password', '')
787 786 return password_hash != session_password_hash
788 787
789 788
790 789 def read_opensource_licenses():
791 790 global _license_cache
792 791
793 792 if not _license_cache:
794 793 licenses = pkg_resources.resource_string(
795 794 'rhodecode', 'config/licenses.json')
796 795 _license_cache = json.loads(licenses)
797 796
798 797 return _license_cache
799 798
800 799
801 800 def generate_platform_uuid():
802 801 """
803 802 Generates platform UUID based on it's name
804 803 """
805 804 import platform
806 805
807 806 try:
808 807 uuid_list = [platform.platform()]
809 808 return sha256_safe(':'.join(uuid_list))
810 809 except Exception as e:
811 810 log.error('Failed to generate host uuid: %s', e)
812 811 return 'UNDEFINED'
813 812
814 813
815 814 def send_test_email(recipients, email_body='TEST EMAIL'):
816 815 """
817 816 Simple code for generating test emails.
818 817 Usage::
819 818
820 819 from rhodecode.lib import utils
821 820 utils.send_test_email()
822 821 """
823 822 from rhodecode.lib.celerylib import tasks, run_task
824 823
825 824 email_body = email_body_plaintext = email_body
826 825 subject = f'SUBJECT FROM: {socket.gethostname()}'
827 826 tasks.send_email(recipients, subject, email_body_plaintext, email_body)
@@ -1,669 +1,668 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 this is forms validation classes
21 21 http://formencode.org/module-formencode.validators.html
22 22 for list off all availible validators
23 23
24 24 we can create our own validators
25 25
26 26 The table below outlines the options which can be used in a schema in addition to the validators themselves
27 27 pre_validators [] These validators will be applied before the schema
28 28 chained_validators [] These validators will be applied after the schema
29 29 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
30 30 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
31 31 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
32 32 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
33 33
34 34
35 35 <name> = formencode.validators.<name of validator>
36 36 <name> must equal form name
37 37 list=[1,2,3,4,5]
38 38 for SELECT use formencode.All(OneOf(list), Int())
39 39
40 40 """
41 41
42 42 import deform
43 43 import logging
44 44 import formencode
45 45
46 46 from pkg_resources import resource_filename
47 47 from formencode import All, Pipe
48 48
49 49 from pyramid.threadlocal import get_current_request
50 50
51 51 from rhodecode import BACKENDS
52 52 from rhodecode.lib import helpers
53 53 from rhodecode.model import validators as v
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 deform_templates = resource_filename('deform', 'templates')
59 59 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
60 60 search_path = (rhodecode_templates, deform_templates)
61 61
62 62
63 63 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
64 64 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
65 65 def __call__(self, template_name, **kw):
66 66 kw['h'] = helpers
67 67 kw['request'] = get_current_request()
68 68 return self.load(template_name)(**kw)
69 69
70 70
71 71 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 72 deform.Form.set_default_renderer(form_renderer)
73 73
74 74
75 75 def LoginForm(localizer):
76 76 _ = localizer
77 77
78 78 class _LoginForm(formencode.Schema):
79 79 allow_extra_fields = True
80 80 filter_extra_fields = True
81 81 username = v.UnicodeString(
82 82 strip=True,
83 83 min=1,
84 84 not_empty=True,
85 85 messages={
86 86 'empty': _('Please enter a login'),
87 87 'tooShort': _('Enter a value %(min)i characters long or more')
88 88 }
89 89 )
90 90
91 91 password = v.UnicodeString(
92 92 strip=False,
93 93 min=3,
94 94 max=72,
95 95 not_empty=True,
96 96 messages={
97 97 'empty': _('Please enter a password'),
98 98 'tooShort': _('Enter %(min)i characters or more')}
99 99 )
100 100
101 101 remember = v.StringBoolean(if_missing=False)
102 102
103 103 chained_validators = [v.ValidAuth(localizer)]
104 104 return _LoginForm
105 105
106 106
107 107 def TOTPForm(localizer, user, allow_recovery_code_use=False):
108 108 _ = localizer
109 109
110 110 class _TOTPForm(formencode.Schema):
111 111 allow_extra_fields = True
112 112 filter_extra_fields = False
113 113 totp = v.Regex(r'^(?:\d{6}|[A-Z0-9]{32})$')
114 114 secret_totp = v.String()
115 115
116 116 def to_python(self, value, state=None):
117 117 validation_checks = [user.is_totp_valid]
118 118 if allow_recovery_code_use:
119 119 validation_checks.append(user.is_2fa_recovery_code_valid)
120 120 form_data = super().to_python(value, state)
121 121 received_code = form_data['totp']
122 122 secret = form_data.get('secret_totp')
123 123
124 124 if not any(map(lambda func: func(received_code, secret), validation_checks)):
125 125 error_msg = _('Code is invalid. Try again!')
126 126 raise formencode.Invalid(error_msg, v, state, error_dict={'totp': error_msg})
127 127 return form_data
128 128
129 129 return _TOTPForm
130 130
131 131
132 132 def WhitelistedVcsClientsForm(localizer):
133 133 _ = localizer
134 134
135 135 class _WhitelistedVcsClientsForm(formencode.Schema):
136 136 regexp = r'^(?:\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\*)\s*(?:,\s*[<>=~^!]*\s*\d{1,2}\.\d{1,2}(?:\.\d{1,2})?\s*|\s*\*\s*)*$'
137 137 allow_extra_fields = True
138 138 filter_extra_fields = True
139 139 git = v.Regex(regexp)
140 140 hg = v.Regex(regexp)
141 141 svn = v.Regex(regexp)
142 142
143 143 return _WhitelistedVcsClientsForm
144 144
145 145
146 146 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
147 147 old_data = old_data or {}
148 148 available_languages = available_languages or []
149 149 _ = localizer
150 150
151 151 class _UserForm(formencode.Schema):
152 152 allow_extra_fields = True
153 153 filter_extra_fields = True
154 154 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
155 155 v.ValidUsername(localizer, edit, old_data))
156 156 if edit:
157 157 new_password = All(
158 158 v.ValidPassword(localizer),
159 159 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
160 160 )
161 161 password_confirmation = All(
162 162 v.ValidPassword(localizer),
163 163 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
164 164 )
165 165 admin = v.StringBoolean(if_missing=False)
166 166 else:
167 167 password = All(
168 168 v.ValidPassword(localizer),
169 169 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
170 170 )
171 171 password_confirmation = All(
172 172 v.ValidPassword(localizer),
173 173 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
174 174 )
175 175
176 176 password_change = v.StringBoolean(if_missing=False)
177 177 create_repo_group = v.StringBoolean(if_missing=False)
178 178
179 179 active = v.StringBoolean(if_missing=False)
180 180 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
181 181 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
182 182 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
183 183 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
184 184 if_missing='')
185 185 extern_name = v.UnicodeString(strip=True)
186 186 extern_type = v.UnicodeString(strip=True)
187 187 language = v.OneOf(available_languages, hideList=False,
188 188 testValueList=True, if_missing=None)
189 189 chained_validators = [v.ValidPasswordsMatch(localizer)]
190 190 return _UserForm
191 191
192 192
193 193 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
194 194 old_data = old_data or {}
195 195 _ = localizer
196 196
197 197 class _UserGroupForm(formencode.Schema):
198 198 allow_extra_fields = True
199 199 filter_extra_fields = True
200 200
201 201 users_group_name = All(
202 202 v.UnicodeString(strip=True, min=1, not_empty=True),
203 203 v.ValidUserGroup(localizer, edit, old_data)
204 204 )
205 205 user_group_description = v.UnicodeString(strip=True, min=1,
206 206 not_empty=False)
207 207
208 208 users_group_active = v.StringBoolean(if_missing=False)
209 209
210 210 if edit:
211 211 # this is user group owner
212 212 user = All(
213 213 v.UnicodeString(not_empty=True),
214 214 v.ValidRepoUser(localizer, allow_disabled))
215 215 return _UserGroupForm
216 216
217 217
218 218 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
219 219 can_create_in_root=False, allow_disabled=False):
220 220 _ = localizer
221 221 old_data = old_data or {}
222 222 available_groups = available_groups or []
223 223
224 224 class _RepoGroupForm(formencode.Schema):
225 225 allow_extra_fields = True
226 226 filter_extra_fields = False
227 227
228 228 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
229 229 v.SlugifyName(localizer),)
230 230 group_description = v.UnicodeString(strip=True, min=1,
231 231 not_empty=False)
232 232 group_copy_permissions = v.StringBoolean(if_missing=False)
233 233
234 234 group_parent_id = v.OneOf(available_groups, hideList=False,
235 235 testValueList=True, not_empty=True)
236 236 enable_locking = v.StringBoolean(if_missing=False)
237 237 chained_validators = [
238 238 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
239 239
240 240 if edit:
241 241 # this is repo group owner
242 242 user = All(
243 243 v.UnicodeString(not_empty=True),
244 244 v.ValidRepoUser(localizer, allow_disabled))
245 245 return _RepoGroupForm
246 246
247 247
248 248 def RegisterForm(localizer, edit=False, old_data=None):
249 249 _ = localizer
250 250 old_data = old_data or {}
251 251
252 252 class _RegisterForm(formencode.Schema):
253 253 allow_extra_fields = True
254 254 filter_extra_fields = True
255 255 username = All(
256 256 v.ValidUsername(localizer, edit, old_data),
257 257 v.UnicodeString(strip=True, min=1, not_empty=True)
258 258 )
259 259 password = All(
260 260 v.ValidPassword(localizer),
261 261 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
262 262 )
263 263 password_confirmation = All(
264 264 v.ValidPassword(localizer),
265 265 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
266 266 )
267 267 active = v.StringBoolean(if_missing=False)
268 268 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
269 269 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
270 270 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
271 271
272 272 chained_validators = [v.ValidPasswordsMatch(localizer)]
273 273 return _RegisterForm
274 274
275 275
276 276 def PasswordResetForm(localizer):
277 277 _ = localizer
278 278
279 279 class _PasswordResetForm(formencode.Schema):
280 280 allow_extra_fields = True
281 281 filter_extra_fields = True
282 282 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
283 283 return _PasswordResetForm
284 284
285 285
286 286 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
287 287 _ = localizer
288 288 old_data = old_data or {}
289 289 repo_groups = repo_groups or []
290 290 supported_backends = BACKENDS.keys()
291 291
292 292 class _RepoForm(formencode.Schema):
293 293 allow_extra_fields = True
294 294 filter_extra_fields = False
295 295 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
296 296 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
297 297 repo_group = All(v.CanWriteGroup(localizer, old_data),
298 298 v.OneOf(repo_groups, hideList=True))
299 299 repo_type = v.OneOf(supported_backends, required=False,
300 300 if_missing=old_data.get('repo_type'))
301 301 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
302 302 repo_private = v.StringBoolean(if_missing=False)
303 303 repo_copy_permissions = v.StringBoolean(if_missing=False)
304 304 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
305 305
306 306 repo_enable_statistics = v.StringBoolean(if_missing=False)
307 307 repo_enable_downloads = v.StringBoolean(if_missing=False)
308 308 repo_enable_locking = v.StringBoolean(if_missing=False)
309 309
310 310 if edit:
311 311 # this is repo owner
312 312 user = All(
313 313 v.UnicodeString(not_empty=True),
314 314 v.ValidRepoUser(localizer, allow_disabled))
315 315 clone_uri_change = v.UnicodeString(
316 316 not_empty=False, if_missing=v.Missing)
317 317
318 318 chained_validators = [v.ValidCloneUri(localizer),
319 319 v.ValidRepoName(localizer, edit, old_data)]
320 320 return _RepoForm
321 321
322 322
323 323 def RepoPermsForm(localizer):
324 324 _ = localizer
325 325
326 326 class _RepoPermsForm(formencode.Schema):
327 327 allow_extra_fields = True
328 328 filter_extra_fields = False
329 329 chained_validators = [v.ValidPerms(localizer, type_='repo')]
330 330 return _RepoPermsForm
331 331
332 332
333 333 def RepoGroupPermsForm(localizer, valid_recursive_choices):
334 334 _ = localizer
335 335
336 336 class _RepoGroupPermsForm(formencode.Schema):
337 337 allow_extra_fields = True
338 338 filter_extra_fields = False
339 339 recursive = v.OneOf(valid_recursive_choices)
340 340 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
341 341 return _RepoGroupPermsForm
342 342
343 343
344 344 def UserGroupPermsForm(localizer):
345 345 _ = localizer
346 346
347 347 class _UserPermsForm(formencode.Schema):
348 348 allow_extra_fields = True
349 349 filter_extra_fields = False
350 350 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
351 351 return _UserPermsForm
352 352
353 353
354 354 def RepoFieldForm(localizer):
355 355 _ = localizer
356 356
357 357 class _RepoFieldForm(formencode.Schema):
358 358 filter_extra_fields = True
359 359 allow_extra_fields = True
360 360
361 361 new_field_key = All(v.FieldKey(localizer),
362 362 v.UnicodeString(strip=True, min=3, not_empty=True))
363 363 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
364 364 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
365 365 if_missing='str')
366 366 new_field_label = v.UnicodeString(not_empty=False)
367 367 new_field_desc = v.UnicodeString(not_empty=False)
368 368 return _RepoFieldForm
369 369
370 370
371 371 def RepoForkForm(localizer, edit=False, old_data=None,
372 372 supported_backends=BACKENDS.keys(), repo_groups=None):
373 373 _ = localizer
374 374 old_data = old_data or {}
375 375 repo_groups = repo_groups or []
376 376
377 377 class _RepoForkForm(formencode.Schema):
378 378 allow_extra_fields = True
379 379 filter_extra_fields = False
380 380 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
381 381 v.SlugifyName(localizer))
382 382 repo_group = All(v.CanWriteGroup(localizer, ),
383 383 v.OneOf(repo_groups, hideList=True))
384 384 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
385 385 description = v.UnicodeString(strip=True, min=1, not_empty=True)
386 386 private = v.StringBoolean(if_missing=False)
387 387 copy_permissions = v.StringBoolean(if_missing=False)
388 388 fork_parent_id = v.UnicodeString()
389 389 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
390 390 return _RepoForkForm
391 391
392 392
393 393 def ApplicationSettingsForm(localizer):
394 394 _ = localizer
395 395
396 396 class _ApplicationSettingsForm(formencode.Schema):
397 397 allow_extra_fields = True
398 398 filter_extra_fields = False
399 399 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
400 400 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
401 401 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
402 402 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
403 403 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
404 404 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
405 405 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
406 406 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
407 407 return _ApplicationSettingsForm
408 408
409 409
410 410 def ApplicationVisualisationForm(localizer):
411 411 from rhodecode.model.db import Repository
412 412 _ = localizer
413 413
414 414 class _ApplicationVisualisationForm(formencode.Schema):
415 415 allow_extra_fields = True
416 416 filter_extra_fields = False
417 417 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
418 418 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
419 419 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
420 420
421 421 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
422 422 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
423 423 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
424 424 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
425 425 rhodecode_show_version = v.StringBoolean(if_missing=False)
426 426 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
427 427 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
428 428 rhodecode_gravatar_url = v.UnicodeString(min=3)
429 429 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
430 430 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
431 431 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
432 432 rhodecode_support_url = v.UnicodeString()
433 433 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
434 434 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
435 435 return _ApplicationVisualisationForm
436 436
437 437
438 438 class _BaseVcsSettingsForm(formencode.Schema):
439 439
440 440 allow_extra_fields = True
441 441 filter_extra_fields = False
442 442 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
443 443 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
444 444 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
445 445
446 446 # PR/Code-review
447 447 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
448 448 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
449 449
450 450 # hg
451 451 extensions_largefiles = v.StringBoolean(if_missing=False)
452 452 extensions_evolve = v.StringBoolean(if_missing=False)
453 453 phases_publish = v.StringBoolean(if_missing=False)
454 454
455 455 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
456 456 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
457 457
458 458 # git
459 459 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
460 460 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
461 461 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
462 462
463 463 # cache
464 464 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
465 465
466 466
467 467 def ApplicationUiSettingsForm(localizer):
468 468 _ = localizer
469 469
470 470 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
471 web_push_ssl = v.StringBoolean(if_missing=False)
472 471 largefiles_usercache = All(
473 472 v.ValidPath(localizer),
474 473 v.UnicodeString(strip=True, min=2, not_empty=True))
475 474 vcs_git_lfs_store_location = All(
476 475 v.ValidPath(localizer),
477 476 v.UnicodeString(strip=True, min=2, not_empty=True))
478 477 extensions_hggit = v.StringBoolean(if_missing=False)
479 478 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
480 479 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
481 480 return _ApplicationUiSettingsForm
482 481
483 482
484 483 def RepoVcsSettingsForm(localizer, repo_name):
485 484 _ = localizer
486 485
487 486 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
488 487 inherit_global_settings = v.StringBoolean(if_missing=False)
489 488 new_svn_branch = v.ValidSvnPattern(localizer,
490 489 section='vcs_svn_branch', repo_name=repo_name)
491 490 new_svn_tag = v.ValidSvnPattern(localizer,
492 491 section='vcs_svn_tag', repo_name=repo_name)
493 492 return _RepoVcsSettingsForm
494 493
495 494
496 495 def LabsSettingsForm(localizer):
497 496 _ = localizer
498 497
499 498 class _LabSettingsForm(formencode.Schema):
500 499 allow_extra_fields = True
501 500 filter_extra_fields = False
502 501 return _LabSettingsForm
503 502
504 503
505 504 def ApplicationPermissionsForm(
506 505 localizer, register_choices, password_reset_choices,
507 506 extern_activate_choices):
508 507 _ = localizer
509 508
510 509 class _DefaultPermissionsForm(formencode.Schema):
511 510 allow_extra_fields = True
512 511 filter_extra_fields = True
513 512
514 513 anonymous = v.StringBoolean(if_missing=False)
515 514 default_register = v.OneOf(register_choices)
516 515 default_register_message = v.UnicodeString()
517 516 default_password_reset = v.OneOf(password_reset_choices)
518 517 default_extern_activate = v.OneOf(extern_activate_choices)
519 518 return _DefaultPermissionsForm
520 519
521 520
522 521 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
523 522 user_group_perms_choices):
524 523 _ = localizer
525 524
526 525 class _ObjectPermissionsForm(formencode.Schema):
527 526 allow_extra_fields = True
528 527 filter_extra_fields = True
529 528 overwrite_default_repo = v.StringBoolean(if_missing=False)
530 529 overwrite_default_group = v.StringBoolean(if_missing=False)
531 530 overwrite_default_user_group = v.StringBoolean(if_missing=False)
532 531
533 532 default_repo_perm = v.OneOf(repo_perms_choices)
534 533 default_group_perm = v.OneOf(group_perms_choices)
535 534 default_user_group_perm = v.OneOf(user_group_perms_choices)
536 535
537 536 return _ObjectPermissionsForm
538 537
539 538
540 539 def BranchPermissionsForm(localizer, branch_perms_choices):
541 540 _ = localizer
542 541
543 542 class _BranchPermissionsForm(formencode.Schema):
544 543 allow_extra_fields = True
545 544 filter_extra_fields = True
546 545 overwrite_default_branch = v.StringBoolean(if_missing=False)
547 546 default_branch_perm = v.OneOf(branch_perms_choices)
548 547
549 548 return _BranchPermissionsForm
550 549
551 550
552 551 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
553 552 repo_group_create_choices, user_group_create_choices,
554 553 fork_choices, inherit_default_permissions_choices):
555 554 _ = localizer
556 555
557 556 class _DefaultPermissionsForm(formencode.Schema):
558 557 allow_extra_fields = True
559 558 filter_extra_fields = True
560 559
561 560 anonymous = v.StringBoolean(if_missing=False)
562 561
563 562 default_repo_create = v.OneOf(create_choices)
564 563 default_repo_create_on_write = v.OneOf(create_on_write_choices)
565 564 default_user_group_create = v.OneOf(user_group_create_choices)
566 565 default_repo_group_create = v.OneOf(repo_group_create_choices)
567 566 default_fork_create = v.OneOf(fork_choices)
568 567 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
569 568 return _DefaultPermissionsForm
570 569
571 570
572 571 def UserIndividualPermissionsForm(localizer):
573 572 _ = localizer
574 573
575 574 class _DefaultPermissionsForm(formencode.Schema):
576 575 allow_extra_fields = True
577 576 filter_extra_fields = True
578 577
579 578 inherit_default_permissions = v.StringBoolean(if_missing=False)
580 579 return _DefaultPermissionsForm
581 580
582 581
583 582 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
584 583 _ = localizer
585 584 old_data = old_data or {}
586 585
587 586 class _DefaultsForm(formencode.Schema):
588 587 allow_extra_fields = True
589 588 filter_extra_fields = True
590 589 default_repo_type = v.OneOf(supported_backends)
591 590 default_repo_private = v.StringBoolean(if_missing=False)
592 591 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
593 592 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
594 593 default_repo_enable_locking = v.StringBoolean(if_missing=False)
595 594 return _DefaultsForm
596 595
597 596
598 597 def AuthSettingsForm(localizer):
599 598 _ = localizer
600 599
601 600 class _AuthSettingsForm(formencode.Schema):
602 601 allow_extra_fields = True
603 602 filter_extra_fields = True
604 603 auth_plugins = All(v.ValidAuthPlugins(localizer),
605 604 v.UniqueListFromString(localizer)(not_empty=True))
606 605 return _AuthSettingsForm
607 606
608 607
609 608 def UserExtraEmailForm(localizer):
610 609 _ = localizer
611 610
612 611 class _UserExtraEmailForm(formencode.Schema):
613 612 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
614 613 return _UserExtraEmailForm
615 614
616 615
617 616 def UserExtraIpForm(localizer):
618 617 _ = localizer
619 618
620 619 class _UserExtraIpForm(formencode.Schema):
621 620 ip = v.ValidIp(localizer)(not_empty=True)
622 621 return _UserExtraIpForm
623 622
624 623
625 624 def PullRequestForm(localizer, repo_id):
626 625 _ = localizer
627 626
628 627 class ReviewerForm(formencode.Schema):
629 628 user_id = v.Int(not_empty=True)
630 629 reasons = All()
631 630 rules = All(v.UniqueList(localizer, convert=int)())
632 631 mandatory = v.StringBoolean()
633 632 role = v.String(if_missing='reviewer')
634 633
635 634 class ObserverForm(formencode.Schema):
636 635 user_id = v.Int(not_empty=True)
637 636 reasons = All()
638 637 rules = All(v.UniqueList(localizer, convert=int)())
639 638 mandatory = v.StringBoolean()
640 639 role = v.String(if_missing='observer')
641 640
642 641 class _PullRequestForm(formencode.Schema):
643 642 allow_extra_fields = True
644 643 filter_extra_fields = True
645 644
646 645 common_ancestor = v.UnicodeString(strip=True, required=True)
647 646 source_repo = v.UnicodeString(strip=True, required=True)
648 647 source_ref = v.UnicodeString(strip=True, required=True)
649 648 target_repo = v.UnicodeString(strip=True, required=True)
650 649 target_ref = v.UnicodeString(strip=True, required=True)
651 650 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
652 651 v.UniqueList(localizer)(not_empty=True))
653 652 review_members = formencode.ForEach(ReviewerForm())
654 653 observer_members = formencode.ForEach(ObserverForm())
655 654 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
656 655 pullrequest_desc = v.UnicodeString(strip=True, required=False)
657 656 description_renderer = v.UnicodeString(strip=True, required=False)
658 657
659 658 return _PullRequestForm
660 659
661 660
662 661 def IssueTrackerPatternsForm(localizer):
663 662 _ = localizer
664 663
665 664 class _IssueTrackerPatternsForm(formencode.Schema):
666 665 allow_extra_fields = True
667 666 filter_extra_fields = False
668 667 chained_validators = [v.ValidPattern(localizer)]
669 668 return _IssueTrackerPatternsForm
@@ -1,902 +1,897 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import re
21 21 import logging
22 22 import time
23 23 import functools
24 24 from collections import namedtuple
25 25
26 26 from pyramid.threadlocal import get_current_request
27 27
28 28 from rhodecode.lib import rc_cache
29 29 from rhodecode.lib.hash_utils import sha1_safe
30 30 from rhodecode.lib.html_filters import sanitize_html
31 31 from rhodecode.lib.utils2 import (
32 32 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
33 33 from rhodecode.lib.vcs.backends import base
34 34 from rhodecode.lib.statsd_client import StatsdClient
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import (
37 37 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
38 38 from rhodecode.model.meta import Session
39 39
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 UiSetting = namedtuple(
45 45 'UiSetting', ['section', 'key', 'value', 'active'])
46 46
47 47 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
48 48
49 49
50 50 class SettingNotFound(Exception):
51 51 def __init__(self, setting_id):
52 52 msg = f'Setting `{setting_id}` is not found'
53 53 super().__init__(msg)
54 54
55 55
56 56 class SettingsModel(BaseModel):
57 57 BUILTIN_HOOKS = (
58 58 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
59 59 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
60 60 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
61 61 RhodeCodeUi.HOOK_PUSH_KEY,)
62 62 HOOKS_SECTION = 'hooks'
63 63
64 64 def __init__(self, sa=None, repo=None):
65 65 self.repo = repo
66 66 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
67 67 self.SettingsDbModel = (
68 68 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
69 69 super().__init__(sa)
70 70
71 71 def get_keyname(self, key_name, prefix='rhodecode_'):
72 72 return f'{prefix}{key_name}'
73 73
74 74 def get_ui_by_key(self, key):
75 75 q = self.UiDbModel.query()
76 76 q = q.filter(self.UiDbModel.ui_key == key)
77 77 q = self._filter_by_repo(RepoRhodeCodeUi, q)
78 78 return q.scalar()
79 79
80 80 def get_ui_by_section(self, section):
81 81 q = self.UiDbModel.query()
82 82 q = q.filter(self.UiDbModel.ui_section == section)
83 83 q = self._filter_by_repo(RepoRhodeCodeUi, q)
84 84 return q.all()
85 85
86 86 def get_ui_by_section_and_key(self, section, key):
87 87 q = self.UiDbModel.query()
88 88 q = q.filter(self.UiDbModel.ui_section == section)
89 89 q = q.filter(self.UiDbModel.ui_key == key)
90 90 q = self._filter_by_repo(RepoRhodeCodeUi, q)
91 91 return q.scalar()
92 92
93 93 def get_ui(self, section=None, key=None):
94 94 q = self.UiDbModel.query()
95 95 q = self._filter_by_repo(RepoRhodeCodeUi, q)
96 96
97 97 if section:
98 98 q = q.filter(self.UiDbModel.ui_section == section)
99 99 if key:
100 100 q = q.filter(self.UiDbModel.ui_key == key)
101 101
102 102 # TODO: mikhail: add caching
103 103 result = [
104 104 UiSetting(
105 105 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
106 106 value=safe_str(r.ui_value), active=r.ui_active
107 107 )
108 108 for r in q.all()
109 109 ]
110 110 return result
111 111
112 112 def get_builtin_hooks(self):
113 113 q = self.UiDbModel.query()
114 114 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
115 115 return self._get_hooks(q)
116 116
117 117 def get_custom_hooks(self):
118 118 q = self.UiDbModel.query()
119 119 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
120 120 return self._get_hooks(q)
121 121
122 122 def create_ui_section_value(self, section, val, key=None, active=True):
123 123 new_ui = self.UiDbModel()
124 124 new_ui.ui_section = section
125 125 new_ui.ui_value = val
126 126 new_ui.ui_active = active
127 127
128 128 repository_id = ''
129 129 if self.repo:
130 130 repo = self._get_repo(self.repo)
131 131 repository_id = repo.repo_id
132 132 new_ui.repository_id = repository_id
133 133
134 134 if not key:
135 135 # keys are unique so they need appended info
136 136 if self.repo:
137 137 key = sha1_safe(f'{section}{val}{repository_id}')
138 138 else:
139 139 key = sha1_safe(f'{section}{val}')
140 140
141 141 new_ui.ui_key = key
142 142
143 143 Session().add(new_ui)
144 144 return new_ui
145 145
146 146 def create_or_update_hook(self, key, value):
147 147 ui = (
148 148 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
149 149 self.UiDbModel())
150 150 ui.ui_section = self.HOOKS_SECTION
151 151 ui.ui_active = True
152 152 ui.ui_key = key
153 153 ui.ui_value = value
154 154
155 155 if self.repo:
156 156 repo = self._get_repo(self.repo)
157 157 repository_id = repo.repo_id
158 158 ui.repository_id = repository_id
159 159
160 160 Session().add(ui)
161 161 return ui
162 162
163 163 def delete_ui(self, id_):
164 164 ui = self.UiDbModel.get(id_)
165 165 if not ui:
166 166 raise SettingNotFound(id_)
167 167 Session().delete(ui)
168 168
169 169 def get_setting_by_name(self, name):
170 170 q = self._get_settings_query()
171 171 q = q.filter(self.SettingsDbModel.app_settings_name == name)
172 172 return q.scalar()
173 173
174 174 def create_or_update_setting(
175 175 self, name, val: Optional | str = Optional(''), type_: Optional | str = Optional('unicode')):
176 176 """
177 177 Creates or updates RhodeCode setting. If updates are triggered, it will
178 178 only update parameters that are explicitly set Optional instance will
179 179 be skipped
180 180
181 181 :param name:
182 182 :param val:
183 183 :param type_:
184 184 :return:
185 185 """
186 186
187 187 res = self.get_setting_by_name(name)
188 188 repo = self._get_repo(self.repo) if self.repo else None
189 189
190 190 if not res:
191 191 val = Optional.extract(val)
192 192 type_ = Optional.extract(type_)
193 193
194 194 args = (
195 195 (repo.repo_id, name, val, type_)
196 196 if repo else (name, val, type_))
197 197 res = self.SettingsDbModel(*args)
198 198
199 199 else:
200 200 if self.repo:
201 201 res.repository_id = repo.repo_id
202 202
203 203 res.app_settings_name = name
204 204 if not isinstance(type_, Optional):
205 205 # update if set
206 206 res.app_settings_type = type_
207 207 if not isinstance(val, Optional):
208 208 # update if set
209 209 res.app_settings_value = val
210 210
211 211 Session().add(res)
212 212 return res
213 213
214 214 def get_cache_region(self):
215 215 repo = self._get_repo(self.repo) if self.repo else None
216 216 cache_key = f"repo.v1.{repo.repo_id}" if repo else "repo.v1.ALL"
217 217 cache_namespace_uid = f'cache_settings.{cache_key}'
218 218 region = rc_cache.get_or_create_region('cache_general', cache_namespace_uid)
219 219 return region, cache_namespace_uid
220 220
221 221 def invalidate_settings_cache(self, hard=False):
222 222 region, namespace_key = self.get_cache_region()
223 223 log.debug('Invalidation cache [%s] region %s for cache_key: %s',
224 224 'invalidate_settings_cache', region, namespace_key)
225 225
226 226 # we use hard cleanup if invalidation is sent
227 227 rc_cache.clear_cache_namespace(region, namespace_key, method=rc_cache.CLEAR_DELETE)
228 228
229 229 def get_cache_call_method(self, cache=True):
230 230 region, cache_key = self.get_cache_region()
231 231
232 232 @region.conditional_cache_on_arguments(condition=cache)
233 233 def _get_all_settings(name, key):
234 234 q = self._get_settings_query()
235 235 if not q:
236 236 raise Exception('Could not get application settings !')
237 237
238 238 settings = {
239 239 self.get_keyname(res.app_settings_name): res.app_settings_value
240 240 for res in q
241 241 }
242 242 return settings
243 243 return _get_all_settings
244 244
245 245 def get_all_settings(self, cache=False, from_request=True):
246 246 # defines if we use GLOBAL, or PER_REPO
247 247 repo = self._get_repo(self.repo) if self.repo else None
248 248
249 249 # initially try the request context; this is the fastest
250 250 # we only fetch global config, NOT for repo-specific
251 251 if from_request and not repo:
252 252 request = get_current_request()
253 253
254 254 if request and hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
255 255 rc_config = request.call_context.rc_config
256 256 if rc_config:
257 257 return rc_config
258 258
259 259 _region, cache_key = self.get_cache_region()
260 260 _get_all_settings = self.get_cache_call_method(cache=cache)
261 261
262 262 start = time.time()
263 263 result = _get_all_settings('rhodecode_settings', cache_key)
264 264 compute_time = time.time() - start
265 265 log.debug('cached method:%s took %.4fs', _get_all_settings.__name__, compute_time)
266 266
267 267 statsd = StatsdClient.statsd
268 268 if statsd:
269 269 elapsed_time_ms = round(1000.0 * compute_time) # use ms only
270 270 statsd.timing("rhodecode_settings_timing.histogram", elapsed_time_ms,
271 271 use_decimals=False)
272 272
273 273 log.debug('Fetching app settings for key: %s took: %.4fs: cache: %s', cache_key, compute_time, cache)
274 274
275 275 return result
276 276
277 277 def get_auth_settings(self):
278 278 q = self._get_settings_query()
279 279 q = q.filter(
280 280 self.SettingsDbModel.app_settings_name.startswith('auth_'))
281 281 rows = q.all()
282 282 auth_settings = {
283 283 row.app_settings_name: row.app_settings_value for row in rows}
284 284 return auth_settings
285 285
286 286 def get_auth_plugins(self):
287 287 auth_plugins = self.get_setting_by_name("auth_plugins")
288 288 return auth_plugins.app_settings_value
289 289
290 290 def get_default_repo_settings(self, strip_prefix=False):
291 291 q = self._get_settings_query()
292 292 q = q.filter(
293 293 self.SettingsDbModel.app_settings_name.startswith('default_'))
294 294 rows = q.all()
295 295
296 296 result = {}
297 297 for row in rows:
298 298 key = row.app_settings_name
299 299 if strip_prefix:
300 300 key = remove_prefix(key, prefix='default_')
301 301 result.update({key: row.app_settings_value})
302 302 return result
303 303
304 304 def get_repo(self):
305 305 repo = self._get_repo(self.repo)
306 306 if not repo:
307 307 raise Exception(
308 308 f'Repository `{self.repo}` cannot be found inside the database')
309 309 return repo
310 310
311 311 def _filter_by_repo(self, model, query):
312 312 if self.repo:
313 313 repo = self.get_repo()
314 314 query = query.filter(model.repository_id == repo.repo_id)
315 315 return query
316 316
317 317 def _get_hooks(self, query):
318 318 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
319 319 query = self._filter_by_repo(RepoRhodeCodeUi, query)
320 320 return query.all()
321 321
322 322 def _get_settings_query(self):
323 323 q = self.SettingsDbModel.query()
324 324 return self._filter_by_repo(RepoRhodeCodeSetting, q)
325 325
326 326 def list_enabled_social_plugins(self, settings):
327 327 enabled = []
328 328 for plug in SOCIAL_PLUGINS_LIST:
329 329 if str2bool(settings.get(f'rhodecode_auth_{plug}_enabled')):
330 330 enabled.append(plug)
331 331 return enabled
332 332
333 333
334 334 def assert_repo_settings(func):
335 335 @functools.wraps(func)
336 336 def _wrapper(self, *args, **kwargs):
337 337 if not self.repo_settings:
338 338 raise Exception('Repository is not specified')
339 339 return func(self, *args, **kwargs)
340 340 return _wrapper
341 341
342 342
343 343 class IssueTrackerSettingsModel(object):
344 344 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
345 345 SETTINGS_PREFIX = 'issuetracker_'
346 346
347 347 def __init__(self, sa=None, repo=None):
348 348 self.global_settings = SettingsModel(sa=sa)
349 349 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
350 350
351 351 @property
352 352 def inherit_global_settings(self):
353 353 if not self.repo_settings:
354 354 return True
355 355 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
356 356 return setting.app_settings_value if setting else True
357 357
358 358 @inherit_global_settings.setter
359 359 def inherit_global_settings(self, value):
360 360 if self.repo_settings:
361 361 settings = self.repo_settings.create_or_update_setting(
362 362 self.INHERIT_SETTINGS, value, type_='bool')
363 363 Session().add(settings)
364 364
365 365 def _get_keyname(self, key, uid, prefix='rhodecode_'):
366 366 return f'{prefix}{self.SETTINGS_PREFIX}{key}_{uid}'
367 367
368 368 def _make_dict_for_settings(self, qs):
369 369 prefix_match = self._get_keyname('pat', '',)
370 370
371 371 issuetracker_entries = {}
372 372 # create keys
373 373 for k, v in qs.items():
374 374 if k.startswith(prefix_match):
375 375 uid = k[len(prefix_match):]
376 376 issuetracker_entries[uid] = None
377 377
378 378 def url_cleaner(input_str):
379 379 input_str = input_str.replace('"', '').replace("'", '')
380 380 input_str = sanitize_html(input_str, strip=True)
381 381 return input_str
382 382
383 383 # populate
384 384 for uid in issuetracker_entries:
385 385 url_data = qs.get(self._get_keyname('url', uid))
386 386
387 387 pat = qs.get(self._get_keyname('pat', uid))
388 388 try:
389 389 pat_compiled = re.compile(r'%s' % pat)
390 390 except re.error:
391 391 pat_compiled = None
392 392
393 393 issuetracker_entries[uid] = AttributeDict({
394 394 'pat': pat,
395 395 'pat_compiled': pat_compiled,
396 396 'url': url_cleaner(
397 397 qs.get(self._get_keyname('url', uid)) or ''),
398 398 'pref': sanitize_html(
399 399 qs.get(self._get_keyname('pref', uid)) or ''),
400 400 'desc': qs.get(
401 401 self._get_keyname('desc', uid)),
402 402 })
403 403
404 404 return issuetracker_entries
405 405
406 406 def get_global_settings(self, cache=False):
407 407 """
408 408 Returns list of global issue tracker settings
409 409 """
410 410 defaults = self.global_settings.get_all_settings(cache=cache)
411 411 settings = self._make_dict_for_settings(defaults)
412 412 return settings
413 413
414 414 def get_repo_settings(self, cache=False):
415 415 """
416 416 Returns list of issue tracker settings per repository
417 417 """
418 418 if not self.repo_settings:
419 419 raise Exception('Repository is not specified')
420 420 all_settings = self.repo_settings.get_all_settings(cache=cache)
421 421 settings = self._make_dict_for_settings(all_settings)
422 422 return settings
423 423
424 424 def get_settings(self, cache=False):
425 425 if self.inherit_global_settings:
426 426 return self.get_global_settings(cache=cache)
427 427 else:
428 428 return self.get_repo_settings(cache=cache)
429 429
430 430 def delete_entries(self, uid):
431 431 if self.repo_settings:
432 432 all_patterns = self.get_repo_settings()
433 433 settings_model = self.repo_settings
434 434 else:
435 435 all_patterns = self.get_global_settings()
436 436 settings_model = self.global_settings
437 437 entries = all_patterns.get(uid, [])
438 438
439 439 for del_key in entries:
440 440 setting_name = self._get_keyname(del_key, uid, prefix='')
441 441 entry = settings_model.get_setting_by_name(setting_name)
442 442 if entry:
443 443 Session().delete(entry)
444 444
445 445 Session().commit()
446 446
447 447 def create_or_update_setting(
448 448 self, name, val=Optional(''), type_=Optional('unicode')):
449 449 if self.repo_settings:
450 450 setting = self.repo_settings.create_or_update_setting(
451 451 name, val, type_)
452 452 else:
453 453 setting = self.global_settings.create_or_update_setting(
454 454 name, val, type_)
455 455 return setting
456 456
457 457
458 458 class VcsSettingsModel(object):
459 459
460 460 INHERIT_SETTINGS = 'inherit_vcs_settings'
461 461 GENERAL_SETTINGS = (
462 462 'use_outdated_comments',
463 463 'pr_merge_enabled',
464 464 'hg_use_rebase_for_merging',
465 465 'hg_close_branch_before_merging',
466 466 'git_use_rebase_for_merging',
467 467 'git_close_branch_before_merging',
468 468 'diff_cache',
469 469 )
470 470
471 471 HOOKS_SETTINGS = (
472 472 ('hooks', 'changegroup.repo_size'),
473 473 ('hooks', 'changegroup.push_logger'),
474 474 ('hooks', 'outgoing.pull_logger'),
475 475 )
476 476 HG_SETTINGS = (
477 477 ('extensions', 'largefiles'),
478 478 ('phases', 'publish'),
479 479 ('extensions', 'evolve'),
480 480 ('extensions', 'topic'),
481 481 ('experimental', 'evolution'),
482 482 ('experimental', 'evolution.exchange'),
483 483 )
484 484 GIT_SETTINGS = (
485 485 ('vcs_git_lfs', 'enabled'),
486 486 )
487 487 GLOBAL_HG_SETTINGS = (
488 488 ('extensions', 'largefiles'),
489 489 ('largefiles', 'usercache'),
490 490 ('phases', 'publish'),
491 491 ('extensions', 'evolve'),
492 492 ('extensions', 'topic'),
493 493 ('experimental', 'evolution'),
494 494 ('experimental', 'evolution.exchange'),
495 495 )
496 496
497 497 GLOBAL_GIT_SETTINGS = (
498 498 ('vcs_git_lfs', 'enabled'),
499 499 ('vcs_git_lfs', 'store_location')
500 500 )
501 501
502 502 SVN_BRANCH_SECTION = 'vcs_svn_branch'
503 503 SVN_TAG_SECTION = 'vcs_svn_tag'
504 SSL_SETTING = ('web', 'push_ssl')
505 504 PATH_SETTING = ('paths', '/')
506 505
507 506 def __init__(self, sa=None, repo=None):
508 507 self.global_settings = SettingsModel(sa=sa)
509 508 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
510 509 self._ui_settings = (
511 510 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
512 511 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
513 512
514 513 @property
515 514 @assert_repo_settings
516 515 def inherit_global_settings(self):
517 516 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
518 517 return setting.app_settings_value if setting else True
519 518
520 519 @inherit_global_settings.setter
521 520 @assert_repo_settings
522 521 def inherit_global_settings(self, value):
523 522 self.repo_settings.create_or_update_setting(
524 523 self.INHERIT_SETTINGS, value, type_='bool')
525 524
526 525 def get_keyname(self, key_name, prefix='rhodecode_'):
527 526 return f'{prefix}{key_name}'
528 527
529 528 def get_global_svn_branch_patterns(self):
530 529 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
531 530
532 531 @assert_repo_settings
533 532 def get_repo_svn_branch_patterns(self):
534 533 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
535 534
536 535 def get_global_svn_tag_patterns(self):
537 536 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
538 537
539 538 @assert_repo_settings
540 539 def get_repo_svn_tag_patterns(self):
541 540 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
542 541
543 542 def get_global_settings(self):
544 543 return self._collect_all_settings(global_=True)
545 544
546 545 @assert_repo_settings
547 546 def get_repo_settings(self):
548 547 return self._collect_all_settings(global_=False)
549 548
550 549 @assert_repo_settings
551 550 def get_repo_settings_inherited(self):
552 551 global_settings = self.get_global_settings()
553 552 global_settings.update(self.get_repo_settings())
554 553 return global_settings
555 554
556 555 @assert_repo_settings
557 556 def create_or_update_repo_settings(
558 557 self, data, inherit_global_settings=False):
559 558 from rhodecode.model.scm import ScmModel
560 559
561 560 self.inherit_global_settings = inherit_global_settings
562 561
563 562 repo = self.repo_settings.get_repo()
564 563 if not inherit_global_settings:
565 564 if repo.repo_type == 'svn':
566 565 self.create_repo_svn_settings(data)
567 566 else:
568 567 self.create_or_update_repo_hook_settings(data)
569 568 self.create_or_update_repo_pr_settings(data)
570 569
571 570 if repo.repo_type == 'hg':
572 571 self.create_or_update_repo_hg_settings(data)
573 572
574 573 if repo.repo_type == 'git':
575 574 self.create_or_update_repo_git_settings(data)
576 575
577 576 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
578 577
579 578 @assert_repo_settings
580 579 def create_or_update_repo_hook_settings(self, data):
581 580 for section, key in self.HOOKS_SETTINGS:
582 581 data_key = self._get_form_ui_key(section, key)
583 582 if data_key not in data:
584 583 raise ValueError(
585 584 f'The given data does not contain {data_key} key')
586 585
587 586 active = data.get(data_key)
588 587 repo_setting = self.repo_settings.get_ui_by_section_and_key(
589 588 section, key)
590 589 if not repo_setting:
591 590 global_setting = self.global_settings.\
592 591 get_ui_by_section_and_key(section, key)
593 592 self.repo_settings.create_ui_section_value(
594 593 section, global_setting.ui_value, key=key, active=active)
595 594 else:
596 595 repo_setting.ui_active = active
597 596 Session().add(repo_setting)
598 597
599 598 def update_global_hook_settings(self, data):
600 599 for section, key in self.HOOKS_SETTINGS:
601 600 data_key = self._get_form_ui_key(section, key)
602 601 if data_key not in data:
603 602 raise ValueError(
604 603 f'The given data does not contain {data_key} key')
605 604 active = data.get(data_key)
606 605 repo_setting = self.global_settings.get_ui_by_section_and_key(
607 606 section, key)
608 607 repo_setting.ui_active = active
609 608 Session().add(repo_setting)
610 609
611 610 @assert_repo_settings
612 611 def create_or_update_repo_pr_settings(self, data):
613 612 return self._create_or_update_general_settings(
614 613 self.repo_settings, data)
615 614
616 615 def create_or_update_global_pr_settings(self, data):
617 616 return self._create_or_update_general_settings(
618 617 self.global_settings, data)
619 618
620 619 @assert_repo_settings
621 620 def create_repo_svn_settings(self, data):
622 621 return self._create_svn_settings(self.repo_settings, data)
623 622
624 623 def _set_evolution(self, settings, is_enabled):
625 624 if is_enabled:
626 625 # if evolve is active set evolution=all
627 626
628 627 self._create_or_update_ui(
629 628 settings, *('experimental', 'evolution'), value='all',
630 629 active=True)
631 630 self._create_or_update_ui(
632 631 settings, *('experimental', 'evolution.exchange'), value='yes',
633 632 active=True)
634 633 # if evolve is active set topics server support
635 634 self._create_or_update_ui(
636 635 settings, *('extensions', 'topic'), value='',
637 636 active=True)
638 637
639 638 else:
640 639 self._create_or_update_ui(
641 640 settings, *('experimental', 'evolution'), value='',
642 641 active=False)
643 642 self._create_or_update_ui(
644 643 settings, *('experimental', 'evolution.exchange'), value='no',
645 644 active=False)
646 645 self._create_or_update_ui(
647 646 settings, *('extensions', 'topic'), value='',
648 647 active=False)
649 648
650 649 @assert_repo_settings
651 650 def create_or_update_repo_hg_settings(self, data):
652 651 largefiles, phases, evolve = \
653 652 self.HG_SETTINGS[:3]
654 653 largefiles_key, phases_key, evolve_key = \
655 654 self._get_settings_keys(self.HG_SETTINGS[:3], data)
656 655
657 656 self._create_or_update_ui(
658 657 self.repo_settings, *largefiles, value='',
659 658 active=data[largefiles_key])
660 659 self._create_or_update_ui(
661 660 self.repo_settings, *evolve, value='',
662 661 active=data[evolve_key])
663 662 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
664 663
665 664 self._create_or_update_ui(
666 665 self.repo_settings, *phases, value=safe_str(data[phases_key]))
667 666
668 667 def create_or_update_global_hg_settings(self, data):
669 668 opts_len = 4
670 669 largefiles, largefiles_store, phases, evolve \
671 670 = self.GLOBAL_HG_SETTINGS[:opts_len]
672 671 largefiles_key, largefiles_store_key, phases_key, evolve_key \
673 672 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:opts_len], data)
674 673
675 674 self._create_or_update_ui(
676 675 self.global_settings, *largefiles, value='',
677 676 active=data[largefiles_key])
678 677 self._create_or_update_ui(
679 678 self.global_settings, *largefiles_store, value=data[largefiles_store_key])
680 679 self._create_or_update_ui(
681 680 self.global_settings, *phases, value=safe_str(data[phases_key]))
682 681 self._create_or_update_ui(
683 682 self.global_settings, *evolve, value='',
684 683 active=data[evolve_key])
685 684 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
686 685
687 686 def create_or_update_repo_git_settings(self, data):
688 687 # NOTE(marcink): # comma makes unpack work properly
689 688 lfs_enabled, \
690 689 = self.GIT_SETTINGS
691 690
692 691 lfs_enabled_key, \
693 692 = self._get_settings_keys(self.GIT_SETTINGS, data)
694 693
695 694 self._create_or_update_ui(
696 695 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
697 696 active=data[lfs_enabled_key])
698 697
699 698 def create_or_update_global_git_settings(self, data):
700 699 lfs_enabled, lfs_store_location \
701 700 = self.GLOBAL_GIT_SETTINGS
702 701 lfs_enabled_key, lfs_store_location_key \
703 702 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
704 703
705 704 self._create_or_update_ui(
706 705 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
707 706 active=data[lfs_enabled_key])
708 707 self._create_or_update_ui(
709 708 self.global_settings, *lfs_store_location,
710 709 value=data[lfs_store_location_key])
711 710
712 711 def create_or_update_global_svn_settings(self, data):
713 712 # branch/tags patterns
714 713 self._create_svn_settings(self.global_settings, data)
715 714
716 def update_global_ssl_setting(self, value):
717 self._create_or_update_ui(
718 self.global_settings, *self.SSL_SETTING, value=value)
719
720 715 @assert_repo_settings
721 716 def delete_repo_svn_pattern(self, id_):
722 717 ui = self.repo_settings.UiDbModel.get(id_)
723 718 if ui and ui.repository.repo_name == self.repo_settings.repo:
724 719 # only delete if it's the same repo as initialized settings
725 720 self.repo_settings.delete_ui(id_)
726 721 else:
727 722 # raise error as if we wouldn't find this option
728 723 self.repo_settings.delete_ui(-1)
729 724
730 725 def delete_global_svn_pattern(self, id_):
731 726 self.global_settings.delete_ui(id_)
732 727
733 728 @assert_repo_settings
734 729 def get_repo_ui_settings(self, section=None, key=None):
735 730 global_uis = self.global_settings.get_ui(section, key)
736 731 repo_uis = self.repo_settings.get_ui(section, key)
737 732
738 733 filtered_repo_uis = self._filter_ui_settings(repo_uis)
739 734 filtered_repo_uis_keys = [
740 735 (s.section, s.key) for s in filtered_repo_uis]
741 736
742 737 def _is_global_ui_filtered(ui):
743 738 return (
744 739 (ui.section, ui.key) in filtered_repo_uis_keys
745 740 or ui.section in self._svn_sections)
746 741
747 742 filtered_global_uis = [
748 743 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
749 744
750 745 return filtered_global_uis + filtered_repo_uis
751 746
752 747 def get_global_ui_settings(self, section=None, key=None):
753 748 return self.global_settings.get_ui(section, key)
754 749
755 750 def get_ui_settings_as_config_obj(self, section=None, key=None):
756 751 config = base.Config()
757 752
758 753 ui_settings = self.get_ui_settings(section=section, key=key)
759 754
760 755 for entry in ui_settings:
761 756 config.set(entry.section, entry.key, entry.value)
762 757
763 758 return config
764 759
765 760 def get_ui_settings(self, section=None, key=None):
766 761 if not self.repo_settings or self.inherit_global_settings:
767 762 return self.get_global_ui_settings(section, key)
768 763 else:
769 764 return self.get_repo_ui_settings(section, key)
770 765
771 766 def get_svn_patterns(self, section=None):
772 767 if not self.repo_settings:
773 768 return self.get_global_ui_settings(section)
774 769 else:
775 770 return self.get_repo_ui_settings(section)
776 771
777 772 @assert_repo_settings
778 773 def get_repo_general_settings(self):
779 774 global_settings = self.global_settings.get_all_settings()
780 775 repo_settings = self.repo_settings.get_all_settings()
781 776 filtered_repo_settings = self._filter_general_settings(repo_settings)
782 777 global_settings.update(filtered_repo_settings)
783 778 return global_settings
784 779
785 780 def get_global_general_settings(self):
786 781 return self.global_settings.get_all_settings()
787 782
788 783 def get_general_settings(self):
789 784 if not self.repo_settings or self.inherit_global_settings:
790 785 return self.get_global_general_settings()
791 786 else:
792 787 return self.get_repo_general_settings()
793 788
794 789 def _filter_ui_settings(self, settings):
795 790 filtered_settings = [
796 791 s for s in settings if self._should_keep_setting(s)]
797 792 return filtered_settings
798 793
799 794 def _should_keep_setting(self, setting):
800 795 keep = (
801 796 (setting.section, setting.key) in self._ui_settings or
802 797 setting.section in self._svn_sections)
803 798 return keep
804 799
805 800 def _filter_general_settings(self, settings):
806 801 keys = [self.get_keyname(key) for key in self.GENERAL_SETTINGS]
807 802 return {
808 803 k: settings[k]
809 804 for k in settings if k in keys}
810 805
811 806 def _collect_all_settings(self, global_=False):
812 807 settings = self.global_settings if global_ else self.repo_settings
813 808 result = {}
814 809
815 810 for section, key in self._ui_settings:
816 811 ui = settings.get_ui_by_section_and_key(section, key)
817 812 result_key = self._get_form_ui_key(section, key)
818 813
819 814 if ui:
820 815 if section in ('hooks', 'extensions'):
821 816 result[result_key] = ui.ui_active
822 817 elif result_key in ['vcs_git_lfs_enabled']:
823 818 result[result_key] = ui.ui_active
824 819 else:
825 820 result[result_key] = ui.ui_value
826 821
827 822 for name in self.GENERAL_SETTINGS:
828 823 setting = settings.get_setting_by_name(name)
829 824 if setting:
830 825 result_key = self.get_keyname(name)
831 826 result[result_key] = setting.app_settings_value
832 827
833 828 return result
834 829
835 830 def _get_form_ui_key(self, section, key):
836 831 return '{section}_{key}'.format(
837 832 section=section, key=key.replace('.', '_'))
838 833
839 834 def _create_or_update_ui(
840 835 self, settings, section, key, value=None, active=None):
841 836 ui = settings.get_ui_by_section_and_key(section, key)
842 837 if not ui:
843 838 active = True if active is None else active
844 839 settings.create_ui_section_value(
845 840 section, value, key=key, active=active)
846 841 else:
847 842 if active is not None:
848 843 ui.ui_active = active
849 844 if value is not None:
850 845 ui.ui_value = value
851 846 Session().add(ui)
852 847
853 848 def _create_svn_settings(self, settings, data):
854 849 svn_settings = {
855 850 'new_svn_branch': self.SVN_BRANCH_SECTION,
856 851 'new_svn_tag': self.SVN_TAG_SECTION
857 852 }
858 853 for key in svn_settings:
859 854 if data.get(key):
860 855 settings.create_ui_section_value(svn_settings[key], data[key])
861 856
862 857 def _create_or_update_general_settings(self, settings, data):
863 858 for name in self.GENERAL_SETTINGS:
864 859 data_key = self.get_keyname(name)
865 860 if data_key not in data:
866 861 raise ValueError(
867 862 f'The given data does not contain {data_key} key')
868 863 setting = settings.create_or_update_setting(
869 864 name, data[data_key], 'bool')
870 865 Session().add(setting)
871 866
872 867 def _get_settings_keys(self, settings, data):
873 868 data_keys = [self._get_form_ui_key(*s) for s in settings]
874 869 for data_key in data_keys:
875 870 if data_key not in data:
876 871 raise ValueError(
877 872 f'The given data does not contain {data_key} key')
878 873 return data_keys
879 874
880 875 def create_largeobjects_dirs_if_needed(self, repo_store_path):
881 876 """
882 877 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
883 878 does a repository scan if enabled in the settings.
884 879 """
885 880
886 881 from rhodecode.lib.vcs.backends.hg import largefiles_store
887 882 from rhodecode.lib.vcs.backends.git import lfs_store
888 883
889 884 paths = [
890 885 largefiles_store(repo_store_path),
891 886 lfs_store(repo_store_path)]
892 887
893 888 for path in paths:
894 889 if os.path.isdir(path):
895 890 continue
896 891 if os.path.isfile(path):
897 892 continue
898 893 # not a file nor dir, we try to create it
899 894 try:
900 895 os.makedirs(path)
901 896 except Exception:
902 897 log.warning('Failed to create largefiles dir:%s', path)
@@ -1,345 +1,330 b''
1 1 ## snippet for displaying vcs settings
2 2 ## usage:
3 3 ## <%namespace name="vcss" file="/base/vcssettings.mako"/>
4 4 ## ${vcss.vcs_settings_fields()}
5 5
6 6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, **kwargs)">
7 7 % if display_globals:
8 <div class="panel panel-default">
9 <div class="panel-heading" id="general">
10 <h3 class="panel-title">${_('General')}<a class="permalink" href="#general"></a></h3>
11 </div>
12 <div class="panel-body">
13 <div class="field">
14 <div class="checkbox">
15 ${h.checkbox('web_push_ssl' + suffix, 'True')}
16 <label for="web_push_ssl${suffix}">${_('Require SSL for vcs operations')}</label>
17 </div>
18 <div class="label">
19 <span class="help-block">${_('Activate to set RhodeCode to require SSL for pushing or pulling. If SSL certificate is missing it will return a HTTP Error 406: Not Acceptable.')}</span>
20 </div>
21 </div>
22 </div>
23 </div>
8
24 9 % endif
25 10
26 11 % if display_globals or repo_type in ['git', 'hg']:
27 12 <div class="panel panel-default">
28 13 <div class="panel-heading" id="vcs-hooks-options">
29 14 <h3 class="panel-title">${_('Internal Hooks')}<a class="permalink" href="#vcs-hooks-options"></a></h3>
30 15 </div>
31 16 <div class="panel-body">
32 17 <div class="field">
33 18 <div class="checkbox">
34 19 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
35 20 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
36 21 </div>
37 22
38 23 <div class="label">
39 24 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
40 25 </div>
41 26 <div class="checkbox">
42 27 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
43 28 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
44 29 </div>
45 30 <div class="label">
46 31 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
47 32 </div>
48 33 <div class="checkbox">
49 34 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
50 35 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
51 36 </div>
52 37 <div class="label">
53 38 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
54 39 </div>
55 40 </div>
56 41 </div>
57 42 </div>
58 43 % endif
59 44
60 45 % if display_globals or repo_type in ['hg']:
61 46 <div class="panel panel-default">
62 47 <div class="panel-heading" id="vcs-hg-options">
63 48 <h3 class="panel-title">${_('Mercurial Settings')}<a class="permalink" href="#vcs-hg-options"></a></h3>
64 49 </div>
65 50 <div class="panel-body">
66 51 <div class="checkbox">
67 52 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
68 53 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
69 54 </div>
70 55 <div class="label">
71 56 % if display_globals:
72 57 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
73 58 % else:
74 59 <span class="help-block">${_('Enable Largefiles extensions for this repository.')}</span>
75 60 % endif
76 61 </div>
77 62
78 63 % if display_globals:
79 64 <div class="field">
80 65 <div class="input">
81 66 ${h.text('largefiles_usercache' + suffix, size=59)}
82 67 </div>
83 68 </div>
84 69 <div class="label">
85 70 <span class="help-block">${_('Filesystem location where Mercurial largefile objects should be stored.')}</span>
86 71 </div>
87 72 % endif
88 73
89 74 <div class="checkbox">
90 75 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
91 76 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
92 77 </div>
93 78 <div class="label">
94 79 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
95 80 </div>
96 81
97 82 <div class="checkbox">
98 83 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
99 84 <label for="extensions_evolve${suffix}">${_('Enable Evolve and Topic extension')}</label>
100 85 </div>
101 86 <div class="label">
102 87 % if display_globals:
103 88 <span class="help-block">${_('Enable Evolve and Topic extensions for all repositories.')}</span>
104 89 % else:
105 90 <span class="help-block">${_('Enable Evolve and Topic extensions for this repository.')}</span>
106 91 % endif
107 92 </div>
108 93
109 94 </div>
110 95 </div>
111 96 % endif
112 97
113 98 % if display_globals or repo_type in ['git']:
114 99 <div class="panel panel-default">
115 100 <div class="panel-heading" id="vcs-git-options">
116 101 <h3 class="panel-title">${_('Git Settings')}<a class="permalink" href="#vcs-git-options"></a></h3>
117 102 </div>
118 103 <div class="panel-body">
119 104 <div class="checkbox">
120 105 ${h.checkbox('vcs_git_lfs_enabled' + suffix, 'True', **kwargs)}
121 106 <label for="vcs_git_lfs_enabled${suffix}">${_('Enable lfs extension')}</label>
122 107 </div>
123 108 <div class="label">
124 109 % if display_globals:
125 110 <span class="help-block">${_('Enable lfs extensions for all repositories.')}</span>
126 111 % else:
127 112 <span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
128 113 % endif
129 114 </div>
130 115
131 116 % if display_globals:
132 117 <div class="field">
133 118 <div class="input">
134 119 ${h.text('vcs_git_lfs_store_location' + suffix, size=59)}
135 120 </div>
136 121 </div>
137 122 <div class="label">
138 123 <span class="help-block">${_('Filesystem location where Git lfs objects should be stored.')}</span>
139 124 </div>
140 125 % endif
141 126 </div>
142 127 </div>
143 128 % endif
144 129
145 130 % if display_globals or repo_type in ['svn']:
146 131 <div class="panel panel-default">
147 132 <div class="panel-heading" id="vcs-svn-options">
148 133 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"></a></h3>
149 134 </div>
150 135 <div class="panel-body">
151 136 % if display_globals:
152 137 <div class="field">
153 138 <div class="content" >
154 139 <label>${_('mod_dav config')}</label><br/>
155 140 <code>path: ${c.svn_config_path}</code>
156 141 </div>
157 142 <br/>
158 143
159 144 <div>
160 145
161 146 % if c.svn_generate_config:
162 147 <span class="buttons">
163 148 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Re-generate Apache Config')}</button>
164 149 </span>
165 150 % endif
166 151 </div>
167 152 </div>
168 153 % endif
169 154
170 155 <div class="field">
171 156 <div class="content" >
172 157 <label>${_('Repository patterns')}</label><br/>
173 158 </div>
174 159 </div>
175 160 <div class="label">
176 161 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
177 162 </div>
178 163
179 164 <div class="field branch_patterns">
180 165 <div class="input" >
181 166 <label>${_('Branches')}:</label><br/>
182 167 </div>
183 168 % if svn_branch_patterns:
184 169 % for branch in svn_branch_patterns:
185 170 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
186 171 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
187 172 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
188 173 % if kwargs.get('disabled') != 'disabled':
189 174 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
190 175 ${_('Delete')}
191 176 </span>
192 177 % endif
193 178 </div>
194 179 % endfor
195 180 %endif
196 181 </div>
197 182 % if kwargs.get('disabled') != 'disabled':
198 183 <div class="field branch_patterns">
199 184 <div class="input" >
200 185 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
201 186 </div>
202 187 </div>
203 188 % endif
204 189 <div class="field tag_patterns">
205 190 <div class="input" >
206 191 <label>${_('Tags')}:</label><br/>
207 192 </div>
208 193 % if svn_tag_patterns:
209 194 % for tag in svn_tag_patterns:
210 195 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
211 196 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
212 197 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
213 198 % if kwargs.get('disabled') != 'disabled':
214 199 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
215 200 ${_('Delete')}
216 201 </span>
217 202 %endif
218 203 </div>
219 204 % endfor
220 205 % endif
221 206 </div>
222 207 % if kwargs.get('disabled') != 'disabled':
223 208 <div class="field tag_patterns">
224 209 <div class="input" >
225 210 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
226 211 </div>
227 212 </div>
228 213 %endif
229 214 </div>
230 215 </div>
231 216 % else:
232 217 ${h.hidden('new_svn_branch' + suffix, '')}
233 218 ${h.hidden('new_svn_tag' + suffix, '')}
234 219 % endif
235 220
236 221
237 222 % if display_globals or repo_type in ['hg', 'git']:
238 223 <div class="panel panel-default">
239 224 <div class="panel-heading" id="vcs-pull-requests-options">
240 225 <h3 class="panel-title">${_('Pull Request Settings')}<a class="permalink" href="#vcs-pull-requests-options"></a></h3>
241 226 </div>
242 227 <div class="panel-body">
243 228 <div class="checkbox">
244 229 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
245 230 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
246 231 </div>
247 232 <div class="label">
248 233 <span class="help-block">${_('Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface.')}</span>
249 234 </div>
250 235 <div class="checkbox">
251 236 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
252 237 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
253 238 </div>
254 239 <div class="label">
255 240 <span class="help-block">${_('During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden.')}</span>
256 241 </div>
257 242 </div>
258 243 </div>
259 244 % endif
260 245
261 246 % if display_globals or repo_type in ['hg', 'git', 'svn']:
262 247 <div class="panel panel-default">
263 248 <div class="panel-heading" id="vcs-pull-requests-options">
264 249 <h3 class="panel-title">${_('Diff cache')}<a class="permalink" href="#vcs-pull-requests-options"></a></h3>
265 250 </div>
266 251 <div class="panel-body">
267 252 <div class="checkbox">
268 253 ${h.checkbox('rhodecode_diff_cache' + suffix, 'True', **kwargs)}
269 254 <label for="rhodecode_diff_cache${suffix}">${_('Enable caching diffs for pull requests cache and commits')}</label>
270 255 </div>
271 256 </div>
272 257 </div>
273 258 % endif
274 259
275 260 % if display_globals or repo_type in ['hg',]:
276 261 <div class="panel panel-default">
277 262 <div class="panel-heading" id="vcs-pull-requests-options">
278 263 <h3 class="panel-title">${_('Mercurial Pull Request Settings')}<a class="permalink" href="#vcs-hg-pull-requests-options"></a></h3>
279 264 </div>
280 265 <div class="panel-body">
281 266 ## Specific HG settings
282 267 <div class="checkbox">
283 268 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
284 269 <label for="rhodecode_hg_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
285 270 </div>
286 271 <div class="label">
287 272 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
288 273 </div>
289 274
290 275 <div class="checkbox">
291 276 ${h.checkbox('rhodecode_hg_close_branch_before_merging' + suffix, 'True', **kwargs)}
292 277 <label for="rhodecode_hg_close_branch_before_merging{suffix}">${_('Close branch before merging it')}</label>
293 278 </div>
294 279 <div class="label">
295 280 <span class="help-block">${_('Close branch before merging it into destination branch. No effect when rebase strategy is use.')}</span>
296 281 </div>
297 282
298 283
299 284 </div>
300 285 </div>
301 286 % endif
302 287
303 288 % if display_globals or repo_type in ['git']:
304 289 <div class="panel panel-default">
305 290 <div class="panel-heading" id="vcs-pull-requests-options">
306 291 <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"></a></h3>
307 292 </div>
308 293 <div class="panel-body">
309 294 ## <div class="checkbox">
310 295 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
311 296 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
312 297 ## </div>
313 298 ## <div class="label">
314 299 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
315 300 ## </div>
316 301
317 302 <div class="checkbox">
318 303 ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
319 304 <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
320 305 </div>
321 306 <div class="label">
322 307 <span class="help-block">${_('Delete branch after merging it into destination branch.')}</span>
323 308 </div>
324 309 </div>
325 310 </div>
326 311 % endif
327 312
328 313 <script type="text/javascript">
329 314
330 315 $(document).ready(function() {
331 316 /* On click handler for the `Generate Apache Config` button. It sends a
332 317 POST request to trigger the (re)generation of the mod_dav_svn config. */
333 318 $('#vcs_svn_generate_cfg').on('click', function(event) {
334 319 event.preventDefault();
335 320 var url = "${h.route_path('admin_settings_vcs_svn_generate_cfg')}";
336 321 var jqxhr = $.post(url, {'csrf_token': CSRF_TOKEN});
337 322 jqxhr.done(function(data) {
338 323 $.Topic('/notifications').publish(data);
339 324 });
340 325 });
341 326 });
342 327
343 328 </script>
344 329 </%def>
345 330
@@ -1,155 +1,154 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import urllib.parse
21 21
22 22 import mock
23 23 import pytest
24 24 import simplejson as json
25 25
26 26 from rhodecode.lib.vcs.backends.base import Config
27 27 from rhodecode.tests.lib.middleware import mock_scm_app
28 28 import rhodecode.lib.middleware.simplehg as simplehg
29 29
30 30
31 31 def get_environ(url):
32 32 """Construct a minimum WSGI environ based on the URL."""
33 33 parsed_url = urllib.parse.urlparse(url)
34 34 environ = {
35 35 'PATH_INFO': parsed_url.path,
36 36 'QUERY_STRING': parsed_url.query,
37 37 }
38 38
39 39 return environ
40 40
41 41
42 42 @pytest.mark.parametrize(
43 43 'url, expected_action',
44 44 [
45 45 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
46 46 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
47 47 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
48 48 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
49 49 ('/foo/bar?cmd=hello', 'pull'),
50 50 ('/foo/bar?cmd=batch', 'push'),
51 51 ('/foo/bar?cmd=putlfile', 'push'),
52 52 # Edge case: unknown argument: assume push
53 53 ('/foo/bar?cmd=unknown&key=tip', 'push'),
54 54 ('/foo/bar?cmd=&key=tip', 'push'),
55 55 # Edge case: not cmd argument
56 56 ('/foo/bar?key=tip', 'push'),
57 57 ])
58 58 def test_get_action(url, expected_action, request_stub):
59 59 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
60 60 registry=request_stub.registry)
61 61 assert expected_action == app._get_action(get_environ(url))
62 62
63 63
64 64 @pytest.mark.parametrize(
65 65 'environ, expected_xargs, expected_batch',
66 66 [
67 67 ({},
68 68 [''], ['push']),
69 69
70 70 ({'HTTP_X_HGARG_1': ''},
71 71 [''], ['push']),
72 72
73 73 ({'HTTP_X_HGARG_1': 'cmds=listkeys+namespace%3Dphases'},
74 74 ['listkeys namespace=phases'], ['pull']),
75 75
76 76 ({'HTTP_X_HGARG_1': 'cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'},
77 77 ['pushkey namespace=bookmarks,key=bm,old=,new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'], ['push']),
78 78
79 79 ({'HTTP_X_HGARG_1': 'namespace=phases'},
80 80 ['namespace=phases'], ['push']),
81 81
82 82 ])
83 83 def test_xarg_and_batch_commands(environ, expected_xargs, expected_batch):
84 84 app = simplehg.SimpleHg
85 85
86 86 result = app._get_xarg_headers(environ)
87 87 result_batch = app._get_batch_cmd(environ)
88 88 assert expected_xargs == result
89 89 assert expected_batch == result_batch
90 90
91 91
92 92 @pytest.mark.parametrize(
93 93 'url, expected_repo_name',
94 94 [
95 95 ('/foo?cmd=unbundle&key=tip', 'foo'),
96 96 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
97 97 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
98 98 # Repos with trailing slashes.
99 99 ('/foo/?cmd=unbundle&key=tip', 'foo'),
100 100 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
101 101 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
102 102 ])
103 103 def test_get_repository_name(url, expected_repo_name, request_stub):
104 104 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
105 105 registry=request_stub.registry)
106 106 assert expected_repo_name == app._get_repository_name(get_environ(url))
107 107
108 108
109 109 def test_get_config(user_util, baseapp, request_stub):
110 110 repo = user_util.create_repo(repo_type='git')
111 111 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
112 112 registry=request_stub.registry)
113 113 extras = [('foo', 'FOO', 'bar', 'BAR')]
114 114
115 115 hg_config = app._create_config(extras, repo_name=repo.repo_name)
116 116
117 117 config = simplehg.utils.make_db_config(repo=repo.repo_name)
118 118 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
119 119 hg_config_org = config
120 120
121 121 expected_config = [
122 122 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
123 ('web', 'push_ssl', 'False'),
124 123 ('web', 'allow_push', '*'),
125 124 ('web', 'allow_archive', 'gz zip bz2'),
126 125 ('web', 'baseurl', '/'),
127 126 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
128 127 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
129 128 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
130 129 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
131 130 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
132 131 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
133 132 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
134 133 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
135 134 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
136 135 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
137 136 ('phases', 'publish', 'True'),
138 137 ('extensions', 'largefiles', ''),
139 138 ('paths', '/', hg_config_org.get('paths', '/')),
140 139 ('rhodecode', 'RC_SCM_DATA', '[["foo","FOO","bar","BAR"]]')
141 140 ]
142 141 for entry in expected_config:
143 142 assert entry in hg_config
144 143
145 144
146 145 def test_create_wsgi_app_uses_scm_app_from_simplevcs(request_stub):
147 146 config = {
148 147 'auth_ret_code': '',
149 148 'base_path': '',
150 149 'vcs.scm_app_implementation':
151 150 'rhodecode.tests.lib.middleware.mock_scm_app',
152 151 }
153 152 app = simplehg.SimpleHg(config=config, registry=request_stub.registry)
154 153 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
155 154 assert wsgi_app is mock_scm_app.mock_hg_wsgi
@@ -1,451 +1,448 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.lib.str_utils import base64_to_str
24 24 from rhodecode.lib.utils2 import AttributeDict
25 25 from rhodecode.tests.utils import CustomTestApp
26 26
27 27 from rhodecode.lib.caching_query import FromCache
28 28 from rhodecode.lib.middleware import simplevcs
29 29 from rhodecode.lib.middleware.https_fixup import HttpsFixup
30 30 from rhodecode.lib.middleware.utils import scm_app_http
31 31 from rhodecode.model.db import User, _hash_key
32 32 from rhodecode.model.meta import Session, cache as db_cache
33 33 from rhodecode.tests import (
34 34 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
35 35 from rhodecode.tests.lib.middleware import mock_scm_app
36 36
37 37
38 38 class StubVCSController(simplevcs.SimpleVCS):
39 39
40 40 SCM = 'hg'
41 41 stub_response_body = tuple()
42 42
43 43 def __init__(self, *args, **kwargs):
44 44 super(StubVCSController, self).__init__(*args, **kwargs)
45 45 self._action = 'pull'
46 46 self._is_shadow_repo_dir = True
47 47 self._name = HG_REPO
48 48 self.set_repo_names(None)
49 49
50 50 @property
51 51 def is_shadow_repo_dir(self):
52 52 return self._is_shadow_repo_dir
53 53
54 54 def _get_repository_name(self, environ):
55 55 return self._name
56 56
57 57 def _get_action(self, environ):
58 58 return self._action
59 59
60 60 def _create_wsgi_app(self, repo_path, repo_name, config):
61 61 def fake_app(environ, start_response):
62 62 headers = [
63 63 ('Http-Accept', 'application/mercurial')
64 64 ]
65 65 start_response('200 OK', headers)
66 66 return self.stub_response_body
67 67 return fake_app
68 68
69 69 def _create_config(self, extras, repo_name, scheme='http'):
70 70 return None
71 71
72 72
73 73 @pytest.fixture()
74 74 def vcscontroller(baseapp, config_stub, request_stub):
75 75 from rhodecode.config.middleware import ce_auth_resources
76 76
77 77 config_stub.testing_securitypolicy()
78 78 config_stub.include('rhodecode.authentication')
79 79
80 80 for resource in ce_auth_resources:
81 81 config_stub.include(resource)
82 82
83 83 controller = StubVCSController(
84 84 baseapp.config.get_settings(), request_stub.registry)
85 85 app = HttpsFixup(controller, baseapp.config.get_settings())
86 86 app = CustomTestApp(app)
87 87
88 88 _remove_default_user_from_query_cache()
89 89
90 90 # Sanity checks that things are set up correctly
91 91 app.get('/' + HG_REPO, status=200)
92 92
93 93 app.controller = controller
94 94 return app
95 95
96 96
97 97 def _remove_default_user_from_query_cache():
98 98 user = User.get_default_user(cache=True)
99 99 query = Session().query(User).filter(User.username == user.username)
100 100 query = query.options(
101 101 FromCache("sql_cache_short", f"get_user_{_hash_key(user.username)}"))
102 102
103 103 db_cache.invalidate(
104 104 query, {},
105 105 FromCache("sql_cache_short", f"get_user_{_hash_key(user.username)}"))
106 106
107 107 Session().expire(user)
108 108
109 109
110 110 def test_handles_exceptions_during_permissions_checks(
111 111 vcscontroller, disable_anonymous_user, enable_auth_plugins, test_user_factory):
112 112
113 113 test_password = 'qweqwe'
114 114 test_user = test_user_factory(password=test_password, extern_type='headers', extern_name='headers')
115 115 test_username = test_user.username
116 116
117 117 enable_auth_plugins.enable([
118 118 'egg:rhodecode-enterprise-ce#headers',
119 119 'egg:rhodecode-enterprise-ce#token',
120 120 'egg:rhodecode-enterprise-ce#rhodecode'],
121 121 override={
122 122 'egg:rhodecode-enterprise-ce#headers': {'auth_headers_header': 'REMOTE_USER'}
123 123 })
124 124
125 125 user_and_pass = f'{test_username}:{test_password}'
126 126 auth_password = base64_to_str(user_and_pass)
127 127
128 128 extra_environ = {
129 129 'AUTH_TYPE': 'Basic',
130 130 'HTTP_AUTHORIZATION': f'Basic {auth_password}',
131 131 'REMOTE_USER': test_username,
132 132 }
133 133
134 134 # Verify that things are hooked up correctly, we pass user with headers bound auth, and headers filled in
135 135 vcscontroller.get('/', status=200, extra_environ=extra_environ)
136 136
137 137 # Simulate trouble during permission checks
138 138 with mock.patch('rhodecode.model.db.User.get_by_username',
139 139 side_effect=Exception('permission_error_test')) as get_user:
140 140 # Verify that a correct 500 is returned and check that the expected
141 141 # code path was hit.
142 142 vcscontroller.get('/', status=500, extra_environ=extra_environ)
143 143 assert get_user.called
144 144
145 145
146 146 class StubFailVCSController(simplevcs.SimpleVCS):
147 147 def _handle_request(self, environ, start_response):
148 148 raise Exception("BOOM")
149 149
150 150
151 151 @pytest.fixture(scope='module')
152 152 def fail_controller(baseapp):
153 153 controller = StubFailVCSController(
154 154 baseapp.config.get_settings(), baseapp.config)
155 155 controller = HttpsFixup(controller, baseapp.config.get_settings())
156 156 controller = CustomTestApp(controller)
157 157 return controller
158 158
159 159
160 160 def test_handles_exceptions_as_internal_server_error(fail_controller):
161 161 fail_controller.get('/', status=500)
162 162
163 163
164 164 def test_provides_traceback_for_appenlight(fail_controller):
165 165 response = fail_controller.get(
166 166 '/', status=500, extra_environ={'appenlight.client': 'fake'})
167 167 assert 'appenlight.__traceback' in response.request.environ
168 168
169 169
170 170 def test_provides_utils_scm_app_as_scm_app_by_default(baseapp, request_stub):
171 171 controller = StubVCSController(baseapp.config.get_settings(), request_stub.registry)
172 172 assert controller.scm_app is scm_app_http
173 173
174 174
175 175 def test_allows_to_override_scm_app_via_config(baseapp, request_stub):
176 176 config = baseapp.config.get_settings().copy()
177 177 config['vcs.scm_app_implementation'] = (
178 178 'rhodecode.tests.lib.middleware.mock_scm_app')
179 179 controller = StubVCSController(config, request_stub.registry)
180 180 assert controller.scm_app is mock_scm_app
181 181
182 182
183 183 @pytest.mark.parametrize('query_string, expected', [
184 184 ('cmd=stub_command', True),
185 185 ('cmd=listkeys', False),
186 186 ])
187 187 def test_should_check_locking(query_string, expected):
188 188 result = simplevcs._should_check_locking(query_string)
189 189 assert result == expected
190 190
191 191
192 192 class TestShadowRepoRegularExpression(object):
193 193 pr_segment = 'pull-request'
194 194 shadow_segment = 'repository'
195 195
196 196 @pytest.mark.parametrize('url, expected', [
197 197 # repo with/without groups
198 198 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
199 199 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
200 200 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
201 201 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
202 202
203 203 # pull request ID
204 204 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
205 205 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
206 206 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
207 207 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
208 208
209 209 # unicode
210 210 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
211 211 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
212 212
213 213 # trailing/leading slash
214 214 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
215 215 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
216 216 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
217 217
218 218 # misc
219 219 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
220 220 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
221 221 ])
222 222 def test_shadow_repo_regular_expression(self, url, expected):
223 223 from rhodecode.lib.middleware.simplevcs import SimpleVCS
224 224 url = url.format(
225 225 pr_segment=self.pr_segment,
226 226 shadow_segment=self.shadow_segment)
227 227 match_obj = SimpleVCS.shadow_repo_re.match(url)
228 228 assert (match_obj is not None) == expected
229 229
230 230
231 231 @pytest.mark.backends('git', 'hg')
232 232 class TestShadowRepoExposure(object):
233 233
234 234 def test_pull_on_shadow_repo_propagates_to_wsgi_app(
235 235 self, baseapp, request_stub):
236 236 """
237 237 Check that a pull action to a shadow repo is propagated to the
238 238 underlying wsgi app.
239 239 """
240 240 controller = StubVCSController(
241 241 baseapp.config.get_settings(), request_stub.registry)
242 controller._check_ssl = mock.Mock()
243 242 controller.is_shadow_repo = True
244 243 controller._action = 'pull'
245 244 controller._is_shadow_repo_dir = True
246 245 controller.stub_response_body = (b'dummy body value',)
247 246 controller._get_default_cache_ttl = mock.Mock(
248 247 return_value=(False, 0))
249 248
250 249 environ_stub = {
251 250 'HTTP_HOST': 'test.example.com',
252 251 'HTTP_ACCEPT': 'application/mercurial',
253 252 'REQUEST_METHOD': 'GET',
254 253 'wsgi.url_scheme': 'http',
255 254 }
256 255
257 256 response = controller(environ_stub, mock.Mock())
258 257 response_body = b''.join(response)
259 258
260 259 # Assert that we got the response from the wsgi app.
261 260 assert response_body == b''.join(controller.stub_response_body)
262 261
263 262 def test_pull_on_shadow_repo_that_is_missing(self, baseapp, request_stub):
264 263 """
265 264 Check that a pull action to a shadow repo is propagated to the
266 265 underlying wsgi app.
267 266 """
268 267 controller = StubVCSController(
269 268 baseapp.config.get_settings(), request_stub.registry)
270 controller._check_ssl = mock.Mock()
271 269 controller.is_shadow_repo = True
272 270 controller._action = 'pull'
273 271 controller._is_shadow_repo_dir = False
274 272 controller.stub_response_body = (b'dummy body value',)
275 273 environ_stub = {
276 274 'HTTP_HOST': 'test.example.com',
277 275 'HTTP_ACCEPT': 'application/mercurial',
278 276 'REQUEST_METHOD': 'GET',
279 277 'wsgi.url_scheme': 'http',
280 278 }
281 279
282 280 response = controller(environ_stub, mock.Mock())
283 281 response_body = b''.join(response)
284 282
285 283 # Assert that we got the response from the wsgi app.
286 284 assert b'404 Not Found' in response_body
287 285
288 286 def test_push_on_shadow_repo_raises(self, baseapp, request_stub):
289 287 """
290 288 Check that a push action to a shadow repo is aborted.
291 289 """
292 290 controller = StubVCSController(
293 291 baseapp.config.get_settings(), request_stub.registry)
294 controller._check_ssl = mock.Mock()
295 292 controller.is_shadow_repo = True
296 293 controller._action = 'push'
297 294 controller.stub_response_body = (b'dummy body value',)
298 295 environ_stub = {
299 296 'HTTP_HOST': 'test.example.com',
300 297 'HTTP_ACCEPT': 'application/mercurial',
301 298 'REQUEST_METHOD': 'GET',
302 299 'wsgi.url_scheme': 'http',
303 300 }
304 301
305 302 response = controller(environ_stub, mock.Mock())
306 303 response_body = b''.join(response)
307 304
308 305 assert response_body != controller.stub_response_body
309 306 # Assert that a 406 error is returned.
310 307 assert b'406 Not Acceptable' in response_body
311 308
312 309 def test_set_repo_names_no_shadow(self, baseapp, request_stub):
313 310 """
314 311 Check that the set_repo_names method sets all names to the one returned
315 312 by the _get_repository_name method on a request to a non shadow repo.
316 313 """
317 314 environ_stub = {}
318 315 controller = StubVCSController(
319 316 baseapp.config.get_settings(), request_stub.registry)
320 317 controller._name = 'RepoGroup/MyRepo'
321 318 controller.set_repo_names(environ_stub)
322 319 assert not controller.is_shadow_repo
323 320 assert (controller.url_repo_name ==
324 321 controller.acl_repo_name ==
325 322 controller.vcs_repo_name ==
326 323 controller._get_repository_name(environ_stub))
327 324
328 325 def test_set_repo_names_with_shadow(
329 326 self, baseapp, pr_util, config_stub, request_stub):
330 327 """
331 328 Check that the set_repo_names method sets correct names on a request
332 329 to a shadow repo.
333 330 """
334 331 from rhodecode.model.pull_request import PullRequestModel
335 332
336 333 pull_request = pr_util.create_pull_request()
337 334 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
338 335 target=pull_request.target_repo.repo_name,
339 336 pr_id=pull_request.pull_request_id,
340 337 pr_segment=TestShadowRepoRegularExpression.pr_segment,
341 338 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
342 339 controller = StubVCSController(
343 340 baseapp.config.get_settings(), request_stub.registry)
344 341 controller._name = shadow_url
345 342 controller.set_repo_names({})
346 343
347 344 # Get file system path to shadow repo for assertions.
348 345 workspace_id = PullRequestModel()._workspace_id(pull_request)
349 346 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
350 347
351 348 assert controller.vcs_repo_name == vcs_repo_name
352 349 assert controller.url_repo_name == shadow_url
353 350 assert controller.acl_repo_name == pull_request.target_repo.repo_name
354 351 assert controller.is_shadow_repo
355 352
356 353 def test_set_repo_names_with_shadow_but_missing_pr(
357 354 self, baseapp, pr_util, config_stub, request_stub):
358 355 """
359 356 Checks that the set_repo_names method enforces matching target repos
360 357 and pull request IDs.
361 358 """
362 359 pull_request = pr_util.create_pull_request()
363 360 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
364 361 target=pull_request.target_repo.repo_name,
365 362 pr_id=999999999,
366 363 pr_segment=TestShadowRepoRegularExpression.pr_segment,
367 364 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
368 365 controller = StubVCSController(
369 366 baseapp.config.get_settings(), request_stub.registry)
370 367 controller._name = shadow_url
371 368 controller.set_repo_names({})
372 369
373 370 assert not controller.is_shadow_repo
374 371 assert (controller.url_repo_name ==
375 372 controller.acl_repo_name ==
376 373 controller.vcs_repo_name)
377 374
378 375
379 376 @pytest.mark.usefixtures('baseapp')
380 377 class TestGenerateVcsResponse(object):
381 378
382 379 def test_ensures_that_start_response_is_called_early_enough(self):
383 380 self.call_controller_with_response_body(iter(['a', 'b']))
384 381 assert self.start_response.called
385 382
386 383 def test_invalidates_cache_after_body_is_consumed(self):
387 384 result = self.call_controller_with_response_body(iter(['a', 'b']))
388 385 assert not self.was_cache_invalidated()
389 386 # Consume the result
390 387 list(result)
391 388 assert self.was_cache_invalidated()
392 389
393 390 def test_raises_unknown_exceptions(self):
394 391 result = self.call_controller_with_response_body(
395 392 self.raise_result_iter(vcs_kind='unknown'))
396 393 with pytest.raises(Exception):
397 394 list(result)
398 395
399 396 def call_controller_with_response_body(self, response_body):
400 397 settings = {
401 398 'base_path': 'fake_base_path',
402 399 'vcs.hooks.protocol.v2': 'celery',
403 400 'vcs.hooks.direct_calls': False,
404 401 }
405 402 registry = AttributeDict()
406 403 controller = StubVCSController(settings, registry)
407 404 controller._invalidate_cache = mock.Mock()
408 405 controller.stub_response_body = response_body
409 406 self.start_response = mock.Mock()
410 407 result = controller._generate_vcs_response(
411 408 environ={}, start_response=self.start_response,
412 409 repo_path='fake_repo_path',
413 410 extras={}, action='push')
414 411 self.controller = controller
415 412 return result
416 413
417 414 def raise_result_iter(self, vcs_kind='repo_locked'):
418 415 """
419 416 Simulates an exception due to a vcs raised exception if kind vcs_kind
420 417 """
421 418 raise self.vcs_exception(vcs_kind=vcs_kind)
422 419 yield "never_reached"
423 420
424 421 def vcs_exception(self, vcs_kind='repo_locked'):
425 422 locked_exception = Exception('TEST_MESSAGE')
426 423 locked_exception._vcs_kind = vcs_kind
427 424 return locked_exception
428 425
429 426 def was_cache_invalidated(self):
430 427 return self.controller._invalidate_cache.called
431 428
432 429
433 430 class TestInitializeGenerator(object):
434 431
435 432 def test_drains_first_element(self):
436 433 gen = self.factory(['__init__', 1, 2])
437 434 result = list(gen)
438 435 assert result == [1, 2]
439 436
440 437 @pytest.mark.parametrize('values', [
441 438 [],
442 439 [1, 2],
443 440 ])
444 441 def test_raises_value_error(self, values):
445 442 with pytest.raises(ValueError):
446 443 self.factory(values)
447 444
448 445 @simplevcs.initialize_generator
449 446 def factory(self, iterable):
450 447 for elem in iterable:
451 448 yield elem
@@ -1,1114 +1,1103 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.lib.utils2 import str2bool
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.model.settings import VcsSettingsModel, UiSetting
26 26
27 27
28 28 HOOKS_FORM_DATA = {
29 29 'hooks_changegroup_repo_size': True,
30 30 'hooks_changegroup_push_logger': True,
31 31 'hooks_outgoing_pull_logger': True
32 32 }
33 33
34 34 SVN_FORM_DATA = {
35 35 'new_svn_branch': 'test-branch',
36 36 'new_svn_tag': 'test-tag'
37 37 }
38 38
39 39 GENERAL_FORM_DATA = {
40 40 'rhodecode_pr_merge_enabled': True,
41 41 'rhodecode_use_outdated_comments': True,
42 42 'rhodecode_hg_use_rebase_for_merging': True,
43 43 'rhodecode_hg_close_branch_before_merging': True,
44 44 'rhodecode_git_use_rebase_for_merging': True,
45 45 'rhodecode_git_close_branch_before_merging': True,
46 46 'rhodecode_diff_cache': True,
47 47 }
48 48
49 49
50 50 class TestInheritGlobalSettingsProperty(object):
51 51 def test_get_raises_exception_when_repository_not_specified(self):
52 52 model = VcsSettingsModel()
53 53 with pytest.raises(Exception) as exc_info:
54 54 model.inherit_global_settings
55 55 assert str(exc_info.value) == 'Repository is not specified'
56 56
57 57 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
58 58 model = VcsSettingsModel(repo=repo_stub.repo_name)
59 59 assert model.inherit_global_settings is True
60 60
61 61 def test_value_is_returned(self, repo_stub, settings_util):
62 62 model = VcsSettingsModel(repo=repo_stub.repo_name)
63 63 settings_util.create_repo_rhodecode_setting(
64 64 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
65 65 assert model.inherit_global_settings is False
66 66
67 67 def test_value_is_set(self, repo_stub):
68 68 model = VcsSettingsModel(repo=repo_stub.repo_name)
69 69 model.inherit_global_settings = False
70 70 setting = model.repo_settings.get_setting_by_name(
71 71 VcsSettingsModel.INHERIT_SETTINGS)
72 72 try:
73 73 assert setting.app_settings_type == 'bool'
74 74 assert setting.app_settings_value is False
75 75 finally:
76 76 Session().delete(setting)
77 77 Session().commit()
78 78
79 79 def test_set_raises_exception_when_repository_not_specified(self):
80 80 model = VcsSettingsModel()
81 81 with pytest.raises(Exception) as exc_info:
82 82 model.inherit_global_settings = False
83 83 assert str(exc_info.value) == 'Repository is not specified'
84 84
85 85
86 86 class TestVcsSettingsModel(object):
87 87 def test_global_svn_branch_patterns(self):
88 88 model = VcsSettingsModel()
89 89 expected_result = {'test': 'test'}
90 90 with mock.patch.object(model, 'global_settings') as settings_mock:
91 91 get_settings = settings_mock.get_ui_by_section
92 92 get_settings.return_value = expected_result
93 93 settings_mock.return_value = expected_result
94 94 result = model.get_global_svn_branch_patterns()
95 95
96 96 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
97 97 assert expected_result == result
98 98
99 99 def test_repo_svn_branch_patterns(self):
100 100 model = VcsSettingsModel()
101 101 expected_result = {'test': 'test'}
102 102 with mock.patch.object(model, 'repo_settings') as settings_mock:
103 103 get_settings = settings_mock.get_ui_by_section
104 104 get_settings.return_value = expected_result
105 105 settings_mock.return_value = expected_result
106 106 result = model.get_repo_svn_branch_patterns()
107 107
108 108 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
109 109 assert expected_result == result
110 110
111 111 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
112 112 self):
113 113 model = VcsSettingsModel()
114 114 with pytest.raises(Exception) as exc_info:
115 115 model.get_repo_svn_branch_patterns()
116 116 assert str(exc_info.value) == 'Repository is not specified'
117 117
118 118 def test_global_svn_tag_patterns(self):
119 119 model = VcsSettingsModel()
120 120 expected_result = {'test': 'test'}
121 121 with mock.patch.object(model, 'global_settings') as settings_mock:
122 122 get_settings = settings_mock.get_ui_by_section
123 123 get_settings.return_value = expected_result
124 124 settings_mock.return_value = expected_result
125 125 result = model.get_global_svn_tag_patterns()
126 126
127 127 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
128 128 assert expected_result == result
129 129
130 130 def test_repo_svn_tag_patterns(self):
131 131 model = VcsSettingsModel()
132 132 expected_result = {'test': 'test'}
133 133 with mock.patch.object(model, 'repo_settings') as settings_mock:
134 134 get_settings = settings_mock.get_ui_by_section
135 135 get_settings.return_value = expected_result
136 136 settings_mock.return_value = expected_result
137 137 result = model.get_repo_svn_tag_patterns()
138 138
139 139 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
140 140 assert expected_result == result
141 141
142 142 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
143 143 model = VcsSettingsModel()
144 144 with pytest.raises(Exception) as exc_info:
145 145 model.get_repo_svn_tag_patterns()
146 146 assert str(exc_info.value) == 'Repository is not specified'
147 147
148 148 def test_get_global_settings(self):
149 149 expected_result = {'test': 'test'}
150 150 model = VcsSettingsModel()
151 151 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
152 152 collect_mock.return_value = expected_result
153 153 result = model.get_global_settings()
154 154
155 155 collect_mock.assert_called_once_with(global_=True)
156 156 assert result == expected_result
157 157
158 158 def test_get_repo_settings(self, repo_stub):
159 159 model = VcsSettingsModel(repo=repo_stub.repo_name)
160 160 expected_result = {'test': 'test'}
161 161 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
162 162 collect_mock.return_value = expected_result
163 163 result = model.get_repo_settings()
164 164
165 165 collect_mock.assert_called_once_with(global_=False)
166 166 assert result == expected_result
167 167
168 168 @pytest.mark.parametrize('settings, global_', [
169 169 ('global_settings', True),
170 170 ('repo_settings', False)
171 171 ])
172 172 def test_collect_all_settings(self, settings, global_):
173 173 model = VcsSettingsModel()
174 174 result_mock = self._mock_result()
175 175
176 176 settings_patch = mock.patch.object(model, settings)
177 177 with settings_patch as settings_mock:
178 178 settings_mock.get_ui_by_section_and_key.return_value = result_mock
179 179 settings_mock.get_setting_by_name.return_value = result_mock
180 180 result = model._collect_all_settings(global_=global_)
181 181
182 182 ui_settings = model.HG_SETTINGS + model.GIT_SETTINGS + model.HOOKS_SETTINGS
183 183 self._assert_get_settings_calls(
184 184 settings_mock, ui_settings, model.GENERAL_SETTINGS)
185 185 self._assert_collect_all_settings_result(
186 186 ui_settings, model.GENERAL_SETTINGS, result)
187 187
188 188 @pytest.mark.parametrize('settings, global_', [
189 189 ('global_settings', True),
190 190 ('repo_settings', False)
191 191 ])
192 192 def test_collect_all_settings_without_empty_value(self, settings, global_):
193 193 model = VcsSettingsModel()
194 194
195 195 settings_patch = mock.patch.object(model, settings)
196 196 with settings_patch as settings_mock:
197 197 settings_mock.get_ui_by_section_and_key.return_value = None
198 198 settings_mock.get_setting_by_name.return_value = None
199 199 result = model._collect_all_settings(global_=global_)
200 200
201 201 assert result == {}
202 202
203 203 def _mock_result(self):
204 204 result_mock = mock.Mock()
205 205 result_mock.ui_value = 'ui_value'
206 206 result_mock.ui_active = True
207 207 result_mock.app_settings_value = 'setting_value'
208 208 return result_mock
209 209
210 210 def _assert_get_settings_calls(
211 211 self, settings_mock, ui_settings, general_settings):
212 212 assert (
213 213 settings_mock.get_ui_by_section_and_key.call_count ==
214 214 len(ui_settings))
215 215 assert (
216 216 settings_mock.get_setting_by_name.call_count ==
217 217 len(general_settings))
218 218
219 219 for section, key in ui_settings:
220 220 expected_call = mock.call(section, key)
221 221 assert (
222 222 expected_call in
223 223 settings_mock.get_ui_by_section_and_key.call_args_list)
224 224
225 225 for name in general_settings:
226 226 expected_call = mock.call(name)
227 227 assert (
228 228 expected_call in
229 229 settings_mock.get_setting_by_name.call_args_list)
230 230
231 231 def _assert_collect_all_settings_result(
232 232 self, ui_settings, general_settings, result):
233 233 expected_result = {}
234 234 for section, key in ui_settings:
235 235 key = '{}_{}'.format(section, key.replace('.', '_'))
236 236
237 237 if section in ('extensions', 'hooks'):
238 238 value = True
239 239 elif key in ['vcs_git_lfs_enabled']:
240 240 value = True
241 241 else:
242 242 value = 'ui_value'
243 243 expected_result[key] = value
244 244
245 245 for name in general_settings:
246 246 key = 'rhodecode_' + name
247 247 expected_result[key] = 'setting_value'
248 248
249 249 assert expected_result == result
250 250
251 251
252 252 class TestCreateOrUpdateRepoHookSettings(object):
253 253 def test_create_when_no_repo_object_found(self, repo_stub):
254 254 model = VcsSettingsModel(repo=repo_stub.repo_name)
255 255
256 256 self._create_settings(model, HOOKS_FORM_DATA)
257 257
258 258 cleanup = []
259 259 try:
260 260 for section, key in model.HOOKS_SETTINGS:
261 261 ui = model.repo_settings.get_ui_by_section_and_key(
262 262 section, key)
263 263 assert ui.ui_active is True
264 264 cleanup.append(ui)
265 265 finally:
266 266 for ui in cleanup:
267 267 Session().delete(ui)
268 268 Session().commit()
269 269
270 270 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
271 271 model = VcsSettingsModel(repo=repo_stub.repo_name)
272 272
273 273 deleted_key = 'hooks_changegroup_repo_size'
274 274 data = HOOKS_FORM_DATA.copy()
275 275 data.pop(deleted_key)
276 276
277 277 with pytest.raises(ValueError) as exc_info:
278 278 model.create_or_update_repo_hook_settings(data)
279 279 Session().commit()
280 280
281 281 msg = 'The given data does not contain {} key'.format(deleted_key)
282 282 assert str(exc_info.value) == msg
283 283
284 284 def test_update_when_repo_object_found(self, repo_stub, settings_util):
285 285 model = VcsSettingsModel(repo=repo_stub.repo_name)
286 286 for section, key in model.HOOKS_SETTINGS:
287 287 settings_util.create_repo_rhodecode_ui(
288 288 repo_stub, section, None, key=key, active=False)
289 289 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
290 290 Session().commit()
291 291
292 292 for section, key in model.HOOKS_SETTINGS:
293 293 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
294 294 assert ui.ui_active is True
295 295
296 296 def _create_settings(self, model, data):
297 297 global_patch = mock.patch.object(model, 'global_settings')
298 298 global_setting = mock.Mock()
299 299 global_setting.ui_value = 'Test value'
300 300 with global_patch as global_mock:
301 301 global_mock.get_ui_by_section_and_key.return_value = global_setting
302 302 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
303 303 Session().commit()
304 304
305 305
306 306 class TestUpdateGlobalHookSettings(object):
307 307 def test_update_raises_exception_when_data_incomplete(self):
308 308 model = VcsSettingsModel()
309 309
310 310 deleted_key = 'hooks_changegroup_repo_size'
311 311 data = HOOKS_FORM_DATA.copy()
312 312 data.pop(deleted_key)
313 313
314 314 with pytest.raises(ValueError) as exc_info:
315 315 model.update_global_hook_settings(data)
316 316 Session().commit()
317 317
318 318 msg = 'The given data does not contain {} key'.format(deleted_key)
319 319 assert str(exc_info.value) == msg
320 320
321 321 def test_update_global_hook_settings(self, settings_util):
322 322 model = VcsSettingsModel()
323 323 setting_mock = mock.MagicMock()
324 324 setting_mock.ui_active = False
325 325 get_settings_patcher = mock.patch.object(
326 326 model.global_settings, 'get_ui_by_section_and_key',
327 327 return_value=setting_mock)
328 328 session_patcher = mock.patch('rhodecode.model.settings.Session')
329 329 with get_settings_patcher as get_settings_mock, session_patcher:
330 330 model.update_global_hook_settings(HOOKS_FORM_DATA)
331 331 Session().commit()
332 332
333 333 assert setting_mock.ui_active is True
334 334 assert get_settings_mock.call_count == 3
335 335
336 336
337 337 class TestCreateOrUpdateRepoGeneralSettings(object):
338 338 def test_calls_create_or_update_general_settings(self, repo_stub):
339 339 model = VcsSettingsModel(repo=repo_stub.repo_name)
340 340 create_patch = mock.patch.object(
341 341 model, '_create_or_update_general_settings')
342 342 with create_patch as create_mock:
343 343 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
344 344 Session().commit()
345 345
346 346 create_mock.assert_called_once_with(
347 347 model.repo_settings, GENERAL_FORM_DATA)
348 348
349 349 def test_raises_exception_when_repository_is_not_specified(self):
350 350 model = VcsSettingsModel()
351 351 with pytest.raises(Exception) as exc_info:
352 352 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
353 353 assert str(exc_info.value) == 'Repository is not specified'
354 354
355 355
356 356 class TestCreateOrUpdatGlobalGeneralSettings(object):
357 357 def test_calls_create_or_update_general_settings(self):
358 358 model = VcsSettingsModel()
359 359 create_patch = mock.patch.object(
360 360 model, '_create_or_update_general_settings')
361 361 with create_patch as create_mock:
362 362 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
363 363 create_mock.assert_called_once_with(
364 364 model.global_settings, GENERAL_FORM_DATA)
365 365
366 366
367 367 class TestCreateOrUpdateGeneralSettings(object):
368 368 def test_create_when_no_repo_settings_found(self, repo_stub):
369 369 model = VcsSettingsModel(repo=repo_stub.repo_name)
370 370 model._create_or_update_general_settings(
371 371 model.repo_settings, GENERAL_FORM_DATA)
372 372
373 373 cleanup = []
374 374 try:
375 375 for name in model.GENERAL_SETTINGS:
376 376 setting = model.repo_settings.get_setting_by_name(name)
377 377 assert setting.app_settings_value is True
378 378 cleanup.append(setting)
379 379 finally:
380 380 for setting in cleanup:
381 381 Session().delete(setting)
382 382 Session().commit()
383 383
384 384 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
385 385 model = VcsSettingsModel(repo=repo_stub.repo_name)
386 386
387 387 deleted_key = 'rhodecode_pr_merge_enabled'
388 388 data = GENERAL_FORM_DATA.copy()
389 389 data.pop(deleted_key)
390 390
391 391 with pytest.raises(ValueError) as exc_info:
392 392 model._create_or_update_general_settings(model.repo_settings, data)
393 393 Session().commit()
394 394
395 395 msg = 'The given data does not contain {} key'.format(deleted_key)
396 396 assert str(exc_info.value) == msg
397 397
398 398 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
399 399 model = VcsSettingsModel(repo=repo_stub.repo_name)
400 400 for name in model.GENERAL_SETTINGS:
401 401 settings_util.create_repo_rhodecode_setting(
402 402 repo_stub, name, False, 'bool')
403 403
404 404 model._create_or_update_general_settings(
405 405 model.repo_settings, GENERAL_FORM_DATA)
406 406 Session().commit()
407 407
408 408 for name in model.GENERAL_SETTINGS:
409 409 setting = model.repo_settings.get_setting_by_name(name)
410 410 assert setting.app_settings_value is True
411 411
412 412
413 413 class TestCreateRepoSvnSettings(object):
414 414 def test_calls_create_svn_settings(self, repo_stub):
415 415 model = VcsSettingsModel(repo=repo_stub.repo_name)
416 416 with mock.patch.object(model, '_create_svn_settings') as create_mock:
417 417 model.create_repo_svn_settings(SVN_FORM_DATA)
418 418 Session().commit()
419 419
420 420 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
421 421
422 422 def test_raises_exception_when_repository_is_not_specified(self):
423 423 model = VcsSettingsModel()
424 424 with pytest.raises(Exception) as exc_info:
425 425 model.create_repo_svn_settings(SVN_FORM_DATA)
426 426 Session().commit()
427 427
428 428 assert str(exc_info.value) == 'Repository is not specified'
429 429
430 430
431 431 class TestCreateSvnSettings(object):
432 432 def test_create(self, repo_stub):
433 433 model = VcsSettingsModel(repo=repo_stub.repo_name)
434 434 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
435 435 Session().commit()
436 436
437 437 branch_ui = model.repo_settings.get_ui_by_section(
438 438 model.SVN_BRANCH_SECTION)
439 439 tag_ui = model.repo_settings.get_ui_by_section(
440 440 model.SVN_TAG_SECTION)
441 441
442 442 try:
443 443 assert len(branch_ui) == 1
444 444 assert len(tag_ui) == 1
445 445 finally:
446 446 Session().delete(branch_ui[0])
447 447 Session().delete(tag_ui[0])
448 448 Session().commit()
449 449
450 450 def test_create_tag(self, repo_stub):
451 451 model = VcsSettingsModel(repo=repo_stub.repo_name)
452 452 data = SVN_FORM_DATA.copy()
453 453 data.pop('new_svn_branch')
454 454 model._create_svn_settings(model.repo_settings, data)
455 455 Session().commit()
456 456
457 457 branch_ui = model.repo_settings.get_ui_by_section(
458 458 model.SVN_BRANCH_SECTION)
459 459 tag_ui = model.repo_settings.get_ui_by_section(
460 460 model.SVN_TAG_SECTION)
461 461
462 462 try:
463 463 assert len(branch_ui) == 0
464 464 assert len(tag_ui) == 1
465 465 finally:
466 466 Session().delete(tag_ui[0])
467 467 Session().commit()
468 468
469 469 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
470 470 model = VcsSettingsModel(repo=repo_stub.repo_name)
471 471 model._create_svn_settings(model.repo_settings, {})
472 472 Session().commit()
473 473
474 474 branch_ui = model.repo_settings.get_ui_by_section(
475 475 model.SVN_BRANCH_SECTION)
476 476 tag_ui = model.repo_settings.get_ui_by_section(
477 477 model.SVN_TAG_SECTION)
478 478
479 479 assert len(branch_ui) == 0
480 480 assert len(tag_ui) == 0
481 481
482 482 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
483 483 model = VcsSettingsModel(repo=repo_stub.repo_name)
484 484 data = {
485 485 'new_svn_branch': '',
486 486 'new_svn_tag': ''
487 487 }
488 488 model._create_svn_settings(model.repo_settings, data)
489 489 Session().commit()
490 490
491 491 branch_ui = model.repo_settings.get_ui_by_section(
492 492 model.SVN_BRANCH_SECTION)
493 493 tag_ui = model.repo_settings.get_ui_by_section(
494 494 model.SVN_TAG_SECTION)
495 495
496 496 assert len(branch_ui) == 0
497 497 assert len(tag_ui) == 0
498 498
499 499
500 500 class TestCreateOrUpdateUi(object):
501 501 def test_create(self, repo_stub):
502 502 model = VcsSettingsModel(repo=repo_stub.repo_name)
503 503 model._create_or_update_ui(
504 504 model.repo_settings, 'test-section', 'test-key', active=False,
505 505 value='False')
506 506 Session().commit()
507 507
508 508 created_ui = model.repo_settings.get_ui_by_section_and_key(
509 509 'test-section', 'test-key')
510 510
511 511 try:
512 512 assert created_ui.ui_active is False
513 513 assert str2bool(created_ui.ui_value) is False
514 514 finally:
515 515 Session().delete(created_ui)
516 516 Session().commit()
517 517
518 518 def test_update(self, repo_stub, settings_util):
519 519 model = VcsSettingsModel(repo=repo_stub.repo_name)
520 520 # care about only 3 first settings
521 521 largefiles, phases, evolve = model.HG_SETTINGS[:3]
522 522
523 523 section = 'test-section'
524 524 key = 'test-key'
525 525 settings_util.create_repo_rhodecode_ui(
526 526 repo_stub, section, 'True', key=key, active=True)
527 527
528 528 model._create_or_update_ui(
529 529 model.repo_settings, section, key, active=False, value='False')
530 530 Session().commit()
531 531
532 532 created_ui = model.repo_settings.get_ui_by_section_and_key(
533 533 section, key)
534 534 assert created_ui.ui_active is False
535 535 assert str2bool(created_ui.ui_value) is False
536 536
537 537
538 538 class TestCreateOrUpdateRepoHgSettings(object):
539 539 FORM_DATA = {
540 540 'extensions_largefiles': False,
541 541 'extensions_evolve': False,
542 542 'phases_publish': False
543 543 }
544 544
545 545 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
546 546 model = VcsSettingsModel(repo=repo_stub.repo_name)
547 547 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
548 548 model.create_or_update_repo_hg_settings(self.FORM_DATA)
549 549 expected_calls = [
550 550 mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''),
551 551 mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''),
552 552 mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''),
553 553 mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
554 554 mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''),
555 555 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
556 556 ]
557 557 assert expected_calls == create_mock.call_args_list
558 558
559 559 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
560 560 def test_key_is_not_found(self, repo_stub, field_to_remove):
561 561 model = VcsSettingsModel(repo=repo_stub.repo_name)
562 562 data = self.FORM_DATA.copy()
563 563 data.pop(field_to_remove)
564 564 with pytest.raises(ValueError) as exc_info:
565 565 model.create_or_update_repo_hg_settings(data)
566 566 Session().commit()
567 567
568 568 expected_message = 'The given data does not contain {} key'.format(
569 569 field_to_remove)
570 570 assert str(exc_info.value) == expected_message
571 571
572 572 def test_create_raises_exception_when_repository_not_specified(self):
573 573 model = VcsSettingsModel()
574 574 with pytest.raises(Exception) as exc_info:
575 575 model.create_or_update_repo_hg_settings(self.FORM_DATA)
576 576 Session().commit()
577 577
578 578 assert str(exc_info.value) == 'Repository is not specified'
579 579
580 580
581 class TestUpdateGlobalSslSetting(object):
582 def test_updates_global_hg_settings(self):
583 model = VcsSettingsModel()
584 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
585 model.update_global_ssl_setting('False')
586 Session().commit()
587
588 create_mock.assert_called_once_with(
589 model.global_settings, 'web', 'push_ssl', value='False')
590
591
592 581 class TestCreateOrUpdateGlobalHgSettings(object):
593 582 FORM_DATA = {
594 583 'extensions_largefiles': False,
595 584 'largefiles_usercache': '/example/largefiles-store',
596 585 'phases_publish': False,
597 586 'extensions_evolve': False
598 587 }
599 588
600 589 def test_creates_repo_hg_settings_when_data_is_correct(self):
601 590 model = VcsSettingsModel()
602 591 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
603 592 model.create_or_update_global_hg_settings(self.FORM_DATA)
604 593 Session().commit()
605 594
606 595 expected_calls = [
607 596 mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''),
608 597 mock.call(model.global_settings, 'largefiles', 'usercache', value='/example/largefiles-store'),
609 598 mock.call(model.global_settings, 'phases', 'publish', value='False'),
610 599 mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''),
611 600 mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''),
612 601 mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
613 602 mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''),
614 603 ]
615 604
616 605 assert expected_calls == create_mock.call_args_list
617 606
618 607 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
619 608 def test_key_is_not_found(self, repo_stub, field_to_remove):
620 609 model = VcsSettingsModel(repo=repo_stub.repo_name)
621 610 data = self.FORM_DATA.copy()
622 611 data.pop(field_to_remove)
623 612 with pytest.raises(Exception) as exc_info:
624 613 model.create_or_update_global_hg_settings(data)
625 614 Session().commit()
626 615
627 616 expected_message = 'The given data does not contain {} key'.format(
628 617 field_to_remove)
629 618 assert str(exc_info.value) == expected_message
630 619
631 620
632 621 class TestCreateOrUpdateGlobalGitSettings(object):
633 622 FORM_DATA = {
634 623 'vcs_git_lfs_enabled': False,
635 624 'vcs_git_lfs_store_location': '/example/lfs-store',
636 625 }
637 626
638 627 def test_creates_repo_hg_settings_when_data_is_correct(self):
639 628 model = VcsSettingsModel()
640 629 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
641 630 model.create_or_update_global_git_settings(self.FORM_DATA)
642 631 Session().commit()
643 632
644 633 expected_calls = [
645 634 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False),
646 635 mock.call(model.global_settings, 'vcs_git_lfs', 'store_location', value='/example/lfs-store'),
647 636 ]
648 637 assert expected_calls == create_mock.call_args_list
649 638
650 639
651 640 class TestDeleteRepoSvnPattern(object):
652 641 def test_success_when_repo_is_set(self, backend_svn, settings_util):
653 642 repo = backend_svn.create_repo()
654 643 repo_name = repo.repo_name
655 644
656 645 model = VcsSettingsModel(repo=repo_name)
657 646 entry = settings_util.create_repo_rhodecode_ui(
658 647 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
659 648 Session().commit()
660 649
661 650 model.delete_repo_svn_pattern(entry.ui_id)
662 651
663 652 def test_fail_when_delete_id_from_other_repo(self, backend_svn):
664 653 repo_name = backend_svn.repo_name
665 654 model = VcsSettingsModel(repo=repo_name)
666 655 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
667 656 with delete_ui_patch as delete_ui_mock:
668 657 model.delete_repo_svn_pattern(123)
669 658 Session().commit()
670 659
671 660 delete_ui_mock.assert_called_once_with(-1)
672 661
673 662 def test_raises_exception_when_repository_is_not_specified(self):
674 663 model = VcsSettingsModel()
675 664 with pytest.raises(Exception) as exc_info:
676 665 model.delete_repo_svn_pattern(123)
677 666 assert str(exc_info.value) == 'Repository is not specified'
678 667
679 668
680 669 class TestDeleteGlobalSvnPattern(object):
681 670 def test_delete_global_svn_pattern_calls_delete_ui(self):
682 671 model = VcsSettingsModel()
683 672 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
684 673 with delete_ui_patch as delete_ui_mock:
685 674 model.delete_global_svn_pattern(123)
686 675 delete_ui_mock.assert_called_once_with(123)
687 676
688 677
689 678 class TestFilterUiSettings(object):
690 679 def test_settings_are_filtered(self):
691 680 model = VcsSettingsModel()
692 681 repo_settings = [
693 682 UiSetting('extensions', 'largefiles', '', True),
694 683 UiSetting('phases', 'publish', 'True', True),
695 684 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
696 685 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
697 686 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
698 687 UiSetting(
699 688 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
700 689 'test_branch', True),
701 690 UiSetting(
702 691 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
703 692 'test_tag', True),
704 693 ]
705 694 non_repo_settings = [
706 695 UiSetting('largefiles', 'usercache', '/example/largefiles-store', True),
707 696 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
708 697 UiSetting('hooks', 'test2', 'hook', True),
709 698 UiSetting(
710 699 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
711 700 'test_tag', True),
712 701 ]
713 702 settings = repo_settings + non_repo_settings
714 703 filtered_settings = model._filter_ui_settings(settings)
715 704 assert sorted(filtered_settings) == sorted(repo_settings)
716 705
717 706
718 707 class TestFilterGeneralSettings(object):
719 708 def test_settings_are_filtered(self):
720 709 model = VcsSettingsModel()
721 710 settings = {
722 711 'rhodecode_abcde': 'value1',
723 712 'rhodecode_vwxyz': 'value2',
724 713 }
725 714 general_settings = {
726 715 'rhodecode_{}'.format(key): 'value'
727 716 for key in VcsSettingsModel.GENERAL_SETTINGS
728 717 }
729 718 settings.update(general_settings)
730 719
731 720 filtered_settings = model._filter_general_settings(general_settings)
732 721 assert sorted(filtered_settings) == sorted(general_settings)
733 722
734 723
735 724 class TestGetRepoUiSettings(object):
736 725 def test_global_uis_are_returned_when_no_repo_uis_found(
737 726 self, repo_stub):
738 727 model = VcsSettingsModel(repo=repo_stub.repo_name)
739 728 result = model.get_repo_ui_settings()
740 729 svn_sections = (
741 730 VcsSettingsModel.SVN_TAG_SECTION,
742 731 VcsSettingsModel.SVN_BRANCH_SECTION)
743 732 expected_result = [
744 733 s for s in model.global_settings.get_ui()
745 734 if s.section not in svn_sections]
746 735 assert sorted(result) == sorted(expected_result)
747 736
748 737 def test_repo_uis_are_overriding_global_uis(
749 738 self, repo_stub, settings_util):
750 739 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
751 740 settings_util.create_repo_rhodecode_ui(
752 741 repo_stub, section, 'repo', key=key, active=False)
753 742 model = VcsSettingsModel(repo=repo_stub.repo_name)
754 743 result = model.get_repo_ui_settings()
755 744 for setting in result:
756 745 locator = (setting.section, setting.key)
757 746 if locator in VcsSettingsModel.HOOKS_SETTINGS:
758 747 assert setting.value == 'repo'
759 748
760 749 assert setting.active is False
761 750
762 751 def test_global_svn_patterns_are_not_in_list(
763 752 self, repo_stub, settings_util):
764 753 svn_sections = (
765 754 VcsSettingsModel.SVN_TAG_SECTION,
766 755 VcsSettingsModel.SVN_BRANCH_SECTION)
767 756 for section in svn_sections:
768 757 settings_util.create_rhodecode_ui(
769 758 section, 'repo', key='deadbeef' + section, active=False)
770 759 Session().commit()
771 760
772 761 model = VcsSettingsModel(repo=repo_stub.repo_name)
773 762 result = model.get_repo_ui_settings()
774 763 for setting in result:
775 764 assert setting.section not in svn_sections
776 765
777 766 def test_repo_uis_filtered_by_section_are_returned(
778 767 self, repo_stub, settings_util):
779 768 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
780 769 settings_util.create_repo_rhodecode_ui(
781 770 repo_stub, section, 'repo', key=key, active=False)
782 771 model = VcsSettingsModel(repo=repo_stub.repo_name)
783 772 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
784 773 result = model.get_repo_ui_settings(section=section)
785 774 for setting in result:
786 775 assert setting.section == section
787 776
788 777 def test_repo_uis_filtered_by_key_are_returned(
789 778 self, repo_stub, settings_util):
790 779 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
791 780 settings_util.create_repo_rhodecode_ui(
792 781 repo_stub, section, 'repo', key=key, active=False)
793 782 model = VcsSettingsModel(repo=repo_stub.repo_name)
794 783 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
795 784 result = model.get_repo_ui_settings(key=key)
796 785 for setting in result:
797 786 assert setting.key == key
798 787
799 788 def test_raises_exception_when_repository_is_not_specified(self):
800 789 model = VcsSettingsModel()
801 790 with pytest.raises(Exception) as exc_info:
802 791 model.get_repo_ui_settings()
803 792 assert str(exc_info.value) == 'Repository is not specified'
804 793
805 794
806 795 class TestGetRepoGeneralSettings(object):
807 796 def test_global_settings_are_returned_when_no_repo_settings_found(
808 797 self, repo_stub):
809 798 model = VcsSettingsModel(repo=repo_stub.repo_name)
810 799 result = model.get_repo_general_settings()
811 800 expected_result = model.global_settings.get_all_settings()
812 801 assert sorted(result) == sorted(expected_result)
813 802
814 803 def test_repo_uis_are_overriding_global_uis(
815 804 self, repo_stub, settings_util):
816 805 for key in VcsSettingsModel.GENERAL_SETTINGS:
817 806 settings_util.create_repo_rhodecode_setting(
818 807 repo_stub, key, 'abcde', type_='unicode')
819 808 Session().commit()
820 809
821 810 model = VcsSettingsModel(repo=repo_stub.repo_name)
822 811 result = model.get_repo_ui_settings()
823 812 for key in result:
824 813 if key in VcsSettingsModel.GENERAL_SETTINGS:
825 814 assert result[key] == 'abcde'
826 815
827 816 def test_raises_exception_when_repository_is_not_specified(self):
828 817 model = VcsSettingsModel()
829 818 with pytest.raises(Exception) as exc_info:
830 819 model.get_repo_general_settings()
831 820 assert str(exc_info.value) == 'Repository is not specified'
832 821
833 822
834 823 class TestGetGlobalGeneralSettings(object):
835 824 def test_global_settings_are_returned(self, repo_stub):
836 825 model = VcsSettingsModel()
837 826 result = model.get_global_general_settings()
838 827 expected_result = model.global_settings.get_all_settings()
839 828 assert sorted(result) == sorted(expected_result)
840 829
841 830 def test_repo_uis_are_not_overriding_global_uis(
842 831 self, repo_stub, settings_util):
843 832 for key in VcsSettingsModel.GENERAL_SETTINGS:
844 833 settings_util.create_repo_rhodecode_setting(
845 834 repo_stub, key, 'abcde', type_='unicode')
846 835 Session().commit()
847 836
848 837 model = VcsSettingsModel(repo=repo_stub.repo_name)
849 838 result = model.get_global_general_settings()
850 839 expected_result = model.global_settings.get_all_settings()
851 840 assert sorted(result) == sorted(expected_result)
852 841
853 842
854 843 class TestGetGlobalUiSettings(object):
855 844 def test_global_uis_are_returned(self, repo_stub):
856 845 model = VcsSettingsModel()
857 846 result = model.get_global_ui_settings()
858 847 expected_result = model.global_settings.get_ui()
859 848 assert sorted(result) == sorted(expected_result)
860 849
861 850 def test_repo_uis_are_not_overriding_global_uis(
862 851 self, repo_stub, settings_util):
863 852 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
864 853 settings_util.create_repo_rhodecode_ui(
865 854 repo_stub, section, 'repo', key=key, active=False)
866 855 Session().commit()
867 856
868 857 model = VcsSettingsModel(repo=repo_stub.repo_name)
869 858 result = model.get_global_ui_settings()
870 859 expected_result = model.global_settings.get_ui()
871 860 assert sorted(result) == sorted(expected_result)
872 861
873 862 def test_ui_settings_filtered_by_section(
874 863 self, repo_stub, settings_util):
875 864 model = VcsSettingsModel(repo=repo_stub.repo_name)
876 865 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
877 866 result = model.get_global_ui_settings(section=section)
878 867 expected_result = model.global_settings.get_ui(section=section)
879 868 assert sorted(result) == sorted(expected_result)
880 869
881 870 def test_ui_settings_filtered_by_key(
882 871 self, repo_stub, settings_util):
883 872 model = VcsSettingsModel(repo=repo_stub.repo_name)
884 873 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
885 874 result = model.get_global_ui_settings(key=key)
886 875 expected_result = model.global_settings.get_ui(key=key)
887 876 assert sorted(result) == sorted(expected_result)
888 877
889 878
890 879 class TestGetGeneralSettings(object):
891 880 def test_global_settings_are_returned_when_inherited_is_true(
892 881 self, repo_stub, settings_util):
893 882 model = VcsSettingsModel(repo=repo_stub.repo_name)
894 883 model.inherit_global_settings = True
895 884 for key in VcsSettingsModel.GENERAL_SETTINGS:
896 885 settings_util.create_repo_rhodecode_setting(
897 886 repo_stub, key, 'abcde', type_='unicode')
898 887 Session().commit()
899 888
900 889 result = model.get_general_settings()
901 890 expected_result = model.get_global_general_settings()
902 891 assert sorted(result) == sorted(expected_result)
903 892
904 893 def test_repo_settings_are_returned_when_inherited_is_false(
905 894 self, repo_stub, settings_util):
906 895 model = VcsSettingsModel(repo=repo_stub.repo_name)
907 896 model.inherit_global_settings = False
908 897 for key in VcsSettingsModel.GENERAL_SETTINGS:
909 898 settings_util.create_repo_rhodecode_setting(
910 899 repo_stub, key, 'abcde', type_='unicode')
911 900 Session().commit()
912 901
913 902 result = model.get_general_settings()
914 903 expected_result = model.get_repo_general_settings()
915 904 assert sorted(result) == sorted(expected_result)
916 905
917 906 def test_global_settings_are_returned_when_no_repository_specified(self):
918 907 model = VcsSettingsModel()
919 908 result = model.get_general_settings()
920 909 expected_result = model.get_global_general_settings()
921 910 assert sorted(result) == sorted(expected_result)
922 911
923 912
924 913 class TestGetUiSettings(object):
925 914 def test_global_settings_are_returned_when_inherited_is_true(
926 915 self, repo_stub, settings_util):
927 916 model = VcsSettingsModel(repo=repo_stub.repo_name)
928 917 model.inherit_global_settings = True
929 918 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
930 919 settings_util.create_repo_rhodecode_ui(
931 920 repo_stub, section, 'repo', key=key, active=True)
932 921 Session().commit()
933 922
934 923 result = model.get_ui_settings()
935 924 expected_result = model.get_global_ui_settings()
936 925 assert sorted(result) == sorted(expected_result)
937 926
938 927 def test_repo_settings_are_returned_when_inherited_is_false(
939 928 self, repo_stub, settings_util):
940 929 model = VcsSettingsModel(repo=repo_stub.repo_name)
941 930 model.inherit_global_settings = False
942 931 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
943 932 settings_util.create_repo_rhodecode_ui(
944 933 repo_stub, section, 'repo', key=key, active=True)
945 934 Session().commit()
946 935
947 936 result = model.get_ui_settings()
948 937 expected_result = model.get_repo_ui_settings()
949 938 assert sorted(result) == sorted(expected_result)
950 939
951 940 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
952 941 model = VcsSettingsModel(repo=repo_stub.repo_name)
953 942 model.inherit_global_settings = False
954 943
955 944 args = ('section', 'key')
956 945 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
957 946 model.get_ui_settings(*args)
958 947 Session().commit()
959 948
960 949 settings_mock.assert_called_once_with(*args)
961 950
962 951 def test_global_settings_filtered_by_section_and_key(self):
963 952 model = VcsSettingsModel()
964 953 args = ('section', 'key')
965 954 with mock.patch.object(model, 'get_global_ui_settings') as (
966 955 settings_mock):
967 956 model.get_ui_settings(*args)
968 957 settings_mock.assert_called_once_with(*args)
969 958
970 959 def test_global_settings_are_returned_when_no_repository_specified(self):
971 960 model = VcsSettingsModel()
972 961 result = model.get_ui_settings()
973 962 expected_result = model.get_global_ui_settings()
974 963 assert sorted(result) == sorted(expected_result)
975 964
976 965
977 966 class TestGetSvnPatterns(object):
978 967 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
979 968 model = VcsSettingsModel(repo=repo_stub.repo_name)
980 969 args = ('section', )
981 970 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
982 971 model.get_svn_patterns(*args)
983 972
984 973 Session().commit()
985 974 settings_mock.assert_called_once_with(*args)
986 975
987 976 def test_global_settings_filtered_by_section_and_key(self):
988 977 model = VcsSettingsModel()
989 978 args = ('section', )
990 979 with mock.patch.object(model, 'get_global_ui_settings') as (
991 980 settings_mock):
992 981 model.get_svn_patterns(*args)
993 982 settings_mock.assert_called_once_with(*args)
994 983
995 984
996 985 class TestCreateOrUpdateRepoSettings(object):
997 986 FORM_DATA = {
998 987 'inherit_global_settings': False,
999 988 'hooks_changegroup_repo_size': False,
1000 989 'hooks_changegroup_push_logger': False,
1001 990 'hooks_outgoing_pull_logger': False,
1002 991 'extensions_largefiles': False,
1003 992 'extensions_evolve': False,
1004 993 'largefiles_usercache': '/example/largefiles-store',
1005 994 'vcs_git_lfs_enabled': False,
1006 995 'vcs_git_lfs_store_location': '/',
1007 996 'phases_publish': 'False',
1008 997 'rhodecode_pr_merge_enabled': False,
1009 998 'rhodecode_use_outdated_comments': False,
1010 999 'new_svn_branch': '',
1011 1000 'new_svn_tag': ''
1012 1001 }
1013 1002
1014 1003 def test_get_raises_exception_when_repository_not_specified(self):
1015 1004 model = VcsSettingsModel()
1016 1005 with pytest.raises(Exception) as exc_info:
1017 1006 model.create_or_update_repo_settings(data=self.FORM_DATA)
1018 1007 Session().commit()
1019 1008
1020 1009 assert str(exc_info.value) == 'Repository is not specified'
1021 1010
1022 1011 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
1023 1012 repo = backend_svn.create_repo()
1024 1013 model = VcsSettingsModel(repo=repo)
1025 1014 with self._patch_model(model) as mocks:
1026 1015 model.create_or_update_repo_settings(
1027 1016 data=self.FORM_DATA, inherit_global_settings=False)
1028 1017 Session().commit()
1029 1018
1030 1019 mocks['create_repo_svn_settings'].assert_called_once_with(
1031 1020 self.FORM_DATA)
1032 1021 non_called_methods = (
1033 1022 'create_or_update_repo_hook_settings',
1034 1023 'create_or_update_repo_pr_settings',
1035 1024 'create_or_update_repo_hg_settings')
1036 1025 for method in non_called_methods:
1037 1026 assert mocks[method].call_count == 0
1038 1027
1039 1028 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
1040 1029 repo = backend_hg.create_repo()
1041 1030 model = VcsSettingsModel(repo=repo)
1042 1031 with self._patch_model(model) as mocks:
1043 1032 model.create_or_update_repo_settings(
1044 1033 data=self.FORM_DATA, inherit_global_settings=False)
1045 1034 Session().commit()
1046 1035
1047 1036 assert mocks['create_repo_svn_settings'].call_count == 0
1048 1037 called_methods = (
1049 1038 'create_or_update_repo_hook_settings',
1050 1039 'create_or_update_repo_pr_settings',
1051 1040 'create_or_update_repo_hg_settings')
1052 1041 for method in called_methods:
1053 1042 mocks[method].assert_called_once_with(self.FORM_DATA)
1054 1043
1055 1044 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
1056 1045 self, backend_git):
1057 1046 repo = backend_git.create_repo()
1058 1047 model = VcsSettingsModel(repo=repo)
1059 1048 with self._patch_model(model) as mocks:
1060 1049 model.create_or_update_repo_settings(
1061 1050 data=self.FORM_DATA, inherit_global_settings=False)
1062 1051
1063 1052 assert mocks['create_repo_svn_settings'].call_count == 0
1064 1053 called_methods = (
1065 1054 'create_or_update_repo_hook_settings',
1066 1055 'create_or_update_repo_pr_settings')
1067 1056 non_called_methods = (
1068 1057 'create_repo_svn_settings',
1069 1058 'create_or_update_repo_hg_settings'
1070 1059 )
1071 1060 for method in called_methods:
1072 1061 mocks[method].assert_called_once_with(self.FORM_DATA)
1073 1062 for method in non_called_methods:
1074 1063 assert mocks[method].call_count == 0
1075 1064
1076 1065 def test_no_methods_are_called_when_settings_are_inherited(
1077 1066 self, backend):
1078 1067 repo = backend.create_repo()
1079 1068 model = VcsSettingsModel(repo=repo)
1080 1069 with self._patch_model(model) as mocks:
1081 1070 model.create_or_update_repo_settings(
1082 1071 data=self.FORM_DATA, inherit_global_settings=True)
1083 1072 for method_name in mocks:
1084 1073 assert mocks[method_name].call_count == 0
1085 1074
1086 1075 def test_cache_is_marked_for_invalidation(self, repo_stub):
1087 1076 model = VcsSettingsModel(repo=repo_stub)
1088 1077 invalidation_patcher = mock.patch(
1089 1078 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
1090 1079 with invalidation_patcher as invalidation_mock:
1091 1080 model.create_or_update_repo_settings(
1092 1081 data=self.FORM_DATA, inherit_global_settings=True)
1093 1082 Session().commit()
1094 1083
1095 1084 invalidation_mock.assert_called_once_with(
1096 1085 repo_stub.repo_name, delete=True)
1097 1086
1098 1087 def test_inherit_flag_is_saved(self, repo_stub):
1099 1088 model = VcsSettingsModel(repo=repo_stub)
1100 1089 model.inherit_global_settings = True
1101 1090 with self._patch_model(model):
1102 1091 model.create_or_update_repo_settings(
1103 1092 data=self.FORM_DATA, inherit_global_settings=False)
1104 1093 Session().commit()
1105 1094
1106 1095 assert model.inherit_global_settings is False
1107 1096
1108 1097 def _patch_model(self, model):
1109 1098 return mock.patch.multiple(
1110 1099 model,
1111 1100 create_repo_svn_settings=mock.DEFAULT,
1112 1101 create_or_update_repo_hook_settings=mock.DEFAULT,
1113 1102 create_or_update_repo_pr_settings=mock.DEFAULT,
1114 1103 create_or_update_repo_hg_settings=mock.DEFAULT)
General Comments 0
You need to be logged in to leave comments. Login now