##// END OF EJS Templates
apps: removed utf8 marker
super-admin -
r5053:8da271d0 default
parent child Browse files
Show More
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,858 +1,858 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23 import operator
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26 26
27 27 from rhodecode.lib import helpers as h, diffs, rc_cache
28 28 from rhodecode.lib.utils import repo_name_slug
29 29 from rhodecode.lib.utils2 import (
30 30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
31 31 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
32 32 from rhodecode.lib.vcs.backends.base import EmptyCommit
33 33 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
34 34 from rhodecode.model import repo
35 35 from rhodecode.model import repo_group
36 36 from rhodecode.model import user_group
37 37 from rhodecode.model import user
38 38 from rhodecode.model.db import User
39 39 from rhodecode.model.scm import ScmModel
40 40 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
41 41 from rhodecode.model.repo import ReadmeFinder
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 ADMIN_PREFIX = '/_admin'
47 47 STATIC_FILE_PREFIX = '/_static'
48 48
49 49 URL_NAME_REQUIREMENTS = {
50 50 # group name can have a slash in them, but they must not end with a slash
51 51 'group_name': r'.*?[^/]',
52 52 'repo_group_name': r'.*?[^/]',
53 53 # repo names can have a slash in them, but they must not end with a slash
54 54 'repo_name': r'.*?[^/]',
55 55 # file path eats up everything at the end
56 56 'f_path': r'.*',
57 57 # reference types
58 58 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
59 59 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
60 60 }
61 61
62 62
63 63 def add_route_with_slash(config,name, pattern, **kw):
64 64 config.add_route(name, pattern, **kw)
65 65 if not pattern.endswith('/'):
66 66 config.add_route(name + '_slash', pattern + '/', **kw)
67 67
68 68
69 69 def add_route_requirements(route_path, requirements=None):
70 70 """
71 71 Adds regex requirements to pyramid routes using a mapping dict
72 72 e.g::
73 73 add_route_requirements('{repo_name}/settings')
74 74 """
75 75 requirements = requirements or URL_NAME_REQUIREMENTS
76 76 for key, regex in requirements.items():
77 77 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
78 78 return route_path
79 79
80 80
81 81 def get_format_ref_id(repo):
82 82 """Returns a `repo` specific reference formatter function"""
83 83 if h.is_svn(repo):
84 84 return _format_ref_id_svn
85 85 else:
86 86 return _format_ref_id
87 87
88 88
89 89 def _format_ref_id(name, raw_id):
90 90 """Default formatting of a given reference `name`"""
91 91 return name
92 92
93 93
94 94 def _format_ref_id_svn(name, raw_id):
95 95 """Special way of formatting a reference for Subversion including path"""
96 96 return '%s@%s' % (name, raw_id)
97 97
98 98
99 99 class TemplateArgs(StrictAttributeDict):
100 100 pass
101 101
102 102
103 103 class BaseAppView(object):
104 104
105 105 def __init__(self, context, request):
106 106 self.request = request
107 107 self.context = context
108 108 self.session = request.session
109 109 if not hasattr(request, 'user'):
110 110 # NOTE(marcink): edge case, we ended up in matched route
111 111 # but probably of web-app context, e.g API CALL/VCS CALL
112 112 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
113 113 log.warning('Unable to process request `%s` in this scope', request)
114 114 raise HTTPBadRequest()
115 115
116 116 self._rhodecode_user = request.user # auth user
117 117 self._rhodecode_db_user = self._rhodecode_user.get_instance()
118 118 self._maybe_needs_password_change(
119 119 request.matched_route.name, self._rhodecode_db_user)
120 120
121 121 def _maybe_needs_password_change(self, view_name, user_obj):
122 122
123 123 dont_check_views = [
124 124 'channelstream_connect',
125 125 'ops_ping'
126 126 ]
127 127 if view_name in dont_check_views:
128 128 return
129 129
130 130 log.debug('Checking if user %s needs password change on view %s',
131 131 user_obj, view_name)
132 132
133 133 skip_user_views = [
134 134 'logout', 'login',
135 135 'my_account_password', 'my_account_password_update'
136 136 ]
137 137
138 138 if not user_obj:
139 139 return
140 140
141 141 if user_obj.username == User.DEFAULT_USER:
142 142 return
143 143
144 144 now = time.time()
145 145 should_change = user_obj.user_data.get('force_password_change')
146 146 change_after = safe_int(should_change) or 0
147 147 if should_change and now > change_after:
148 148 log.debug('User %s requires password change', user_obj)
149 149 h.flash('You are required to change your password', 'warning',
150 150 ignore_duplicate=True)
151 151
152 152 if view_name not in skip_user_views:
153 153 raise HTTPFound(
154 154 self.request.route_path('my_account_password'))
155 155
156 156 def _log_creation_exception(self, e, repo_name):
157 157 _ = self.request.translate
158 158 reason = None
159 159 if len(e.args) == 2:
160 160 reason = e.args[1]
161 161
162 162 if reason == 'INVALID_CERTIFICATE':
163 163 log.exception(
164 164 'Exception creating a repository: invalid certificate')
165 165 msg = (_('Error creating repository %s: invalid certificate')
166 166 % repo_name)
167 167 else:
168 168 log.exception("Exception creating a repository")
169 169 msg = (_('Error creating repository %s')
170 170 % repo_name)
171 171 return msg
172 172
173 173 def _get_local_tmpl_context(self, include_app_defaults=True):
174 174 c = TemplateArgs()
175 175 c.auth_user = self.request.user
176 176 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
177 177 c.rhodecode_user = self.request.user
178 178
179 179 if include_app_defaults:
180 180 from rhodecode.lib.base import attach_context_attributes
181 181 attach_context_attributes(c, self.request, self.request.user.user_id)
182 182
183 183 c.is_super_admin = c.auth_user.is_admin
184 184
185 185 c.can_create_repo = c.is_super_admin
186 186 c.can_create_repo_group = c.is_super_admin
187 187 c.can_create_user_group = c.is_super_admin
188 188
189 189 c.is_delegated_admin = False
190 190
191 191 if not c.auth_user.is_default and not c.is_super_admin:
192 192 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
193 193 user=self.request.user)
194 194 repositories = c.auth_user.repositories_admin or c.can_create_repo
195 195
196 196 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
197 197 user=self.request.user)
198 198 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
199 199
200 200 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
201 201 user=self.request.user)
202 202 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
203 203 # delegated admin can create, or manage some objects
204 204 c.is_delegated_admin = repositories or repository_groups or user_groups
205 205 return c
206 206
207 207 def _get_template_context(self, tmpl_args, **kwargs):
208 208
209 209 local_tmpl_args = {
210 210 'defaults': {},
211 211 'errors': {},
212 212 'c': tmpl_args
213 213 }
214 214 local_tmpl_args.update(kwargs)
215 215 return local_tmpl_args
216 216
217 217 def load_default_context(self):
218 218 """
219 219 example:
220 220
221 221 def load_default_context(self):
222 222 c = self._get_local_tmpl_context()
223 223 c.custom_var = 'foobar'
224 224
225 225 return c
226 226 """
227 227 raise NotImplementedError('Needs implementation in view class')
228 228
229 229
230 230 class RepoAppView(BaseAppView):
231 231
232 232 def __init__(self, context, request):
233 233 super(RepoAppView, self).__init__(context, request)
234 234 self.db_repo = request.db_repo
235 235 self.db_repo_name = self.db_repo.repo_name
236 236 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
237 237 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
238 238 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
239 239
240 240 def _handle_missing_requirements(self, error):
241 241 log.error(
242 242 'Requirements are missing for repository %s: %s',
243 243 self.db_repo_name, safe_unicode(error))
244 244
245 245 def _prepare_and_set_clone_url(self, c):
246 246 username = ''
247 247 if self._rhodecode_user.username != User.DEFAULT_USER:
248 248 username = self._rhodecode_user.username
249 249
250 250 _def_clone_uri = c.clone_uri_tmpl
251 251 _def_clone_uri_id = c.clone_uri_id_tmpl
252 252 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
253 253
254 254 c.clone_repo_url = self.db_repo.clone_url(
255 255 user=username, uri_tmpl=_def_clone_uri)
256 256 c.clone_repo_url_id = self.db_repo.clone_url(
257 257 user=username, uri_tmpl=_def_clone_uri_id)
258 258 c.clone_repo_url_ssh = self.db_repo.clone_url(
259 259 uri_tmpl=_def_clone_uri_ssh, ssh=True)
260 260
261 261 def _get_local_tmpl_context(self, include_app_defaults=True):
262 262 _ = self.request.translate
263 263 c = super(RepoAppView, self)._get_local_tmpl_context(
264 264 include_app_defaults=include_app_defaults)
265 265
266 266 # register common vars for this type of view
267 267 c.rhodecode_db_repo = self.db_repo
268 268 c.repo_name = self.db_repo_name
269 269 c.repository_pull_requests = self.db_repo_pull_requests
270 270 c.repository_artifacts = self.db_repo_artifacts
271 271 c.repository_is_user_following = ScmModel().is_following_repo(
272 272 self.db_repo_name, self._rhodecode_user.user_id)
273 273 self.path_filter = PathFilter(None)
274 274
275 275 c.repository_requirements_missing = {}
276 276 try:
277 277 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
278 278 # NOTE(marcink):
279 279 # comparison to None since if it's an object __bool__ is expensive to
280 280 # calculate
281 281 if self.rhodecode_vcs_repo is not None:
282 282 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
283 283 c.auth_user.username)
284 284 self.path_filter = PathFilter(path_perms)
285 285 except RepositoryRequirementError as e:
286 286 c.repository_requirements_missing = {'error': str(e)}
287 287 self._handle_missing_requirements(e)
288 288 self.rhodecode_vcs_repo = None
289 289
290 290 c.path_filter = self.path_filter # used by atom_feed_entry.mako
291 291
292 292 if self.rhodecode_vcs_repo is None:
293 293 # unable to fetch this repo as vcs instance, report back to user
294 294 log.debug('Repository was not found on filesystem, check if it exists or is not damaged')
295 295 h.flash(_(
296 296 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
297 297 "Please check if it exist, or is not damaged.") %
298 298 {'repo_name': c.repo_name},
299 299 category='error', ignore_duplicate=True)
300 300 if c.repository_requirements_missing:
301 301 route = self.request.matched_route.name
302 302 if route.startswith(('edit_repo', 'repo_summary')):
303 303 # allow summary and edit repo on missing requirements
304 304 return c
305 305
306 306 raise HTTPFound(
307 307 h.route_path('repo_summary', repo_name=self.db_repo_name))
308 308
309 309 else: # redirect if we don't show missing requirements
310 310 raise HTTPFound(h.route_path('home'))
311 311
312 312 c.has_origin_repo_read_perm = False
313 313 if self.db_repo.fork:
314 314 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
315 315 'repository.write', 'repository.read', 'repository.admin')(
316 316 self.db_repo.fork.repo_name, 'summary fork link')
317 317
318 318 return c
319 319
320 320 def _get_f_path_unchecked(self, matchdict, default=None):
321 321 """
322 322 Should only be used by redirects, everything else should call _get_f_path
323 323 """
324 324 f_path = matchdict.get('f_path')
325 325 if f_path:
326 326 # fix for multiple initial slashes that causes errors for GIT
327 327 return f_path.lstrip('/')
328 328
329 329 return default
330 330
331 331 def _get_f_path(self, matchdict, default=None):
332 332 f_path_match = self._get_f_path_unchecked(matchdict, default)
333 333 return self.path_filter.assert_path_permissions(f_path_match)
334 334
335 335 def _get_general_setting(self, target_repo, settings_key, default=False):
336 336 settings_model = VcsSettingsModel(repo=target_repo)
337 337 settings = settings_model.get_general_settings()
338 338 return settings.get(settings_key, default)
339 339
340 340 def _get_repo_setting(self, target_repo, settings_key, default=False):
341 341 settings_model = VcsSettingsModel(repo=target_repo)
342 342 settings = settings_model.get_repo_settings_inherited()
343 343 return settings.get(settings_key, default)
344 344
345 345 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
346 346 log.debug('Looking for README file at path %s', path)
347 347 if commit_id:
348 348 landing_commit_id = commit_id
349 349 else:
350 350 landing_commit = db_repo.get_landing_commit()
351 351 if isinstance(landing_commit, EmptyCommit):
352 352 return None, None
353 353 landing_commit_id = landing_commit.raw_id
354 354
355 355 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
356 356 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
357 357 start = time.time()
358 358
359 359 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
360 360 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
361 361 readme_data = None
362 362 readme_filename = None
363 363
364 364 commit = db_repo.get_commit(_commit_id)
365 365 log.debug("Searching for a README file at commit %s.", _commit_id)
366 366 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
367 367
368 368 if readme_node:
369 369 log.debug('Found README node: %s', readme_node)
370 370 relative_urls = {
371 371 'raw': h.route_path(
372 372 'repo_file_raw', repo_name=_repo_name,
373 373 commit_id=commit.raw_id, f_path=readme_node.path),
374 374 'standard': h.route_path(
375 375 'repo_files', repo_name=_repo_name,
376 376 commit_id=commit.raw_id, f_path=readme_node.path),
377 377 }
378 378
379 379 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
380 380 readme_filename = readme_node.unicode_path
381 381
382 382 return readme_data, readme_filename
383 383
384 384 readme_data, readme_filename = generate_repo_readme(
385 385 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
386 386
387 387 compute_time = time.time() - start
388 388 log.debug('Repo README for path %s generated and computed in %.4fs',
389 389 path, compute_time)
390 390 return readme_data, readme_filename
391 391
392 392 def _render_readme_or_none(self, commit, readme_node, relative_urls):
393 393 log.debug('Found README file `%s` rendering...', readme_node.path)
394 394 renderer = MarkupRenderer()
395 395 try:
396 396 html_source = renderer.render(
397 397 readme_node.content, filename=readme_node.path)
398 398 if relative_urls:
399 399 return relative_links(html_source, relative_urls)
400 400 return html_source
401 401 except Exception:
402 402 log.exception(
403 403 "Exception while trying to render the README")
404 404
405 405 def get_recache_flag(self):
406 406 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
407 407 flag_val = self.request.GET.get(flag_name)
408 408 if str2bool(flag_val):
409 409 return True
410 410 return False
411 411
412 412 def get_commit_preload_attrs(cls):
413 413 pre_load = ['author', 'branch', 'date', 'message', 'parents',
414 414 'obsolete', 'phase', 'hidden']
415 415 return pre_load
416 416
417 417
418 418 class PathFilter(object):
419 419
420 420 # Expects and instance of BasePathPermissionChecker or None
421 421 def __init__(self, permission_checker):
422 422 self.permission_checker = permission_checker
423 423
424 424 def assert_path_permissions(self, path):
425 425 if self.path_access_allowed(path):
426 426 return path
427 427 raise HTTPForbidden()
428 428
429 429 def path_access_allowed(self, path):
430 430 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
431 431 if self.permission_checker:
432 432 has_access = path and self.permission_checker.has_access(path)
433 433 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
434 434 return has_access
435 435
436 436 log.debug('ACL permissions checker not enabled, skipping...')
437 437 return True
438 438
439 439 def filter_patchset(self, patchset):
440 440 if not self.permission_checker or not patchset:
441 441 return patchset, False
442 442 had_filtered = False
443 443 filtered_patchset = []
444 444 for patch in patchset:
445 445 filename = patch.get('filename', None)
446 446 if not filename or self.permission_checker.has_access(filename):
447 447 filtered_patchset.append(patch)
448 448 else:
449 449 had_filtered = True
450 450 if had_filtered:
451 451 if isinstance(patchset, diffs.LimitedDiffContainer):
452 452 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
453 453 return filtered_patchset, True
454 454 else:
455 455 return patchset, False
456 456
457 457 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
458 458 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
459 459 result = diffset.render_patchset(
460 460 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
461 461 result.has_hidden_changes = has_hidden_changes
462 462 return result
463 463
464 464 def get_raw_patch(self, diff_processor):
465 465 if self.permission_checker is None:
466 466 return diff_processor.as_raw()
467 467 elif self.permission_checker.has_full_access:
468 468 return diff_processor.as_raw()
469 469 else:
470 470 return '# Repository has user-specific filters, raw patch generation is disabled.'
471 471
472 472 @property
473 473 def is_enabled(self):
474 474 return self.permission_checker is not None
475 475
476 476
477 477 class RepoGroupAppView(BaseAppView):
478 478 def __init__(self, context, request):
479 479 super(RepoGroupAppView, self).__init__(context, request)
480 480 self.db_repo_group = request.db_repo_group
481 481 self.db_repo_group_name = self.db_repo_group.group_name
482 482
483 483 def _get_local_tmpl_context(self, include_app_defaults=True):
484 484 _ = self.request.translate
485 485 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
486 486 include_app_defaults=include_app_defaults)
487 487 c.repo_group = self.db_repo_group
488 488 return c
489 489
490 490 def _revoke_perms_on_yourself(self, form_result):
491 491 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
492 492 form_result['perm_updates'])
493 493 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
494 494 form_result['perm_additions'])
495 495 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
496 496 form_result['perm_deletions'])
497 497 admin_perm = 'group.admin'
498 498 if _updates and _updates[0][1] != admin_perm or \
499 499 _additions and _additions[0][1] != admin_perm or \
500 500 _deletions and _deletions[0][1] != admin_perm:
501 501 return True
502 502 return False
503 503
504 504
505 505 class UserGroupAppView(BaseAppView):
506 506 def __init__(self, context, request):
507 507 super(UserGroupAppView, self).__init__(context, request)
508 508 self.db_user_group = request.db_user_group
509 509 self.db_user_group_name = self.db_user_group.users_group_name
510 510
511 511
512 512 class UserAppView(BaseAppView):
513 513 def __init__(self, context, request):
514 514 super(UserAppView, self).__init__(context, request)
515 515 self.db_user = request.db_user
516 516 self.db_user_id = self.db_user.user_id
517 517
518 518 _ = self.request.translate
519 519 if not request.db_user_supports_default:
520 520 if self.db_user.username == User.DEFAULT_USER:
521 521 h.flash(_("Editing user `{}` is disabled.".format(
522 522 User.DEFAULT_USER)), category='warning')
523 523 raise HTTPFound(h.route_path('users'))
524 524
525 525
526 526 class DataGridAppView(object):
527 527 """
528 528 Common class to have re-usable grid rendering components
529 529 """
530 530
531 531 def _extract_ordering(self, request, column_map=None):
532 532 column_map = column_map or {}
533 533 column_index = safe_int(request.GET.get('order[0][column]'))
534 534 order_dir = request.GET.get(
535 535 'order[0][dir]', 'desc')
536 536 order_by = request.GET.get(
537 537 'columns[%s][data][sort]' % column_index, 'name_raw')
538 538
539 539 # translate datatable to DB columns
540 540 order_by = column_map.get(order_by) or order_by
541 541
542 542 search_q = request.GET.get('search[value]')
543 543 return search_q, order_by, order_dir
544 544
545 545 def _extract_chunk(self, request):
546 546 start = safe_int(request.GET.get('start'), 0)
547 547 length = safe_int(request.GET.get('length'), 25)
548 548 draw = safe_int(request.GET.get('draw'))
549 549 return draw, start, length
550 550
551 551 def _get_order_col(self, order_by, model):
552 552 if isinstance(order_by, str):
553 553 try:
554 554 return operator.attrgetter(order_by)(model)
555 555 except AttributeError:
556 556 return None
557 557 else:
558 558 return order_by
559 559
560 560
561 561 class BaseReferencesView(RepoAppView):
562 562 """
563 563 Base for reference view for branches, tags and bookmarks.
564 564 """
565 565 def load_default_context(self):
566 566 c = self._get_local_tmpl_context()
567 567 return c
568 568
569 569 def load_refs_context(self, ref_items, partials_template):
570 570 _render = self.request.get_partial_renderer(partials_template)
571 571 pre_load = ["author", "date", "message", "parents"]
572 572
573 573 is_svn = h.is_svn(self.rhodecode_vcs_repo)
574 574 is_hg = h.is_hg(self.rhodecode_vcs_repo)
575 575
576 576 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
577 577
578 578 closed_refs = {}
579 579 if is_hg:
580 580 closed_refs = self.rhodecode_vcs_repo.branches_closed
581 581
582 582 data = []
583 583 for ref_name, commit_id in ref_items:
584 584 commit = self.rhodecode_vcs_repo.get_commit(
585 585 commit_id=commit_id, pre_load=pre_load)
586 586 closed = ref_name in closed_refs
587 587
588 588 # TODO: johbo: Unify generation of reference links
589 589 use_commit_id = '/' in ref_name or is_svn
590 590
591 591 if use_commit_id:
592 592 files_url = h.route_path(
593 593 'repo_files',
594 594 repo_name=self.db_repo_name,
595 595 f_path=ref_name if is_svn else '',
596 596 commit_id=commit_id,
597 597 _query=dict(at=ref_name)
598 598 )
599 599
600 600 else:
601 601 files_url = h.route_path(
602 602 'repo_files',
603 603 repo_name=self.db_repo_name,
604 604 f_path=ref_name if is_svn else '',
605 605 commit_id=ref_name,
606 606 _query=dict(at=ref_name)
607 607 )
608 608
609 609 data.append({
610 610 "name": _render('name', ref_name, files_url, closed),
611 611 "name_raw": ref_name,
612 612 "date": _render('date', commit.date),
613 613 "date_raw": datetime_to_time(commit.date),
614 614 "author": _render('author', commit.author),
615 615 "commit": _render(
616 616 'commit', commit.message, commit.raw_id, commit.idx),
617 617 "commit_raw": commit.idx,
618 618 "compare": _render(
619 619 'compare', format_ref_id(ref_name, commit.raw_id)),
620 620 })
621 621
622 622 return data
623 623
624 624
625 625 class RepoRoutePredicate(object):
626 626 def __init__(self, val, config):
627 627 self.val = val
628 628
629 629 def text(self):
630 630 return 'repo_route = %s' % self.val
631 631
632 632 phash = text
633 633
634 634 def __call__(self, info, request):
635 635 if hasattr(request, 'vcs_call'):
636 636 # skip vcs calls
637 637 return
638 638
639 639 repo_name = info['match']['repo_name']
640 640
641 641 repo_name_parts = repo_name.split('/')
642 642 repo_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_name_parts)]
643 643
644 644 if repo_name_parts != repo_slugs:
645 645 # short-skip if the repo-name doesn't follow slug rule
646 646 log.warning('repo_name: %s is different than slug %s', repo_name_parts, repo_slugs)
647 647 return False
648 648
649 649 repo_model = repo.RepoModel()
650 650
651 651 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
652 652
653 653 def redirect_if_creating(route_info, db_repo):
654 654 skip_views = ['edit_repo_advanced_delete']
655 655 route = route_info['route']
656 656 # we should skip delete view so we can actually "remove" repositories
657 657 # if they get stuck in creating state.
658 658 if route.name in skip_views:
659 659 return
660 660
661 661 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
662 662 repo_creating_url = request.route_path(
663 663 'repo_creating', repo_name=db_repo.repo_name)
664 664 raise HTTPFound(repo_creating_url)
665 665
666 666 if by_name_match:
667 667 # register this as request object we can re-use later
668 668 request.db_repo = by_name_match
669 669 redirect_if_creating(info, by_name_match)
670 670 return True
671 671
672 672 by_id_match = repo_model.get_repo_by_id(repo_name)
673 673 if by_id_match:
674 674 request.db_repo = by_id_match
675 675 redirect_if_creating(info, by_id_match)
676 676 return True
677 677
678 678 return False
679 679
680 680
681 681 class RepoForbidArchivedRoutePredicate(object):
682 682 def __init__(self, val, config):
683 683 self.val = val
684 684
685 685 def text(self):
686 686 return 'repo_forbid_archived = %s' % self.val
687 687
688 688 phash = text
689 689
690 690 def __call__(self, info, request):
691 691 _ = request.translate
692 692 rhodecode_db_repo = request.db_repo
693 693
694 694 log.debug(
695 695 '%s checking if archived flag for repo for %s',
696 696 self.__class__.__name__, rhodecode_db_repo.repo_name)
697 697
698 698 if rhodecode_db_repo.archived:
699 699 log.warning('Current view is not supported for archived repo:%s',
700 700 rhodecode_db_repo.repo_name)
701 701
702 702 h.flash(
703 703 h.literal(_('Action not supported for archived repository.')),
704 704 category='warning')
705 705 summary_url = request.route_path(
706 706 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
707 707 raise HTTPFound(summary_url)
708 708 return True
709 709
710 710
711 711 class RepoTypeRoutePredicate(object):
712 712 def __init__(self, val, config):
713 713 self.val = val or ['hg', 'git', 'svn']
714 714
715 715 def text(self):
716 716 return 'repo_accepted_type = %s' % self.val
717 717
718 718 phash = text
719 719
720 720 def __call__(self, info, request):
721 721 if hasattr(request, 'vcs_call'):
722 722 # skip vcs calls
723 723 return
724 724
725 725 rhodecode_db_repo = request.db_repo
726 726
727 727 log.debug(
728 728 '%s checking repo type for %s in %s',
729 729 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
730 730
731 731 if rhodecode_db_repo.repo_type in self.val:
732 732 return True
733 733 else:
734 734 log.warning('Current view is not supported for repo type:%s',
735 735 rhodecode_db_repo.repo_type)
736 736 return False
737 737
738 738
739 739 class RepoGroupRoutePredicate(object):
740 740 def __init__(self, val, config):
741 741 self.val = val
742 742
743 743 def text(self):
744 744 return 'repo_group_route = %s' % self.val
745 745
746 746 phash = text
747 747
748 748 def __call__(self, info, request):
749 749 if hasattr(request, 'vcs_call'):
750 750 # skip vcs calls
751 751 return
752 752
753 753 repo_group_name = info['match']['repo_group_name']
754 754
755 755 repo_group_name_parts = repo_group_name.split('/')
756 756 repo_group_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_group_name_parts)]
757 757 if repo_group_name_parts != repo_group_slugs:
758 758 # short-skip if the repo-name doesn't follow slug rule
759 759 log.warning('repo_group_name: %s is different than slug %s', repo_group_name_parts, repo_group_slugs)
760 760 return False
761 761
762 762 repo_group_model = repo_group.RepoGroupModel()
763 763 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
764 764
765 765 if by_name_match:
766 766 # register this as request object we can re-use later
767 767 request.db_repo_group = by_name_match
768 768 return True
769 769
770 770 return False
771 771
772 772
773 773 class UserGroupRoutePredicate(object):
774 774 def __init__(self, val, config):
775 775 self.val = val
776 776
777 777 def text(self):
778 778 return 'user_group_route = %s' % self.val
779 779
780 780 phash = text
781 781
782 782 def __call__(self, info, request):
783 783 if hasattr(request, 'vcs_call'):
784 784 # skip vcs calls
785 785 return
786 786
787 787 user_group_id = info['match']['user_group_id']
788 788 user_group_model = user_group.UserGroup()
789 789 by_id_match = user_group_model.get(user_group_id, cache=False)
790 790
791 791 if by_id_match:
792 792 # register this as request object we can re-use later
793 793 request.db_user_group = by_id_match
794 794 return True
795 795
796 796 return False
797 797
798 798
799 799 class UserRoutePredicateBase(object):
800 800 supports_default = None
801 801
802 802 def __init__(self, val, config):
803 803 self.val = val
804 804
805 805 def text(self):
806 806 raise NotImplementedError()
807 807
808 808 def __call__(self, info, request):
809 809 if hasattr(request, 'vcs_call'):
810 810 # skip vcs calls
811 811 return
812 812
813 813 user_id = info['match']['user_id']
814 814 user_model = user.User()
815 815 by_id_match = user_model.get(user_id, cache=False)
816 816
817 817 if by_id_match:
818 818 # register this as request object we can re-use later
819 819 request.db_user = by_id_match
820 820 request.db_user_supports_default = self.supports_default
821 821 return True
822 822
823 823 return False
824 824
825 825
826 826 class UserRoutePredicate(UserRoutePredicateBase):
827 827 supports_default = False
828 828
829 829 def text(self):
830 830 return 'user_route = %s' % self.val
831 831
832 832 phash = text
833 833
834 834
835 835 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
836 836 supports_default = True
837 837
838 838 def text(self):
839 839 return 'user_with_default_route = %s' % self.val
840 840
841 841 phash = text
842 842
843 843
844 844 def includeme(config):
845 845 config.add_route_predicate(
846 846 'repo_route', RepoRoutePredicate)
847 847 config.add_route_predicate(
848 848 'repo_accepted_types', RepoTypeRoutePredicate)
849 849 config.add_route_predicate(
850 850 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
851 851 config.add_route_predicate(
852 852 'repo_group_route', RepoGroupRoutePredicate)
853 853 config.add_route_predicate(
854 854 'user_group_route', UserGroupRoutePredicate)
855 855 config.add_route_predicate(
856 856 'user_route_with_default', UserRouteWithDefaultPredicate)
857 857 config.add_route_predicate(
858 858 'user_route', UserRoutePredicate)
@@ -1,29 +1,29 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from zope.interface import Interface
22 22
23 23
24 24 class IAdminNavigationRegistry(Interface):
25 25 """
26 26 Interface for the admin navigation registry. Currently this is only
27 27 used to register and retrieve it via pyramids registry.
28 28 """
29 29 pass
@@ -1,148 +1,147 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2016-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20
22 21 import logging
23 22 import collections
24 23
25 24 from zope.interface import implementer
26 25
27 26 from rhodecode.apps._base.interfaces import IAdminNavigationRegistry
28 27 from rhodecode.lib.utils2 import str2bool
29 28 from rhodecode.translation import _
30 29
31 30
32 31 log = logging.getLogger(__name__)
33 32
34 33 NavListEntry = collections.namedtuple(
35 34 'NavListEntry', ['key', 'name', 'url', 'active_list'])
36 35
37 36
38 37 class NavEntry(object):
39 38 """
40 39 Represents an entry in the admin navigation.
41 40
42 41 :param key: Unique identifier used to store reference in an OrderedDict.
43 42 :param name: Display name, usually a translation string.
44 43 :param view_name: Name of the view, used generate the URL.
45 44 :param active_list: list of urls that we select active for this element
46 45 """
47 46
48 47 def __init__(self, key, name, view_name, active_list=None):
49 48 self.key = key
50 49 self.name = name
51 50 self.view_name = view_name
52 51 self._active_list = active_list or []
53 52
54 53 def generate_url(self, request):
55 54 return request.route_path(self.view_name)
56 55
57 56 def get_localized_name(self, request):
58 57 return request.translate(self.name)
59 58
60 59 @property
61 60 def active_list(self):
62 61 active_list = [self.key]
63 62 if self._active_list:
64 63 active_list = self._active_list
65 64 return active_list
66 65
67 66
68 67 @implementer(IAdminNavigationRegistry)
69 68 class NavigationRegistry(object):
70 69
71 70 _base_entries = [
72 71 NavEntry('global', _('Global'),
73 72 'admin_settings_global'),
74 73 NavEntry('vcs', _('VCS'),
75 74 'admin_settings_vcs'),
76 75 NavEntry('visual', _('Visual'),
77 76 'admin_settings_visual'),
78 77 NavEntry('mapping', _('Remap and Rescan'),
79 78 'admin_settings_mapping'),
80 79 NavEntry('issuetracker', _('Issue Tracker'),
81 80 'admin_settings_issuetracker'),
82 81 NavEntry('email', _('Email'),
83 82 'admin_settings_email'),
84 83 NavEntry('hooks', _('Hooks'),
85 84 'admin_settings_hooks'),
86 85 NavEntry('search', _('Full Text Search'),
87 86 'admin_settings_search'),
88 87 NavEntry('system', _('System Info'),
89 88 'admin_settings_system'),
90 89 NavEntry('exceptions', _('Exceptions Tracker'),
91 90 'admin_settings_exception_tracker',
92 91 active_list=['exceptions', 'exceptions_browse']),
93 92 NavEntry('process_management', _('Processes'),
94 93 'admin_settings_process_management'),
95 94 NavEntry('sessions', _('User Sessions'),
96 95 'admin_settings_sessions'),
97 96 NavEntry('open_source', _('Open Source Licenses'),
98 97 'admin_settings_open_source'),
99 98 NavEntry('automation', _('Automation'),
100 99 'admin_settings_automation')
101 100 ]
102 101
103 102 _labs_entry = NavEntry('labs', _('Labs'),
104 103 'admin_settings_labs')
105 104
106 105 def __init__(self, labs_active=False):
107 106 self._registered_entries = collections.OrderedDict()
108 107 for item in self.__class__._base_entries:
109 108 self._registered_entries[item.key] = item
110 109
111 110 if labs_active:
112 111 self.add_entry(self._labs_entry)
113 112
114 113 def add_entry(self, entry):
115 114 self._registered_entries[entry.key] = entry
116 115
117 116 def get_navlist(self, request):
118 117 nav_list = [
119 118 NavListEntry(i.key, i.get_localized_name(request),
120 119 i.generate_url(request), i.active_list)
121 120 for i in self._registered_entries.values()]
122 121 return nav_list
123 122
124 123
125 124 def navigation_registry(request, registry=None):
126 125 """
127 126 Helper that returns the admin navigation registry.
128 127 """
129 128 pyramid_registry = registry or request.registry
130 129 nav_registry = pyramid_registry.queryUtility(IAdminNavigationRegistry)
131 130 return nav_registry
132 131
133 132
134 133 def navigation_list(request):
135 134 """
136 135 Helper that returns the admin navigation as list of NavListEntry objects.
137 136 """
138 137 return navigation_registry(request).get_navlist(request)
139 138
140 139
141 140 def includeme(config):
142 141 # Create admin navigation registry and add it to the pyramid registry.
143 142 settings = config.get_settings()
144 143 labs_active = str2bool(settings.get('labs_settings_active', False))
145 144 navigation_registry_instance = NavigationRegistry(labs_active=labs_active)
146 145 config.registry.registerUtility(navigation_registry_instance)
147 146 log.debug('Created new navigation instance, %s', navigation_registry_instance)
148 147
@@ -1,56 +1,56 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from rhodecode import events
24 24 from rhodecode.lib import rc_cache
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28 # names of namespaces used for different permission related cached
29 29 # during flush operation we need to take care of all those
30 30 cache_namespaces = [
31 31 'cache_user_auth.{}',
32 32 'cache_user_repo_acl_ids.{}',
33 33 'cache_user_user_group_acl_ids.{}',
34 34 'cache_user_repo_group_acl_ids.{}'
35 35 ]
36 36
37 37
38 38 def trigger_user_permission_flush(event):
39 39 """
40 40 Subscriber to the `UserPermissionsChange`. This triggers the
41 41 automatic flush of permission caches, so the users affected receive new permissions
42 42 Right Away
43 43 """
44 44 invalidate = True
45 45 affected_user_ids = set(event.user_ids)
46 46 for user_id in affected_user_ids:
47 47 for cache_namespace_uid_tmpl in cache_namespaces:
48 48 cache_namespace_uid = cache_namespace_uid_tmpl.format(user_id)
49 49 del_keys = rc_cache.clear_cache_namespace(
50 50 'cache_perms', cache_namespace_uid, invalidate=invalidate)
51 51 log.debug('Invalidated %s cache keys for user_id: %s and namespace %s',
52 52 del_keys, user_id, cache_namespace_uid)
53 53
54 54
55 55 def includeme(config):
56 56 config.add_subscriber(trigger_user_permission_flush, events.UserPermissionsChange)
@@ -1,1084 +1,1084 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def admin_routes(config):
26 26 """
27 27 Admin prefixed routes
28 28 """
29 29 from rhodecode.apps.admin.views.audit_logs import AdminAuditLogsView
30 30 from rhodecode.apps.admin.views.artifacts import AdminArtifactsView
31 31 from rhodecode.apps.admin.views.defaults import AdminDefaultSettingsView
32 32 from rhodecode.apps.admin.views.exception_tracker import ExceptionsTrackerView
33 33 from rhodecode.apps.admin.views.main_views import AdminMainView
34 34 from rhodecode.apps.admin.views.open_source_licenses import OpenSourceLicensesAdminSettingsView
35 35 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
36 36 from rhodecode.apps.admin.views.process_management import AdminProcessManagementView
37 37 from rhodecode.apps.admin.views.repo_groups import AdminRepoGroupsView
38 38 from rhodecode.apps.admin.views.repositories import AdminReposView
39 39 from rhodecode.apps.admin.views.sessions import AdminSessionSettingsView
40 40 from rhodecode.apps.admin.views.settings import AdminSettingsView
41 41 from rhodecode.apps.admin.views.svn_config import AdminSvnConfigView
42 42 from rhodecode.apps.admin.views.system_info import AdminSystemInfoSettingsView
43 43 from rhodecode.apps.admin.views.user_groups import AdminUserGroupsView
44 44 from rhodecode.apps.admin.views.users import AdminUsersView, UsersView
45 45
46 46 config.add_route(
47 47 name='admin_audit_logs',
48 48 pattern='/audit_logs')
49 49 config.add_view(
50 50 AdminAuditLogsView,
51 51 attr='admin_audit_logs',
52 52 route_name='admin_audit_logs', request_method='GET',
53 53 renderer='rhodecode:templates/admin/admin_audit_logs.mako')
54 54
55 55 config.add_route(
56 56 name='admin_audit_log_entry',
57 57 pattern='/audit_logs/{audit_log_id}')
58 58 config.add_view(
59 59 AdminAuditLogsView,
60 60 attr='admin_audit_log_entry',
61 61 route_name='admin_audit_log_entry', request_method='GET',
62 62 renderer='rhodecode:templates/admin/admin_audit_log_entry.mako')
63 63
64 64 # Artifacts EE feature
65 65 config.add_route(
66 66 'admin_artifacts',
67 67 pattern=ADMIN_PREFIX + '/artifacts')
68 68 config.add_route(
69 69 'admin_artifacts_show_all',
70 70 pattern=ADMIN_PREFIX + '/artifacts')
71 71 config.add_view(
72 72 AdminArtifactsView,
73 73 attr='artifacts',
74 74 route_name='admin_artifacts', request_method='GET',
75 75 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
76 76 config.add_view(
77 77 AdminArtifactsView,
78 78 attr='artifacts',
79 79 route_name='admin_artifacts_show_all', request_method='GET',
80 80 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
81 81 # EE views
82 82 config.add_route(
83 83 name='admin_artifacts_show_info',
84 84 pattern=ADMIN_PREFIX + '/artifacts/{uid}')
85 85 config.add_route(
86 86 name='admin_artifacts_delete',
87 87 pattern=ADMIN_PREFIX + '/artifacts/{uid}/delete')
88 88 config.add_route(
89 89 name='admin_artifacts_update',
90 90 pattern=ADMIN_PREFIX + '/artifacts/{uid}/update')
91 91
92 92 config.add_route(
93 93 name='admin_settings_open_source',
94 94 pattern='/settings/open_source')
95 95 config.add_view(
96 96 OpenSourceLicensesAdminSettingsView,
97 97 attr='open_source_licenses',
98 98 route_name='admin_settings_open_source', request_method='GET',
99 99 renderer='rhodecode:templates/admin/settings/settings.mako')
100 100
101 101 config.add_route(
102 102 name='admin_settings_vcs_svn_generate_cfg',
103 103 pattern='/settings/vcs/svn_generate_cfg')
104 104 config.add_view(
105 105 AdminSvnConfigView,
106 106 attr='vcs_svn_generate_config',
107 107 route_name='admin_settings_vcs_svn_generate_cfg',
108 108 request_method='POST', renderer='json')
109 109
110 110 config.add_route(
111 111 name='admin_settings_system',
112 112 pattern='/settings/system')
113 113 config.add_view(
114 114 AdminSystemInfoSettingsView,
115 115 attr='settings_system_info',
116 116 route_name='admin_settings_system', request_method='GET',
117 117 renderer='rhodecode:templates/admin/settings/settings.mako')
118 118
119 119 config.add_route(
120 120 name='admin_settings_system_update',
121 121 pattern='/settings/system/updates')
122 122 config.add_view(
123 123 AdminSystemInfoSettingsView,
124 124 attr='settings_system_info_check_update',
125 125 route_name='admin_settings_system_update', request_method='GET',
126 126 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
127 127
128 128 config.add_route(
129 129 name='admin_settings_exception_tracker',
130 130 pattern='/settings/exceptions')
131 131 config.add_view(
132 132 ExceptionsTrackerView,
133 133 attr='browse_exceptions',
134 134 route_name='admin_settings_exception_tracker', request_method='GET',
135 135 renderer='rhodecode:templates/admin/settings/settings.mako')
136 136
137 137 config.add_route(
138 138 name='admin_settings_exception_tracker_delete_all',
139 139 pattern='/settings/exceptions_delete_all')
140 140 config.add_view(
141 141 ExceptionsTrackerView,
142 142 attr='exception_delete_all',
143 143 route_name='admin_settings_exception_tracker_delete_all', request_method='POST',
144 144 renderer='rhodecode:templates/admin/settings/settings.mako')
145 145
146 146 config.add_route(
147 147 name='admin_settings_exception_tracker_show',
148 148 pattern='/settings/exceptions/{exception_id}')
149 149 config.add_view(
150 150 ExceptionsTrackerView,
151 151 attr='exception_show',
152 152 route_name='admin_settings_exception_tracker_show', request_method='GET',
153 153 renderer='rhodecode:templates/admin/settings/settings.mako')
154 154
155 155 config.add_route(
156 156 name='admin_settings_exception_tracker_delete',
157 157 pattern='/settings/exceptions/{exception_id}/delete')
158 158 config.add_view(
159 159 ExceptionsTrackerView,
160 160 attr='exception_delete',
161 161 route_name='admin_settings_exception_tracker_delete', request_method='POST',
162 162 renderer='rhodecode:templates/admin/settings/settings.mako')
163 163
164 164 config.add_route(
165 165 name='admin_settings_sessions',
166 166 pattern='/settings/sessions')
167 167 config.add_view(
168 168 AdminSessionSettingsView,
169 169 attr='settings_sessions',
170 170 route_name='admin_settings_sessions', request_method='GET',
171 171 renderer='rhodecode:templates/admin/settings/settings.mako')
172 172
173 173 config.add_route(
174 174 name='admin_settings_sessions_cleanup',
175 175 pattern='/settings/sessions/cleanup')
176 176 config.add_view(
177 177 AdminSessionSettingsView,
178 178 attr='settings_sessions_cleanup',
179 179 route_name='admin_settings_sessions_cleanup', request_method='POST')
180 180
181 181 config.add_route(
182 182 name='admin_settings_process_management',
183 183 pattern='/settings/process_management')
184 184 config.add_view(
185 185 AdminProcessManagementView,
186 186 attr='process_management',
187 187 route_name='admin_settings_process_management', request_method='GET',
188 188 renderer='rhodecode:templates/admin/settings/settings.mako')
189 189
190 190 config.add_route(
191 191 name='admin_settings_process_management_data',
192 192 pattern='/settings/process_management/data')
193 193 config.add_view(
194 194 AdminProcessManagementView,
195 195 attr='process_management_data',
196 196 route_name='admin_settings_process_management_data', request_method='GET',
197 197 renderer='rhodecode:templates/admin/settings/settings_process_management_data.mako')
198 198
199 199 config.add_route(
200 200 name='admin_settings_process_management_signal',
201 201 pattern='/settings/process_management/signal')
202 202 config.add_view(
203 203 AdminProcessManagementView,
204 204 attr='process_management_signal',
205 205 route_name='admin_settings_process_management_signal',
206 206 request_method='POST', renderer='json_ext')
207 207
208 208 config.add_route(
209 209 name='admin_settings_process_management_master_signal',
210 210 pattern='/settings/process_management/master_signal')
211 211 config.add_view(
212 212 AdminProcessManagementView,
213 213 attr='process_management_master_signal',
214 214 route_name='admin_settings_process_management_master_signal',
215 215 request_method='POST', renderer='json_ext')
216 216
217 217 # default settings
218 218 config.add_route(
219 219 name='admin_defaults_repositories',
220 220 pattern='/defaults/repositories')
221 221 config.add_view(
222 222 AdminDefaultSettingsView,
223 223 attr='defaults_repository_show',
224 224 route_name='admin_defaults_repositories', request_method='GET',
225 225 renderer='rhodecode:templates/admin/defaults/defaults.mako')
226 226
227 227 config.add_route(
228 228 name='admin_defaults_repositories_update',
229 229 pattern='/defaults/repositories/update')
230 230 config.add_view(
231 231 AdminDefaultSettingsView,
232 232 attr='defaults_repository_update',
233 233 route_name='admin_defaults_repositories_update', request_method='POST',
234 234 renderer='rhodecode:templates/admin/defaults/defaults.mako')
235 235
236 236 # admin settings
237 237
238 238 config.add_route(
239 239 name='admin_settings',
240 240 pattern='/settings')
241 241 config.add_view(
242 242 AdminSettingsView,
243 243 attr='settings_global',
244 244 route_name='admin_settings', request_method='GET',
245 245 renderer='rhodecode:templates/admin/settings/settings.mako')
246 246
247 247 config.add_route(
248 248 name='admin_settings_update',
249 249 pattern='/settings/update')
250 250 config.add_view(
251 251 AdminSettingsView,
252 252 attr='settings_global_update',
253 253 route_name='admin_settings_update', request_method='POST',
254 254 renderer='rhodecode:templates/admin/settings/settings.mako')
255 255
256 256 config.add_route(
257 257 name='admin_settings_global',
258 258 pattern='/settings/global')
259 259 config.add_view(
260 260 AdminSettingsView,
261 261 attr='settings_global',
262 262 route_name='admin_settings_global', request_method='GET',
263 263 renderer='rhodecode:templates/admin/settings/settings.mako')
264 264
265 265 config.add_route(
266 266 name='admin_settings_global_update',
267 267 pattern='/settings/global/update')
268 268 config.add_view(
269 269 AdminSettingsView,
270 270 attr='settings_global_update',
271 271 route_name='admin_settings_global_update', request_method='POST',
272 272 renderer='rhodecode:templates/admin/settings/settings.mako')
273 273
274 274 config.add_route(
275 275 name='admin_settings_vcs',
276 276 pattern='/settings/vcs')
277 277 config.add_view(
278 278 AdminSettingsView,
279 279 attr='settings_vcs',
280 280 route_name='admin_settings_vcs', request_method='GET',
281 281 renderer='rhodecode:templates/admin/settings/settings.mako')
282 282
283 283 config.add_route(
284 284 name='admin_settings_vcs_update',
285 285 pattern='/settings/vcs/update')
286 286 config.add_view(
287 287 AdminSettingsView,
288 288 attr='settings_vcs_update',
289 289 route_name='admin_settings_vcs_update', request_method='POST',
290 290 renderer='rhodecode:templates/admin/settings/settings.mako')
291 291
292 292 config.add_route(
293 293 name='admin_settings_vcs_svn_pattern_delete',
294 294 pattern='/settings/vcs/svn_pattern_delete')
295 295 config.add_view(
296 296 AdminSettingsView,
297 297 attr='settings_vcs_delete_svn_pattern',
298 298 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
299 299 renderer='json_ext', xhr=True)
300 300
301 301 config.add_route(
302 302 name='admin_settings_mapping',
303 303 pattern='/settings/mapping')
304 304 config.add_view(
305 305 AdminSettingsView,
306 306 attr='settings_mapping',
307 307 route_name='admin_settings_mapping', request_method='GET',
308 308 renderer='rhodecode:templates/admin/settings/settings.mako')
309 309
310 310 config.add_route(
311 311 name='admin_settings_mapping_update',
312 312 pattern='/settings/mapping/update')
313 313 config.add_view(
314 314 AdminSettingsView,
315 315 attr='settings_mapping_update',
316 316 route_name='admin_settings_mapping_update', request_method='POST',
317 317 renderer='rhodecode:templates/admin/settings/settings.mako')
318 318
319 319 config.add_route(
320 320 name='admin_settings_visual',
321 321 pattern='/settings/visual')
322 322 config.add_view(
323 323 AdminSettingsView,
324 324 attr='settings_visual',
325 325 route_name='admin_settings_visual', request_method='GET',
326 326 renderer='rhodecode:templates/admin/settings/settings.mako')
327 327
328 328 config.add_route(
329 329 name='admin_settings_visual_update',
330 330 pattern='/settings/visual/update')
331 331 config.add_view(
332 332 AdminSettingsView,
333 333 attr='settings_visual_update',
334 334 route_name='admin_settings_visual_update', request_method='POST',
335 335 renderer='rhodecode:templates/admin/settings/settings.mako')
336 336
337 337 config.add_route(
338 338 name='admin_settings_issuetracker',
339 339 pattern='/settings/issue-tracker')
340 340 config.add_view(
341 341 AdminSettingsView,
342 342 attr='settings_issuetracker',
343 343 route_name='admin_settings_issuetracker', request_method='GET',
344 344 renderer='rhodecode:templates/admin/settings/settings.mako')
345 345
346 346 config.add_route(
347 347 name='admin_settings_issuetracker_update',
348 348 pattern='/settings/issue-tracker/update')
349 349 config.add_view(
350 350 AdminSettingsView,
351 351 attr='settings_issuetracker_update',
352 352 route_name='admin_settings_issuetracker_update', request_method='POST',
353 353 renderer='rhodecode:templates/admin/settings/settings.mako')
354 354
355 355 config.add_route(
356 356 name='admin_settings_issuetracker_test',
357 357 pattern='/settings/issue-tracker/test')
358 358 config.add_view(
359 359 AdminSettingsView,
360 360 attr='settings_issuetracker_test',
361 361 route_name='admin_settings_issuetracker_test', request_method='POST',
362 362 renderer='string', xhr=True)
363 363
364 364 config.add_route(
365 365 name='admin_settings_issuetracker_delete',
366 366 pattern='/settings/issue-tracker/delete')
367 367 config.add_view(
368 368 AdminSettingsView,
369 369 attr='settings_issuetracker_delete',
370 370 route_name='admin_settings_issuetracker_delete', request_method='POST',
371 371 renderer='json_ext', xhr=True)
372 372
373 373 config.add_route(
374 374 name='admin_settings_email',
375 375 pattern='/settings/email')
376 376 config.add_view(
377 377 AdminSettingsView,
378 378 attr='settings_email',
379 379 route_name='admin_settings_email', request_method='GET',
380 380 renderer='rhodecode:templates/admin/settings/settings.mako')
381 381
382 382 config.add_route(
383 383 name='admin_settings_email_update',
384 384 pattern='/settings/email/update')
385 385 config.add_view(
386 386 AdminSettingsView,
387 387 attr='settings_email_update',
388 388 route_name='admin_settings_email_update', request_method='POST',
389 389 renderer='rhodecode:templates/admin/settings/settings.mako')
390 390
391 391 config.add_route(
392 392 name='admin_settings_hooks',
393 393 pattern='/settings/hooks')
394 394 config.add_view(
395 395 AdminSettingsView,
396 396 attr='settings_hooks',
397 397 route_name='admin_settings_hooks', request_method='GET',
398 398 renderer='rhodecode:templates/admin/settings/settings.mako')
399 399
400 400 config.add_route(
401 401 name='admin_settings_hooks_update',
402 402 pattern='/settings/hooks/update')
403 403 config.add_view(
404 404 AdminSettingsView,
405 405 attr='settings_hooks_update',
406 406 route_name='admin_settings_hooks_update', request_method='POST',
407 407 renderer='rhodecode:templates/admin/settings/settings.mako')
408 408
409 409 config.add_route(
410 410 name='admin_settings_hooks_delete',
411 411 pattern='/settings/hooks/delete')
412 412 config.add_view(
413 413 AdminSettingsView,
414 414 attr='settings_hooks_update',
415 415 route_name='admin_settings_hooks_delete', request_method='POST',
416 416 renderer='rhodecode:templates/admin/settings/settings.mako')
417 417
418 418 config.add_route(
419 419 name='admin_settings_search',
420 420 pattern='/settings/search')
421 421 config.add_view(
422 422 AdminSettingsView,
423 423 attr='settings_search',
424 424 route_name='admin_settings_search', request_method='GET',
425 425 renderer='rhodecode:templates/admin/settings/settings.mako')
426 426
427 427 config.add_route(
428 428 name='admin_settings_labs',
429 429 pattern='/settings/labs')
430 430 config.add_view(
431 431 AdminSettingsView,
432 432 attr='settings_labs',
433 433 route_name='admin_settings_labs', request_method='GET',
434 434 renderer='rhodecode:templates/admin/settings/settings.mako')
435 435
436 436 config.add_route(
437 437 name='admin_settings_labs_update',
438 438 pattern='/settings/labs/update')
439 439 config.add_view(
440 440 AdminSettingsView,
441 441 attr='settings_labs_update',
442 442 route_name='admin_settings_labs_update', request_method='POST',
443 443 renderer='rhodecode:templates/admin/settings/settings.mako')
444 444
445 445 # Automation EE feature
446 446 config.add_route(
447 447 'admin_settings_automation',
448 448 pattern=ADMIN_PREFIX + '/settings/automation')
449 449 config.add_view(
450 450 AdminSettingsView,
451 451 attr='settings_automation',
452 452 route_name='admin_settings_automation', request_method='GET',
453 453 renderer='rhodecode:templates/admin/settings/settings.mako')
454 454
455 455 # global permissions
456 456
457 457 config.add_route(
458 458 name='admin_permissions_application',
459 459 pattern='/permissions/application')
460 460 config.add_view(
461 461 AdminPermissionsView,
462 462 attr='permissions_application',
463 463 route_name='admin_permissions_application', request_method='GET',
464 464 renderer='rhodecode:templates/admin/permissions/permissions.mako')
465 465
466 466 config.add_route(
467 467 name='admin_permissions_application_update',
468 468 pattern='/permissions/application/update')
469 469 config.add_view(
470 470 AdminPermissionsView,
471 471 attr='permissions_application_update',
472 472 route_name='admin_permissions_application_update', request_method='POST',
473 473 renderer='rhodecode:templates/admin/permissions/permissions.mako')
474 474
475 475 config.add_route(
476 476 name='admin_permissions_global',
477 477 pattern='/permissions/global')
478 478 config.add_view(
479 479 AdminPermissionsView,
480 480 attr='permissions_global',
481 481 route_name='admin_permissions_global', request_method='GET',
482 482 renderer='rhodecode:templates/admin/permissions/permissions.mako')
483 483
484 484 config.add_route(
485 485 name='admin_permissions_global_update',
486 486 pattern='/permissions/global/update')
487 487 config.add_view(
488 488 AdminPermissionsView,
489 489 attr='permissions_global_update',
490 490 route_name='admin_permissions_global_update', request_method='POST',
491 491 renderer='rhodecode:templates/admin/permissions/permissions.mako')
492 492
493 493 config.add_route(
494 494 name='admin_permissions_object',
495 495 pattern='/permissions/object')
496 496 config.add_view(
497 497 AdminPermissionsView,
498 498 attr='permissions_objects',
499 499 route_name='admin_permissions_object', request_method='GET',
500 500 renderer='rhodecode:templates/admin/permissions/permissions.mako')
501 501
502 502 config.add_route(
503 503 name='admin_permissions_object_update',
504 504 pattern='/permissions/object/update')
505 505 config.add_view(
506 506 AdminPermissionsView,
507 507 attr='permissions_objects_update',
508 508 route_name='admin_permissions_object_update', request_method='POST',
509 509 renderer='rhodecode:templates/admin/permissions/permissions.mako')
510 510
511 511 # Branch perms EE feature
512 512 config.add_route(
513 513 name='admin_permissions_branch',
514 514 pattern='/permissions/branch')
515 515 config.add_view(
516 516 AdminPermissionsView,
517 517 attr='permissions_branch',
518 518 route_name='admin_permissions_branch', request_method='GET',
519 519 renderer='rhodecode:templates/admin/permissions/permissions.mako')
520 520
521 521 config.add_route(
522 522 name='admin_permissions_ips',
523 523 pattern='/permissions/ips')
524 524 config.add_view(
525 525 AdminPermissionsView,
526 526 attr='permissions_ips',
527 527 route_name='admin_permissions_ips', request_method='GET',
528 528 renderer='rhodecode:templates/admin/permissions/permissions.mako')
529 529
530 530 config.add_route(
531 531 name='admin_permissions_overview',
532 532 pattern='/permissions/overview')
533 533 config.add_view(
534 534 AdminPermissionsView,
535 535 attr='permissions_overview',
536 536 route_name='admin_permissions_overview', request_method='GET',
537 537 renderer='rhodecode:templates/admin/permissions/permissions.mako')
538 538
539 539 config.add_route(
540 540 name='admin_permissions_auth_token_access',
541 541 pattern='/permissions/auth_token_access')
542 542 config.add_view(
543 543 AdminPermissionsView,
544 544 attr='auth_token_access',
545 545 route_name='admin_permissions_auth_token_access', request_method='GET',
546 546 renderer='rhodecode:templates/admin/permissions/permissions.mako')
547 547
548 548 config.add_route(
549 549 name='admin_permissions_ssh_keys',
550 550 pattern='/permissions/ssh_keys')
551 551 config.add_view(
552 552 AdminPermissionsView,
553 553 attr='ssh_keys',
554 554 route_name='admin_permissions_ssh_keys', request_method='GET',
555 555 renderer='rhodecode:templates/admin/permissions/permissions.mako')
556 556
557 557 config.add_route(
558 558 name='admin_permissions_ssh_keys_data',
559 559 pattern='/permissions/ssh_keys/data')
560 560 config.add_view(
561 561 AdminPermissionsView,
562 562 attr='ssh_keys_data',
563 563 route_name='admin_permissions_ssh_keys_data', request_method='GET',
564 564 renderer='json_ext', xhr=True)
565 565
566 566 config.add_route(
567 567 name='admin_permissions_ssh_keys_update',
568 568 pattern='/permissions/ssh_keys/update')
569 569 config.add_view(
570 570 AdminPermissionsView,
571 571 attr='ssh_keys_update',
572 572 route_name='admin_permissions_ssh_keys_update', request_method='POST',
573 573 renderer='rhodecode:templates/admin/permissions/permissions.mako')
574 574
575 575 # users admin
576 576 config.add_route(
577 577 name='users',
578 578 pattern='/users')
579 579 config.add_view(
580 580 AdminUsersView,
581 581 attr='users_list',
582 582 route_name='users', request_method='GET',
583 583 renderer='rhodecode:templates/admin/users/users.mako')
584 584
585 585 config.add_route(
586 586 name='users_data',
587 587 pattern='/users_data')
588 588 config.add_view(
589 589 AdminUsersView,
590 590 attr='users_list_data',
591 591 # renderer defined below
592 592 route_name='users_data', request_method='GET',
593 593 renderer='json_ext', xhr=True)
594 594
595 595 config.add_route(
596 596 name='users_create',
597 597 pattern='/users/create')
598 598 config.add_view(
599 599 AdminUsersView,
600 600 attr='users_create',
601 601 route_name='users_create', request_method='POST',
602 602 renderer='rhodecode:templates/admin/users/user_add.mako')
603 603
604 604 config.add_route(
605 605 name='users_new',
606 606 pattern='/users/new')
607 607 config.add_view(
608 608 AdminUsersView,
609 609 attr='users_new',
610 610 route_name='users_new', request_method='GET',
611 611 renderer='rhodecode:templates/admin/users/user_add.mako')
612 612
613 613 # user management
614 614 config.add_route(
615 615 name='user_edit',
616 616 pattern='/users/{user_id:\d+}/edit',
617 617 user_route=True)
618 618 config.add_view(
619 619 UsersView,
620 620 attr='user_edit',
621 621 route_name='user_edit', request_method='GET',
622 622 renderer='rhodecode:templates/admin/users/user_edit.mako')
623 623
624 624 config.add_route(
625 625 name='user_edit_advanced',
626 626 pattern='/users/{user_id:\d+}/edit/advanced',
627 627 user_route=True)
628 628 config.add_view(
629 629 UsersView,
630 630 attr='user_edit_advanced',
631 631 route_name='user_edit_advanced', request_method='GET',
632 632 renderer='rhodecode:templates/admin/users/user_edit.mako')
633 633
634 634 config.add_route(
635 635 name='user_edit_global_perms',
636 636 pattern='/users/{user_id:\d+}/edit/global_permissions',
637 637 user_route=True)
638 638 config.add_view(
639 639 UsersView,
640 640 attr='user_edit_global_perms',
641 641 route_name='user_edit_global_perms', request_method='GET',
642 642 renderer='rhodecode:templates/admin/users/user_edit.mako')
643 643
644 644 config.add_route(
645 645 name='user_edit_global_perms_update',
646 646 pattern='/users/{user_id:\d+}/edit/global_permissions/update',
647 647 user_route=True)
648 648 config.add_view(
649 649 UsersView,
650 650 attr='user_edit_global_perms_update',
651 651 route_name='user_edit_global_perms_update', request_method='POST',
652 652 renderer='rhodecode:templates/admin/users/user_edit.mako')
653 653
654 654 config.add_route(
655 655 name='user_update',
656 656 pattern='/users/{user_id:\d+}/update',
657 657 user_route=True)
658 658 config.add_view(
659 659 UsersView,
660 660 attr='user_update',
661 661 route_name='user_update', request_method='POST',
662 662 renderer='rhodecode:templates/admin/users/user_edit.mako')
663 663
664 664 config.add_route(
665 665 name='user_delete',
666 666 pattern='/users/{user_id:\d+}/delete',
667 667 user_route=True)
668 668 config.add_view(
669 669 UsersView,
670 670 attr='user_delete',
671 671 route_name='user_delete', request_method='POST',
672 672 renderer='rhodecode:templates/admin/users/user_edit.mako')
673 673
674 674 config.add_route(
675 675 name='user_enable_force_password_reset',
676 676 pattern='/users/{user_id:\d+}/password_reset_enable',
677 677 user_route=True)
678 678 config.add_view(
679 679 UsersView,
680 680 attr='user_enable_force_password_reset',
681 681 route_name='user_enable_force_password_reset', request_method='POST',
682 682 renderer='rhodecode:templates/admin/users/user_edit.mako')
683 683
684 684 config.add_route(
685 685 name='user_disable_force_password_reset',
686 686 pattern='/users/{user_id:\d+}/password_reset_disable',
687 687 user_route=True)
688 688 config.add_view(
689 689 UsersView,
690 690 attr='user_disable_force_password_reset',
691 691 route_name='user_disable_force_password_reset', request_method='POST',
692 692 renderer='rhodecode:templates/admin/users/user_edit.mako')
693 693
694 694 config.add_route(
695 695 name='user_create_personal_repo_group',
696 696 pattern='/users/{user_id:\d+}/create_repo_group',
697 697 user_route=True)
698 698 config.add_view(
699 699 UsersView,
700 700 attr='user_create_personal_repo_group',
701 701 route_name='user_create_personal_repo_group', request_method='POST',
702 702 renderer='rhodecode:templates/admin/users/user_edit.mako')
703 703
704 704 # user notice
705 705 config.add_route(
706 706 name='user_notice_dismiss',
707 707 pattern='/users/{user_id:\d+}/notice_dismiss',
708 708 user_route=True)
709 709 config.add_view(
710 710 UsersView,
711 711 attr='user_notice_dismiss',
712 712 route_name='user_notice_dismiss', request_method='POST',
713 713 renderer='json_ext', xhr=True)
714 714
715 715 # user auth tokens
716 716 config.add_route(
717 717 name='edit_user_auth_tokens',
718 718 pattern='/users/{user_id:\d+}/edit/auth_tokens',
719 719 user_route=True)
720 720 config.add_view(
721 721 UsersView,
722 722 attr='auth_tokens',
723 723 route_name='edit_user_auth_tokens', request_method='GET',
724 724 renderer='rhodecode:templates/admin/users/user_edit.mako')
725 725
726 726 config.add_route(
727 727 name='edit_user_auth_tokens_view',
728 728 pattern='/users/{user_id:\d+}/edit/auth_tokens/view',
729 729 user_route=True)
730 730 config.add_view(
731 731 UsersView,
732 732 attr='auth_tokens_view',
733 733 route_name='edit_user_auth_tokens_view', request_method='POST',
734 734 renderer='json_ext', xhr=True)
735 735
736 736 config.add_route(
737 737 name='edit_user_auth_tokens_add',
738 738 pattern='/users/{user_id:\d+}/edit/auth_tokens/new',
739 739 user_route=True)
740 740 config.add_view(
741 741 UsersView,
742 742 attr='auth_tokens_add',
743 743 route_name='edit_user_auth_tokens_add', request_method='POST')
744 744
745 745 config.add_route(
746 746 name='edit_user_auth_tokens_delete',
747 747 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete',
748 748 user_route=True)
749 749 config.add_view(
750 750 UsersView,
751 751 attr='auth_tokens_delete',
752 752 route_name='edit_user_auth_tokens_delete', request_method='POST')
753 753
754 754 # user ssh keys
755 755 config.add_route(
756 756 name='edit_user_ssh_keys',
757 757 pattern='/users/{user_id:\d+}/edit/ssh_keys',
758 758 user_route=True)
759 759 config.add_view(
760 760 UsersView,
761 761 attr='ssh_keys',
762 762 route_name='edit_user_ssh_keys', request_method='GET',
763 763 renderer='rhodecode:templates/admin/users/user_edit.mako')
764 764
765 765 config.add_route(
766 766 name='edit_user_ssh_keys_generate_keypair',
767 767 pattern='/users/{user_id:\d+}/edit/ssh_keys/generate',
768 768 user_route=True)
769 769 config.add_view(
770 770 UsersView,
771 771 attr='ssh_keys_generate_keypair',
772 772 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
773 773 renderer='rhodecode:templates/admin/users/user_edit.mako')
774 774
775 775 config.add_route(
776 776 name='edit_user_ssh_keys_add',
777 777 pattern='/users/{user_id:\d+}/edit/ssh_keys/new',
778 778 user_route=True)
779 779 config.add_view(
780 780 UsersView,
781 781 attr='ssh_keys_add',
782 782 route_name='edit_user_ssh_keys_add', request_method='POST')
783 783
784 784 config.add_route(
785 785 name='edit_user_ssh_keys_delete',
786 786 pattern='/users/{user_id:\d+}/edit/ssh_keys/delete',
787 787 user_route=True)
788 788 config.add_view(
789 789 UsersView,
790 790 attr='ssh_keys_delete',
791 791 route_name='edit_user_ssh_keys_delete', request_method='POST')
792 792
793 793 # user emails
794 794 config.add_route(
795 795 name='edit_user_emails',
796 796 pattern='/users/{user_id:\d+}/edit/emails',
797 797 user_route=True)
798 798 config.add_view(
799 799 UsersView,
800 800 attr='emails',
801 801 route_name='edit_user_emails', request_method='GET',
802 802 renderer='rhodecode:templates/admin/users/user_edit.mako')
803 803
804 804 config.add_route(
805 805 name='edit_user_emails_add',
806 806 pattern='/users/{user_id:\d+}/edit/emails/new',
807 807 user_route=True)
808 808 config.add_view(
809 809 UsersView,
810 810 attr='emails_add',
811 811 route_name='edit_user_emails_add', request_method='POST')
812 812
813 813 config.add_route(
814 814 name='edit_user_emails_delete',
815 815 pattern='/users/{user_id:\d+}/edit/emails/delete',
816 816 user_route=True)
817 817 config.add_view(
818 818 UsersView,
819 819 attr='emails_delete',
820 820 route_name='edit_user_emails_delete', request_method='POST')
821 821
822 822 # user IPs
823 823 config.add_route(
824 824 name='edit_user_ips',
825 825 pattern='/users/{user_id:\d+}/edit/ips',
826 826 user_route=True)
827 827 config.add_view(
828 828 UsersView,
829 829 attr='ips',
830 830 route_name='edit_user_ips', request_method='GET',
831 831 renderer='rhodecode:templates/admin/users/user_edit.mako')
832 832
833 833 config.add_route(
834 834 name='edit_user_ips_add',
835 835 pattern='/users/{user_id:\d+}/edit/ips/new',
836 836 user_route_with_default=True) # enabled for default user too
837 837 config.add_view(
838 838 UsersView,
839 839 attr='ips_add',
840 840 route_name='edit_user_ips_add', request_method='POST')
841 841
842 842 config.add_route(
843 843 name='edit_user_ips_delete',
844 844 pattern='/users/{user_id:\d+}/edit/ips/delete',
845 845 user_route_with_default=True) # enabled for default user too
846 846 config.add_view(
847 847 UsersView,
848 848 attr='ips_delete',
849 849 route_name='edit_user_ips_delete', request_method='POST')
850 850
851 851 # user perms
852 852 config.add_route(
853 853 name='edit_user_perms_summary',
854 854 pattern='/users/{user_id:\d+}/edit/permissions_summary',
855 855 user_route=True)
856 856 config.add_view(
857 857 UsersView,
858 858 attr='user_perms_summary',
859 859 route_name='edit_user_perms_summary', request_method='GET',
860 860 renderer='rhodecode:templates/admin/users/user_edit.mako')
861 861
862 862 config.add_route(
863 863 name='edit_user_perms_summary_json',
864 864 pattern='/users/{user_id:\d+}/edit/permissions_summary/json',
865 865 user_route=True)
866 866 config.add_view(
867 867 UsersView,
868 868 attr='user_perms_summary_json',
869 869 route_name='edit_user_perms_summary_json', request_method='GET',
870 870 renderer='json_ext')
871 871
872 872 # user user groups management
873 873 config.add_route(
874 874 name='edit_user_groups_management',
875 875 pattern='/users/{user_id:\d+}/edit/groups_management',
876 876 user_route=True)
877 877 config.add_view(
878 878 UsersView,
879 879 attr='groups_management',
880 880 route_name='edit_user_groups_management', request_method='GET',
881 881 renderer='rhodecode:templates/admin/users/user_edit.mako')
882 882
883 883 config.add_route(
884 884 name='edit_user_groups_management_updates',
885 885 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates',
886 886 user_route=True)
887 887 config.add_view(
888 888 UsersView,
889 889 attr='groups_management_updates',
890 890 route_name='edit_user_groups_management_updates', request_method='POST')
891 891
892 892 # user audit logs
893 893 config.add_route(
894 894 name='edit_user_audit_logs',
895 895 pattern='/users/{user_id:\d+}/edit/audit', user_route=True)
896 896 config.add_view(
897 897 UsersView,
898 898 attr='user_audit_logs',
899 899 route_name='edit_user_audit_logs', request_method='GET',
900 900 renderer='rhodecode:templates/admin/users/user_edit.mako')
901 901
902 902 config.add_route(
903 903 name='edit_user_audit_logs_download',
904 904 pattern='/users/{user_id:\d+}/edit/audit/download', user_route=True)
905 905 config.add_view(
906 906 UsersView,
907 907 attr='user_audit_logs_download',
908 908 route_name='edit_user_audit_logs_download', request_method='GET',
909 909 renderer='string')
910 910
911 911 # user caches
912 912 config.add_route(
913 913 name='edit_user_caches',
914 914 pattern='/users/{user_id:\d+}/edit/caches',
915 915 user_route=True)
916 916 config.add_view(
917 917 UsersView,
918 918 attr='user_caches',
919 919 route_name='edit_user_caches', request_method='GET',
920 920 renderer='rhodecode:templates/admin/users/user_edit.mako')
921 921
922 922 config.add_route(
923 923 name='edit_user_caches_update',
924 924 pattern='/users/{user_id:\d+}/edit/caches/update',
925 925 user_route=True)
926 926 config.add_view(
927 927 UsersView,
928 928 attr='user_caches_update',
929 929 route_name='edit_user_caches_update', request_method='POST')
930 930
931 931 # user-groups admin
932 932 config.add_route(
933 933 name='user_groups',
934 934 pattern='/user_groups')
935 935 config.add_view(
936 936 AdminUserGroupsView,
937 937 attr='user_groups_list',
938 938 route_name='user_groups', request_method='GET',
939 939 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
940 940
941 941 config.add_route(
942 942 name='user_groups_data',
943 943 pattern='/user_groups_data')
944 944 config.add_view(
945 945 AdminUserGroupsView,
946 946 attr='user_groups_list_data',
947 947 route_name='user_groups_data', request_method='GET',
948 948 renderer='json_ext', xhr=True)
949 949
950 950 config.add_route(
951 951 name='user_groups_new',
952 952 pattern='/user_groups/new')
953 953 config.add_view(
954 954 AdminUserGroupsView,
955 955 attr='user_groups_new',
956 956 route_name='user_groups_new', request_method='GET',
957 957 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
958 958
959 959 config.add_route(
960 960 name='user_groups_create',
961 961 pattern='/user_groups/create')
962 962 config.add_view(
963 963 AdminUserGroupsView,
964 964 attr='user_groups_create',
965 965 route_name='user_groups_create', request_method='POST',
966 966 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
967 967
968 968 # repos admin
969 969 config.add_route(
970 970 name='repos',
971 971 pattern='/repos')
972 972 config.add_view(
973 973 AdminReposView,
974 974 attr='repository_list',
975 975 route_name='repos', request_method='GET',
976 976 renderer='rhodecode:templates/admin/repos/repos.mako')
977 977
978 978 config.add_route(
979 979 name='repos_data',
980 980 pattern='/repos_data')
981 981 config.add_view(
982 982 AdminReposView,
983 983 attr='repository_list_data',
984 984 route_name='repos_data', request_method='GET',
985 985 renderer='json_ext', xhr=True)
986 986
987 987 config.add_route(
988 988 name='repo_new',
989 989 pattern='/repos/new')
990 990 config.add_view(
991 991 AdminReposView,
992 992 attr='repository_new',
993 993 route_name='repo_new', request_method='GET',
994 994 renderer='rhodecode:templates/admin/repos/repo_add.mako')
995 995
996 996 config.add_route(
997 997 name='repo_create',
998 998 pattern='/repos/create')
999 999 config.add_view(
1000 1000 AdminReposView,
1001 1001 attr='repository_create',
1002 1002 route_name='repo_create', request_method='POST',
1003 1003 renderer='rhodecode:templates/admin/repos/repos.mako')
1004 1004
1005 1005 # repo groups admin
1006 1006 config.add_route(
1007 1007 name='repo_groups',
1008 1008 pattern='/repo_groups')
1009 1009 config.add_view(
1010 1010 AdminRepoGroupsView,
1011 1011 attr='repo_group_list',
1012 1012 route_name='repo_groups', request_method='GET',
1013 1013 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
1014 1014
1015 1015 config.add_route(
1016 1016 name='repo_groups_data',
1017 1017 pattern='/repo_groups_data')
1018 1018 config.add_view(
1019 1019 AdminRepoGroupsView,
1020 1020 attr='repo_group_list_data',
1021 1021 route_name='repo_groups_data', request_method='GET',
1022 1022 renderer='json_ext', xhr=True)
1023 1023
1024 1024 config.add_route(
1025 1025 name='repo_group_new',
1026 1026 pattern='/repo_group/new')
1027 1027 config.add_view(
1028 1028 AdminRepoGroupsView,
1029 1029 attr='repo_group_new',
1030 1030 route_name='repo_group_new', request_method='GET',
1031 1031 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1032 1032
1033 1033 config.add_route(
1034 1034 name='repo_group_create',
1035 1035 pattern='/repo_group/create')
1036 1036 config.add_view(
1037 1037 AdminRepoGroupsView,
1038 1038 attr='repo_group_create',
1039 1039 route_name='repo_group_create', request_method='POST',
1040 1040 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1041 1041
1042 1042
1043 1043 def includeme(config):
1044 1044 from rhodecode.apps._base.navigation import includeme as nav_includeme
1045 1045 from rhodecode.apps.admin.views.main_views import AdminMainView
1046 1046
1047 1047 # Create admin navigation registry and add it to the pyramid registry.
1048 1048 nav_includeme(config)
1049 1049
1050 1050 # main admin routes
1051 1051 config.add_route(
1052 1052 name='admin_home', pattern=ADMIN_PREFIX)
1053 1053 config.add_view(
1054 1054 AdminMainView,
1055 1055 attr='admin_main',
1056 1056 route_name='admin_home', request_method='GET',
1057 1057 renderer='rhodecode:templates/admin/main.mako')
1058 1058
1059 1059 # pr global redirect
1060 1060 config.add_route(
1061 1061 name='pull_requests_global_0', # backward compat
1062 1062 pattern=ADMIN_PREFIX + '/pull_requests/{pull_request_id:\d+}')
1063 1063 config.add_view(
1064 1064 AdminMainView,
1065 1065 attr='pull_requests',
1066 1066 route_name='pull_requests_global_0', request_method='GET')
1067 1067
1068 1068 config.add_route(
1069 1069 name='pull_requests_global_1', # backward compat
1070 1070 pattern=ADMIN_PREFIX + '/pull-requests/{pull_request_id:\d+}')
1071 1071 config.add_view(
1072 1072 AdminMainView,
1073 1073 attr='pull_requests',
1074 1074 route_name='pull_requests_global_1', request_method='GET')
1075 1075
1076 1076 config.add_route(
1077 1077 name='pull_requests_global',
1078 1078 pattern=ADMIN_PREFIX + '/pull-request/{pull_request_id:\d+}')
1079 1079 config.add_view(
1080 1080 AdminMainView,
1081 1081 attr='pull_requests',
1082 1082 route_name='pull_requests_global', request_method='GET')
1083 1083
1084 1084 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
@@ -1,171 +1,170 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import os
22 21 import csv
23 22 import datetime
24 23
25 24 import pytest
26 25
27 26 from rhodecode.tests import *
28 27 from rhodecode.tests.fixture import FIXTURES
29 28 from rhodecode.model.db import UserLog
30 29 from rhodecode.model.meta import Session
31 30 from rhodecode.lib.utils2 import safe_unicode
32 31
33 32
34 33 def route_path(name, params=None, **kwargs):
35 34 import urllib.request, urllib.parse, urllib.error
36 35 from rhodecode.apps._base import ADMIN_PREFIX
37 36
38 37 base_url = {
39 38 'admin_home': ADMIN_PREFIX,
40 39 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
41 40
42 41 }[name].format(**kwargs)
43 42
44 43 if params:
45 44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 45 return base_url
47 46
48 47
49 48 @pytest.mark.usefixtures('app')
50 49 class TestAdminController(object):
51 50
52 51 @pytest.fixture(scope='class', autouse=True)
53 52 def prepare(self, request, baseapp):
54 53 UserLog.query().delete()
55 54 Session().commit()
56 55
57 56 def strptime(val):
58 57 fmt = '%Y-%m-%d %H:%M:%S'
59 58 if '.' not in val:
60 59 return datetime.datetime.strptime(val, fmt)
61 60
62 61 nofrag, frag = val.split(".")
63 62 date = datetime.datetime.strptime(nofrag, fmt)
64 63
65 64 frag = frag[:6] # truncate to microseconds
66 65 frag += (6 - len(frag)) * '0' # add 0s
67 66 return date.replace(microsecond=int(frag))
68 67
69 68 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
70 69 for row in csv.DictReader(f):
71 70 ul = UserLog()
72 71 for k, v in row.items():
73 72 v = safe_unicode(v)
74 73 if k == 'action_date':
75 74 v = strptime(v)
76 75 if k in ['user_id', 'repository_id']:
77 76 # nullable due to FK problems
78 77 v = None
79 78 setattr(ul, k, v)
80 79 Session().add(ul)
81 80 Session().commit()
82 81
83 82 @request.addfinalizer
84 83 def cleanup():
85 84 UserLog.query().delete()
86 85 Session().commit()
87 86
88 87 def test_index(self, autologin_user):
89 88 response = self.app.get(route_path('admin_audit_logs'))
90 89 response.mustcontain('Admin audit logs')
91 90
92 91 def test_filter_all_entries(self, autologin_user):
93 92 response = self.app.get(route_path('admin_audit_logs'))
94 93 all_count = UserLog.query().count()
95 94 response.mustcontain('%s entries' % all_count)
96 95
97 96 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
98 97 response = self.app.get(route_path('admin_audit_logs',
99 98 params=dict(filter='repository:rhodecode')))
100 99 response.mustcontain('3 entries')
101 100
102 101 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
103 102 response = self.app.get(route_path('admin_audit_logs',
104 103 params=dict(filter='repository:RhodeCode')))
105 104 response.mustcontain('3 entries')
106 105
107 106 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
108 107 response = self.app.get(route_path('admin_audit_logs',
109 108 params=dict(filter='repository:*test*')))
110 109 response.mustcontain('862 entries')
111 110
112 111 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
113 112 response = self.app.get(route_path('admin_audit_logs',
114 113 params=dict(filter='repository:test*')))
115 114 response.mustcontain('257 entries')
116 115
117 116 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
118 117 response = self.app.get(route_path('admin_audit_logs',
119 118 params=dict(filter='repository:Test*')))
120 119 response.mustcontain('257 entries')
121 120
122 121 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
123 122 response = self.app.get(route_path('admin_audit_logs',
124 123 params=dict(filter='repository:test* AND username:demo')))
125 124 response.mustcontain('130 entries')
126 125
127 126 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
128 127 response = self.app.get(route_path('admin_audit_logs',
129 128 params=dict(filter='repository:test* OR repository:rhodecode')))
130 129 response.mustcontain('260 entries') # 257 + 3
131 130
132 131 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
133 132 response = self.app.get(route_path('admin_audit_logs',
134 133 params=dict(filter='username:demo')))
135 134 response.mustcontain('1087 entries')
136 135
137 136 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
138 137 response = self.app.get(route_path('admin_audit_logs',
139 138 params=dict(filter='username:DemO')))
140 139 response.mustcontain('1087 entries')
141 140
142 141 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
143 142 response = self.app.get(route_path('admin_audit_logs',
144 143 params=dict(filter='username:*test*')))
145 144 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
146 145 response.mustcontain('{} entries'.format(entries_count))
147 146
148 147 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
149 148 response = self.app.get(route_path('admin_audit_logs',
150 149 params=dict(filter='username:demo*')))
151 150 response.mustcontain('1101 entries')
152 151
153 152 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
154 153 response = self.app.get(route_path('admin_audit_logs',
155 154 params=dict(filter='username:demo OR username:volcan')))
156 155 response.mustcontain('1095 entries') # 1087 + 8
157 156
158 157 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
159 158 response = self.app.get(route_path('admin_audit_logs',
160 159 params=dict(filter='action:*pull_request*')))
161 160 response.mustcontain('187 entries')
162 161
163 162 def test_filter_journal_filter_on_date(self, autologin_user):
164 163 response = self.app.get(route_path('admin_audit_logs',
165 164 params=dict(filter='date:20121010')))
166 165 response.mustcontain('47 entries')
167 166
168 167 def test_filter_journal_filter_on_date_2(self, autologin_user):
169 168 response = self.app.get(route_path('admin_audit_logs',
170 169 params=dict(filter='date:20121020')))
171 170 response.mustcontain('17 entries')
@@ -1,202 +1,201 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests import assert_session_flash
24 23 from rhodecode.tests.utils import AssertResponse
25 24 from rhodecode.model.db import Session
26 25 from rhodecode.model.settings import SettingsModel
27 26
28 27
29 28 def assert_auth_settings_updated(response):
30 29 assert response.status_int == 302, 'Expected response HTTP Found 302'
31 30 assert_session_flash(response, 'Auth settings updated successfully')
32 31
33 32
34 33 @pytest.mark.usefixtures("autologin_user", "app")
35 34 class TestAuthSettingsView(object):
36 35
37 36 def _enable_plugins(self, plugins_list, csrf_token, override=None,
38 37 verify_response=False):
39 38 test_url = '/_admin/auth'
40 39 params = {
41 40 'auth_plugins': plugins_list,
42 41 'csrf_token': csrf_token,
43 42 }
44 43 if override:
45 44 params.update(override)
46 45 _enabled_plugins = []
47 46 for plugin in plugins_list.split(','):
48 47 plugin_name = plugin.partition('#')[-1]
49 48 enabled_plugin = '%s_enabled' % plugin_name
50 49 cache_ttl = '%s_cache_ttl' % plugin_name
51 50
52 51 # default params that are needed for each plugin,
53 52 # `enabled` and `cache_ttl`
54 53 params.update({
55 54 enabled_plugin: True,
56 55 cache_ttl: 0
57 56 })
58 57 _enabled_plugins.append(enabled_plugin)
59 58
60 59 # we need to clean any enabled plugin before, since they require
61 60 # form params to be present
62 61 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
63 62 db_plugin.app_settings_value = \
64 63 'egg:rhodecode-enterprise-ce#rhodecode'
65 64 Session().add(db_plugin)
66 65 Session().commit()
67 66 for _plugin in _enabled_plugins:
68 67 db_plugin = SettingsModel().get_setting_by_name(_plugin)
69 68 if db_plugin:
70 69 Session().delete(db_plugin)
71 70 Session().commit()
72 71
73 72 response = self.app.post(url=test_url, params=params)
74 73
75 74 if verify_response:
76 75 assert_auth_settings_updated(response)
77 76 return params
78 77
79 78 def _post_ldap_settings(self, params, override=None, force=False):
80 79
81 80 params.update({
82 81 'filter': 'user',
83 82 'user_member_of': '',
84 83 'user_search_base': '',
85 84 'user_search_filter': 'test_filter',
86 85
87 86 'host': 'dc.example.com',
88 87 'port': '999',
89 88 'timeout': 3600,
90 89 'tls_kind': 'PLAIN',
91 90 'tls_reqcert': 'NEVER',
92 91 'tls_cert_dir':'/etc/openldap/cacerts',
93 92 'dn_user': 'test_user',
94 93 'dn_pass': 'test_pass',
95 94 'base_dn': 'test_base_dn',
96 95 'search_scope': 'BASE',
97 96 'attr_login': 'test_attr_login',
98 97 'attr_firstname': 'ima',
99 98 'attr_lastname': 'tester',
100 99 'attr_email': 'test@example.com',
101 100 'cache_ttl': '0',
102 101 })
103 102 if force:
104 103 params = {}
105 104 params.update(override or {})
106 105
107 106 test_url = '/_admin/auth/ldap/'
108 107
109 108 response = self.app.post(url=test_url, params=params)
110 109 return response
111 110
112 111 def test_index(self):
113 112 response = self.app.get('/_admin/auth')
114 113 response.mustcontain('Authentication Plugins')
115 114
116 115 @pytest.mark.parametrize("disable_plugin, needs_import", [
117 116 ('egg:rhodecode-enterprise-ce#headers', None),
118 117 ('egg:rhodecode-enterprise-ce#crowd', None),
119 118 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
120 119 ('egg:rhodecode-enterprise-ce#ldap', None),
121 120 ('egg:rhodecode-enterprise-ce#pam', "pam"),
122 121 ])
123 122 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
124 123 # TODO: johbo: "pam" is currently not available on darwin,
125 124 # although the docs state that it should work on darwin.
126 125 if needs_import:
127 126 pytest.importorskip(needs_import)
128 127
129 128 self._enable_plugins(
130 129 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
131 130 csrf_token, verify_response=True)
132 131
133 132 self._enable_plugins(
134 133 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
135 134 verify_response=True)
136 135
137 136 def test_ldap_save_settings(self, csrf_token):
138 137 params = self._enable_plugins(
139 138 'egg:rhodecode-enterprise-ce#rhodecode,'
140 139 'egg:rhodecode-enterprise-ce#ldap',
141 140 csrf_token)
142 141 response = self._post_ldap_settings(params)
143 142 assert_auth_settings_updated(response)
144 143
145 144 new_settings = SettingsModel().get_auth_settings()
146 145 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
147 146 'fail db write compare'
148 147
149 148 def test_ldap_error_form_wrong_port_number(self, csrf_token):
150 149 params = self._enable_plugins(
151 150 'egg:rhodecode-enterprise-ce#rhodecode,'
152 151 'egg:rhodecode-enterprise-ce#ldap',
153 152 csrf_token)
154 153 invalid_port_value = 'invalid-port-number'
155 154 response = self._post_ldap_settings(params, override={
156 155 'port': invalid_port_value,
157 156 })
158 157 assertr = response.assert_response()
159 158 assertr.element_contains(
160 159 '.form .field #port ~ .error-message',
161 160 invalid_port_value)
162 161
163 162 def test_ldap_error_form(self, csrf_token):
164 163 params = self._enable_plugins(
165 164 'egg:rhodecode-enterprise-ce#rhodecode,'
166 165 'egg:rhodecode-enterprise-ce#ldap',
167 166 csrf_token)
168 167 response = self._post_ldap_settings(params, override={
169 168 'attr_login': '',
170 169 })
171 170 response.mustcontain("""<span class="error-message">The LDAP Login"""
172 171 """ attribute of the CN must be specified""")
173 172
174 173 def test_post_ldap_group_settings(self, csrf_token):
175 174 params = self._enable_plugins(
176 175 'egg:rhodecode-enterprise-ce#rhodecode,'
177 176 'egg:rhodecode-enterprise-ce#ldap',
178 177 csrf_token)
179 178
180 179 response = self._post_ldap_settings(params, override={
181 180 'host': 'dc-legacy.example.com',
182 181 'port': '999',
183 182 'tls_kind': 'PLAIN',
184 183 'tls_reqcert': 'NEVER',
185 184 'dn_user': 'test_user',
186 185 'dn_pass': 'test_pass',
187 186 'base_dn': 'test_base_dn',
188 187 'filter': 'test_filter',
189 188 'search_scope': 'BASE',
190 189 'attr_login': 'test_attr_login',
191 190 'attr_firstname': 'ima',
192 191 'attr_lastname': 'tester',
193 192 'attr_email': 'test@example.com',
194 193 'cache_ttl': '60',
195 194 'csrf_token': csrf_token,
196 195 }
197 196 )
198 197 assert_auth_settings_updated(response)
199 198
200 199 new_settings = SettingsModel().get_auth_settings()
201 200 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
202 201 'fail db write compare'
@@ -1,85 +1,84 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests import assert_session_flash
24 23 from rhodecode.model.settings import SettingsModel
25 24
26 25
27 26 def route_path(name, params=None, **kwargs):
28 27 import urllib.request, urllib.parse, urllib.error
29 28 from rhodecode.apps._base import ADMIN_PREFIX
30 29
31 30 base_url = {
32 31 'admin_defaults_repositories':
33 32 ADMIN_PREFIX + '/defaults/repositories',
34 33 'admin_defaults_repositories_update':
35 34 ADMIN_PREFIX + '/defaults/repositories/update',
36 35 }[name].format(**kwargs)
37 36
38 37 if params:
39 38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 39 return base_url
41 40
42 41
43 42 @pytest.mark.usefixtures("app")
44 43 class TestDefaultsView(object):
45 44
46 45 def test_index(self, autologin_user):
47 46 response = self.app.get(route_path('admin_defaults_repositories'))
48 47 response.mustcontain('default_repo_private')
49 48 response.mustcontain('default_repo_enable_statistics')
50 49 response.mustcontain('default_repo_enable_downloads')
51 50 response.mustcontain('default_repo_enable_locking')
52 51
53 52 def test_update_params_true_hg(self, autologin_user, csrf_token):
54 53 params = {
55 54 'default_repo_enable_locking': True,
56 55 'default_repo_enable_downloads': True,
57 56 'default_repo_enable_statistics': True,
58 57 'default_repo_private': True,
59 58 'default_repo_type': 'hg',
60 59 'csrf_token': csrf_token,
61 60 }
62 61 response = self.app.post(
63 62 route_path('admin_defaults_repositories_update'), params=params)
64 63 assert_session_flash(response, 'Default settings updated successfully')
65 64
66 65 defs = SettingsModel().get_default_repo_settings()
67 66 del params['csrf_token']
68 67 assert params == defs
69 68
70 69 def test_update_params_false_git(self, autologin_user, csrf_token):
71 70 params = {
72 71 'default_repo_enable_locking': False,
73 72 'default_repo_enable_downloads': False,
74 73 'default_repo_enable_statistics': False,
75 74 'default_repo_private': False,
76 75 'default_repo_type': 'git',
77 76 'csrf_token': csrf_token,
78 77 }
79 78 response = self.app.post(
80 79 route_path('admin_defaults_repositories_update'), params=params)
81 80 assert_session_flash(response, 'Default settings updated successfully')
82 81
83 82 defs = SettingsModel().get_default_repo_settings()
84 83 del params['csrf_token']
85 84 assert params == defs
@@ -1,82 +1,81 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests import TestController
24 23 from rhodecode.tests.fixture import Fixture
25 24
26 25 fixture = Fixture()
27 26
28 27
29 28 def route_path(name, params=None, **kwargs):
30 29 import urllib.request, urllib.parse, urllib.error
31 30 from rhodecode.apps._base import ADMIN_PREFIX
32 31
33 32 base_url = {
34 33 'admin_home': ADMIN_PREFIX,
35 34 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
36 35 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
37 36 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
38 37 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
39 38
40 39 }[name].format(**kwargs)
41 40
42 41 if params:
43 42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
44 43 return base_url
45 44
46 45
47 46 class TestAdminMainView(TestController):
48 47
49 48 def test_access_admin_home(self):
50 49 self.log_user()
51 50 response = self.app.get(route_path('admin_home'), status=200)
52 51 response.mustcontain("Administration area")
53 52
54 53 def test_redirect_pull_request_view(self, view):
55 54 self.log_user()
56 55 self.app.get(
57 56 route_path(view, pull_request_id='xxxx'),
58 57 status=404)
59 58
60 59 @pytest.mark.backends("git", "hg")
61 60 @pytest.mark.parametrize('view', [
62 61 'pull_requests_global',
63 62 'pull_requests_global_0',
64 63 'pull_requests_global_1',
65 64 ])
66 65 def test_redirect_pull_request_view(self, view, pr_util):
67 66 self.log_user()
68 67 pull_request = pr_util.create_pull_request()
69 68 pull_request_id = pull_request.pull_request_id
70 69 repo_name = pull_request.target_repo.repo_name
71 70
72 71 response = self.app.get(
73 72 route_path(view, pull_request_id=pull_request_id),
74 73 status=302)
75 74 assert response.location.endswith(
76 75 'pull-request/{}'.format(pull_request_id))
77 76
78 77 redirect_url = route_path(
79 78 'pullrequest_show', repo_name=repo_name,
80 79 pull_request_id=pull_request_id)
81 80
82 81 assert redirect_url in response.location
@@ -1,299 +1,298 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import mock
22 21 import pytest
23 22 from rhodecode.model.db import User, UserIpMap
24 23 from rhodecode.model.meta import Session
25 24 from rhodecode.model.permission import PermissionModel
26 25 from rhodecode.model.ssh_key import SshKeyModel
27 26 from rhodecode.tests import (
28 27 TestController, clear_cache_regions, assert_session_flash)
29 28
30 29
31 30 def route_path(name, params=None, **kwargs):
32 31 import urllib.request, urllib.parse, urllib.error
33 32 from rhodecode.apps._base import ADMIN_PREFIX
34 33
35 34 base_url = {
36 35 'edit_user_ips':
37 36 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
38 37 'edit_user_ips_add':
39 38 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
40 39 'edit_user_ips_delete':
41 40 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
42 41
43 42 'admin_permissions_application':
44 43 ADMIN_PREFIX + '/permissions/application',
45 44 'admin_permissions_application_update':
46 45 ADMIN_PREFIX + '/permissions/application/update',
47 46
48 47 'admin_permissions_global':
49 48 ADMIN_PREFIX + '/permissions/global',
50 49 'admin_permissions_global_update':
51 50 ADMIN_PREFIX + '/permissions/global/update',
52 51
53 52 'admin_permissions_object':
54 53 ADMIN_PREFIX + '/permissions/object',
55 54 'admin_permissions_object_update':
56 55 ADMIN_PREFIX + '/permissions/object/update',
57 56
58 57 'admin_permissions_ips':
59 58 ADMIN_PREFIX + '/permissions/ips',
60 59 'admin_permissions_overview':
61 60 ADMIN_PREFIX + '/permissions/overview',
62 61
63 62 'admin_permissions_ssh_keys':
64 63 ADMIN_PREFIX + '/permissions/ssh_keys',
65 64 'admin_permissions_ssh_keys_data':
66 65 ADMIN_PREFIX + '/permissions/ssh_keys/data',
67 66 'admin_permissions_ssh_keys_update':
68 67 ADMIN_PREFIX + '/permissions/ssh_keys/update'
69 68
70 69 }[name].format(**kwargs)
71 70
72 71 if params:
73 72 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
74 73 return base_url
75 74
76 75
77 76 class TestAdminPermissionsController(TestController):
78 77
79 78 @pytest.fixture(scope='class', autouse=True)
80 79 def prepare(self, request):
81 80 # cleanup and reset to default permissions after
82 81 @request.addfinalizer
83 82 def cleanup():
84 83 PermissionModel().create_default_user_permissions(
85 84 User.get_default_user(), force=True)
86 85
87 86 def test_index_application(self):
88 87 self.log_user()
89 88 self.app.get(route_path('admin_permissions_application'))
90 89
91 90 @pytest.mark.parametrize(
92 91 'anonymous, default_register, default_register_message, default_password_reset,'
93 92 'default_extern_activate, expect_error, expect_form_error', [
94 93 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
95 94 False, False),
96 95 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
97 96 False, False),
98 97 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
99 98 False, False),
100 99 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
101 100 False, False),
102 101 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
103 102 False, True),
104 103 (True, '', '', 'hg.password_reset.enabled', '', True, False),
105 104 ])
106 105 def test_update_application_permissions(
107 106 self, anonymous, default_register, default_register_message, default_password_reset,
108 107 default_extern_activate, expect_error, expect_form_error):
109 108
110 109 self.log_user()
111 110
112 111 # TODO: anonymous access set here to False, breaks some other tests
113 112 params = {
114 113 'csrf_token': self.csrf_token,
115 114 'anonymous': anonymous,
116 115 'default_register': default_register,
117 116 'default_register_message': default_register_message,
118 117 'default_password_reset': default_password_reset,
119 118 'default_extern_activate': default_extern_activate,
120 119 }
121 120 response = self.app.post(route_path('admin_permissions_application_update'),
122 121 params=params)
123 122 if expect_form_error:
124 123 assert response.status_int == 200
125 124 response.mustcontain('Value must be one of')
126 125 else:
127 126 if expect_error:
128 127 msg = 'Error occurred during update of permissions'
129 128 else:
130 129 msg = 'Application permissions updated successfully'
131 130 assert_session_flash(response, msg)
132 131
133 132 def test_index_object(self):
134 133 self.log_user()
135 134 self.app.get(route_path('admin_permissions_object'))
136 135
137 136 @pytest.mark.parametrize(
138 137 'repo, repo_group, user_group, expect_error, expect_form_error', [
139 138 ('repository.none', 'group.none', 'usergroup.none', False, False),
140 139 ('repository.read', 'group.read', 'usergroup.read', False, False),
141 140 ('repository.write', 'group.write', 'usergroup.write',
142 141 False, False),
143 142 ('repository.admin', 'group.admin', 'usergroup.admin',
144 143 False, False),
145 144 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
146 145 ('', '', '', True, False),
147 146 ])
148 147 def test_update_object_permissions(self, repo, repo_group, user_group,
149 148 expect_error, expect_form_error):
150 149 self.log_user()
151 150
152 151 params = {
153 152 'csrf_token': self.csrf_token,
154 153 'default_repo_perm': repo,
155 154 'overwrite_default_repo': False,
156 155 'default_group_perm': repo_group,
157 156 'overwrite_default_group': False,
158 157 'default_user_group_perm': user_group,
159 158 'overwrite_default_user_group': False,
160 159 }
161 160 response = self.app.post(route_path('admin_permissions_object_update'),
162 161 params=params)
163 162 if expect_form_error:
164 163 assert response.status_int == 200
165 164 response.mustcontain('Value must be one of')
166 165 else:
167 166 if expect_error:
168 167 msg = 'Error occurred during update of permissions'
169 168 else:
170 169 msg = 'Object permissions updated successfully'
171 170 assert_session_flash(response, msg)
172 171
173 172 def test_index_global(self):
174 173 self.log_user()
175 174 self.app.get(route_path('admin_permissions_global'))
176 175
177 176 @pytest.mark.parametrize(
178 177 'repo_create, repo_create_write, user_group_create, repo_group_create,'
179 178 'fork_create, inherit_default_permissions, expect_error,'
180 179 'expect_form_error', [
181 180 ('hg.create.none', 'hg.create.write_on_repogroup.false',
182 181 'hg.usergroup.create.false', 'hg.repogroup.create.false',
183 182 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
184 183 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
185 184 'hg.usergroup.create.true', 'hg.repogroup.create.true',
186 185 'hg.fork.repository', 'hg.inherit_default_perms.false',
187 186 False, False),
188 187 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
189 188 'hg.usergroup.create.true', 'hg.repogroup.create.true',
190 189 'hg.fork.repository', 'hg.inherit_default_perms.false',
191 190 False, True),
192 191 ('', '', '', '', '', '', True, False),
193 192 ])
194 193 def test_update_global_permissions(
195 194 self, repo_create, repo_create_write, user_group_create,
196 195 repo_group_create, fork_create, inherit_default_permissions,
197 196 expect_error, expect_form_error):
198 197 self.log_user()
199 198
200 199 params = {
201 200 'csrf_token': self.csrf_token,
202 201 'default_repo_create': repo_create,
203 202 'default_repo_create_on_write': repo_create_write,
204 203 'default_user_group_create': user_group_create,
205 204 'default_repo_group_create': repo_group_create,
206 205 'default_fork_create': fork_create,
207 206 'default_inherit_default_permissions': inherit_default_permissions
208 207 }
209 208 response = self.app.post(route_path('admin_permissions_global_update'),
210 209 params=params)
211 210 if expect_form_error:
212 211 assert response.status_int == 200
213 212 response.mustcontain('Value must be one of')
214 213 else:
215 214 if expect_error:
216 215 msg = 'Error occurred during update of permissions'
217 216 else:
218 217 msg = 'Global permissions updated successfully'
219 218 assert_session_flash(response, msg)
220 219
221 220 def test_index_ips(self):
222 221 self.log_user()
223 222 response = self.app.get(route_path('admin_permissions_ips'))
224 223 response.mustcontain('All IP addresses are allowed')
225 224
226 225 def test_add_delete_ips(self):
227 226 clear_cache_regions(['sql_cache_short'])
228 227 self.log_user()
229 228
230 229 # ADD
231 230 default_user_id = User.get_default_user_id()
232 231 self.app.post(
233 232 route_path('edit_user_ips_add', user_id=default_user_id),
234 233 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
235 234
236 235 response = self.app.get(route_path('admin_permissions_ips'))
237 236 response.mustcontain('0.0.0.0/24')
238 237 response.mustcontain('0.0.0.0 - 0.0.0.255')
239 238
240 239 # DELETE
241 240 default_user_id = User.get_default_user_id()
242 241 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
243 242 default_user_id).first().ip_id
244 243
245 244 response = self.app.post(
246 245 route_path('edit_user_ips_delete', user_id=default_user_id),
247 246 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
248 247
249 248 assert_session_flash(response, 'Removed ip address from user whitelist')
250 249
251 250 clear_cache_regions(['sql_cache_short'])
252 251 response = self.app.get(route_path('admin_permissions_ips'))
253 252 response.mustcontain('All IP addresses are allowed')
254 253 response.mustcontain(no=['0.0.0.0/24'])
255 254 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
256 255
257 256 def test_index_overview(self):
258 257 self.log_user()
259 258 self.app.get(route_path('admin_permissions_overview'))
260 259
261 260 def test_ssh_keys(self):
262 261 self.log_user()
263 262 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
264 263
265 264 def test_ssh_keys_data(self, user_util, xhr_header):
266 265 self.log_user()
267 266 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
268 267 extra_environ=xhr_header)
269 268 assert response.json == {u'data': [], u'draw': None,
270 269 u'recordsFiltered': 0, u'recordsTotal': 0}
271 270
272 271 dummy_user = user_util.create_user()
273 272 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
274 273 Session().commit()
275 274 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
276 275 extra_environ=xhr_header)
277 276 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
278 277
279 278 def test_ssh_keys_update(self):
280 279 self.log_user()
281 280 response = self.app.post(
282 281 route_path('admin_permissions_ssh_keys_update'),
283 282 dict(csrf_token=self.csrf_token), status=302)
284 283
285 284 assert_session_flash(
286 285 response, 'Updated SSH keys file')
287 286
288 287 def test_ssh_keys_update_disabled(self):
289 288 self.log_user()
290 289
291 290 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
292 291 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
293 292 return_value=False):
294 293 response = self.app.post(
295 294 route_path('admin_permissions_ssh_keys_update'),
296 295 dict(csrf_token=self.csrf_token), status=302)
297 296
298 297 assert_session_flash(
299 298 response, 'SSH key support is disabled in .ini file') No newline at end of file
@@ -1,512 +1,511 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import urllib.request, urllib.parse, urllib.error
22 21
23 22 import mock
24 23 import pytest
25 24
26 25 from rhodecode.apps._base import ADMIN_PREFIX
27 26 from rhodecode.lib import auth
28 27 from rhodecode.lib.utils2 import safe_str
29 28 from rhodecode.lib import helpers as h
30 29 from rhodecode.model.db import (
31 30 Repository, RepoGroup, UserRepoToPerm, User, Permission)
32 31 from rhodecode.model.meta import Session
33 32 from rhodecode.model.repo import RepoModel
34 33 from rhodecode.model.repo_group import RepoGroupModel
35 34 from rhodecode.model.user import UserModel
36 35 from rhodecode.tests import (
37 36 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
38 37 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
39 38 from rhodecode.tests.fixture import Fixture, error_function
40 39 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
41 40
42 41 fixture = Fixture()
43 42
44 43
45 44 def route_path(name, params=None, **kwargs):
46 45 import urllib.request, urllib.parse, urllib.error
47 46
48 47 base_url = {
49 48 'repos': ADMIN_PREFIX + '/repos',
50 49 'repos_data': ADMIN_PREFIX + '/repos_data',
51 50 'repo_new': ADMIN_PREFIX + '/repos/new',
52 51 'repo_create': ADMIN_PREFIX + '/repos/create',
53 52
54 53 'repo_creating_check': '/{repo_name}/repo_creating_check',
55 54 }[name].format(**kwargs)
56 55
57 56 if params:
58 57 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
59 58 return base_url
60 59
61 60
62 61 def _get_permission_for_user(user, repo):
63 62 perm = UserRepoToPerm.query()\
64 63 .filter(UserRepoToPerm.repository ==
65 64 Repository.get_by_repo_name(repo))\
66 65 .filter(UserRepoToPerm.user == User.get_by_username(user))\
67 66 .all()
68 67 return perm
69 68
70 69
71 70 @pytest.mark.usefixtures("app")
72 71 class TestAdminRepos(object):
73 72
74 73 def test_repo_list(self, autologin_user, user_util, xhr_header):
75 74 repo = user_util.create_repo()
76 75 repo_name = repo.repo_name
77 76 response = self.app.get(
78 77 route_path('repos_data'), status=200,
79 78 extra_environ=xhr_header)
80 79
81 80 response.mustcontain(repo_name)
82 81
83 82 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
84 83 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
85 84 response = self.app.get(route_path('repo_new'), status=200)
86 85 assert_response = response.assert_response()
87 86 element = assert_response.get_element('[name=repo_type]')
88 87 assert element.get('value') == 'git'
89 88
90 89 def test_create_page_non_restricted_backends(self, autologin_user, backend):
91 90 response = self.app.get(route_path('repo_new'), status=200)
92 91 assert_response = response.assert_response()
93 92 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
94 93
95 94 @pytest.mark.parametrize(
96 95 "suffix", [u'', u'xxa'], ids=['', 'non-ascii'])
97 96 def test_create(self, autologin_user, backend, suffix, csrf_token):
98 97 repo_name_unicode = backend.new_repo_name(suffix=suffix)
99 98 repo_name = repo_name_unicode.encode('utf8')
100 99 description_unicode = u'description for newly created repo' + suffix
101 100 description = description_unicode.encode('utf8')
102 101 response = self.app.post(
103 102 route_path('repo_create'),
104 103 fixture._get_repo_create_params(
105 104 repo_private=False,
106 105 repo_name=repo_name,
107 106 repo_type=backend.alias,
108 107 repo_description=description,
109 108 csrf_token=csrf_token),
110 109 status=302)
111 110
112 111 self.assert_repository_is_created_correctly(
113 112 repo_name, description, backend)
114 113
115 114 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
116 115 numeric_repo = '1234'
117 116 repo_name = numeric_repo
118 117 description = 'description for newly created repo' + numeric_repo
119 118 self.app.post(
120 119 route_path('repo_create'),
121 120 fixture._get_repo_create_params(
122 121 repo_private=False,
123 122 repo_name=repo_name,
124 123 repo_type=backend.alias,
125 124 repo_description=description,
126 125 csrf_token=csrf_token))
127 126
128 127 self.assert_repository_is_created_correctly(
129 128 repo_name, description, backend)
130 129
131 130 @pytest.mark.parametrize("suffix", [u'', u'ąćę'], ids=['', 'non-ascii'])
132 131 def test_create_in_group(
133 132 self, autologin_user, backend, suffix, csrf_token):
134 133 # create GROUP
135 134 group_name = 'sometest_%s' % backend.alias
136 135 gr = RepoGroupModel().create(group_name=group_name,
137 136 group_description='test',
138 137 owner=TEST_USER_ADMIN_LOGIN)
139 138 Session().commit()
140 139
141 140 repo_name = u'ingroup' + suffix
142 141 repo_name_full = RepoGroup.url_sep().join(
143 142 [group_name, repo_name])
144 143 description = u'description for newly created repo'
145 144 self.app.post(
146 145 route_path('repo_create'),
147 146 fixture._get_repo_create_params(
148 147 repo_private=False,
149 148 repo_name=safe_str(repo_name),
150 149 repo_type=backend.alias,
151 150 repo_description=description,
152 151 repo_group=gr.group_id,
153 152 csrf_token=csrf_token))
154 153
155 154 # TODO: johbo: Cleanup work to fixture
156 155 try:
157 156 self.assert_repository_is_created_correctly(
158 157 repo_name_full, description, backend)
159 158
160 159 new_repo = RepoModel().get_by_repo_name(repo_name_full)
161 160 inherited_perms = UserRepoToPerm.query().filter(
162 161 UserRepoToPerm.repository_id == new_repo.repo_id).all()
163 162 assert len(inherited_perms) == 1
164 163 finally:
165 164 RepoModel().delete(repo_name_full)
166 165 RepoGroupModel().delete(group_name)
167 166 Session().commit()
168 167
169 168 def test_create_in_group_numeric_name(
170 169 self, autologin_user, backend, csrf_token):
171 170 # create GROUP
172 171 group_name = 'sometest_%s' % backend.alias
173 172 gr = RepoGroupModel().create(group_name=group_name,
174 173 group_description='test',
175 174 owner=TEST_USER_ADMIN_LOGIN)
176 175 Session().commit()
177 176
178 177 repo_name = '12345'
179 178 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
180 179 description = 'description for newly created repo'
181 180 self.app.post(
182 181 route_path('repo_create'),
183 182 fixture._get_repo_create_params(
184 183 repo_private=False,
185 184 repo_name=repo_name,
186 185 repo_type=backend.alias,
187 186 repo_description=description,
188 187 repo_group=gr.group_id,
189 188 csrf_token=csrf_token))
190 189
191 190 # TODO: johbo: Cleanup work to fixture
192 191 try:
193 192 self.assert_repository_is_created_correctly(
194 193 repo_name_full, description, backend)
195 194
196 195 new_repo = RepoModel().get_by_repo_name(repo_name_full)
197 196 inherited_perms = UserRepoToPerm.query()\
198 197 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
199 198 assert len(inherited_perms) == 1
200 199 finally:
201 200 RepoModel().delete(repo_name_full)
202 201 RepoGroupModel().delete(group_name)
203 202 Session().commit()
204 203
205 204 def test_create_in_group_without_needed_permissions(self, backend):
206 205 session = login_user_session(
207 206 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
208 207 csrf_token = auth.get_csrf_token(session)
209 208 # revoke
210 209 user_model = UserModel()
211 210 # disable fork and create on default user
212 211 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
213 212 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
214 213 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
215 214 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
216 215
217 216 # disable on regular user
218 217 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
219 218 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
220 219 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
221 220 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
222 221 Session().commit()
223 222
224 223 # create GROUP
225 224 group_name = 'reg_sometest_%s' % backend.alias
226 225 gr = RepoGroupModel().create(group_name=group_name,
227 226 group_description='test',
228 227 owner=TEST_USER_ADMIN_LOGIN)
229 228 Session().commit()
230 229 repo_group_id = gr.group_id
231 230
232 231 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
233 232 gr_allowed = RepoGroupModel().create(
234 233 group_name=group_name_allowed,
235 234 group_description='test',
236 235 owner=TEST_USER_REGULAR_LOGIN)
237 236 allowed_repo_group_id = gr_allowed.group_id
238 237 Session().commit()
239 238
240 239 repo_name = 'ingroup'
241 240 description = 'description for newly created repo'
242 241 response = self.app.post(
243 242 route_path('repo_create'),
244 243 fixture._get_repo_create_params(
245 244 repo_private=False,
246 245 repo_name=repo_name,
247 246 repo_type=backend.alias,
248 247 repo_description=description,
249 248 repo_group=repo_group_id,
250 249 csrf_token=csrf_token))
251 250
252 251 response.mustcontain('Invalid value')
253 252
254 253 # user is allowed to create in this group
255 254 repo_name = 'ingroup'
256 255 repo_name_full = RepoGroup.url_sep().join(
257 256 [group_name_allowed, repo_name])
258 257 description = 'description for newly created repo'
259 258 response = self.app.post(
260 259 route_path('repo_create'),
261 260 fixture._get_repo_create_params(
262 261 repo_private=False,
263 262 repo_name=repo_name,
264 263 repo_type=backend.alias,
265 264 repo_description=description,
266 265 repo_group=allowed_repo_group_id,
267 266 csrf_token=csrf_token))
268 267
269 268 # TODO: johbo: Cleanup in pytest fixture
270 269 try:
271 270 self.assert_repository_is_created_correctly(
272 271 repo_name_full, description, backend)
273 272
274 273 new_repo = RepoModel().get_by_repo_name(repo_name_full)
275 274 inherited_perms = UserRepoToPerm.query().filter(
276 275 UserRepoToPerm.repository_id == new_repo.repo_id).all()
277 276 assert len(inherited_perms) == 1
278 277
279 278 assert repo_on_filesystem(repo_name_full)
280 279 finally:
281 280 RepoModel().delete(repo_name_full)
282 281 RepoGroupModel().delete(group_name)
283 282 RepoGroupModel().delete(group_name_allowed)
284 283 Session().commit()
285 284
286 285 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
287 286 csrf_token):
288 287 # create GROUP
289 288 group_name = 'sometest_%s' % backend.alias
290 289 gr = RepoGroupModel().create(group_name=group_name,
291 290 group_description='test',
292 291 owner=TEST_USER_ADMIN_LOGIN)
293 292 perm = Permission.get_by_key('repository.write')
294 293 RepoGroupModel().grant_user_permission(
295 294 gr, TEST_USER_REGULAR_LOGIN, perm)
296 295
297 296 # add repo permissions
298 297 Session().commit()
299 298 repo_group_id = gr.group_id
300 299 repo_name = 'ingroup_inherited_%s' % backend.alias
301 300 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
302 301 description = 'description for newly created repo'
303 302 self.app.post(
304 303 route_path('repo_create'),
305 304 fixture._get_repo_create_params(
306 305 repo_private=False,
307 306 repo_name=repo_name,
308 307 repo_type=backend.alias,
309 308 repo_description=description,
310 309 repo_group=repo_group_id,
311 310 repo_copy_permissions=True,
312 311 csrf_token=csrf_token))
313 312
314 313 # TODO: johbo: Cleanup to pytest fixture
315 314 try:
316 315 self.assert_repository_is_created_correctly(
317 316 repo_name_full, description, backend)
318 317 except Exception:
319 318 RepoGroupModel().delete(group_name)
320 319 Session().commit()
321 320 raise
322 321
323 322 # check if inherited permissions are applied
324 323 new_repo = RepoModel().get_by_repo_name(repo_name_full)
325 324 inherited_perms = UserRepoToPerm.query().filter(
326 325 UserRepoToPerm.repository_id == new_repo.repo_id).all()
327 326 assert len(inherited_perms) == 2
328 327
329 328 assert TEST_USER_REGULAR_LOGIN in [
330 329 x.user.username for x in inherited_perms]
331 330 assert 'repository.write' in [
332 331 x.permission.permission_name for x in inherited_perms]
333 332
334 333 RepoModel().delete(repo_name_full)
335 334 RepoGroupModel().delete(group_name)
336 335 Session().commit()
337 336
338 337 @pytest.mark.xfail_backends(
339 338 "git", "hg", reason="Missing reposerver support")
340 339 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
341 340 csrf_token):
342 341 source_repo = backend.create_repo(number_of_commits=2)
343 342 source_repo_name = source_repo.repo_name
344 343 reposerver.serve(source_repo.scm_instance())
345 344
346 345 repo_name = backend.new_repo_name()
347 346 response = self.app.post(
348 347 route_path('repo_create'),
349 348 fixture._get_repo_create_params(
350 349 repo_private=False,
351 350 repo_name=repo_name,
352 351 repo_type=backend.alias,
353 352 repo_description='',
354 353 clone_uri=reposerver.url,
355 354 csrf_token=csrf_token),
356 355 status=302)
357 356
358 357 # Should be redirected to the creating page
359 358 response.mustcontain('repo_creating')
360 359
361 360 # Expecting that both repositories have same history
362 361 source_repo = RepoModel().get_by_repo_name(source_repo_name)
363 362 source_vcs = source_repo.scm_instance()
364 363 repo = RepoModel().get_by_repo_name(repo_name)
365 364 repo_vcs = repo.scm_instance()
366 365 assert source_vcs[0].message == repo_vcs[0].message
367 366 assert source_vcs.count() == repo_vcs.count()
368 367 assert source_vcs.commit_ids == repo_vcs.commit_ids
369 368
370 369 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
371 370 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
372 371 csrf_token):
373 372 repo_name = backend.new_repo_name()
374 373 description = 'description for newly created repo'
375 374 response = self.app.post(
376 375 route_path('repo_create'),
377 376 fixture._get_repo_create_params(
378 377 repo_private=False,
379 378 repo_name=repo_name,
380 379 repo_type=backend.alias,
381 380 repo_description=description,
382 381 clone_uri='http://repo.invalid/repo',
383 382 csrf_token=csrf_token))
384 383 response.mustcontain('invalid clone url')
385 384
386 385 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
387 386 def test_create_remote_repo_wrong_clone_uri_hg_svn(
388 387 self, autologin_user, backend, csrf_token):
389 388 repo_name = backend.new_repo_name()
390 389 description = 'description for newly created repo'
391 390 response = self.app.post(
392 391 route_path('repo_create'),
393 392 fixture._get_repo_create_params(
394 393 repo_private=False,
395 394 repo_name=repo_name,
396 395 repo_type=backend.alias,
397 396 repo_description=description,
398 397 clone_uri='svn+http://svn.invalid/repo',
399 398 csrf_token=csrf_token))
400 399 response.mustcontain('invalid clone url')
401 400
402 401 def test_create_with_git_suffix(
403 402 self, autologin_user, backend, csrf_token):
404 403 repo_name = backend.new_repo_name() + ".git"
405 404 description = 'description for newly created repo'
406 405 response = self.app.post(
407 406 route_path('repo_create'),
408 407 fixture._get_repo_create_params(
409 408 repo_private=False,
410 409 repo_name=repo_name,
411 410 repo_type=backend.alias,
412 411 repo_description=description,
413 412 csrf_token=csrf_token))
414 413 response.mustcontain('Repository name cannot end with .git')
415 414
416 415 def test_default_user_cannot_access_private_repo_in_a_group(
417 416 self, autologin_user, user_util, backend):
418 417
419 418 group = user_util.create_repo_group()
420 419
421 420 repo = backend.create_repo(
422 421 repo_private=True, repo_group=group, repo_copy_permissions=True)
423 422
424 423 permissions = _get_permission_for_user(
425 424 user='default', repo=repo.repo_name)
426 425 assert len(permissions) == 1
427 426 assert permissions[0].permission.permission_name == 'repository.none'
428 427 assert permissions[0].repository.private is True
429 428
430 429 def test_create_on_top_level_without_permissions(self, backend):
431 430 session = login_user_session(
432 431 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
433 432 csrf_token = auth.get_csrf_token(session)
434 433
435 434 # revoke
436 435 user_model = UserModel()
437 436 # disable fork and create on default user
438 437 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
439 438 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
440 439 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
441 440 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
442 441
443 442 # disable on regular user
444 443 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
445 444 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
446 445 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
447 446 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
448 447 Session().commit()
449 448
450 449 repo_name = backend.new_repo_name()
451 450 description = 'description for newly created repo'
452 451 response = self.app.post(
453 452 route_path('repo_create'),
454 453 fixture._get_repo_create_params(
455 454 repo_private=False,
456 455 repo_name=repo_name,
457 456 repo_type=backend.alias,
458 457 repo_description=description,
459 458 csrf_token=csrf_token))
460 459
461 460 response.mustcontain(
462 461 u"You do not have the permission to store repositories in "
463 462 u"the root location.")
464 463
465 464 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
466 465 def test_create_repo_when_filesystem_op_fails(
467 466 self, autologin_user, backend, csrf_token):
468 467 repo_name = backend.new_repo_name()
469 468 description = 'description for newly created repo'
470 469
471 470 response = self.app.post(
472 471 route_path('repo_create'),
473 472 fixture._get_repo_create_params(
474 473 repo_private=False,
475 474 repo_name=repo_name,
476 475 repo_type=backend.alias,
477 476 repo_description=description,
478 477 csrf_token=csrf_token))
479 478
480 479 assert_session_flash(
481 480 response, 'Error creating repository %s' % repo_name)
482 481 # repo must not be in db
483 482 assert backend.repo is None
484 483 # repo must not be in filesystem !
485 484 assert not repo_on_filesystem(repo_name)
486 485
487 486 def assert_repository_is_created_correctly(
488 487 self, repo_name, description, backend):
489 488 repo_name_utf8 = safe_str(repo_name)
490 489
491 490 # run the check page that triggers the flash message
492 491 response = self.app.get(
493 492 route_path('repo_creating_check', repo_name=safe_str(repo_name)))
494 493 assert response.json == {u'result': True}
495 494
496 495 flash_msg = u'Created repository <a href="/{}">{}</a>'.format(
497 496 urllib.parse.quote(repo_name_utf8), repo_name)
498 497 assert_session_flash(response, flash_msg)
499 498
500 499 # test if the repo was created in the database
501 500 new_repo = RepoModel().get_by_repo_name(repo_name)
502 501
503 502 assert new_repo.repo_name == repo_name
504 503 assert new_repo.description == description
505 504
506 505 # test if the repository is visible in the list ?
507 506 response = self.app.get(
508 507 h.route_path('repo_summary', repo_name=safe_str(repo_name)))
509 508 response.mustcontain(repo_name)
510 509 response.mustcontain(backend.alias)
511 510
512 511 assert repo_on_filesystem(repo_name)
@@ -1,194 +1,193 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import os
22 21 import pytest
23 22
24 23 from rhodecode.apps._base import ADMIN_PREFIX
25 24 from rhodecode.lib import helpers as h
26 25 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
27 26 from rhodecode.model.meta import Session
28 27 from rhodecode.model.repo_group import RepoGroupModel
29 28 from rhodecode.tests import (
30 29 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
31 30 from rhodecode.tests.fixture import Fixture
32 31
33 32 fixture = Fixture()
34 33
35 34
36 35 def route_path(name, params=None, **kwargs):
37 36 import urllib.request, urllib.parse, urllib.error
38 37
39 38 base_url = {
40 39 'repo_groups': ADMIN_PREFIX + '/repo_groups',
41 40 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
42 41 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
43 42 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
44 43
45 44 }[name].format(**kwargs)
46 45
47 46 if params:
48 47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 48 return base_url
50 49
51 50
52 51 def _get_permission_for_user(user, repo):
53 52 perm = UserRepoToPerm.query()\
54 53 .filter(UserRepoToPerm.repository ==
55 54 Repository.get_by_repo_name(repo))\
56 55 .filter(UserRepoToPerm.user == User.get_by_username(user))\
57 56 .all()
58 57 return perm
59 58
60 59
61 60 @pytest.mark.usefixtures("app")
62 61 class TestAdminRepositoryGroups(object):
63 62
64 63 def test_show_repo_groups(self, autologin_user):
65 64 self.app.get(route_path('repo_groups'))
66 65
67 66 def test_show_repo_groups_data(self, autologin_user, xhr_header):
68 67 response = self.app.get(route_path(
69 68 'repo_groups_data'), extra_environ=xhr_header)
70 69
71 70 all_repo_groups = RepoGroup.query().count()
72 71 assert response.json['recordsTotal'] == all_repo_groups
73 72
74 73 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
75 74 response = self.app.get(route_path(
76 75 'repo_groups_data', params={'search[value]': 'empty_search'}),
77 76 extra_environ=xhr_header)
78 77
79 78 all_repo_groups = RepoGroup.query().count()
80 79 assert response.json['recordsTotal'] == all_repo_groups
81 80 assert response.json['recordsFiltered'] == 0
82 81
83 82 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
84 83 fixture.create_repo_group('test_repo_group')
85 84 response = self.app.get(route_path(
86 85 'repo_groups_data'), extra_environ=xhr_header)
87 86 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
88 87 fixture.destroy_repo_group('test_repo_group')
89 88
90 89 def test_new(self, autologin_user):
91 90 self.app.get(route_path('repo_group_new'))
92 91
93 92 def test_new_with_parent_group(self, autologin_user, user_util):
94 93 gr = user_util.create_repo_group()
95 94
96 95 self.app.get(route_path('repo_group_new'),
97 96 params=dict(parent_group=gr.group_name))
98 97
99 98 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
100 99 self.app.get(route_path('repo_group_new'), status=403)
101 100
102 101 @pytest.mark.parametrize('repo_group_name', [
103 102 'git_repo',
104 103 'git_repo_ąć',
105 104 'hg_repo',
106 105 '12345',
107 106 'hg_repo_ąć',
108 107 ])
109 108 def test_create(self, autologin_user, repo_group_name, csrf_token):
110 109 repo_group_name_unicode = repo_group_name.decode('utf8')
111 110 description = 'description for newly created repo group'
112 111
113 112 response = self.app.post(
114 113 route_path('repo_group_create'),
115 114 fixture._get_group_create_params(
116 115 group_name=repo_group_name,
117 116 group_description=description,
118 117 csrf_token=csrf_token))
119 118
120 119 # run the check page that triggers the flash message
121 120 repo_gr_url = h.route_path(
122 121 'repo_group_home', repo_group_name=repo_group_name)
123 122
124 123 assert_session_flash(
125 124 response,
126 125 'Created repository group <a href="%s">%s</a>' % (
127 126 repo_gr_url, repo_group_name_unicode))
128 127
129 128 # # test if the repo group was created in the database
130 129 new_repo_group = RepoGroupModel()._get_repo_group(
131 130 repo_group_name_unicode)
132 131 assert new_repo_group is not None
133 132
134 133 assert new_repo_group.group_name == repo_group_name_unicode
135 134 assert new_repo_group.group_description == description
136 135
137 136 # test if the repository is visible in the list ?
138 137 response = self.app.get(repo_gr_url)
139 138 response.mustcontain(repo_group_name)
140 139
141 140 # test if the repository group was created on filesystem
142 141 is_on_filesystem = os.path.isdir(
143 142 os.path.join(TESTS_TMP_PATH, repo_group_name))
144 143 if not is_on_filesystem:
145 144 self.fail('no repo group %s in filesystem' % repo_group_name)
146 145
147 146 RepoGroupModel().delete(repo_group_name_unicode)
148 147 Session().commit()
149 148
150 149 @pytest.mark.parametrize('repo_group_name', [
151 150 'git_repo',
152 151 'git_repo_ąć',
153 152 'hg_repo',
154 153 '12345',
155 154 'hg_repo_ąć',
156 155 ])
157 156 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
158 157 parent_group = user_util.create_repo_group()
159 158 parent_group_name = parent_group.group_name
160 159
161 160 expected_group_name = '{}/{}'.format(
162 161 parent_group_name, repo_group_name)
163 162 expected_group_name_unicode = expected_group_name.decode('utf8')
164 163
165 164 try:
166 165 response = self.app.post(
167 166 route_path('repo_group_create'),
168 167 fixture._get_group_create_params(
169 168 group_name=repo_group_name,
170 169 group_parent_id=parent_group.group_id,
171 170 group_description='Test desciption',
172 171 csrf_token=csrf_token))
173 172
174 173 assert_session_flash(
175 174 response,
176 175 u'Created repository group <a href="%s">%s</a>' % (
177 176 h.route_path('repo_group_home',
178 177 repo_group_name=expected_group_name),
179 178 expected_group_name_unicode))
180 179 finally:
181 180 RepoGroupModel().delete(expected_group_name_unicode)
182 181 Session().commit()
183 182
184 183 def test_user_with_creation_permissions_cannot_create_subgroups(
185 184 self, autologin_regular_user, user_util):
186 185
187 186 user_util.grant_user_permission(
188 187 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
189 188 parent_group = user_util.create_repo_group()
190 189 parent_group_id = parent_group.group_id
191 190 self.app.get(
192 191 route_path('repo_group_new',
193 192 params=dict(parent_group=parent_group_id), ),
194 193 status=403)
@@ -1,767 +1,766 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import mock
22 21 import pytest
23 22
24 23 import rhodecode
25 24 from rhodecode.apps._base import ADMIN_PREFIX
26 25 from rhodecode.lib.utils2 import md5
27 26 from rhodecode.model.db import RhodeCodeUi
28 27 from rhodecode.model.meta import Session
29 28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
30 29 from rhodecode.tests import assert_session_flash
31 30 from rhodecode.tests.utils import AssertResponse
32 31
33 32
34 33 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
35 34
36 35
37 36 def route_path(name, params=None, **kwargs):
38 37 import urllib.request, urllib.parse, urllib.error
39 38 from rhodecode.apps._base import ADMIN_PREFIX
40 39
41 40 base_url = {
42 41
43 42 'admin_settings':
44 43 ADMIN_PREFIX +'/settings',
45 44 'admin_settings_update':
46 45 ADMIN_PREFIX + '/settings/update',
47 46 'admin_settings_global':
48 47 ADMIN_PREFIX + '/settings/global',
49 48 'admin_settings_global_update':
50 49 ADMIN_PREFIX + '/settings/global/update',
51 50 'admin_settings_vcs':
52 51 ADMIN_PREFIX + '/settings/vcs',
53 52 'admin_settings_vcs_update':
54 53 ADMIN_PREFIX + '/settings/vcs/update',
55 54 'admin_settings_vcs_svn_pattern_delete':
56 55 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
57 56 'admin_settings_mapping':
58 57 ADMIN_PREFIX + '/settings/mapping',
59 58 'admin_settings_mapping_update':
60 59 ADMIN_PREFIX + '/settings/mapping/update',
61 60 'admin_settings_visual':
62 61 ADMIN_PREFIX + '/settings/visual',
63 62 'admin_settings_visual_update':
64 63 ADMIN_PREFIX + '/settings/visual/update',
65 64 'admin_settings_issuetracker':
66 65 ADMIN_PREFIX + '/settings/issue-tracker',
67 66 'admin_settings_issuetracker_update':
68 67 ADMIN_PREFIX + '/settings/issue-tracker/update',
69 68 'admin_settings_issuetracker_test':
70 69 ADMIN_PREFIX + '/settings/issue-tracker/test',
71 70 'admin_settings_issuetracker_delete':
72 71 ADMIN_PREFIX + '/settings/issue-tracker/delete',
73 72 'admin_settings_email':
74 73 ADMIN_PREFIX + '/settings/email',
75 74 'admin_settings_email_update':
76 75 ADMIN_PREFIX + '/settings/email/update',
77 76 'admin_settings_hooks':
78 77 ADMIN_PREFIX + '/settings/hooks',
79 78 'admin_settings_hooks_update':
80 79 ADMIN_PREFIX + '/settings/hooks/update',
81 80 'admin_settings_hooks_delete':
82 81 ADMIN_PREFIX + '/settings/hooks/delete',
83 82 'admin_settings_search':
84 83 ADMIN_PREFIX + '/settings/search',
85 84 'admin_settings_labs':
86 85 ADMIN_PREFIX + '/settings/labs',
87 86 'admin_settings_labs_update':
88 87 ADMIN_PREFIX + '/settings/labs/update',
89 88
90 89 'admin_settings_sessions':
91 90 ADMIN_PREFIX + '/settings/sessions',
92 91 'admin_settings_sessions_cleanup':
93 92 ADMIN_PREFIX + '/settings/sessions/cleanup',
94 93 'admin_settings_system':
95 94 ADMIN_PREFIX + '/settings/system',
96 95 'admin_settings_system_update':
97 96 ADMIN_PREFIX + '/settings/system/updates',
98 97 'admin_settings_open_source':
99 98 ADMIN_PREFIX + '/settings/open_source',
100 99
101 100
102 101 }[name].format(**kwargs)
103 102
104 103 if params:
105 104 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
106 105 return base_url
107 106
108 107
109 108 @pytest.mark.usefixtures('autologin_user', 'app')
110 109 class TestAdminSettingsController(object):
111 110
112 111 @pytest.mark.parametrize('urlname', [
113 112 'admin_settings_vcs',
114 113 'admin_settings_mapping',
115 114 'admin_settings_global',
116 115 'admin_settings_visual',
117 116 'admin_settings_email',
118 117 'admin_settings_hooks',
119 118 'admin_settings_search',
120 119 ])
121 120 def test_simple_get(self, urlname):
122 121 self.app.get(route_path(urlname))
123 122
124 123 def test_create_custom_hook(self, csrf_token):
125 124 response = self.app.post(
126 125 route_path('admin_settings_hooks_update'),
127 126 params={
128 127 'new_hook_ui_key': 'test_hooks_1',
129 128 'new_hook_ui_value': 'cd /tmp',
130 129 'csrf_token': csrf_token})
131 130
132 131 response = response.follow()
133 132 response.mustcontain('test_hooks_1')
134 133 response.mustcontain('cd /tmp')
135 134
136 135 def test_create_custom_hook_delete(self, csrf_token):
137 136 response = self.app.post(
138 137 route_path('admin_settings_hooks_update'),
139 138 params={
140 139 'new_hook_ui_key': 'test_hooks_2',
141 140 'new_hook_ui_value': 'cd /tmp2',
142 141 'csrf_token': csrf_token})
143 142
144 143 response = response.follow()
145 144 response.mustcontain('test_hooks_2')
146 145 response.mustcontain('cd /tmp2')
147 146
148 147 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
149 148
150 149 # delete
151 150 self.app.post(
152 151 route_path('admin_settings_hooks_delete'),
153 152 params={'hook_id': hook_id, 'csrf_token': csrf_token})
154 153 response = self.app.get(route_path('admin_settings_hooks'))
155 154 response.mustcontain(no=['test_hooks_2'])
156 155 response.mustcontain(no=['cd /tmp2'])
157 156
158 157
159 158 @pytest.mark.usefixtures('autologin_user', 'app')
160 159 class TestAdminSettingsGlobal(object):
161 160
162 161 def test_pre_post_code_code_active(self, csrf_token):
163 162 pre_code = 'rc-pre-code-187652122'
164 163 post_code = 'rc-postcode-98165231'
165 164
166 165 response = self.post_and_verify_settings({
167 166 'rhodecode_pre_code': pre_code,
168 167 'rhodecode_post_code': post_code,
169 168 'csrf_token': csrf_token,
170 169 })
171 170
172 171 response = response.follow()
173 172 response.mustcontain(pre_code, post_code)
174 173
175 174 def test_pre_post_code_code_inactive(self, csrf_token):
176 175 pre_code = 'rc-pre-code-187652122'
177 176 post_code = 'rc-postcode-98165231'
178 177 response = self.post_and_verify_settings({
179 178 'rhodecode_pre_code': '',
180 179 'rhodecode_post_code': '',
181 180 'csrf_token': csrf_token,
182 181 })
183 182
184 183 response = response.follow()
185 184 response.mustcontain(no=[pre_code, post_code])
186 185
187 186 def test_captcha_activate(self, csrf_token):
188 187 self.post_and_verify_settings({
189 188 'rhodecode_captcha_private_key': '1234567890',
190 189 'rhodecode_captcha_public_key': '1234567890',
191 190 'csrf_token': csrf_token,
192 191 })
193 192
194 193 response = self.app.get(ADMIN_PREFIX + '/register')
195 194 response.mustcontain('captcha')
196 195
197 196 def test_captcha_deactivate(self, csrf_token):
198 197 self.post_and_verify_settings({
199 198 'rhodecode_captcha_private_key': '',
200 199 'rhodecode_captcha_public_key': '1234567890',
201 200 'csrf_token': csrf_token,
202 201 })
203 202
204 203 response = self.app.get(ADMIN_PREFIX + '/register')
205 204 response.mustcontain(no=['captcha'])
206 205
207 206 def test_title_change(self, csrf_token):
208 207 old_title = 'RhodeCode'
209 208
210 209 for new_title in ['Changed', 'Żółwik', old_title]:
211 210 response = self.post_and_verify_settings({
212 211 'rhodecode_title': new_title,
213 212 'csrf_token': csrf_token,
214 213 })
215 214
216 215 response = response.follow()
217 216 response.mustcontain(new_title)
218 217
219 218 def post_and_verify_settings(self, settings):
220 219 old_title = 'RhodeCode'
221 220 old_realm = 'RhodeCode authentication'
222 221 params = {
223 222 'rhodecode_title': old_title,
224 223 'rhodecode_realm': old_realm,
225 224 'rhodecode_pre_code': '',
226 225 'rhodecode_post_code': '',
227 226 'rhodecode_captcha_private_key': '',
228 227 'rhodecode_captcha_public_key': '',
229 228 'rhodecode_create_personal_repo_group': False,
230 229 'rhodecode_personal_repo_group_pattern': '${username}',
231 230 }
232 231 params.update(settings)
233 232 response = self.app.post(
234 233 route_path('admin_settings_global_update'), params=params)
235 234
236 235 assert_session_flash(response, 'Updated application settings')
237 236 app_settings = SettingsModel().get_all_settings()
238 237 del settings['csrf_token']
239 238 for key, value in settings.items():
240 239 assert app_settings[key] == value
241 240
242 241 return response
243 242
244 243
245 244 @pytest.mark.usefixtures('autologin_user', 'app')
246 245 class TestAdminSettingsVcs(object):
247 246
248 247 def test_contains_svn_default_patterns(self):
249 248 response = self.app.get(route_path('admin_settings_vcs'))
250 249 expected_patterns = [
251 250 '/trunk',
252 251 '/branches/*',
253 252 '/tags/*',
254 253 ]
255 254 for pattern in expected_patterns:
256 255 response.mustcontain(pattern)
257 256
258 257 def test_add_new_svn_branch_and_tag_pattern(
259 258 self, backend_svn, form_defaults, disable_sql_cache,
260 259 csrf_token):
261 260 form_defaults.update({
262 261 'new_svn_branch': '/exp/branches/*',
263 262 'new_svn_tag': '/important_tags/*',
264 263 'csrf_token': csrf_token,
265 264 })
266 265
267 266 response = self.app.post(
268 267 route_path('admin_settings_vcs_update'),
269 268 params=form_defaults, status=302)
270 269 response = response.follow()
271 270
272 271 # Expect to find the new values on the page
273 272 response.mustcontain('/exp/branches/*')
274 273 response.mustcontain('/important_tags/*')
275 274
276 275 # Expect that those patterns are used to match branches and tags now
277 276 repo = backend_svn['svn-simple-layout'].scm_instance()
278 277 assert 'exp/branches/exp-sphinx-docs' in repo.branches
279 278 assert 'important_tags/v0.5' in repo.tags
280 279
281 280 def test_add_same_svn_value_twice_shows_an_error_message(
282 281 self, form_defaults, csrf_token, settings_util):
283 282 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
284 283 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
285 284
286 285 response = self.app.post(
287 286 route_path('admin_settings_vcs_update'),
288 287 params={
289 288 'paths_root_path': form_defaults['paths_root_path'],
290 289 'new_svn_branch': '/test',
291 290 'new_svn_tag': '/test',
292 291 'csrf_token': csrf_token,
293 292 },
294 293 status=200)
295 294
296 295 response.mustcontain("Pattern already exists")
297 296 response.mustcontain("Some form inputs contain invalid data.")
298 297
299 298 @pytest.mark.parametrize('section', [
300 299 'vcs_svn_branch',
301 300 'vcs_svn_tag',
302 301 ])
303 302 def test_delete_svn_patterns(
304 303 self, section, csrf_token, settings_util):
305 304 setting = settings_util.create_rhodecode_ui(
306 305 section, '/test_delete', cleanup=False)
307 306
308 307 self.app.post(
309 308 route_path('admin_settings_vcs_svn_pattern_delete'),
310 309 params={
311 310 'delete_svn_pattern': setting.ui_id,
312 311 'csrf_token': csrf_token},
313 312 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
314 313
315 314 @pytest.mark.parametrize('section', [
316 315 'vcs_svn_branch',
317 316 'vcs_svn_tag',
318 317 ])
319 318 def test_delete_svn_patterns_raises_404_when_no_xhr(
320 319 self, section, csrf_token, settings_util):
321 320 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
322 321
323 322 self.app.post(
324 323 route_path('admin_settings_vcs_svn_pattern_delete'),
325 324 params={
326 325 'delete_svn_pattern': setting.ui_id,
327 326 'csrf_token': csrf_token},
328 327 status=404)
329 328
330 329 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
331 330 form_defaults.update({
332 331 'csrf_token': csrf_token,
333 332 'extensions_hgsubversion': 'True',
334 333 })
335 334 response = self.app.post(
336 335 route_path('admin_settings_vcs_update'),
337 336 params=form_defaults,
338 337 status=302)
339 338
340 339 response = response.follow()
341 340 extensions_input = (
342 341 '<input id="extensions_hgsubversion" '
343 342 'name="extensions_hgsubversion" type="checkbox" '
344 343 'value="True" checked="checked" />')
345 344 response.mustcontain(extensions_input)
346 345
347 346 def test_extensions_hgevolve(self, form_defaults, csrf_token):
348 347 form_defaults.update({
349 348 'csrf_token': csrf_token,
350 349 'extensions_evolve': 'True',
351 350 })
352 351 response = self.app.post(
353 352 route_path('admin_settings_vcs_update'),
354 353 params=form_defaults,
355 354 status=302)
356 355
357 356 response = response.follow()
358 357 extensions_input = (
359 358 '<input id="extensions_evolve" '
360 359 'name="extensions_evolve" type="checkbox" '
361 360 'value="True" checked="checked" />')
362 361 response.mustcontain(extensions_input)
363 362
364 363 def test_has_a_section_for_pull_request_settings(self):
365 364 response = self.app.get(route_path('admin_settings_vcs'))
366 365 response.mustcontain('Pull Request Settings')
367 366
368 367 def test_has_an_input_for_invalidation_of_inline_comments(self):
369 368 response = self.app.get(route_path('admin_settings_vcs'))
370 369 assert_response = response.assert_response()
371 370 assert_response.one_element_exists(
372 371 '[name=rhodecode_use_outdated_comments]')
373 372
374 373 @pytest.mark.parametrize('new_value', [True, False])
375 374 def test_allows_to_change_invalidation_of_inline_comments(
376 375 self, form_defaults, csrf_token, new_value):
377 376 setting_key = 'use_outdated_comments'
378 377 setting = SettingsModel().create_or_update_setting(
379 378 setting_key, not new_value, 'bool')
380 379 Session().add(setting)
381 380 Session().commit()
382 381
383 382 form_defaults.update({
384 383 'csrf_token': csrf_token,
385 384 'rhodecode_use_outdated_comments': str(new_value),
386 385 })
387 386 response = self.app.post(
388 387 route_path('admin_settings_vcs_update'),
389 388 params=form_defaults,
390 389 status=302)
391 390 response = response.follow()
392 391 setting = SettingsModel().get_setting_by_name(setting_key)
393 392 assert setting.app_settings_value is new_value
394 393
395 394 @pytest.mark.parametrize('new_value', [True, False])
396 395 def test_allows_to_change_hg_rebase_merge_strategy(
397 396 self, form_defaults, csrf_token, new_value):
398 397 setting_key = 'hg_use_rebase_for_merging'
399 398
400 399 form_defaults.update({
401 400 'csrf_token': csrf_token,
402 401 'rhodecode_' + setting_key: str(new_value),
403 402 })
404 403
405 404 with mock.patch.dict(
406 405 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
407 406 self.app.post(
408 407 route_path('admin_settings_vcs_update'),
409 408 params=form_defaults,
410 409 status=302)
411 410
412 411 setting = SettingsModel().get_setting_by_name(setting_key)
413 412 assert setting.app_settings_value is new_value
414 413
415 414 @pytest.fixture()
416 415 def disable_sql_cache(self, request):
417 416 patcher = mock.patch(
418 417 'rhodecode.lib.caching_query.FromCache.process_query')
419 418 request.addfinalizer(patcher.stop)
420 419 patcher.start()
421 420
422 421 @pytest.fixture()
423 422 def form_defaults(self):
424 423 from rhodecode.apps.admin.views.settings import AdminSettingsView
425 424 return AdminSettingsView._form_defaults()
426 425
427 426 # TODO: johbo: What we really want is to checkpoint before a test run and
428 427 # reset the session afterwards.
429 428 @pytest.fixture(scope='class', autouse=True)
430 429 def cleanup_settings(self, request, baseapp):
431 430 ui_id = RhodeCodeUi.ui_id
432 431 original_ids = list(
433 432 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
434 433
435 434 @request.addfinalizer
436 435 def cleanup():
437 436 RhodeCodeUi.query().filter(
438 437 ui_id.notin_(original_ids)).delete(False)
439 438
440 439
441 440 @pytest.mark.usefixtures('autologin_user', 'app')
442 441 class TestLabsSettings(object):
443 442 def test_get_settings_page_disabled(self):
444 443 with mock.patch.dict(
445 444 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
446 445
447 446 response = self.app.get(
448 447 route_path('admin_settings_labs'), status=302)
449 448
450 449 assert response.location.endswith(route_path('admin_settings'))
451 450
452 451 def test_get_settings_page_enabled(self):
453 452 from rhodecode.apps.admin.views import settings
454 453 lab_settings = [
455 454 settings.LabSetting(
456 455 key='rhodecode_bool',
457 456 type='bool',
458 457 group='bool group',
459 458 label='bool label',
460 459 help='bool help'
461 460 ),
462 461 settings.LabSetting(
463 462 key='rhodecode_text',
464 463 type='unicode',
465 464 group='text group',
466 465 label='text label',
467 466 help='text help'
468 467 ),
469 468 ]
470 469 with mock.patch.dict(rhodecode.CONFIG,
471 470 {'labs_settings_active': 'true'}):
472 471 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
473 472 response = self.app.get(route_path('admin_settings_labs'))
474 473
475 474 assert '<label>bool group:</label>' in response
476 475 assert '<label for="rhodecode_bool">bool label</label>' in response
477 476 assert '<p class="help-block">bool help</p>' in response
478 477 assert 'name="rhodecode_bool" type="checkbox"' in response
479 478
480 479 assert '<label>text group:</label>' in response
481 480 assert '<label for="rhodecode_text">text label</label>' in response
482 481 assert '<p class="help-block">text help</p>' in response
483 482 assert 'name="rhodecode_text" size="60" type="text"' in response
484 483
485 484
486 485 @pytest.mark.usefixtures('app')
487 486 class TestOpenSourceLicenses(object):
488 487
489 488 def test_records_are_displayed(self, autologin_user):
490 489 sample_licenses = [
491 490 {
492 491 "license": [
493 492 {
494 493 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
495 494 "shortName": "bsdOriginal",
496 495 "spdxId": "BSD-4-Clause",
497 496 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
498 497 }
499 498 ],
500 499 "name": "python2.7-coverage-3.7.1"
501 500 },
502 501 {
503 502 "license": [
504 503 {
505 504 "fullName": "MIT License",
506 505 "shortName": "mit",
507 506 "spdxId": "MIT",
508 507 "url": "http://spdx.org/licenses/MIT.html"
509 508 }
510 509 ],
511 510 "name": "python2.7-bootstrapped-pip-9.0.1"
512 511 },
513 512 ]
514 513 read_licenses_patch = mock.patch(
515 514 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
516 515 return_value=sample_licenses)
517 516 with read_licenses_patch:
518 517 response = self.app.get(
519 518 route_path('admin_settings_open_source'), status=200)
520 519
521 520 assert_response = response.assert_response()
522 521 assert_response.element_contains(
523 522 '.panel-heading', 'Licenses of Third Party Packages')
524 523 for license_data in sample_licenses:
525 524 response.mustcontain(license_data["license"][0]["spdxId"])
526 525 assert_response.element_contains('.panel-body', license_data["name"])
527 526
528 527 def test_records_can_be_read(self, autologin_user):
529 528 response = self.app.get(
530 529 route_path('admin_settings_open_source'), status=200)
531 530 assert_response = response.assert_response()
532 531 assert_response.element_contains(
533 532 '.panel-heading', 'Licenses of Third Party Packages')
534 533
535 534 def test_forbidden_when_normal_user(self, autologin_regular_user):
536 535 self.app.get(
537 536 route_path('admin_settings_open_source'), status=404)
538 537
539 538
540 539 @pytest.mark.usefixtures('app')
541 540 class TestUserSessions(object):
542 541
543 542 def test_forbidden_when_normal_user(self, autologin_regular_user):
544 543 self.app.get(route_path('admin_settings_sessions'), status=404)
545 544
546 545 def test_show_sessions_page(self, autologin_user):
547 546 response = self.app.get(route_path('admin_settings_sessions'), status=200)
548 547 response.mustcontain('file')
549 548
550 549 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
551 550
552 551 post_data = {
553 552 'csrf_token': csrf_token,
554 553 'expire_days': '60'
555 554 }
556 555 response = self.app.post(
557 556 route_path('admin_settings_sessions_cleanup'), params=post_data,
558 557 status=302)
559 558 assert_session_flash(response, 'Cleaned up old sessions')
560 559
561 560
562 561 @pytest.mark.usefixtures('app')
563 562 class TestAdminSystemInfo(object):
564 563
565 564 def test_forbidden_when_normal_user(self, autologin_regular_user):
566 565 self.app.get(route_path('admin_settings_system'), status=404)
567 566
568 567 def test_system_info_page(self, autologin_user):
569 568 response = self.app.get(route_path('admin_settings_system'))
570 569 response.mustcontain('RhodeCode Community Edition, version {}'.format(
571 570 rhodecode.__version__))
572 571
573 572 def test_system_update_new_version(self, autologin_user):
574 573 update_data = {
575 574 'versions': [
576 575 {
577 576 'version': '100.3.1415926535',
578 577 'general': 'The latest version we are ever going to ship'
579 578 },
580 579 {
581 580 'version': '0.0.0',
582 581 'general': 'The first version we ever shipped'
583 582 }
584 583 ]
585 584 }
586 585 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
587 586 response = self.app.get(route_path('admin_settings_system_update'))
588 587 response.mustcontain('A <b>new version</b> is available')
589 588
590 589 def test_system_update_nothing_new(self, autologin_user):
591 590 update_data = {
592 591 'versions': [
593 592 {
594 593 'version': '0.0.0',
595 594 'general': 'The first version we ever shipped'
596 595 }
597 596 ]
598 597 }
599 598 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
600 599 response = self.app.get(route_path('admin_settings_system_update'))
601 600 response.mustcontain(
602 601 'This instance is already running the <b>latest</b> stable version')
603 602
604 603 def test_system_update_bad_response(self, autologin_user):
605 604 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
606 605 response = self.app.get(route_path('admin_settings_system_update'))
607 606 response.mustcontain(
608 607 'Bad data sent from update server')
609 608
610 609
611 610 @pytest.mark.usefixtures("app")
612 611 class TestAdminSettingsIssueTracker(object):
613 612 RC_PREFIX = 'rhodecode_'
614 613 SHORT_PATTERN_KEY = 'issuetracker_pat_'
615 614 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
616 615 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
617 616
618 617 def test_issuetracker_index(self, autologin_user):
619 618 response = self.app.get(route_path('admin_settings_issuetracker'))
620 619 assert response.status_code == 200
621 620
622 621 def test_add_empty_issuetracker_pattern(
623 622 self, request, autologin_user, csrf_token):
624 623 post_url = route_path('admin_settings_issuetracker_update')
625 624 post_data = {
626 625 'csrf_token': csrf_token
627 626 }
628 627 self.app.post(post_url, post_data, status=302)
629 628
630 629 def test_add_issuetracker_pattern(
631 630 self, request, autologin_user, csrf_token):
632 631 pattern = 'issuetracker_pat'
633 632 another_pattern = pattern+'1'
634 633 post_url = route_path('admin_settings_issuetracker_update')
635 634 post_data = {
636 635 'new_pattern_pattern_0': pattern,
637 636 'new_pattern_url_0': 'http://url',
638 637 'new_pattern_prefix_0': 'prefix',
639 638 'new_pattern_description_0': 'description',
640 639 'new_pattern_pattern_1': another_pattern,
641 640 'new_pattern_url_1': 'https://url1',
642 641 'new_pattern_prefix_1': 'prefix1',
643 642 'new_pattern_description_1': 'description1',
644 643 'csrf_token': csrf_token
645 644 }
646 645 self.app.post(post_url, post_data, status=302)
647 646 settings = SettingsModel().get_all_settings()
648 647 self.uid = md5(pattern)
649 648 assert settings[self.PATTERN_KEY+self.uid] == pattern
650 649 self.another_uid = md5(another_pattern)
651 650 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
652 651
653 652 @request.addfinalizer
654 653 def cleanup():
655 654 defaults = SettingsModel().get_all_settings()
656 655
657 656 entries = [name for name in defaults if (
658 657 (self.uid in name) or (self.another_uid) in name)]
659 658 start = len(self.RC_PREFIX)
660 659 for del_key in entries:
661 660 # TODO: anderson: get_by_name needs name without prefix
662 661 entry = SettingsModel().get_setting_by_name(del_key[start:])
663 662 Session().delete(entry)
664 663
665 664 Session().commit()
666 665
667 666 def test_edit_issuetracker_pattern(
668 667 self, autologin_user, backend, csrf_token, request):
669 668
670 669 old_pattern = 'issuetracker_pat1'
671 670 old_uid = md5(old_pattern)
672 671
673 672 post_url = route_path('admin_settings_issuetracker_update')
674 673 post_data = {
675 674 'new_pattern_pattern_0': old_pattern,
676 675 'new_pattern_url_0': 'http://url',
677 676 'new_pattern_prefix_0': 'prefix',
678 677 'new_pattern_description_0': 'description',
679 678
680 679 'csrf_token': csrf_token
681 680 }
682 681 self.app.post(post_url, post_data, status=302)
683 682
684 683 new_pattern = 'issuetracker_pat1_edited'
685 684 self.new_uid = md5(new_pattern)
686 685
687 686 post_url = route_path('admin_settings_issuetracker_update')
688 687 post_data = {
689 688 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
690 689 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
691 690 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
692 691 'new_pattern_description_{}'.format(old_uid): 'description_edited',
693 692 'uid': old_uid,
694 693 'csrf_token': csrf_token
695 694 }
696 695 self.app.post(post_url, post_data, status=302)
697 696
698 697 settings = SettingsModel().get_all_settings()
699 698 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
700 699 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
701 700 assert self.PATTERN_KEY+old_uid not in settings
702 701
703 702 @request.addfinalizer
704 703 def cleanup():
705 704 IssueTrackerSettingsModel().delete_entries(old_uid)
706 705 IssueTrackerSettingsModel().delete_entries(self.new_uid)
707 706
708 707 def test_replace_issuetracker_pattern_description(
709 708 self, autologin_user, csrf_token, request, settings_util):
710 709 prefix = 'issuetracker'
711 710 pattern = 'issuetracker_pat'
712 711 self.uid = md5(pattern)
713 712 pattern_key = '_'.join([prefix, 'pat', self.uid])
714 713 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
715 714 desc_key = '_'.join([prefix, 'desc', self.uid])
716 715 rc_desc_key = '_'.join(['rhodecode', desc_key])
717 716 new_description = 'new_description'
718 717
719 718 settings_util.create_rhodecode_setting(
720 719 pattern_key, pattern, 'unicode', cleanup=False)
721 720 settings_util.create_rhodecode_setting(
722 721 desc_key, 'old description', 'unicode', cleanup=False)
723 722
724 723 post_url = route_path('admin_settings_issuetracker_update')
725 724 post_data = {
726 725 'new_pattern_pattern_0': pattern,
727 726 'new_pattern_url_0': 'https://url',
728 727 'new_pattern_prefix_0': 'prefix',
729 728 'new_pattern_description_0': new_description,
730 729 'uid': self.uid,
731 730 'csrf_token': csrf_token
732 731 }
733 732 self.app.post(post_url, post_data, status=302)
734 733 settings = SettingsModel().get_all_settings()
735 734 assert settings[rc_pattern_key] == pattern
736 735 assert settings[rc_desc_key] == new_description
737 736
738 737 @request.addfinalizer
739 738 def cleanup():
740 739 IssueTrackerSettingsModel().delete_entries(self.uid)
741 740
742 741 def test_delete_issuetracker_pattern(
743 742 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
744 743
745 744 old_pattern = 'issuetracker_pat_deleted'
746 745 old_uid = md5(old_pattern)
747 746
748 747 post_url = route_path('admin_settings_issuetracker_update')
749 748 post_data = {
750 749 'new_pattern_pattern_0': old_pattern,
751 750 'new_pattern_url_0': 'http://url',
752 751 'new_pattern_prefix_0': 'prefix',
753 752 'new_pattern_description_0': 'description',
754 753
755 754 'csrf_token': csrf_token
756 755 }
757 756 self.app.post(post_url, post_data, status=302)
758 757
759 758 post_url = route_path('admin_settings_issuetracker_delete')
760 759 post_data = {
761 760 'uid': old_uid,
762 761 'csrf_token': csrf_token
763 762 }
764 763 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
765 764 settings = SettingsModel().get_all_settings()
766 765 assert self.PATTERN_KEY+old_uid not in settings
767 766 assert self.DESC_KEY + old_uid not in settings
@@ -1,170 +1,169 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.model.db import UserGroup, User
24 23 from rhodecode.model.meta import Session
25 24
26 25 from rhodecode.tests import (
27 26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
28 27 from rhodecode.tests.fixture import Fixture
29 28
30 29 fixture = Fixture()
31 30
32 31
33 32 def route_path(name, params=None, **kwargs):
34 33 import urllib.request, urllib.parse, urllib.error
35 34 from rhodecode.apps._base import ADMIN_PREFIX
36 35
37 36 base_url = {
38 37 'user_groups': ADMIN_PREFIX + '/user_groups',
39 38 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
40 39 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
41 40 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
42 41 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
43 42 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
44 43 }[name].format(**kwargs)
45 44
46 45 if params:
47 46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 47 return base_url
49 48
50 49
51 50 class TestAdminUserGroupsView(TestController):
52 51
53 52 def test_show_users(self):
54 53 self.log_user()
55 54 self.app.get(route_path('user_groups'))
56 55
57 56 def test_show_user_groups_data(self, xhr_header):
58 57 self.log_user()
59 58 response = self.app.get(route_path(
60 59 'user_groups_data'), extra_environ=xhr_header)
61 60
62 61 all_user_groups = UserGroup.query().count()
63 62 assert response.json['recordsTotal'] == all_user_groups
64 63
65 64 def test_show_user_groups_data_filtered(self, xhr_header):
66 65 self.log_user()
67 66 response = self.app.get(route_path(
68 67 'user_groups_data', params={'search[value]': 'empty_search'}),
69 68 extra_environ=xhr_header)
70 69
71 70 all_user_groups = UserGroup.query().count()
72 71 assert response.json['recordsTotal'] == all_user_groups
73 72 assert response.json['recordsFiltered'] == 0
74 73
75 74 def test_usergroup_escape(self, user_util, xhr_header):
76 75 self.log_user()
77 76
78 77 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
79 78 user = user_util.create_user()
80 79 user.name = xss_img
81 80 user.lastname = xss_img
82 81 Session().add(user)
83 82 Session().commit()
84 83
85 84 user_group = user_util.create_user_group()
86 85
87 86 user_group.users_group_name = xss_img
88 87 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
89 88
90 89 response = self.app.get(
91 90 route_path('user_groups_data'), extra_environ=xhr_header)
92 91
93 92 response.mustcontain(
94 93 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
95 94 response.mustcontain(
96 95 '&lt;img src=&#34;/image1&#34; onload=&#34;'
97 96 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
98 97
99 98 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
100 99 self.log_user()
101 100 ug = user_util.create_user_group()
102 101 response = self.app.get(
103 102 route_path('user_group_members_data', user_group_id=ug.users_group_id),
104 103 extra_environ=xhr_header)
105 104
106 105 assert response.json == {'members': []}
107 106
108 107 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
109 108 self.log_user()
110 109 members = [u.user_id for u in User.get_all()]
111 110 ug = user_util.create_user_group(members=members)
112 111 response = self.app.get(
113 112 route_path('user_group_members_data',
114 113 user_group_id=ug.users_group_id),
115 114 extra_environ=xhr_header)
116 115
117 116 assert len(response.json['members']) == len(members)
118 117
119 118 def test_creation_page(self):
120 119 self.log_user()
121 120 self.app.get(route_path('user_groups_new'), status=200)
122 121
123 122 def test_create(self):
124 123 from rhodecode.lib import helpers as h
125 124
126 125 self.log_user()
127 126 users_group_name = 'test_user_group'
128 127 response = self.app.post(route_path('user_groups_create'), {
129 128 'users_group_name': users_group_name,
130 129 'user_group_description': 'DESC',
131 130 'active': True,
132 131 'csrf_token': self.csrf_token})
133 132
134 133 user_group_id = UserGroup.get_by_group_name(
135 134 users_group_name).users_group_id
136 135
137 136 user_group_link = h.link_to(
138 137 users_group_name,
139 138 route_path('edit_user_group', user_group_id=user_group_id))
140 139
141 140 assert_session_flash(
142 141 response,
143 142 'Created user group %s' % user_group_link)
144 143
145 144 fixture.destroy_user_group(users_group_name)
146 145
147 146 def test_create_with_empty_name(self):
148 147 self.log_user()
149 148
150 149 response = self.app.post(route_path('user_groups_create'), {
151 150 'users_group_name': '',
152 151 'user_group_description': 'DESC',
153 152 'active': True,
154 153 'csrf_token': self.csrf_token}, status=200)
155 154
156 155 response.mustcontain('Please enter a value')
157 156
158 157 def test_create_duplicate(self, user_util):
159 158 self.log_user()
160 159
161 160 user_group = user_util.create_user_group()
162 161 duplicate_name = user_group.users_group_name
163 162 response = self.app.post(route_path('user_groups_create'), {
164 163 'users_group_name': duplicate_name,
165 164 'user_group_description': 'DESC',
166 165 'active': True,
167 166 'csrf_token': self.csrf_token}, status=200)
168 167
169 168 response.mustcontain(
170 169 'User group `{}` already exists'.format(duplicate_name))
@@ -1,794 +1,793 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21 from sqlalchemy.orm.exc import NoResultFound
23 22
24 23 from rhodecode.lib import auth
25 24 from rhodecode.lib import helpers as h
26 25 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
27 26 from rhodecode.model.meta import Session
28 27 from rhodecode.model.user import UserModel
29 28
30 29 from rhodecode.tests import (
31 30 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
32 31 from rhodecode.tests.fixture import Fixture
33 32
34 33 fixture = Fixture()
35 34
36 35
37 36 def route_path(name, params=None, **kwargs):
38 37 import urllib.request, urllib.parse, urllib.error
39 38 from rhodecode.apps._base import ADMIN_PREFIX
40 39
41 40 base_url = {
42 41 'users':
43 42 ADMIN_PREFIX + '/users',
44 43 'users_data':
45 44 ADMIN_PREFIX + '/users_data',
46 45 'users_create':
47 46 ADMIN_PREFIX + '/users/create',
48 47 'users_new':
49 48 ADMIN_PREFIX + '/users/new',
50 49 'user_edit':
51 50 ADMIN_PREFIX + '/users/{user_id}/edit',
52 51 'user_edit_advanced':
53 52 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
54 53 'user_edit_global_perms':
55 54 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
56 55 'user_edit_global_perms_update':
57 56 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
58 57 'user_update':
59 58 ADMIN_PREFIX + '/users/{user_id}/update',
60 59 'user_delete':
61 60 ADMIN_PREFIX + '/users/{user_id}/delete',
62 61 'user_create_personal_repo_group':
63 62 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
64 63
65 64 'edit_user_auth_tokens':
66 65 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
67 66 'edit_user_auth_tokens_add':
68 67 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
69 68 'edit_user_auth_tokens_delete':
70 69 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
71 70
72 71 'edit_user_emails':
73 72 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
74 73 'edit_user_emails_add':
75 74 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
76 75 'edit_user_emails_delete':
77 76 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
78 77
79 78 'edit_user_ips':
80 79 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
81 80 'edit_user_ips_add':
82 81 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
83 82 'edit_user_ips_delete':
84 83 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
85 84
86 85 'edit_user_perms_summary':
87 86 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
88 87 'edit_user_perms_summary_json':
89 88 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
90 89
91 90 'edit_user_audit_logs':
92 91 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
93 92
94 93 'edit_user_audit_logs_download':
95 94 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
96 95
97 96 }[name].format(**kwargs)
98 97
99 98 if params:
100 99 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
101 100 return base_url
102 101
103 102
104 103 class TestAdminUsersView(TestController):
105 104
106 105 def test_show_users(self):
107 106 self.log_user()
108 107 self.app.get(route_path('users'))
109 108
110 109 def test_show_users_data(self, xhr_header):
111 110 self.log_user()
112 111 response = self.app.get(route_path(
113 112 'users_data'), extra_environ=xhr_header)
114 113
115 114 all_users = User.query().filter(
116 115 User.username != User.DEFAULT_USER).count()
117 116 assert response.json['recordsTotal'] == all_users
118 117
119 118 def test_show_users_data_filtered(self, xhr_header):
120 119 self.log_user()
121 120 response = self.app.get(route_path(
122 121 'users_data', params={'search[value]': 'empty_search'}),
123 122 extra_environ=xhr_header)
124 123
125 124 all_users = User.query().filter(
126 125 User.username != User.DEFAULT_USER).count()
127 126 assert response.json['recordsTotal'] == all_users
128 127 assert response.json['recordsFiltered'] == 0
129 128
130 129 def test_auth_tokens_default_user(self):
131 130 self.log_user()
132 131 user = User.get_default_user()
133 132 response = self.app.get(
134 133 route_path('edit_user_auth_tokens', user_id=user.user_id),
135 134 status=302)
136 135
137 136 def test_auth_tokens(self):
138 137 self.log_user()
139 138
140 139 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
141 140 user_id = user.user_id
142 141 auth_tokens = user.auth_tokens
143 142 response = self.app.get(
144 143 route_path('edit_user_auth_tokens', user_id=user_id))
145 144 for token in auth_tokens:
146 145 response.mustcontain(token[:4])
147 146 response.mustcontain('never')
148 147
149 148 @pytest.mark.parametrize("desc, lifetime", [
150 149 ('forever', -1),
151 150 ('5mins', 60*5),
152 151 ('30days', 60*60*24*30),
153 152 ])
154 153 def test_add_auth_token(self, desc, lifetime, user_util):
155 154 self.log_user()
156 155 user = user_util.create_user()
157 156 user_id = user.user_id
158 157
159 158 response = self.app.post(
160 159 route_path('edit_user_auth_tokens_add', user_id=user_id),
161 160 {'description': desc, 'lifetime': lifetime,
162 161 'csrf_token': self.csrf_token})
163 162 assert_session_flash(response, 'Auth token successfully created')
164 163
165 164 response = response.follow()
166 165 user = User.get(user_id)
167 166 for auth_token in user.auth_tokens:
168 167 response.mustcontain(auth_token[:4])
169 168
170 169 def test_delete_auth_token(self, user_util):
171 170 self.log_user()
172 171 user = user_util.create_user()
173 172 user_id = user.user_id
174 173 keys = user.auth_tokens
175 174 assert 2 == len(keys)
176 175
177 176 response = self.app.post(
178 177 route_path('edit_user_auth_tokens_add', user_id=user_id),
179 178 {'description': 'desc', 'lifetime': -1,
180 179 'csrf_token': self.csrf_token})
181 180 assert_session_flash(response, 'Auth token successfully created')
182 181 response.follow()
183 182
184 183 # now delete our key
185 184 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
186 185 assert 3 == len(keys)
187 186
188 187 response = self.app.post(
189 188 route_path('edit_user_auth_tokens_delete', user_id=user_id),
190 189 {'del_auth_token': keys[0].user_api_key_id,
191 190 'csrf_token': self.csrf_token})
192 191
193 192 assert_session_flash(response, 'Auth token successfully deleted')
194 193 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
195 194 assert 2 == len(keys)
196 195
197 196 def test_ips(self):
198 197 self.log_user()
199 198 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
200 199 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
201 200 response.mustcontain('All IP addresses are allowed')
202 201
203 202 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
204 203 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
205 204 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
206 205 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
207 206 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
208 207 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
209 208 ('127_bad_ip', 'foobar', 'foobar', True),
210 209 ])
211 210 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
212 211 self.log_user()
213 212 user = user_util.create_user(username=test_name)
214 213 user_id = user.user_id
215 214
216 215 response = self.app.post(
217 216 route_path('edit_user_ips_add', user_id=user_id),
218 217 params={'new_ip': ip, 'csrf_token': self.csrf_token})
219 218
220 219 if failure:
221 220 assert_session_flash(
222 221 response, 'Please enter a valid IPv4 or IpV6 address')
223 222 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
224 223
225 224 response.mustcontain(no=[ip])
226 225 response.mustcontain(no=[ip_range])
227 226
228 227 else:
229 228 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
230 229 response.mustcontain(ip)
231 230 response.mustcontain(ip_range)
232 231
233 232 def test_ips_delete(self, user_util):
234 233 self.log_user()
235 234 user = user_util.create_user()
236 235 user_id = user.user_id
237 236 ip = '127.0.0.1/32'
238 237 ip_range = '127.0.0.1 - 127.0.0.1'
239 238 new_ip = UserModel().add_extra_ip(user_id, ip)
240 239 Session().commit()
241 240 new_ip_id = new_ip.ip_id
242 241
243 242 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
244 243 response.mustcontain(ip)
245 244 response.mustcontain(ip_range)
246 245
247 246 self.app.post(
248 247 route_path('edit_user_ips_delete', user_id=user_id),
249 248 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
250 249
251 250 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
252 251 response.mustcontain('All IP addresses are allowed')
253 252 response.mustcontain(no=[ip])
254 253 response.mustcontain(no=[ip_range])
255 254
256 255 def test_emails(self):
257 256 self.log_user()
258 257 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
259 258 response = self.app.get(
260 259 route_path('edit_user_emails', user_id=user.user_id))
261 260 response.mustcontain('No additional emails specified')
262 261
263 262 def test_emails_add(self, user_util):
264 263 self.log_user()
265 264 user = user_util.create_user()
266 265 user_id = user.user_id
267 266
268 267 self.app.post(
269 268 route_path('edit_user_emails_add', user_id=user_id),
270 269 params={'new_email': 'example@rhodecode.com',
271 270 'csrf_token': self.csrf_token})
272 271
273 272 response = self.app.get(
274 273 route_path('edit_user_emails', user_id=user_id))
275 274 response.mustcontain('example@rhodecode.com')
276 275
277 276 def test_emails_add_existing_email(self, user_util, user_regular):
278 277 existing_email = user_regular.email
279 278
280 279 self.log_user()
281 280 user = user_util.create_user()
282 281 user_id = user.user_id
283 282
284 283 response = self.app.post(
285 284 route_path('edit_user_emails_add', user_id=user_id),
286 285 params={'new_email': existing_email,
287 286 'csrf_token': self.csrf_token})
288 287 assert_session_flash(
289 288 response, 'This e-mail address is already taken')
290 289
291 290 response = self.app.get(
292 291 route_path('edit_user_emails', user_id=user_id))
293 292 response.mustcontain(no=[existing_email])
294 293
295 294 def test_emails_delete(self, user_util):
296 295 self.log_user()
297 296 user = user_util.create_user()
298 297 user_id = user.user_id
299 298
300 299 self.app.post(
301 300 route_path('edit_user_emails_add', user_id=user_id),
302 301 params={'new_email': 'example@rhodecode.com',
303 302 'csrf_token': self.csrf_token})
304 303
305 304 response = self.app.get(
306 305 route_path('edit_user_emails', user_id=user_id))
307 306 response.mustcontain('example@rhodecode.com')
308 307
309 308 user_email = UserEmailMap.query()\
310 309 .filter(UserEmailMap.email == 'example@rhodecode.com') \
311 310 .filter(UserEmailMap.user_id == user_id)\
312 311 .one()
313 312
314 313 del_email_id = user_email.email_id
315 314 self.app.post(
316 315 route_path('edit_user_emails_delete', user_id=user_id),
317 316 params={'del_email_id': del_email_id,
318 317 'csrf_token': self.csrf_token})
319 318
320 319 response = self.app.get(
321 320 route_path('edit_user_emails', user_id=user_id))
322 321 response.mustcontain(no=['example@rhodecode.com'])
323 322
324 323 def test_create(self, request, xhr_header):
325 324 self.log_user()
326 325 username = 'newtestuser'
327 326 password = 'test12'
328 327 password_confirmation = password
329 328 name = 'name'
330 329 lastname = 'lastname'
331 330 email = 'mail@mail.com'
332 331
333 332 self.app.get(route_path('users_new'))
334 333
335 334 response = self.app.post(route_path('users_create'), params={
336 335 'username': username,
337 336 'password': password,
338 337 'description': 'mr CTO',
339 338 'password_confirmation': password_confirmation,
340 339 'firstname': name,
341 340 'active': True,
342 341 'lastname': lastname,
343 342 'extern_name': 'rhodecode',
344 343 'extern_type': 'rhodecode',
345 344 'email': email,
346 345 'csrf_token': self.csrf_token,
347 346 })
348 347 user_link = h.link_to(
349 348 username,
350 349 route_path(
351 350 'user_edit', user_id=User.get_by_username(username).user_id))
352 351 assert_session_flash(response, 'Created user %s' % (user_link,))
353 352
354 353 @request.addfinalizer
355 354 def cleanup():
356 355 fixture.destroy_user(username)
357 356 Session().commit()
358 357
359 358 new_user = User.query().filter(User.username == username).one()
360 359
361 360 assert new_user.username == username
362 361 assert auth.check_password(password, new_user.password)
363 362 assert new_user.name == name
364 363 assert new_user.lastname == lastname
365 364 assert new_user.email == email
366 365
367 366 response = self.app.get(route_path('users_data'),
368 367 extra_environ=xhr_header)
369 368 response.mustcontain(username)
370 369
371 370 def test_create_err(self):
372 371 self.log_user()
373 372 username = 'new_user'
374 373 password = ''
375 374 name = 'name'
376 375 lastname = 'lastname'
377 376 email = 'errmail.com'
378 377
379 378 self.app.get(route_path('users_new'))
380 379
381 380 response = self.app.post(route_path('users_create'), params={
382 381 'username': username,
383 382 'password': password,
384 383 'name': name,
385 384 'active': False,
386 385 'lastname': lastname,
387 386 'description': 'mr CTO',
388 387 'email': email,
389 388 'csrf_token': self.csrf_token,
390 389 })
391 390
392 391 msg = u'Username "%(username)s" is forbidden'
393 392 msg = h.html_escape(msg % {'username': 'new_user'})
394 393 response.mustcontain('<span class="error-message">%s</span>' % msg)
395 394 response.mustcontain(
396 395 '<span class="error-message">Please enter a value</span>')
397 396 response.mustcontain(
398 397 '<span class="error-message">An email address must contain a'
399 398 ' single @</span>')
400 399
401 400 def get_user():
402 401 Session().query(User).filter(User.username == username).one()
403 402
404 403 with pytest.raises(NoResultFound):
405 404 get_user()
406 405
407 406 def test_new(self):
408 407 self.log_user()
409 408 self.app.get(route_path('users_new'))
410 409
411 410 @pytest.mark.parametrize("name, attrs", [
412 411 ('firstname', {'firstname': 'new_username'}),
413 412 ('lastname', {'lastname': 'new_username'}),
414 413 ('admin', {'admin': True}),
415 414 ('admin', {'admin': False}),
416 415 ('extern_type', {'extern_type': 'ldap'}),
417 416 ('extern_type', {'extern_type': None}),
418 417 ('extern_name', {'extern_name': 'test'}),
419 418 ('extern_name', {'extern_name': None}),
420 419 ('active', {'active': False}),
421 420 ('active', {'active': True}),
422 421 ('email', {'email': 'some@email.com'}),
423 422 ('language', {'language': 'de'}),
424 423 ('language', {'language': 'en'}),
425 424 ('description', {'description': 'hello CTO'}),
426 425 # ('new_password', {'new_password': 'foobar123',
427 426 # 'password_confirmation': 'foobar123'})
428 427 ])
429 428 def test_update(self, name, attrs, user_util):
430 429 self.log_user()
431 430 usr = user_util.create_user(
432 431 password='qweqwe',
433 432 email='testme@rhodecode.org',
434 433 extern_type='rhodecode',
435 434 extern_name='xxx',
436 435 )
437 436 user_id = usr.user_id
438 437 Session().commit()
439 438
440 439 params = usr.get_api_data()
441 440 cur_lang = params['language'] or 'en'
442 441 params.update({
443 442 'password_confirmation': '',
444 443 'new_password': '',
445 444 'language': cur_lang,
446 445 'csrf_token': self.csrf_token,
447 446 })
448 447 params.update({'new_password': ''})
449 448 params.update(attrs)
450 449 if name == 'email':
451 450 params['emails'] = [attrs['email']]
452 451 elif name == 'extern_type':
453 452 # cannot update this via form, expected value is original one
454 453 params['extern_type'] = "rhodecode"
455 454 elif name == 'extern_name':
456 455 # cannot update this via form, expected value is original one
457 456 params['extern_name'] = 'xxx'
458 457 # special case since this user is not
459 458 # logged in yet his data is not filled
460 459 # so we use creation data
461 460
462 461 response = self.app.post(
463 462 route_path('user_update', user_id=usr.user_id), params)
464 463 assert response.status_int == 302
465 464 assert_session_flash(response, 'User updated successfully')
466 465
467 466 updated_user = User.get(user_id)
468 467 updated_params = updated_user.get_api_data()
469 468 updated_params.update({'password_confirmation': ''})
470 469 updated_params.update({'new_password': ''})
471 470
472 471 del params['csrf_token']
473 472 assert params == updated_params
474 473
475 474 def test_update_and_migrate_password(
476 475 self, autologin_user, real_crypto_backend, user_util):
477 476
478 477 user = user_util.create_user()
479 478 temp_user = user.username
480 479 user.password = auth._RhodeCodeCryptoSha256().hash_create(
481 480 b'test123')
482 481 Session().add(user)
483 482 Session().commit()
484 483
485 484 params = user.get_api_data()
486 485
487 486 params.update({
488 487 'password_confirmation': 'qweqwe123',
489 488 'new_password': 'qweqwe123',
490 489 'language': 'en',
491 490 'csrf_token': autologin_user.csrf_token,
492 491 })
493 492
494 493 response = self.app.post(
495 494 route_path('user_update', user_id=user.user_id), params)
496 495 assert response.status_int == 302
497 496 assert_session_flash(response, 'User updated successfully')
498 497
499 498 # new password should be bcrypted, after log-in and transfer
500 499 user = User.get_by_username(temp_user)
501 500 assert user.password.startswith('$')
502 501
503 502 updated_user = User.get_by_username(temp_user)
504 503 updated_params = updated_user.get_api_data()
505 504 updated_params.update({'password_confirmation': 'qweqwe123'})
506 505 updated_params.update({'new_password': 'qweqwe123'})
507 506
508 507 del params['csrf_token']
509 508 assert params == updated_params
510 509
511 510 def test_delete(self):
512 511 self.log_user()
513 512 username = 'newtestuserdeleteme'
514 513
515 514 fixture.create_user(name=username)
516 515
517 516 new_user = Session().query(User)\
518 517 .filter(User.username == username).one()
519 518 response = self.app.post(
520 519 route_path('user_delete', user_id=new_user.user_id),
521 520 params={'csrf_token': self.csrf_token})
522 521
523 522 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
524 523
525 524 def test_delete_owner_of_repository(self, request, user_util):
526 525 self.log_user()
527 526 obj_name = 'test_repo'
528 527 usr = user_util.create_user()
529 528 username = usr.username
530 529 fixture.create_repo(obj_name, cur_user=usr.username)
531 530
532 531 new_user = Session().query(User)\
533 532 .filter(User.username == username).one()
534 533 response = self.app.post(
535 534 route_path('user_delete', user_id=new_user.user_id),
536 535 params={'csrf_token': self.csrf_token})
537 536
538 537 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
539 538 'Switch owners or remove those repositories:%s' % (username, obj_name)
540 539 assert_session_flash(response, msg)
541 540 fixture.destroy_repo(obj_name)
542 541
543 542 def test_delete_owner_of_repository_detaching(self, request, user_util):
544 543 self.log_user()
545 544 obj_name = 'test_repo'
546 545 usr = user_util.create_user(auto_cleanup=False)
547 546 username = usr.username
548 547 fixture.create_repo(obj_name, cur_user=usr.username)
549 548 Session().commit()
550 549
551 550 new_user = Session().query(User)\
552 551 .filter(User.username == username).one()
553 552 response = self.app.post(
554 553 route_path('user_delete', user_id=new_user.user_id),
555 554 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
556 555
557 556 msg = 'Detached 1 repositories'
558 557 assert_session_flash(response, msg)
559 558 fixture.destroy_repo(obj_name)
560 559
561 560 def test_delete_owner_of_repository_deleting(self, request, user_util):
562 561 self.log_user()
563 562 obj_name = 'test_repo'
564 563 usr = user_util.create_user(auto_cleanup=False)
565 564 username = usr.username
566 565 fixture.create_repo(obj_name, cur_user=usr.username)
567 566
568 567 new_user = Session().query(User)\
569 568 .filter(User.username == username).one()
570 569 response = self.app.post(
571 570 route_path('user_delete', user_id=new_user.user_id),
572 571 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
573 572
574 573 msg = 'Deleted 1 repositories'
575 574 assert_session_flash(response, msg)
576 575
577 576 def test_delete_owner_of_repository_group(self, request, user_util):
578 577 self.log_user()
579 578 obj_name = 'test_group'
580 579 usr = user_util.create_user()
581 580 username = usr.username
582 581 fixture.create_repo_group(obj_name, cur_user=usr.username)
583 582
584 583 new_user = Session().query(User)\
585 584 .filter(User.username == username).one()
586 585 response = self.app.post(
587 586 route_path('user_delete', user_id=new_user.user_id),
588 587 params={'csrf_token': self.csrf_token})
589 588
590 589 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
591 590 'Switch owners or remove those repository groups:%s' % (username, obj_name)
592 591 assert_session_flash(response, msg)
593 592 fixture.destroy_repo_group(obj_name)
594 593
595 594 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
596 595 self.log_user()
597 596 obj_name = 'test_group'
598 597 usr = user_util.create_user(auto_cleanup=False)
599 598 username = usr.username
600 599 fixture.create_repo_group(obj_name, cur_user=usr.username)
601 600
602 601 new_user = Session().query(User)\
603 602 .filter(User.username == username).one()
604 603 response = self.app.post(
605 604 route_path('user_delete', user_id=new_user.user_id),
606 605 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
607 606
608 607 msg = 'Deleted 1 repository groups'
609 608 assert_session_flash(response, msg)
610 609
611 610 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
612 611 self.log_user()
613 612 obj_name = 'test_group'
614 613 usr = user_util.create_user(auto_cleanup=False)
615 614 username = usr.username
616 615 fixture.create_repo_group(obj_name, cur_user=usr.username)
617 616
618 617 new_user = Session().query(User)\
619 618 .filter(User.username == username).one()
620 619 response = self.app.post(
621 620 route_path('user_delete', user_id=new_user.user_id),
622 621 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
623 622
624 623 msg = 'Detached 1 repository groups'
625 624 assert_session_flash(response, msg)
626 625 fixture.destroy_repo_group(obj_name)
627 626
628 627 def test_delete_owner_of_user_group(self, request, user_util):
629 628 self.log_user()
630 629 obj_name = 'test_user_group'
631 630 usr = user_util.create_user()
632 631 username = usr.username
633 632 fixture.create_user_group(obj_name, cur_user=usr.username)
634 633
635 634 new_user = Session().query(User)\
636 635 .filter(User.username == username).one()
637 636 response = self.app.post(
638 637 route_path('user_delete', user_id=new_user.user_id),
639 638 params={'csrf_token': self.csrf_token})
640 639
641 640 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
642 641 'Switch owners or remove those user groups:%s' % (username, obj_name)
643 642 assert_session_flash(response, msg)
644 643 fixture.destroy_user_group(obj_name)
645 644
646 645 def test_delete_owner_of_user_group_detaching(self, request, user_util):
647 646 self.log_user()
648 647 obj_name = 'test_user_group'
649 648 usr = user_util.create_user(auto_cleanup=False)
650 649 username = usr.username
651 650 fixture.create_user_group(obj_name, cur_user=usr.username)
652 651
653 652 new_user = Session().query(User)\
654 653 .filter(User.username == username).one()
655 654 try:
656 655 response = self.app.post(
657 656 route_path('user_delete', user_id=new_user.user_id),
658 657 params={'user_user_groups': 'detach',
659 658 'csrf_token': self.csrf_token})
660 659
661 660 msg = 'Detached 1 user groups'
662 661 assert_session_flash(response, msg)
663 662 finally:
664 663 fixture.destroy_user_group(obj_name)
665 664
666 665 def test_delete_owner_of_user_group_deleting(self, request, user_util):
667 666 self.log_user()
668 667 obj_name = 'test_user_group'
669 668 usr = user_util.create_user(auto_cleanup=False)
670 669 username = usr.username
671 670 fixture.create_user_group(obj_name, cur_user=usr.username)
672 671
673 672 new_user = Session().query(User)\
674 673 .filter(User.username == username).one()
675 674 response = self.app.post(
676 675 route_path('user_delete', user_id=new_user.user_id),
677 676 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
678 677
679 678 msg = 'Deleted 1 user groups'
680 679 assert_session_flash(response, msg)
681 680
682 681 def test_edit(self, user_util):
683 682 self.log_user()
684 683 user = user_util.create_user()
685 684 self.app.get(route_path('user_edit', user_id=user.user_id))
686 685
687 686 def test_edit_default_user_redirect(self):
688 687 self.log_user()
689 688 user = User.get_default_user()
690 689 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
691 690
692 691 @pytest.mark.parametrize(
693 692 'repo_create, repo_create_write, user_group_create, repo_group_create,'
694 693 'fork_create, inherit_default_permissions, expect_error,'
695 694 'expect_form_error', [
696 695 ('hg.create.none', 'hg.create.write_on_repogroup.false',
697 696 'hg.usergroup.create.false', 'hg.repogroup.create.false',
698 697 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
699 698 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
700 699 'hg.usergroup.create.false', 'hg.repogroup.create.false',
701 700 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
702 701 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
703 702 'hg.usergroup.create.true', 'hg.repogroup.create.true',
704 703 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
705 704 False),
706 705 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
707 706 'hg.usergroup.create.true', 'hg.repogroup.create.true',
708 707 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
709 708 True),
710 709 ('', '', '', '', '', '', True, False),
711 710 ])
712 711 def test_global_perms_on_user(
713 712 self, repo_create, repo_create_write, user_group_create,
714 713 repo_group_create, fork_create, expect_error, expect_form_error,
715 714 inherit_default_permissions, user_util):
716 715 self.log_user()
717 716 user = user_util.create_user()
718 717 uid = user.user_id
719 718
720 719 # ENABLE REPO CREATE ON A GROUP
721 720 perm_params = {
722 721 'inherit_default_permissions': False,
723 722 'default_repo_create': repo_create,
724 723 'default_repo_create_on_write': repo_create_write,
725 724 'default_user_group_create': user_group_create,
726 725 'default_repo_group_create': repo_group_create,
727 726 'default_fork_create': fork_create,
728 727 'default_inherit_default_permissions': inherit_default_permissions,
729 728 'csrf_token': self.csrf_token,
730 729 }
731 730 response = self.app.post(
732 731 route_path('user_edit_global_perms_update', user_id=uid),
733 732 params=perm_params)
734 733
735 734 if expect_form_error:
736 735 assert response.status_int == 200
737 736 response.mustcontain('Value must be one of')
738 737 else:
739 738 if expect_error:
740 739 msg = 'An error occurred during permissions saving'
741 740 else:
742 741 msg = 'User global permissions updated successfully'
743 742 ug = User.get(uid)
744 743 del perm_params['inherit_default_permissions']
745 744 del perm_params['csrf_token']
746 745 assert perm_params == ug.get_default_perms()
747 746 assert_session_flash(response, msg)
748 747
749 748 def test_global_permissions_initial_values(self, user_util):
750 749 self.log_user()
751 750 user = user_util.create_user()
752 751 uid = user.user_id
753 752 response = self.app.get(
754 753 route_path('user_edit_global_perms', user_id=uid))
755 754 default_user = User.get_default_user()
756 755 default_permissions = default_user.get_default_perms()
757 756 assert_response = response.assert_response()
758 757 expected_permissions = (
759 758 'default_repo_create', 'default_repo_create_on_write',
760 759 'default_fork_create', 'default_repo_group_create',
761 760 'default_user_group_create', 'default_inherit_default_permissions')
762 761 for permission in expected_permissions:
763 762 css_selector = '[name={}][checked=checked]'.format(permission)
764 763 element = assert_response.get_element(css_selector)
765 764 assert element.value == default_permissions[permission]
766 765
767 766 def test_perms_summary_page(self):
768 767 user = self.log_user()
769 768 response = self.app.get(
770 769 route_path('edit_user_perms_summary', user_id=user['user_id']))
771 770 for repo in Repository.query().all():
772 771 response.mustcontain(repo.repo_name)
773 772
774 773 def test_perms_summary_page_json(self):
775 774 user = self.log_user()
776 775 response = self.app.get(
777 776 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
778 777 for repo in Repository.query().all():
779 778 response.mustcontain(repo.repo_name)
780 779
781 780 def test_audit_log_page(self):
782 781 user = self.log_user()
783 782 self.app.get(
784 783 route_path('edit_user_audit_logs', user_id=user['user_id']))
785 784
786 785 def test_audit_log_page_download(self):
787 786 user = self.log_user()
788 787 user_id = user['user_id']
789 788 response = self.app.get(
790 789 route_path('edit_user_audit_logs_download', user_id=user_id))
791 790
792 791 assert response.content_disposition == \
793 792 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
794 793 assert response.content_type == "application/json"
@@ -1,176 +1,175 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.model.db import User, UserSshKeys
24 23
25 24 from rhodecode.tests import TestController, assert_session_flash
26 25 from rhodecode.tests.fixture import Fixture
27 26
28 27 fixture = Fixture()
29 28
30 29
31 30 def route_path(name, params=None, **kwargs):
32 31 import urllib.request, urllib.parse, urllib.error
33 32 from rhodecode.apps._base import ADMIN_PREFIX
34 33
35 34 base_url = {
36 35 'edit_user_ssh_keys':
37 36 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
38 37 'edit_user_ssh_keys_generate_keypair':
39 38 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
40 39 'edit_user_ssh_keys_add':
41 40 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
42 41 'edit_user_ssh_keys_delete':
43 42 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
44 43
45 44 }[name].format(**kwargs)
46 45
47 46 if params:
48 47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 48 return base_url
50 49
51 50
52 51 class TestAdminUsersSshKeysView(TestController):
53 52 INVALID_KEY = """\
54 53 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 54 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 55 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 56 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 57 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 58 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 59 your_email@example.com
61 60 """
62 61 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 62 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 63 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 64 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 65 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 66 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 67 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 68 'your_email@example.com'
70 69 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
71 70
72 71 def test_ssh_keys_default_user(self):
73 72 self.log_user()
74 73 user = User.get_default_user()
75 74 self.app.get(
76 75 route_path('edit_user_ssh_keys', user_id=user.user_id),
77 76 status=302)
78 77
79 78 def test_add_ssh_key_error(self, user_util):
80 79 self.log_user()
81 80 user = user_util.create_user()
82 81 user_id = user.user_id
83 82
84 83 key_data = self.INVALID_KEY
85 84
86 85 desc = 'MY SSH KEY'
87 86 response = self.app.post(
88 87 route_path('edit_user_ssh_keys_add', user_id=user_id),
89 88 {'description': desc, 'key_data': key_data,
90 89 'csrf_token': self.csrf_token})
91 90 assert_session_flash(response, 'An error occurred during ssh '
92 91 'key saving: Unable to decode the key')
93 92
94 93 def test_ssh_key_duplicate(self, user_util):
95 94 self.log_user()
96 95 user = user_util.create_user()
97 96 user_id = user.user_id
98 97
99 98 key_data = self.VALID_KEY
100 99
101 100 desc = 'MY SSH KEY'
102 101 response = self.app.post(
103 102 route_path('edit_user_ssh_keys_add', user_id=user_id),
104 103 {'description': desc, 'key_data': key_data,
105 104 'csrf_token': self.csrf_token})
106 105 assert_session_flash(response, 'Ssh Key successfully created')
107 106 response.follow() # flush session flash
108 107
109 108 # add the same key AGAIN
110 109 desc = 'MY SSH KEY'
111 110 response = self.app.post(
112 111 route_path('edit_user_ssh_keys_add', user_id=user_id),
113 112 {'description': desc, 'key_data': key_data,
114 113 'csrf_token': self.csrf_token})
115 114
116 115 err = 'Such key with fingerprint `{}` already exists, ' \
117 116 'please use a different one'.format(self.FINGERPRINT)
118 117 assert_session_flash(response, 'An error occurred during ssh key '
119 118 'saving: {}'.format(err))
120 119
121 120 def test_add_ssh_key(self, user_util):
122 121 self.log_user()
123 122 user = user_util.create_user()
124 123 user_id = user.user_id
125 124
126 125 key_data = self.VALID_KEY
127 126
128 127 desc = 'MY SSH KEY'
129 128 response = self.app.post(
130 129 route_path('edit_user_ssh_keys_add', user_id=user_id),
131 130 {'description': desc, 'key_data': key_data,
132 131 'csrf_token': self.csrf_token})
133 132 assert_session_flash(response, 'Ssh Key successfully created')
134 133
135 134 response = response.follow()
136 135 response.mustcontain(desc)
137 136
138 137 def test_delete_ssh_key(self, user_util):
139 138 self.log_user()
140 139 user = user_util.create_user()
141 140 user_id = user.user_id
142 141
143 142 key_data = self.VALID_KEY
144 143
145 144 desc = 'MY SSH KEY'
146 145 response = self.app.post(
147 146 route_path('edit_user_ssh_keys_add', user_id=user_id),
148 147 {'description': desc, 'key_data': key_data,
149 148 'csrf_token': self.csrf_token})
150 149 assert_session_flash(response, 'Ssh Key successfully created')
151 150 response = response.follow() # flush the Session flash
152 151
153 152 # now delete our key
154 153 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
155 154 assert 1 == len(keys)
156 155
157 156 response = self.app.post(
158 157 route_path('edit_user_ssh_keys_delete', user_id=user_id),
159 158 {'del_ssh_key': keys[0].ssh_key_id,
160 159 'csrf_token': self.csrf_token})
161 160
162 161 assert_session_flash(response, 'Ssh key successfully deleted')
163 162 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
164 163 assert 0 == len(keys)
165 164
166 165 def test_generate_keypair(self, user_util):
167 166 self.log_user()
168 167 user = user_util.create_user()
169 168 user_id = user.user_id
170 169
171 170 response = self.app.get(
172 171 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
173 172
174 173 response.mustcontain('Private key')
175 174 response.mustcontain('Public key')
176 175 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,40 +1,40 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from rhodecode.apps._base import BaseAppView, DataGridAppView
24 24 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 class AdminArtifactsView(BaseAppView, DataGridAppView):
30 30
31 31 def load_default_context(self):
32 32 c = self._get_local_tmpl_context()
33 33 return c
34 34
35 35 @LoginRequired()
36 36 @HasPermissionAllDecorator('hg.admin')
37 37 def artifacts(self):
38 38 c = self.load_default_context()
39 39 c.active = 'artifacts'
40 40 return self._get_template_context(c)
@@ -1,87 +1,87 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPNotFound
24 24
25 25 from rhodecode.apps._base import BaseAppView
26 26 from rhodecode.model.db import joinedload, UserLog
27 27 from rhodecode.lib.user_log_filter import user_log_filter
28 28 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
29 29 from rhodecode.lib.utils2 import safe_int
30 30 from rhodecode.lib.helpers import SqlPage
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class AdminAuditLogsView(BaseAppView):
36 36
37 37 def load_default_context(self):
38 38 c = self._get_local_tmpl_context()
39 39 return c
40 40
41 41 @LoginRequired()
42 42 @HasPermissionAllDecorator('hg.admin')
43 43 def admin_audit_logs(self):
44 44 c = self.load_default_context()
45 45
46 46 users_log = UserLog.query()\
47 47 .options(joinedload(UserLog.user))\
48 48 .options(joinedload(UserLog.repository))
49 49
50 50 # FILTERING
51 51 c.search_term = self.request.GET.get('filter')
52 52 try:
53 53 users_log = user_log_filter(users_log, c.search_term)
54 54 except Exception:
55 55 # we want this to crash for now
56 56 raise
57 57
58 58 users_log = users_log.order_by(UserLog.action_date.desc())
59 59
60 60 p = safe_int(self.request.GET.get('page', 1), 1)
61 61
62 62 def url_generator(page_num):
63 63 query_params = {
64 64 'page': page_num
65 65 }
66 66 if c.search_term:
67 67 query_params['filter'] = c.search_term
68 68 return self.request.current_route_path(_query=query_params)
69 69
70 70 c.audit_logs = SqlPage(users_log, page=p, items_per_page=10,
71 71 url_maker=url_generator)
72 72 return self._get_template_context(c)
73 73
74 74 @LoginRequired()
75 75 @HasPermissionAllDecorator('hg.admin')
76 76 def admin_audit_log_entry(self):
77 77 c = self.load_default_context()
78 78 audit_log_id = self.request.matchdict['audit_log_id']
79 79
80 80 c.audit_log_entry = UserLog.query()\
81 81 .options(joinedload(UserLog.user))\
82 82 .options(joinedload(UserLog.repository))\
83 83 .filter(UserLog.user_log_id == audit_log_id).scalar()
84 84 if not c.audit_log_entry:
85 85 raise HTTPNotFound()
86 86
87 87 return self._get_template_context(c)
@@ -1,103 +1,103 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import BaseAppView
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model.forms import DefaultsForm
35 35 from rhodecode.model.meta import Session
36 36 from rhodecode import BACKENDS
37 37 from rhodecode.model.settings import SettingsModel
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class AdminDefaultSettingsView(BaseAppView):
43 43
44 44 def load_default_context(self):
45 45 c = self._get_local_tmpl_context()
46 46 return c
47 47
48 48 @LoginRequired()
49 49 @HasPermissionAllDecorator('hg.admin')
50 50 def defaults_repository_show(self):
51 51 c = self.load_default_context()
52 52 c.backends = BACKENDS.keys()
53 53 c.active = 'repositories'
54 54 defaults = SettingsModel().get_default_repo_settings()
55 55
56 56 data = render(
57 57 'rhodecode:templates/admin/defaults/defaults.mako',
58 58 self._get_template_context(c), self.request)
59 59 html = formencode.htmlfill.render(
60 60 data,
61 61 defaults=defaults,
62 62 encoding="UTF-8",
63 63 force_defaults=False
64 64 )
65 65 return Response(html)
66 66
67 67 @LoginRequired()
68 68 @HasPermissionAllDecorator('hg.admin')
69 69 @CSRFRequired()
70 70 def defaults_repository_update(self):
71 71 _ = self.request.translate
72 72 c = self.load_default_context()
73 73 c.active = 'repositories'
74 74 form = DefaultsForm(self.request.translate)()
75 75
76 76 try:
77 77 form_result = form.to_python(dict(self.request.POST))
78 78 for k, v in form_result.items():
79 79 setting = SettingsModel().create_or_update_setting(k, v)
80 80 Session().add(setting)
81 81 Session().commit()
82 82 h.flash(_('Default settings updated successfully'),
83 83 category='success')
84 84
85 85 except formencode.Invalid as errors:
86 86 data = render(
87 87 'rhodecode:templates/admin/defaults/defaults.mako',
88 88 self._get_template_context(c), self.request)
89 89 html = formencode.htmlfill.render(
90 90 data,
91 91 defaults=errors.value,
92 92 errors=errors.unpack_errors() or {},
93 93 prefix_error=False,
94 94 encoding="UTF-8",
95 95 force_defaults=False
96 96 )
97 97 return Response(html)
98 98 except Exception:
99 99 log.exception('Exception in update action')
100 100 h.flash(_('Error occurred during update of default values'),
101 101 category='error')
102 102
103 103 raise HTTPFound(h.route_path('admin_defaults_repositories'))
@@ -1,161 +1,161 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2018-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import os
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound
24 24
25 25 from rhodecode.apps._base import BaseAppView
26 26 from rhodecode.apps._base.navigation import navigation_list
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.auth import (
29 29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 30 from rhodecode.lib.utils2 import time_to_utcdatetime, safe_int
31 31 from rhodecode.lib import exc_tracking
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class ExceptionsTrackerView(BaseAppView):
37 37 def load_default_context(self):
38 38 c = self._get_local_tmpl_context()
39 39 c.navlist = navigation_list(self.request)
40 40 return c
41 41
42 42 def count_all_exceptions(self):
43 43 exc_store_path = exc_tracking.get_exc_store()
44 44 count = 0
45 45 for fname in os.listdir(exc_store_path):
46 46 parts = fname.split('_', 2)
47 47 if not len(parts) == 3:
48 48 continue
49 49 count +=1
50 50 return count
51 51
52 52 def get_all_exceptions(self, read_metadata=False, limit=None, type_filter=None):
53 53 exc_store_path = exc_tracking.get_exc_store()
54 54 exception_list = []
55 55
56 56 def key_sorter(val):
57 57 try:
58 58 return val.split('_')[-1]
59 59 except Exception:
60 60 return 0
61 61
62 62 for fname in reversed(sorted(os.listdir(exc_store_path), key=key_sorter)):
63 63
64 64 parts = fname.split('_', 2)
65 65 if not len(parts) == 3:
66 66 continue
67 67
68 68 exc_id, app_type, exc_timestamp = parts
69 69
70 70 exc = {'exc_id': exc_id, 'app_type': app_type, 'exc_type': 'unknown',
71 71 'exc_utc_date': '', 'exc_timestamp': exc_timestamp}
72 72
73 73 if read_metadata:
74 74 full_path = os.path.join(exc_store_path, fname)
75 75 if not os.path.isfile(full_path):
76 76 continue
77 77 try:
78 78 # we can read our metadata
79 79 with open(full_path, 'rb') as f:
80 80 exc_metadata = exc_tracking.exc_unserialize(f.read())
81 81 exc.update(exc_metadata)
82 82 except Exception:
83 83 log.exception('Failed to read exc data from:{}'.format(full_path))
84 84 pass
85 85 # convert our timestamp to a date obj, for nicer representation
86 86 exc['exc_utc_date'] = time_to_utcdatetime(exc['exc_timestamp'])
87 87
88 88 type_present = exc.get('exc_type')
89 89 if type_filter:
90 90 if type_present and type_present == type_filter:
91 91 exception_list.append(exc)
92 92 else:
93 93 exception_list.append(exc)
94 94
95 95 if limit and len(exception_list) >= limit:
96 96 break
97 97 return exception_list
98 98
99 99 @LoginRequired()
100 100 @HasPermissionAllDecorator('hg.admin')
101 101 def browse_exceptions(self):
102 102 _ = self.request.translate
103 103 c = self.load_default_context()
104 104 c.active = 'exceptions_browse'
105 105 c.limit = safe_int(self.request.GET.get('limit')) or 50
106 106 c.type_filter = self.request.GET.get('type_filter')
107 107 c.next_limit = c.limit + 50
108 108 c.exception_list = self.get_all_exceptions(
109 109 read_metadata=True, limit=c.limit, type_filter=c.type_filter)
110 110 c.exception_list_count = self.count_all_exceptions()
111 111 c.exception_store_dir = exc_tracking.get_exc_store()
112 112 return self._get_template_context(c)
113 113
114 114 @LoginRequired()
115 115 @HasPermissionAllDecorator('hg.admin')
116 116 def exception_show(self):
117 117 _ = self.request.translate
118 118 c = self.load_default_context()
119 119
120 120 c.active = 'exceptions'
121 121 c.exception_id = self.request.matchdict['exception_id']
122 122 c.traceback = exc_tracking.read_exception(c.exception_id, prefix=None)
123 123 return self._get_template_context(c)
124 124
125 125 @LoginRequired()
126 126 @HasPermissionAllDecorator('hg.admin')
127 127 @CSRFRequired()
128 128 def exception_delete_all(self):
129 129 _ = self.request.translate
130 130 c = self.load_default_context()
131 131 type_filter = self.request.POST.get('type_filter')
132 132
133 133 c.active = 'exceptions'
134 134 all_exc = self.get_all_exceptions(read_metadata=bool(type_filter), type_filter=type_filter)
135 135 exc_count = 0
136 136
137 137 for exc in all_exc:
138 138 if type_filter:
139 139 if exc.get('exc_type') == type_filter:
140 140 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
141 141 exc_count += 1
142 142 else:
143 143 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
144 144 exc_count += 1
145 145
146 146 h.flash(_('Removed {} Exceptions').format(exc_count), category='success')
147 147 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
148 148
149 149 @LoginRequired()
150 150 @HasPermissionAllDecorator('hg.admin')
151 151 @CSRFRequired()
152 152 def exception_delete(self):
153 153 _ = self.request.translate
154 154 c = self.load_default_context()
155 155
156 156 c.active = 'exceptions'
157 157 c.exception_id = self.request.matchdict['exception_id']
158 158 exc_tracking.delete_exception(c.exception_id, prefix=None)
159 159
160 160 h.flash(_('Removed Exception {}').format(c.exception_id), category='success')
161 161 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
@@ -1,72 +1,72 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 24
25 25 from rhodecode.apps._base import BaseAppView
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.auth import (LoginRequired, NotAnonymous, HasRepoPermissionAny)
28 28 from rhodecode.model.db import PullRequest
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class AdminMainView(BaseAppView):
35 35 def load_default_context(self):
36 36 c = self._get_local_tmpl_context()
37 37 return c
38 38
39 39 @LoginRequired()
40 40 @NotAnonymous()
41 41 def admin_main(self):
42 42 c = self.load_default_context()
43 43 c.active = 'admin'
44 44
45 45 if not (c.is_super_admin or c.is_delegated_admin):
46 46 raise HTTPNotFound()
47 47
48 48 return self._get_template_context(c)
49 49
50 50 @LoginRequired()
51 51 def pull_requests(self):
52 52 """
53 53 Global redirect for Pull Requests
54 54 pull_request_id: id of pull requests in the system
55 55 """
56 56
57 57 pull_request = PullRequest.get_or_404(
58 58 self.request.matchdict['pull_request_id'])
59 59 pull_request_id = pull_request.pull_request_id
60 60
61 61 repo_name = pull_request.target_repo.repo_name
62 62 # NOTE(marcink):
63 63 # check permissions so we don't redirect to repo that we don't have access to
64 64 # exposing it's name
65 65 target_repo_perm = HasRepoPermissionAny(
66 66 'repository.read', 'repository.write', 'repository.admin')(repo_name)
67 67 if not target_repo_perm:
68 68 raise HTTPNotFound()
69 69
70 70 raise HTTPFound(
71 71 h.route_path('pullrequest_show', repo_name=repo_name,
72 72 pull_request_id=pull_request_id))
@@ -1,46 +1,46 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import collections
22 22 import logging
23 23
24 24 from rhodecode.apps._base import BaseAppView
25 25 from rhodecode.apps._base.navigation import navigation_list
26 26 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
27 27 from rhodecode.lib.utils import read_opensource_licenses
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 class OpenSourceLicensesAdminSettingsView(BaseAppView):
33 33
34 34 def load_default_context(self):
35 35 c = self._get_local_tmpl_context()
36 36 return c
37 37
38 38 @LoginRequired()
39 39 @HasPermissionAllDecorator('hg.admin')
40 40 def open_source_licenses(self):
41 41 c = self.load_default_context()
42 42 c.active = 'open_source'
43 43 c.navlist = navigation_list(self.request)
44 44 c.opensource_licenses = sorted(
45 45 read_opensource_licenses(), key=lambda d: d["name"])
46 46 return self._get_template_context(c)
@@ -1,479 +1,479 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import formencode
24 24 import formencode.htmlfill
25 25 import datetime
26 26 from pyramid.interfaces import IRoutesMapper
27 27
28 28 from pyramid.httpexceptions import HTTPFound
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 34 from rhodecode import events
35 35
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.utils2 import aslist, safe_unicode
40 40 from rhodecode.model.db import (
41 41 or_, coalesce, User, UserIpMap, UserSshKeys)
42 42 from rhodecode.model.forms import (
43 43 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.permission import PermissionModel
46 46 from rhodecode.model.settings import SettingsModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class AdminPermissionsView(BaseAppView, DataGridAppView):
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55 PermissionModel().set_global_permission_choices(
56 56 c, gettext_translator=self.request.translate)
57 57 return c
58 58
59 59 @LoginRequired()
60 60 @HasPermissionAllDecorator('hg.admin')
61 61 def permissions_application(self):
62 62 c = self.load_default_context()
63 63 c.active = 'application'
64 64
65 65 c.user = User.get_default_user(refresh=True)
66 66
67 67 app_settings = c.rc_config
68 68
69 69 defaults = {
70 70 'anonymous': c.user.active,
71 71 'default_register_message': app_settings.get(
72 72 'rhodecode_register_message')
73 73 }
74 74 defaults.update(c.user.get_default_perms())
75 75
76 76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
77 77 self._get_template_context(c), self.request)
78 78 html = formencode.htmlfill.render(
79 79 data,
80 80 defaults=defaults,
81 81 encoding="UTF-8",
82 82 force_defaults=False
83 83 )
84 84 return Response(html)
85 85
86 86 @LoginRequired()
87 87 @HasPermissionAllDecorator('hg.admin')
88 88 @CSRFRequired()
89 89 def permissions_application_update(self):
90 90 _ = self.request.translate
91 91 c = self.load_default_context()
92 92 c.active = 'application'
93 93
94 94 _form = ApplicationPermissionsForm(
95 95 self.request.translate,
96 96 [x[0] for x in c.register_choices],
97 97 [x[0] for x in c.password_reset_choices],
98 98 [x[0] for x in c.extern_activate_choices])()
99 99
100 100 try:
101 101 form_result = _form.to_python(dict(self.request.POST))
102 102 form_result.update({'perm_user_name': User.DEFAULT_USER})
103 103 PermissionModel().update_application_permissions(form_result)
104 104
105 105 settings = [
106 106 ('register_message', 'default_register_message'),
107 107 ]
108 108 for setting, form_key in settings:
109 109 sett = SettingsModel().create_or_update_setting(
110 110 setting, form_result[form_key])
111 111 Session().add(sett)
112 112
113 113 Session().commit()
114 114 h.flash(_('Application permissions updated successfully'),
115 115 category='success')
116 116
117 117 except formencode.Invalid as errors:
118 118 defaults = errors.value
119 119
120 120 data = render(
121 121 'rhodecode:templates/admin/permissions/permissions.mako',
122 122 self._get_template_context(c), self.request)
123 123 html = formencode.htmlfill.render(
124 124 data,
125 125 defaults=defaults,
126 126 errors=errors.unpack_errors() or {},
127 127 prefix_error=False,
128 128 encoding="UTF-8",
129 129 force_defaults=False
130 130 )
131 131 return Response(html)
132 132
133 133 except Exception:
134 134 log.exception("Exception during update of permissions")
135 135 h.flash(_('Error occurred during update of permissions'),
136 136 category='error')
137 137
138 138 affected_user_ids = [User.get_default_user_id()]
139 139 PermissionModel().trigger_permission_flush(affected_user_ids)
140 140
141 141 raise HTTPFound(h.route_path('admin_permissions_application'))
142 142
143 143 @LoginRequired()
144 144 @HasPermissionAllDecorator('hg.admin')
145 145 def permissions_objects(self):
146 146 c = self.load_default_context()
147 147 c.active = 'objects'
148 148
149 149 c.user = User.get_default_user(refresh=True)
150 150 defaults = {}
151 151 defaults.update(c.user.get_default_perms())
152 152
153 153 data = render(
154 154 'rhodecode:templates/admin/permissions/permissions.mako',
155 155 self._get_template_context(c), self.request)
156 156 html = formencode.htmlfill.render(
157 157 data,
158 158 defaults=defaults,
159 159 encoding="UTF-8",
160 160 force_defaults=False
161 161 )
162 162 return Response(html)
163 163
164 164 @LoginRequired()
165 165 @HasPermissionAllDecorator('hg.admin')
166 166 @CSRFRequired()
167 167 def permissions_objects_update(self):
168 168 _ = self.request.translate
169 169 c = self.load_default_context()
170 170 c.active = 'objects'
171 171
172 172 _form = ObjectPermissionsForm(
173 173 self.request.translate,
174 174 [x[0] for x in c.repo_perms_choices],
175 175 [x[0] for x in c.group_perms_choices],
176 176 [x[0] for x in c.user_group_perms_choices],
177 177 )()
178 178
179 179 try:
180 180 form_result = _form.to_python(dict(self.request.POST))
181 181 form_result.update({'perm_user_name': User.DEFAULT_USER})
182 182 PermissionModel().update_object_permissions(form_result)
183 183
184 184 Session().commit()
185 185 h.flash(_('Object permissions updated successfully'),
186 186 category='success')
187 187
188 188 except formencode.Invalid as errors:
189 189 defaults = errors.value
190 190
191 191 data = render(
192 192 'rhodecode:templates/admin/permissions/permissions.mako',
193 193 self._get_template_context(c), self.request)
194 194 html = formencode.htmlfill.render(
195 195 data,
196 196 defaults=defaults,
197 197 errors=errors.unpack_errors() or {},
198 198 prefix_error=False,
199 199 encoding="UTF-8",
200 200 force_defaults=False
201 201 )
202 202 return Response(html)
203 203 except Exception:
204 204 log.exception("Exception during update of permissions")
205 205 h.flash(_('Error occurred during update of permissions'),
206 206 category='error')
207 207
208 208 affected_user_ids = [User.get_default_user_id()]
209 209 PermissionModel().trigger_permission_flush(affected_user_ids)
210 210
211 211 raise HTTPFound(h.route_path('admin_permissions_object'))
212 212
213 213 @LoginRequired()
214 214 @HasPermissionAllDecorator('hg.admin')
215 215 def permissions_branch(self):
216 216 c = self.load_default_context()
217 217 c.active = 'branch'
218 218
219 219 c.user = User.get_default_user(refresh=True)
220 220 defaults = {}
221 221 defaults.update(c.user.get_default_perms())
222 222
223 223 data = render(
224 224 'rhodecode:templates/admin/permissions/permissions.mako',
225 225 self._get_template_context(c), self.request)
226 226 html = formencode.htmlfill.render(
227 227 data,
228 228 defaults=defaults,
229 229 encoding="UTF-8",
230 230 force_defaults=False
231 231 )
232 232 return Response(html)
233 233
234 234 @LoginRequired()
235 235 @HasPermissionAllDecorator('hg.admin')
236 236 def permissions_global(self):
237 237 c = self.load_default_context()
238 238 c.active = 'global'
239 239
240 240 c.user = User.get_default_user(refresh=True)
241 241 defaults = {}
242 242 defaults.update(c.user.get_default_perms())
243 243
244 244 data = render(
245 245 'rhodecode:templates/admin/permissions/permissions.mako',
246 246 self._get_template_context(c), self.request)
247 247 html = formencode.htmlfill.render(
248 248 data,
249 249 defaults=defaults,
250 250 encoding="UTF-8",
251 251 force_defaults=False
252 252 )
253 253 return Response(html)
254 254
255 255 @LoginRequired()
256 256 @HasPermissionAllDecorator('hg.admin')
257 257 @CSRFRequired()
258 258 def permissions_global_update(self):
259 259 _ = self.request.translate
260 260 c = self.load_default_context()
261 261 c.active = 'global'
262 262
263 263 _form = UserPermissionsForm(
264 264 self.request.translate,
265 265 [x[0] for x in c.repo_create_choices],
266 266 [x[0] for x in c.repo_create_on_write_choices],
267 267 [x[0] for x in c.repo_group_create_choices],
268 268 [x[0] for x in c.user_group_create_choices],
269 269 [x[0] for x in c.fork_choices],
270 270 [x[0] for x in c.inherit_default_permission_choices])()
271 271
272 272 try:
273 273 form_result = _form.to_python(dict(self.request.POST))
274 274 form_result.update({'perm_user_name': User.DEFAULT_USER})
275 275 PermissionModel().update_user_permissions(form_result)
276 276
277 277 Session().commit()
278 278 h.flash(_('Global permissions updated successfully'),
279 279 category='success')
280 280
281 281 except formencode.Invalid as errors:
282 282 defaults = errors.value
283 283
284 284 data = render(
285 285 'rhodecode:templates/admin/permissions/permissions.mako',
286 286 self._get_template_context(c), self.request)
287 287 html = formencode.htmlfill.render(
288 288 data,
289 289 defaults=defaults,
290 290 errors=errors.unpack_errors() or {},
291 291 prefix_error=False,
292 292 encoding="UTF-8",
293 293 force_defaults=False
294 294 )
295 295 return Response(html)
296 296 except Exception:
297 297 log.exception("Exception during update of permissions")
298 298 h.flash(_('Error occurred during update of permissions'),
299 299 category='error')
300 300
301 301 affected_user_ids = [User.get_default_user_id()]
302 302 PermissionModel().trigger_permission_flush(affected_user_ids)
303 303
304 304 raise HTTPFound(h.route_path('admin_permissions_global'))
305 305
306 306 @LoginRequired()
307 307 @HasPermissionAllDecorator('hg.admin')
308 308 def permissions_ips(self):
309 309 c = self.load_default_context()
310 310 c.active = 'ips'
311 311
312 312 c.user = User.get_default_user(refresh=True)
313 313 c.user_ip_map = (
314 314 UserIpMap.query().filter(UserIpMap.user == c.user).all())
315 315
316 316 return self._get_template_context(c)
317 317
318 318 @LoginRequired()
319 319 @HasPermissionAllDecorator('hg.admin')
320 320 def permissions_overview(self):
321 321 c = self.load_default_context()
322 322 c.active = 'perms'
323 323
324 324 c.user = User.get_default_user(refresh=True)
325 325 c.perm_user = c.user.AuthUser()
326 326 return self._get_template_context(c)
327 327
328 328 @LoginRequired()
329 329 @HasPermissionAllDecorator('hg.admin')
330 330 def auth_token_access(self):
331 331 from rhodecode import CONFIG
332 332
333 333 c = self.load_default_context()
334 334 c.active = 'auth_token_access'
335 335
336 336 c.user = User.get_default_user(refresh=True)
337 337 c.perm_user = c.user.AuthUser()
338 338
339 339 mapper = self.request.registry.queryUtility(IRoutesMapper)
340 340 c.view_data = []
341 341
342 342 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
343 343 introspector = self.request.registry.introspector
344 344
345 345 view_intr = {}
346 346 for view_data in introspector.get_category('views'):
347 347 intr = view_data['introspectable']
348 348
349 349 if 'route_name' in intr and intr['attr']:
350 350 view_intr[intr['route_name']] = '{}:{}'.format(
351 351 str(intr['derived_callable'].__name__), intr['attr']
352 352 )
353 353
354 354 c.whitelist_key = 'api_access_controllers_whitelist'
355 355 c.whitelist_file = CONFIG.get('__file__')
356 356 whitelist_views = aslist(
357 357 CONFIG.get(c.whitelist_key), sep=',')
358 358
359 359 for route_info in mapper.get_routes():
360 360 if not route_info.name.startswith('__'):
361 361 routepath = route_info.pattern
362 362
363 363 def replace(matchobj):
364 364 if matchobj.group(1):
365 365 return "{%s}" % matchobj.group(1).split(':')[0]
366 366 else:
367 367 return "{%s}" % matchobj.group(2)
368 368
369 369 routepath = _argument_prog.sub(replace, routepath)
370 370
371 371 if not routepath.startswith('/'):
372 372 routepath = '/' + routepath
373 373
374 374 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
375 375 active = view_fqn in whitelist_views
376 376 c.view_data.append((route_info.name, view_fqn, routepath, active))
377 377
378 378 c.whitelist_views = whitelist_views
379 379 return self._get_template_context(c)
380 380
381 381 def ssh_enabled(self):
382 382 return self.request.registry.settings.get(
383 383 'ssh.generate_authorized_keyfile')
384 384
385 385 @LoginRequired()
386 386 @HasPermissionAllDecorator('hg.admin')
387 387 def ssh_keys(self):
388 388 c = self.load_default_context()
389 389 c.active = 'ssh_keys'
390 390 c.ssh_enabled = self.ssh_enabled()
391 391 return self._get_template_context(c)
392 392
393 393 @LoginRequired()
394 394 @HasPermissionAllDecorator('hg.admin')
395 395 def ssh_keys_data(self):
396 396 _ = self.request.translate
397 397 self.load_default_context()
398 398 column_map = {
399 399 'fingerprint': 'ssh_key_fingerprint',
400 400 'username': User.username
401 401 }
402 402 draw, start, limit = self._extract_chunk(self.request)
403 403 search_q, order_by, order_dir = self._extract_ordering(
404 404 self.request, column_map=column_map)
405 405
406 406 ssh_keys_data_total_count = UserSshKeys.query()\
407 407 .count()
408 408
409 409 # json generate
410 410 base_q = UserSshKeys.query().join(UserSshKeys.user)
411 411
412 412 if search_q:
413 413 like_expression = u'%{}%'.format(safe_unicode(search_q))
414 414 base_q = base_q.filter(or_(
415 415 User.username.ilike(like_expression),
416 416 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
417 417 ))
418 418
419 419 users_data_total_filtered_count = base_q.count()
420 420
421 421 sort_col = self._get_order_col(order_by, UserSshKeys)
422 422 if sort_col:
423 423 if order_dir == 'asc':
424 424 # handle null values properly to order by NULL last
425 425 if order_by in ['created_on']:
426 426 sort_col = coalesce(sort_col, datetime.date.max)
427 427 sort_col = sort_col.asc()
428 428 else:
429 429 # handle null values properly to order by NULL last
430 430 if order_by in ['created_on']:
431 431 sort_col = coalesce(sort_col, datetime.date.min)
432 432 sort_col = sort_col.desc()
433 433
434 434 base_q = base_q.order_by(sort_col)
435 435 base_q = base_q.offset(start).limit(limit)
436 436
437 437 ssh_keys = base_q.all()
438 438
439 439 ssh_keys_data = []
440 440 for ssh_key in ssh_keys:
441 441 ssh_keys_data.append({
442 442 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
443 443 "fingerprint": ssh_key.ssh_key_fingerprint,
444 444 "description": ssh_key.description,
445 445 "created_on": h.format_date(ssh_key.created_on),
446 446 "accessed_on": h.format_date(ssh_key.accessed_on),
447 447 "action": h.link_to(
448 448 _('Edit'), h.route_path('edit_user_ssh_keys',
449 449 user_id=ssh_key.user.user_id))
450 450 })
451 451
452 452 data = ({
453 453 'draw': draw,
454 454 'data': ssh_keys_data,
455 455 'recordsTotal': ssh_keys_data_total_count,
456 456 'recordsFiltered': users_data_total_filtered_count,
457 457 })
458 458
459 459 return data
460 460
461 461 @LoginRequired()
462 462 @HasPermissionAllDecorator('hg.admin')
463 463 @CSRFRequired()
464 464 def ssh_keys_update(self):
465 465 _ = self.request.translate
466 466 self.load_default_context()
467 467
468 468 ssh_enabled = self.ssh_enabled()
469 469 key_file = self.request.registry.settings.get(
470 470 'ssh.authorized_keys_file_path')
471 471 if ssh_enabled:
472 472 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
473 473 h.flash(_('Updated SSH keys file: {}').format(key_file),
474 474 category='success')
475 475 else:
476 476 h.flash(_('SSH key support is disabled in .ini file'),
477 477 category='warning')
478 478
479 479 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,170 +1,170 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import psutil
24 24 import signal
25 25
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.apps._base.navigation import navigation_list
29 29 from rhodecode.lib import system_info
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 32 from rhodecode.lib.utils2 import safe_int, StrictAttributeDict
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class AdminProcessManagementView(BaseAppView):
38 38 def load_default_context(self):
39 39 c = self._get_local_tmpl_context()
40 40 return c
41 41
42 42 def _format_proc(self, proc, with_children=False):
43 43 try:
44 44 mem = proc.memory_info()
45 45 proc_formatted = StrictAttributeDict({
46 46 'pid': proc.pid,
47 47 'name': proc.name(),
48 48 'mem_rss': mem.rss,
49 49 'mem_vms': mem.vms,
50 50 'cpu_percent': proc.cpu_percent(interval=0.1),
51 51 'create_time': proc.create_time(),
52 52 'cmd': ' '.join(proc.cmdline()),
53 53 })
54 54
55 55 if with_children:
56 56 proc_formatted.update({
57 57 'children': [self._format_proc(x)
58 58 for x in proc.children(recursive=True)]
59 59 })
60 60 except Exception:
61 61 log.exception('Failed to load proc')
62 62 proc_formatted = None
63 63 return proc_formatted
64 64
65 65 def get_processes(self):
66 66 proc_list = []
67 67 for p in psutil.process_iter():
68 68 if 'gunicorn' in p.name():
69 69 proc = self._format_proc(p, with_children=True)
70 70 if proc:
71 71 proc_list.append(proc)
72 72
73 73 return proc_list
74 74
75 75 def get_workers(self):
76 76 workers = None
77 77 try:
78 78 rc_config = system_info.rhodecode_config().value['config']
79 79 workers = rc_config['server:main'].get('workers')
80 80 except Exception:
81 81 pass
82 82
83 83 return workers or '?'
84 84
85 85 @LoginRequired()
86 86 @HasPermissionAllDecorator('hg.admin')
87 87 def process_management(self):
88 88 _ = self.request.translate
89 89 c = self.load_default_context()
90 90
91 91 c.active = 'process_management'
92 92 c.navlist = navigation_list(self.request)
93 93 c.gunicorn_processes = self.get_processes()
94 94 c.gunicorn_workers = self.get_workers()
95 95 return self._get_template_context(c)
96 96
97 97 @LoginRequired()
98 98 @HasPermissionAllDecorator('hg.admin')
99 99 def process_management_data(self):
100 100 _ = self.request.translate
101 101 c = self.load_default_context()
102 102 c.gunicorn_processes = self.get_processes()
103 103 return self._get_template_context(c)
104 104
105 105 @LoginRequired()
106 106 @HasPermissionAllDecorator('hg.admin')
107 107 @CSRFRequired()
108 108 def process_management_signal(self):
109 109 pids = self.request.json.get('pids', [])
110 110 result = []
111 111
112 112 def on_terminate(proc):
113 113 msg = "terminated"
114 114 result.append(msg)
115 115
116 116 procs = []
117 117 for pid in pids:
118 118 pid = safe_int(pid)
119 119 if pid:
120 120 try:
121 121 proc = psutil.Process(pid)
122 122 except psutil.NoSuchProcess:
123 123 continue
124 124
125 125 children = proc.children(recursive=True)
126 126 if children:
127 127 log.warning('Wont kill Master Process')
128 128 else:
129 129 procs.append(proc)
130 130
131 131 for p in procs:
132 132 try:
133 133 p.terminate()
134 134 except psutil.AccessDenied as e:
135 135 log.warning('Access denied: {}'.format(e))
136 136
137 137 gone, alive = psutil.wait_procs(procs, timeout=10, callback=on_terminate)
138 138 for p in alive:
139 139 try:
140 140 p.kill()
141 141 except psutil.AccessDenied as e:
142 142 log.warning('Access denied: {}'.format(e))
143 143
144 144 return {'result': result}
145 145
146 146 @LoginRequired()
147 147 @HasPermissionAllDecorator('hg.admin')
148 148 @CSRFRequired()
149 149 def process_management_master_signal(self):
150 150 pid_data = self.request.json.get('pid_data', {})
151 151 pid = safe_int(pid_data['pid'])
152 152 action = pid_data['action']
153 153 if pid:
154 154 try:
155 155 proc = psutil.Process(pid)
156 156 except psutil.NoSuchProcess:
157 157 return {'result': 'failure_no_such_process'}
158 158
159 159 children = proc.children(recursive=True)
160 160 if children:
161 161 # master process
162 162 if action == '+' and len(children) <= 20:
163 163 proc.send_signal(signal.SIGTTIN)
164 164 elif action == '-' and len(children) >= 2:
165 165 proc.send_signal(signal.SIGTTOU)
166 166 else:
167 167 return {'result': 'failure_wrong_action'}
168 168 return {'result': 'success'}
169 169
170 170 return {'result': 'failure_not_master'}
@@ -1,356 +1,356 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import datetime
21 21 import logging
22 22 import time
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26
27 27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28 28
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import BaseAppView, DataGridAppView
34 34
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, CSRFRequired, NotAnonymous,
37 37 HasPermissionAny, HasRepoGroupPermissionAny)
38 38 from rhodecode.lib import helpers as h, audit_logger
39 39 from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
40 40 from rhodecode.model.forms import RepoGroupForm
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo_group import RepoGroupModel
43 43 from rhodecode.model.scm import RepoGroupList
44 44 from rhodecode.model.db import (
45 45 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54
55 55 return c
56 56
57 57 def _load_form_data(self, c):
58 58 allow_empty_group = False
59 59
60 60 if self._can_create_repo_group():
61 61 # we're global admin, we're ok and we can create TOP level groups
62 62 allow_empty_group = True
63 63
64 64 # override the choices for this form, we need to filter choices
65 65 # and display only those we have ADMIN right
66 66 groups_with_admin_rights = RepoGroupList(
67 67 RepoGroup.query().all(),
68 68 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
69 69 c.repo_groups = RepoGroup.groups_choices(
70 70 groups=groups_with_admin_rights,
71 71 show_empty_group=allow_empty_group)
72 72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
73 73
74 74 def _can_create_repo_group(self, parent_group_id=None):
75 75 is_admin = HasPermissionAny('hg.admin')('group create controller')
76 76 create_repo_group = HasPermissionAny(
77 77 'hg.repogroup.create.true')('group create controller')
78 78 if is_admin or (create_repo_group and not parent_group_id):
79 79 # we're global admin, or we have global repo group create
80 80 # permission
81 81 # we're ok and we can create TOP level groups
82 82 return True
83 83 elif parent_group_id:
84 84 # we check the permission if we can write to parent group
85 85 group = RepoGroup.get(parent_group_id)
86 86 group_name = group.group_name if group else None
87 87 if HasRepoGroupPermissionAny('group.admin')(
88 88 group_name, 'check if user is an admin of group'):
89 89 # we're an admin of passed in group, we're ok.
90 90 return True
91 91 else:
92 92 return False
93 93 return False
94 94
95 95 # permission check in data loading of
96 96 # `repo_group_list_data` via RepoGroupList
97 97 @LoginRequired()
98 98 @NotAnonymous()
99 99 def repo_group_list(self):
100 100 c = self.load_default_context()
101 101 return self._get_template_context(c)
102 102
103 103 # permission check inside
104 104 @LoginRequired()
105 105 @NotAnonymous()
106 106 def repo_group_list_data(self):
107 107 self.load_default_context()
108 108 column_map = {
109 109 'name': 'group_name_hash',
110 110 'desc': 'group_description',
111 111 'last_change': 'updated_on',
112 112 'top_level_repos': 'repos_total',
113 113 'owner': 'user_username',
114 114 }
115 115 draw, start, limit = self._extract_chunk(self.request)
116 116 search_q, order_by, order_dir = self._extract_ordering(
117 117 self.request, column_map=column_map)
118 118
119 119 _render = self.request.get_partial_renderer(
120 120 'rhodecode:templates/data_table/_dt_elements.mako')
121 121 c = _render.get_call_context()
122 122
123 123 def quick_menu(repo_group_name):
124 124 return _render('quick_repo_group_menu', repo_group_name)
125 125
126 126 def repo_group_lnk(repo_group_name):
127 127 return _render('repo_group_name', repo_group_name)
128 128
129 129 def last_change(last_change):
130 130 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
131 131 ts = time.time()
132 132 utc_offset = (datetime.datetime.fromtimestamp(ts)
133 133 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
134 134 last_change = last_change + datetime.timedelta(seconds=utc_offset)
135 135 return _render("last_change", last_change)
136 136
137 137 def desc(desc, personal):
138 138 return _render(
139 139 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
140 140
141 141 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
142 142 return _render(
143 143 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
144 144
145 145 def user_profile(username):
146 146 return _render('user_profile', username)
147 147
148 148 _perms = ['group.admin']
149 149 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
150 150
151 151 repo_groups_data_total_count = RepoGroup.query()\
152 152 .filter(or_(
153 153 # generate multiple IN to fix limitation problems
154 154 *in_filter_generator(RepoGroup.group_id, allowed_ids)
155 155 )) \
156 156 .count()
157 157
158 158 repo_groups_data_total_inactive_count = RepoGroup.query()\
159 159 .filter(RepoGroup.group_id.in_(allowed_ids))\
160 160 .count()
161 161
162 162 repo_count = count(Repository.repo_id)
163 163 base_q = Session.query(
164 164 RepoGroup.group_name,
165 165 RepoGroup.group_name_hash,
166 166 RepoGroup.group_description,
167 167 RepoGroup.group_id,
168 168 RepoGroup.personal,
169 169 RepoGroup.updated_on,
170 170 User,
171 171 repo_count.label('repos_count')
172 172 ) \
173 173 .filter(or_(
174 174 # generate multiple IN to fix limitation problems
175 175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
176 176 )) \
177 177 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
178 178 .join(User, User.user_id == RepoGroup.user_id) \
179 179 .group_by(RepoGroup, User)
180 180
181 181 if search_q:
182 182 like_expression = u'%{}%'.format(safe_unicode(search_q))
183 183 base_q = base_q.filter(or_(
184 184 RepoGroup.group_name.ilike(like_expression),
185 185 ))
186 186
187 187 repo_groups_data_total_filtered_count = base_q.count()
188 188 # the inactive isn't really used, but we still make it same as other data grids
189 189 # which use inactive (users,user groups)
190 190 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
191 191
192 192 sort_defined = False
193 193 if order_by == 'group_name':
194 194 sort_col = func.lower(RepoGroup.group_name)
195 195 sort_defined = True
196 196 elif order_by == 'repos_total':
197 197 sort_col = repo_count
198 198 sort_defined = True
199 199 elif order_by == 'user_username':
200 200 sort_col = User.username
201 201 else:
202 202 sort_col = getattr(RepoGroup, order_by, None)
203 203
204 204 if sort_defined or sort_col:
205 205 if order_dir == 'asc':
206 206 sort_col = sort_col.asc()
207 207 else:
208 208 sort_col = sort_col.desc()
209 209
210 210 base_q = base_q.order_by(sort_col)
211 211 base_q = base_q.offset(start).limit(limit)
212 212
213 213 # authenticated access to user groups
214 214 auth_repo_group_list = base_q.all()
215 215
216 216 repo_groups_data = []
217 217 for repo_gr in auth_repo_group_list:
218 218 row = {
219 219 "menu": quick_menu(repo_gr.group_name),
220 220 "name": repo_group_lnk(repo_gr.group_name),
221 221
222 222 "last_change": last_change(repo_gr.updated_on),
223 223
224 224 "last_changeset": "",
225 225 "last_changeset_raw": "",
226 226
227 227 "desc": desc(repo_gr.group_description, repo_gr.personal),
228 228 "owner": user_profile(repo_gr.User.username),
229 229 "top_level_repos": repo_gr.repos_count,
230 230 "action": repo_group_actions(
231 231 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
232 232
233 233 }
234 234
235 235 repo_groups_data.append(row)
236 236
237 237 data = ({
238 238 'draw': draw,
239 239 'data': repo_groups_data,
240 240 'recordsTotal': repo_groups_data_total_count,
241 241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
242 242 'recordsFiltered': repo_groups_data_total_filtered_count,
243 243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
244 244 })
245 245
246 246 return data
247 247
248 248 @LoginRequired()
249 249 @NotAnonymous()
250 250 # perm checks inside
251 251 def repo_group_new(self):
252 252 c = self.load_default_context()
253 253
254 254 # perm check for admin, create_group perm or admin of parent_group
255 255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
256 256 _gr = RepoGroup.get(parent_group_id)
257 257 if not self._can_create_repo_group(parent_group_id):
258 258 raise HTTPForbidden()
259 259
260 260 self._load_form_data(c)
261 261
262 262 defaults = {} # Future proof for default of repo group
263 263
264 264 parent_group_choice = '-1'
265 265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
266 266 parent_group_choice = self._rhodecode_user.personal_repo_group
267 267
268 268 if parent_group_id and _gr:
269 269 if parent_group_id in [x[0] for x in c.repo_groups]:
270 270 parent_group_choice = safe_unicode(parent_group_id)
271 271
272 272 defaults.update({'group_parent_id': parent_group_choice})
273 273
274 274 data = render(
275 275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
276 276 self._get_template_context(c), self.request)
277 277
278 278 html = formencode.htmlfill.render(
279 279 data,
280 280 defaults=defaults,
281 281 encoding="UTF-8",
282 282 force_defaults=False
283 283 )
284 284 return Response(html)
285 285
286 286 @LoginRequired()
287 287 @NotAnonymous()
288 288 @CSRFRequired()
289 289 # perm checks inside
290 290 def repo_group_create(self):
291 291 c = self.load_default_context()
292 292 _ = self.request.translate
293 293
294 294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
295 295 can_create = self._can_create_repo_group(parent_group_id)
296 296
297 297 self._load_form_data(c)
298 298 # permissions for can create group based on parent_id are checked
299 299 # here in the Form
300 300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
301 301 repo_group_form = RepoGroupForm(
302 302 self.request.translate, available_groups=available_groups,
303 303 can_create_in_root=can_create)()
304 304
305 305 repo_group_name = self.request.POST.get('group_name')
306 306 try:
307 307 owner = self._rhodecode_user
308 308 form_result = repo_group_form.to_python(dict(self.request.POST))
309 309 copy_permissions = form_result.get('group_copy_permissions')
310 310 repo_group = RepoGroupModel().create(
311 311 group_name=form_result['group_name_full'],
312 312 group_description=form_result['group_description'],
313 313 owner=owner.user_id,
314 314 copy_permissions=form_result['group_copy_permissions']
315 315 )
316 316 Session().flush()
317 317
318 318 repo_group_data = repo_group.get_api_data()
319 319 audit_logger.store_web(
320 320 'repo_group.create', action_data={'data': repo_group_data},
321 321 user=self._rhodecode_user)
322 322
323 323 Session().commit()
324 324
325 325 _new_group_name = form_result['group_name_full']
326 326
327 327 repo_group_url = h.link_to(
328 328 _new_group_name,
329 329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
330 330 h.flash(h.literal(_('Created repository group %s')
331 331 % repo_group_url), category='success')
332 332
333 333 except formencode.Invalid as errors:
334 334 data = render(
335 335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
336 336 self._get_template_context(c), self.request)
337 337 html = formencode.htmlfill.render(
338 338 data,
339 339 defaults=errors.value,
340 340 errors=errors.unpack_errors() or {},
341 341 prefix_error=False,
342 342 encoding="UTF-8",
343 343 force_defaults=False
344 344 )
345 345 return Response(html)
346 346 except Exception:
347 347 log.exception("Exception during creation of repository group")
348 348 h.flash(_('Error occurred during creation of repository group %s')
349 349 % repo_group_name, category='error')
350 350 raise HTTPFound(h.route_path('home'))
351 351
352 352 PermissionModel().trigger_permission_flush()
353 353
354 354 raise HTTPFound(
355 355 h.route_path('repo_group_home',
356 356 repo_group_name=form_result['group_name_full']))
@@ -1,250 +1,250 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import formencode
23 23 import formencode.htmlfill
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 26
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode.lib.celerylib.utils import get_task_id
33 33
34 34 from rhodecode.lib.auth import (
35 35 LoginRequired, CSRFRequired, NotAnonymous,
36 36 HasPermissionAny, HasRepoGroupPermissionAny)
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.utils import repo_name_slug
39 39 from rhodecode.lib.utils2 import safe_int, safe_unicode
40 40 from rhodecode.model.forms import RepoForm
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
44 44 from rhodecode.model.settings import SettingsModel
45 45 from rhodecode.model.db import (
46 46 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class AdminReposView(BaseAppView, DataGridAppView):
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55 return c
56 56
57 57 def _load_form_data(self, c):
58 58 acl_groups = RepoGroupList(RepoGroup.query().all(),
59 59 perm_set=['group.write', 'group.admin'])
60 60 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
61 61 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
62 62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
63 63
64 64 @LoginRequired()
65 65 @NotAnonymous()
66 66 # perms check inside
67 67 def repository_list(self):
68 68 c = self.load_default_context()
69 69 return self._get_template_context(c)
70 70
71 71 @LoginRequired()
72 72 @NotAnonymous()
73 73 # perms check inside
74 74 def repository_list_data(self):
75 75 self.load_default_context()
76 76 column_map = {
77 77 'name': 'repo_name',
78 78 'desc': 'description',
79 79 'last_change': 'updated_on',
80 80 'owner': 'user_username',
81 81 }
82 82 draw, start, limit = self._extract_chunk(self.request)
83 83 search_q, order_by, order_dir = self._extract_ordering(
84 84 self.request, column_map=column_map)
85 85
86 86 _perms = ['repository.admin']
87 87 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
88 88
89 89 repos_data_total_count = Repository.query() \
90 90 .filter(or_(
91 91 # generate multiple IN to fix limitation problems
92 92 *in_filter_generator(Repository.repo_id, allowed_ids))
93 93 ) \
94 94 .count()
95 95
96 96 base_q = Session.query(
97 97 Repository.repo_id,
98 98 Repository.repo_name,
99 99 Repository.description,
100 100 Repository.repo_type,
101 101 Repository.repo_state,
102 102 Repository.private,
103 103 Repository.archived,
104 104 Repository.fork,
105 105 Repository.updated_on,
106 106 Repository._changeset_cache,
107 107 User,
108 108 ) \
109 109 .filter(or_(
110 110 # generate multiple IN to fix limitation problems
111 111 *in_filter_generator(Repository.repo_id, allowed_ids))
112 112 ) \
113 113 .join(User, User.user_id == Repository.user_id) \
114 114 .group_by(Repository, User)
115 115
116 116 if search_q:
117 117 like_expression = u'%{}%'.format(safe_unicode(search_q))
118 118 base_q = base_q.filter(or_(
119 119 Repository.repo_name.ilike(like_expression),
120 120 ))
121 121
122 122 repos_data_total_filtered_count = base_q.count()
123 123
124 124 sort_defined = False
125 125 if order_by == 'repo_name':
126 126 sort_col = func.lower(Repository.repo_name)
127 127 sort_defined = True
128 128 elif order_by == 'user_username':
129 129 sort_col = User.username
130 130 else:
131 131 sort_col = getattr(Repository, order_by, None)
132 132
133 133 if sort_defined or sort_col:
134 134 if order_dir == 'asc':
135 135 sort_col = sort_col.asc()
136 136 else:
137 137 sort_col = sort_col.desc()
138 138
139 139 base_q = base_q.order_by(sort_col)
140 140 base_q = base_q.offset(start).limit(limit)
141 141
142 142 repos_list = base_q.all()
143 143
144 144 repos_data = RepoModel().get_repos_as_dict(
145 145 repo_list=repos_list, admin=True, super_user_actions=True)
146 146
147 147 data = ({
148 148 'draw': draw,
149 149 'data': repos_data,
150 150 'recordsTotal': repos_data_total_count,
151 151 'recordsFiltered': repos_data_total_filtered_count,
152 152 })
153 153 return data
154 154
155 155 @LoginRequired()
156 156 @NotAnonymous()
157 157 # perms check inside
158 158 def repository_new(self):
159 159 c = self.load_default_context()
160 160
161 161 new_repo = self.request.GET.get('repo', '')
162 162 parent_group_id = safe_int(self.request.GET.get('parent_group'))
163 163 _gr = RepoGroup.get(parent_group_id)
164 164
165 165 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
166 166 # you're not super admin nor have global create permissions,
167 167 # but maybe you have at least write permission to a parent group ?
168 168
169 169 gr_name = _gr.group_name if _gr else None
170 170 # create repositories with write permission on group is set to true
171 171 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
172 172 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
173 173 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
174 174 if not (group_admin or (group_write and create_on_write)):
175 175 raise HTTPForbidden()
176 176
177 177 self._load_form_data(c)
178 178 c.new_repo = repo_name_slug(new_repo)
179 179
180 180 # apply the defaults from defaults page
181 181 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
182 182 # set checkbox to autochecked
183 183 defaults['repo_copy_permissions'] = True
184 184
185 185 parent_group_choice = '-1'
186 186 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
187 187 parent_group_choice = self._rhodecode_user.personal_repo_group
188 188
189 189 if parent_group_id and _gr:
190 190 if parent_group_id in [x[0] for x in c.repo_groups]:
191 191 parent_group_choice = safe_unicode(parent_group_id)
192 192
193 193 defaults.update({'repo_group': parent_group_choice})
194 194
195 195 data = render('rhodecode:templates/admin/repos/repo_add.mako',
196 196 self._get_template_context(c), self.request)
197 197 html = formencode.htmlfill.render(
198 198 data,
199 199 defaults=defaults,
200 200 encoding="UTF-8",
201 201 force_defaults=False
202 202 )
203 203 return Response(html)
204 204
205 205 @LoginRequired()
206 206 @NotAnonymous()
207 207 @CSRFRequired()
208 208 # perms check inside
209 209 def repository_create(self):
210 210 c = self.load_default_context()
211 211
212 212 form_result = {}
213 213 self._load_form_data(c)
214 214
215 215 try:
216 216 # CanWriteToGroup validators checks permissions of this POST
217 217 form = RepoForm(
218 218 self.request.translate, repo_groups=c.repo_groups_choices)()
219 219 form_result = form.to_python(dict(self.request.POST))
220 220 copy_permissions = form_result.get('repo_copy_permissions')
221 221 # create is done sometimes async on celery, db transaction
222 222 # management is handled there.
223 223 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
224 224 task_id = get_task_id(task)
225 225 except formencode.Invalid as errors:
226 226 data = render('rhodecode:templates/admin/repos/repo_add.mako',
227 227 self._get_template_context(c), self.request)
228 228 html = formencode.htmlfill.render(
229 229 data,
230 230 defaults=errors.value,
231 231 errors=errors.unpack_errors() or {},
232 232 prefix_error=False,
233 233 encoding="UTF-8",
234 234 force_defaults=False
235 235 )
236 236 return Response(html)
237 237
238 238 except Exception as e:
239 239 msg = self._log_creation_exception(e, form_result.get('repo_name'))
240 240 h.flash(msg, category='error')
241 241 raise HTTPFound(h.route_path('home'))
242 242
243 243 repo_name = form_result.get('repo_name_full')
244 244
245 245 affected_user_ids = [self._rhodecode_user.user_id]
246 246 PermissionModel().trigger_permission_flush(affected_user_ids)
247 247
248 248 raise HTTPFound(
249 249 h.route_path('repo_creating', repo_name=repo_name,
250 250 _query=dict(task_id=task_id)))
@@ -1,95 +1,95 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.apps._base.navigation import navigation_list
28 28 from rhodecode.lib.auth import (
29 29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 30 from rhodecode.lib.utils2 import safe_int
31 31 from rhodecode.lib import system_info
32 32 from rhodecode.lib import user_sessions
33 33 from rhodecode.lib import helpers as h
34 34
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class AdminSessionSettingsView(BaseAppView):
40 40
41 41 def load_default_context(self):
42 42 c = self._get_local_tmpl_context()
43 43 return c
44 44
45 45 @LoginRequired()
46 46 @HasPermissionAllDecorator('hg.admin')
47 47 def settings_sessions(self):
48 48 c = self.load_default_context()
49 49
50 50 c.active = 'sessions'
51 51 c.navlist = navigation_list(self.request)
52 52
53 53 c.cleanup_older_days = 60
54 54 older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days
55 55
56 56 config = system_info.rhodecode_config().get_value()['value']['config']
57 57 c.session_model = user_sessions.get_session_handler(
58 58 config.get('beaker.session.type', 'memory'))(config)
59 59
60 60 c.session_conf = c.session_model.config
61 61 c.session_count = c.session_model.get_count()
62 62 c.session_expired_count = c.session_model.get_expired_count(
63 63 older_than_seconds)
64 64
65 65 return self._get_template_context(c)
66 66
67 67 @LoginRequired()
68 68 @HasPermissionAllDecorator('hg.admin')
69 69 @CSRFRequired()
70 70 def settings_sessions_cleanup(self):
71 71 _ = self.request.translate
72 72 expire_days = safe_int(self.request.params.get('expire_days'))
73 73
74 74 if expire_days is None:
75 75 expire_days = 60
76 76
77 77 older_than_seconds = 60 * 60 * 24 * expire_days
78 78
79 79 config = system_info.rhodecode_config().get_value()['value']['config']
80 80 session_model = user_sessions.get_session_handler(
81 81 config.get('beaker.session.type', 'memory'))(config)
82 82
83 83 try:
84 84 session_model.clean_sessions(
85 85 older_than_seconds=older_than_seconds)
86 86 h.flash(_('Cleaned up old sessions'), category='success')
87 87 except user_sessions.CleanupCommand as msg:
88 88 h.flash(msg.message, category='warning')
89 89 except Exception as e:
90 90 log.exception('Failed session cleanup')
91 91 h.flash(_('Failed to cleanup up old sessions'), category='error')
92 92
93 93 redirect_to = self.request.resource_path(
94 94 self.context, route_name='admin_settings_sessions')
95 95 return HTTPFound(redirect_to)
@@ -1,720 +1,719 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20
22 21 import logging
23 22 import collections
24 23
25 24 import datetime
26 25 import formencode
27 26 import formencode.htmlfill
28 27
29 28 import rhodecode
30 29
31 30 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 31 from pyramid.renderers import render
33 32 from pyramid.response import Response
34 33
35 34 from rhodecode.apps._base import BaseAppView
36 35 from rhodecode.apps._base.navigation import navigation_list
37 36 from rhodecode.apps.svn_support.config_keys import generate_config
38 37 from rhodecode.lib import helpers as h
39 38 from rhodecode.lib.auth import (
40 39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 40 from rhodecode.lib.celerylib import tasks, run_task
42 41 from rhodecode.lib.utils import repo2db_mapper
43 42 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 43 from rhodecode.lib.index import searcher_from_config
45 44
46 45 from rhodecode.model.db import RhodeCodeUi, Repository
47 46 from rhodecode.model.forms import (ApplicationSettingsForm,
48 47 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 48 LabsSettingsForm, IssueTrackerPatternsForm)
50 49 from rhodecode.model.permission import PermissionModel
51 50 from rhodecode.model.repo_group import RepoGroupModel
52 51
53 52 from rhodecode.model.scm import ScmModel
54 53 from rhodecode.model.notification import EmailNotificationModel
55 54 from rhodecode.model.meta import Session
56 55 from rhodecode.model.settings import (
57 56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
58 57 SettingsModel)
59 58
60 59
61 60 log = logging.getLogger(__name__)
62 61
63 62
64 63 class AdminSettingsView(BaseAppView):
65 64
66 65 def load_default_context(self):
67 66 c = self._get_local_tmpl_context()
68 67 c.labs_active = str2bool(
69 68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
70 69 c.navlist = navigation_list(self.request)
71 70 return c
72 71
73 72 @classmethod
74 73 def _get_ui_settings(cls):
75 74 ret = RhodeCodeUi.query().all()
76 75
77 76 if not ret:
78 77 raise Exception('Could not get application ui settings !')
79 78 settings = {}
80 79 for each in ret:
81 80 k = each.ui_key
82 81 v = each.ui_value
83 82 if k == '/':
84 83 k = 'root_path'
85 84
86 85 if k in ['push_ssl', 'publish', 'enabled']:
87 86 v = str2bool(v)
88 87
89 88 if k.find('.') != -1:
90 89 k = k.replace('.', '_')
91 90
92 91 if each.ui_section in ['hooks', 'extensions']:
93 92 v = each.ui_active
94 93
95 94 settings[each.ui_section + '_' + k] = v
96 95 return settings
97 96
98 97 @classmethod
99 98 def _form_defaults(cls):
100 99 defaults = SettingsModel().get_all_settings()
101 100 defaults.update(cls._get_ui_settings())
102 101
103 102 defaults.update({
104 103 'new_svn_branch': '',
105 104 'new_svn_tag': '',
106 105 })
107 106 return defaults
108 107
109 108 @LoginRequired()
110 109 @HasPermissionAllDecorator('hg.admin')
111 110 def settings_vcs(self):
112 111 c = self.load_default_context()
113 112 c.active = 'vcs'
114 113 model = VcsSettingsModel()
115 114 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
116 115 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
117 116
118 117 settings = self.request.registry.settings
119 118 c.svn_proxy_generate_config = settings[generate_config]
120 119
121 120 defaults = self._form_defaults()
122 121
123 122 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
124 123
125 124 data = render('rhodecode:templates/admin/settings/settings.mako',
126 125 self._get_template_context(c), self.request)
127 126 html = formencode.htmlfill.render(
128 127 data,
129 128 defaults=defaults,
130 129 encoding="UTF-8",
131 130 force_defaults=False
132 131 )
133 132 return Response(html)
134 133
135 134 @LoginRequired()
136 135 @HasPermissionAllDecorator('hg.admin')
137 136 @CSRFRequired()
138 137 def settings_vcs_update(self):
139 138 _ = self.request.translate
140 139 c = self.load_default_context()
141 140 c.active = 'vcs'
142 141
143 142 model = VcsSettingsModel()
144 143 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
145 144 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
146 145
147 146 settings = self.request.registry.settings
148 147 c.svn_proxy_generate_config = settings[generate_config]
149 148
150 149 application_form = ApplicationUiSettingsForm(self.request.translate)()
151 150
152 151 try:
153 152 form_result = application_form.to_python(dict(self.request.POST))
154 153 except formencode.Invalid as errors:
155 154 h.flash(
156 155 _("Some form inputs contain invalid data."),
157 156 category='error')
158 157 data = render('rhodecode:templates/admin/settings/settings.mako',
159 158 self._get_template_context(c), self.request)
160 159 html = formencode.htmlfill.render(
161 160 data,
162 161 defaults=errors.value,
163 162 errors=errors.unpack_errors() or {},
164 163 prefix_error=False,
165 164 encoding="UTF-8",
166 165 force_defaults=False
167 166 )
168 167 return Response(html)
169 168
170 169 try:
171 170 if c.visual.allow_repo_location_change:
172 171 model.update_global_path_setting(form_result['paths_root_path'])
173 172
174 173 model.update_global_ssl_setting(form_result['web_push_ssl'])
175 174 model.update_global_hook_settings(form_result)
176 175
177 176 model.create_or_update_global_svn_settings(form_result)
178 177 model.create_or_update_global_hg_settings(form_result)
179 178 model.create_or_update_global_git_settings(form_result)
180 179 model.create_or_update_global_pr_settings(form_result)
181 180 except Exception:
182 181 log.exception("Exception while updating settings")
183 182 h.flash(_('Error occurred during updating '
184 183 'application settings'), category='error')
185 184 else:
186 185 Session().commit()
187 186 h.flash(_('Updated VCS settings'), category='success')
188 187 raise HTTPFound(h.route_path('admin_settings_vcs'))
189 188
190 189 data = render('rhodecode:templates/admin/settings/settings.mako',
191 190 self._get_template_context(c), self.request)
192 191 html = formencode.htmlfill.render(
193 192 data,
194 193 defaults=self._form_defaults(),
195 194 encoding="UTF-8",
196 195 force_defaults=False
197 196 )
198 197 return Response(html)
199 198
200 199 @LoginRequired()
201 200 @HasPermissionAllDecorator('hg.admin')
202 201 @CSRFRequired()
203 202 def settings_vcs_delete_svn_pattern(self):
204 203 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
205 204 model = VcsSettingsModel()
206 205 try:
207 206 model.delete_global_svn_pattern(delete_pattern_id)
208 207 except SettingNotFound:
209 208 log.exception(
210 209 'Failed to delete svn_pattern with id %s', delete_pattern_id)
211 210 raise HTTPNotFound()
212 211
213 212 Session().commit()
214 213 return True
215 214
216 215 @LoginRequired()
217 216 @HasPermissionAllDecorator('hg.admin')
218 217 def settings_mapping(self):
219 218 c = self.load_default_context()
220 219 c.active = 'mapping'
221 220
222 221 data = render('rhodecode:templates/admin/settings/settings.mako',
223 222 self._get_template_context(c), self.request)
224 223 html = formencode.htmlfill.render(
225 224 data,
226 225 defaults=self._form_defaults(),
227 226 encoding="UTF-8",
228 227 force_defaults=False
229 228 )
230 229 return Response(html)
231 230
232 231 @LoginRequired()
233 232 @HasPermissionAllDecorator('hg.admin')
234 233 @CSRFRequired()
235 234 def settings_mapping_update(self):
236 235 _ = self.request.translate
237 236 c = self.load_default_context()
238 237 c.active = 'mapping'
239 238 rm_obsolete = self.request.POST.get('destroy', False)
240 239 invalidate_cache = self.request.POST.get('invalidate', False)
241 240 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
242 241
243 242 if invalidate_cache:
244 243 log.debug('invalidating all repositories cache')
245 244 for repo in Repository.get_all():
246 245 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
247 246
248 247 filesystem_repos = ScmModel().repo_scan()
249 248 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
250 249 PermissionModel().trigger_permission_flush()
251 250
252 251 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
253 252 h.flash(_('Repositories successfully '
254 253 'rescanned added: %s ; removed: %s') %
255 254 (_repr(added), _repr(removed)),
256 255 category='success')
257 256 raise HTTPFound(h.route_path('admin_settings_mapping'))
258 257
259 258 @LoginRequired()
260 259 @HasPermissionAllDecorator('hg.admin')
261 260 def settings_global(self):
262 261 c = self.load_default_context()
263 262 c.active = 'global'
264 263 c.personal_repo_group_default_pattern = RepoGroupModel()\
265 264 .get_personal_group_name_pattern()
266 265
267 266 data = render('rhodecode:templates/admin/settings/settings.mako',
268 267 self._get_template_context(c), self.request)
269 268 html = formencode.htmlfill.render(
270 269 data,
271 270 defaults=self._form_defaults(),
272 271 encoding="UTF-8",
273 272 force_defaults=False
274 273 )
275 274 return Response(html)
276 275
277 276 @LoginRequired()
278 277 @HasPermissionAllDecorator('hg.admin')
279 278 @CSRFRequired()
280 279 def settings_global_update(self):
281 280 _ = self.request.translate
282 281 c = self.load_default_context()
283 282 c.active = 'global'
284 283 c.personal_repo_group_default_pattern = RepoGroupModel()\
285 284 .get_personal_group_name_pattern()
286 285 application_form = ApplicationSettingsForm(self.request.translate)()
287 286 try:
288 287 form_result = application_form.to_python(dict(self.request.POST))
289 288 except formencode.Invalid as errors:
290 289 h.flash(
291 290 _("Some form inputs contain invalid data."),
292 291 category='error')
293 292 data = render('rhodecode:templates/admin/settings/settings.mako',
294 293 self._get_template_context(c), self.request)
295 294 html = formencode.htmlfill.render(
296 295 data,
297 296 defaults=errors.value,
298 297 errors=errors.unpack_errors() or {},
299 298 prefix_error=False,
300 299 encoding="UTF-8",
301 300 force_defaults=False
302 301 )
303 302 return Response(html)
304 303
305 304 settings = [
306 305 ('title', 'rhodecode_title', 'unicode'),
307 306 ('realm', 'rhodecode_realm', 'unicode'),
308 307 ('pre_code', 'rhodecode_pre_code', 'unicode'),
309 308 ('post_code', 'rhodecode_post_code', 'unicode'),
310 309 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
311 310 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
312 311 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
313 312 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
314 313 ]
315 314 try:
316 315 for setting, form_key, type_ in settings:
317 316 sett = SettingsModel().create_or_update_setting(
318 317 setting, form_result[form_key], type_)
319 318 Session().add(sett)
320 319
321 320 Session().commit()
322 321 SettingsModel().invalidate_settings_cache()
323 322 h.flash(_('Updated application settings'), category='success')
324 323 except Exception:
325 324 log.exception("Exception while updating application settings")
326 325 h.flash(
327 326 _('Error occurred during updating application settings'),
328 327 category='error')
329 328
330 329 raise HTTPFound(h.route_path('admin_settings_global'))
331 330
332 331 @LoginRequired()
333 332 @HasPermissionAllDecorator('hg.admin')
334 333 def settings_visual(self):
335 334 c = self.load_default_context()
336 335 c.active = 'visual'
337 336
338 337 data = render('rhodecode:templates/admin/settings/settings.mako',
339 338 self._get_template_context(c), self.request)
340 339 html = formencode.htmlfill.render(
341 340 data,
342 341 defaults=self._form_defaults(),
343 342 encoding="UTF-8",
344 343 force_defaults=False
345 344 )
346 345 return Response(html)
347 346
348 347 @LoginRequired()
349 348 @HasPermissionAllDecorator('hg.admin')
350 349 @CSRFRequired()
351 350 def settings_visual_update(self):
352 351 _ = self.request.translate
353 352 c = self.load_default_context()
354 353 c.active = 'visual'
355 354 application_form = ApplicationVisualisationForm(self.request.translate)()
356 355 try:
357 356 form_result = application_form.to_python(dict(self.request.POST))
358 357 except formencode.Invalid as errors:
359 358 h.flash(
360 359 _("Some form inputs contain invalid data."),
361 360 category='error')
362 361 data = render('rhodecode:templates/admin/settings/settings.mako',
363 362 self._get_template_context(c), self.request)
364 363 html = formencode.htmlfill.render(
365 364 data,
366 365 defaults=errors.value,
367 366 errors=errors.unpack_errors() or {},
368 367 prefix_error=False,
369 368 encoding="UTF-8",
370 369 force_defaults=False
371 370 )
372 371 return Response(html)
373 372
374 373 try:
375 374 settings = [
376 375 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
377 376 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
378 377 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
379 378 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
380 379 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
381 380 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
382 381 ('show_version', 'rhodecode_show_version', 'bool'),
383 382 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
384 383 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
385 384 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
386 385 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
387 386 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
388 387 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
389 388 ('support_url', 'rhodecode_support_url', 'unicode'),
390 389 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
391 390 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
392 391 ]
393 392 for setting, form_key, type_ in settings:
394 393 sett = SettingsModel().create_or_update_setting(
395 394 setting, form_result[form_key], type_)
396 395 Session().add(sett)
397 396
398 397 Session().commit()
399 398 SettingsModel().invalidate_settings_cache()
400 399 h.flash(_('Updated visualisation settings'), category='success')
401 400 except Exception:
402 401 log.exception("Exception updating visualization settings")
403 402 h.flash(_('Error occurred during updating '
404 403 'visualisation settings'),
405 404 category='error')
406 405
407 406 raise HTTPFound(h.route_path('admin_settings_visual'))
408 407
409 408 @LoginRequired()
410 409 @HasPermissionAllDecorator('hg.admin')
411 410 def settings_issuetracker(self):
412 411 c = self.load_default_context()
413 412 c.active = 'issuetracker'
414 413 defaults = c.rc_config
415 414
416 415 entry_key = 'rhodecode_issuetracker_pat_'
417 416
418 417 c.issuetracker_entries = {}
419 418 for k, v in defaults.items():
420 419 if k.startswith(entry_key):
421 420 uid = k[len(entry_key):]
422 421 c.issuetracker_entries[uid] = None
423 422
424 423 for uid in c.issuetracker_entries:
425 424 c.issuetracker_entries[uid] = AttributeDict({
426 425 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
427 426 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
428 427 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
429 428 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
430 429 })
431 430
432 431 return self._get_template_context(c)
433 432
434 433 @LoginRequired()
435 434 @HasPermissionAllDecorator('hg.admin')
436 435 @CSRFRequired()
437 436 def settings_issuetracker_test(self):
438 437 error_container = []
439 438
440 439 urlified_commit = h.urlify_commit_message(
441 440 self.request.POST.get('test_text', ''),
442 441 'repo_group/test_repo1', error_container=error_container)
443 442 if error_container:
444 443 def converter(inp):
445 444 return h.html_escape(inp)
446 445
447 446 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
448 447
449 448 return urlified_commit
450 449
451 450 @LoginRequired()
452 451 @HasPermissionAllDecorator('hg.admin')
453 452 @CSRFRequired()
454 453 def settings_issuetracker_update(self):
455 454 _ = self.request.translate
456 455 self.load_default_context()
457 456 settings_model = IssueTrackerSettingsModel()
458 457
459 458 try:
460 459 form = IssueTrackerPatternsForm(self.request.translate)()
461 460 data = form.to_python(self.request.POST)
462 461 except formencode.Invalid as errors:
463 462 log.exception('Failed to add new pattern')
464 463 error = errors
465 464 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
466 465 category='error')
467 466 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
468 467
469 468 if data:
470 469 for uid in data.get('delete_patterns', []):
471 470 settings_model.delete_entries(uid)
472 471
473 472 for pattern in data.get('patterns', []):
474 473 for setting, value, type_ in pattern:
475 474 sett = settings_model.create_or_update_setting(
476 475 setting, value, type_)
477 476 Session().add(sett)
478 477
479 478 Session().commit()
480 479
481 480 SettingsModel().invalidate_settings_cache()
482 481 h.flash(_('Updated issue tracker entries'), category='success')
483 482 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
484 483
485 484 @LoginRequired()
486 485 @HasPermissionAllDecorator('hg.admin')
487 486 @CSRFRequired()
488 487 def settings_issuetracker_delete(self):
489 488 _ = self.request.translate
490 489 self.load_default_context()
491 490 uid = self.request.POST.get('uid')
492 491 try:
493 492 IssueTrackerSettingsModel().delete_entries(uid)
494 493 except Exception:
495 494 log.exception('Failed to delete issue tracker setting %s', uid)
496 495 raise HTTPNotFound()
497 496
498 497 SettingsModel().invalidate_settings_cache()
499 498 h.flash(_('Removed issue tracker entry.'), category='success')
500 499
501 500 return {'deleted': uid}
502 501
503 502 @LoginRequired()
504 503 @HasPermissionAllDecorator('hg.admin')
505 504 def settings_email(self):
506 505 c = self.load_default_context()
507 506 c.active = 'email'
508 507 c.rhodecode_ini = rhodecode.CONFIG
509 508
510 509 data = render('rhodecode:templates/admin/settings/settings.mako',
511 510 self._get_template_context(c), self.request)
512 511 html = formencode.htmlfill.render(
513 512 data,
514 513 defaults=self._form_defaults(),
515 514 encoding="UTF-8",
516 515 force_defaults=False
517 516 )
518 517 return Response(html)
519 518
520 519 @LoginRequired()
521 520 @HasPermissionAllDecorator('hg.admin')
522 521 @CSRFRequired()
523 522 def settings_email_update(self):
524 523 _ = self.request.translate
525 524 c = self.load_default_context()
526 525 c.active = 'email'
527 526
528 527 test_email = self.request.POST.get('test_email')
529 528
530 529 if not test_email:
531 530 h.flash(_('Please enter email address'), category='error')
532 531 raise HTTPFound(h.route_path('admin_settings_email'))
533 532
534 533 email_kwargs = {
535 534 'date': datetime.datetime.now(),
536 535 'user': self._rhodecode_db_user
537 536 }
538 537
539 538 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
540 539 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
541 540
542 541 recipients = [test_email] if test_email else None
543 542
544 543 run_task(tasks.send_email, recipients, subject,
545 544 email_body_plaintext, email_body)
546 545
547 546 h.flash(_('Send email task created'), category='success')
548 547 raise HTTPFound(h.route_path('admin_settings_email'))
549 548
550 549 @LoginRequired()
551 550 @HasPermissionAllDecorator('hg.admin')
552 551 def settings_hooks(self):
553 552 c = self.load_default_context()
554 553 c.active = 'hooks'
555 554
556 555 model = SettingsModel()
557 556 c.hooks = model.get_builtin_hooks()
558 557 c.custom_hooks = model.get_custom_hooks()
559 558
560 559 data = render('rhodecode:templates/admin/settings/settings.mako',
561 560 self._get_template_context(c), self.request)
562 561 html = formencode.htmlfill.render(
563 562 data,
564 563 defaults=self._form_defaults(),
565 564 encoding="UTF-8",
566 565 force_defaults=False
567 566 )
568 567 return Response(html)
569 568
570 569 @LoginRequired()
571 570 @HasPermissionAllDecorator('hg.admin')
572 571 @CSRFRequired()
573 572 def settings_hooks_update(self):
574 573 _ = self.request.translate
575 574 c = self.load_default_context()
576 575 c.active = 'hooks'
577 576 if c.visual.allow_custom_hooks_settings:
578 577 ui_key = self.request.POST.get('new_hook_ui_key')
579 578 ui_value = self.request.POST.get('new_hook_ui_value')
580 579
581 580 hook_id = self.request.POST.get('hook_id')
582 581 new_hook = False
583 582
584 583 model = SettingsModel()
585 584 try:
586 585 if ui_value and ui_key:
587 586 model.create_or_update_hook(ui_key, ui_value)
588 587 h.flash(_('Added new hook'), category='success')
589 588 new_hook = True
590 589 elif hook_id:
591 590 RhodeCodeUi.delete(hook_id)
592 591 Session().commit()
593 592
594 593 # check for edits
595 594 update = False
596 595 _d = self.request.POST.dict_of_lists()
597 596 for k, v in zip(_d.get('hook_ui_key', []),
598 597 _d.get('hook_ui_value_new', [])):
599 598 model.create_or_update_hook(k, v)
600 599 update = True
601 600
602 601 if update and not new_hook:
603 602 h.flash(_('Updated hooks'), category='success')
604 603 Session().commit()
605 604 except Exception:
606 605 log.exception("Exception during hook creation")
607 606 h.flash(_('Error occurred during hook creation'),
608 607 category='error')
609 608
610 609 raise HTTPFound(h.route_path('admin_settings_hooks'))
611 610
612 611 @LoginRequired()
613 612 @HasPermissionAllDecorator('hg.admin')
614 613 def settings_search(self):
615 614 c = self.load_default_context()
616 615 c.active = 'search'
617 616
618 617 c.searcher = searcher_from_config(self.request.registry.settings)
619 618 c.statistics = c.searcher.statistics(self.request.translate)
620 619
621 620 return self._get_template_context(c)
622 621
623 622 @LoginRequired()
624 623 @HasPermissionAllDecorator('hg.admin')
625 624 def settings_automation(self):
626 625 c = self.load_default_context()
627 626 c.active = 'automation'
628 627
629 628 return self._get_template_context(c)
630 629
631 630 @LoginRequired()
632 631 @HasPermissionAllDecorator('hg.admin')
633 632 def settings_labs(self):
634 633 c = self.load_default_context()
635 634 if not c.labs_active:
636 635 raise HTTPFound(h.route_path('admin_settings'))
637 636
638 637 c.active = 'labs'
639 638 c.lab_settings = _LAB_SETTINGS
640 639
641 640 data = render('rhodecode:templates/admin/settings/settings.mako',
642 641 self._get_template_context(c), self.request)
643 642 html = formencode.htmlfill.render(
644 643 data,
645 644 defaults=self._form_defaults(),
646 645 encoding="UTF-8",
647 646 force_defaults=False
648 647 )
649 648 return Response(html)
650 649
651 650 @LoginRequired()
652 651 @HasPermissionAllDecorator('hg.admin')
653 652 @CSRFRequired()
654 653 def settings_labs_update(self):
655 654 _ = self.request.translate
656 655 c = self.load_default_context()
657 656 c.active = 'labs'
658 657
659 658 application_form = LabsSettingsForm(self.request.translate)()
660 659 try:
661 660 form_result = application_form.to_python(dict(self.request.POST))
662 661 except formencode.Invalid as errors:
663 662 h.flash(
664 663 _("Some form inputs contain invalid data."),
665 664 category='error')
666 665 data = render('rhodecode:templates/admin/settings/settings.mako',
667 666 self._get_template_context(c), self.request)
668 667 html = formencode.htmlfill.render(
669 668 data,
670 669 defaults=errors.value,
671 670 errors=errors.unpack_errors() or {},
672 671 prefix_error=False,
673 672 encoding="UTF-8",
674 673 force_defaults=False
675 674 )
676 675 return Response(html)
677 676
678 677 try:
679 678 session = Session()
680 679 for setting in _LAB_SETTINGS:
681 680 setting_name = setting.key[len('rhodecode_'):]
682 681 sett = SettingsModel().create_or_update_setting(
683 682 setting_name, form_result[setting.key], setting.type)
684 683 session.add(sett)
685 684
686 685 except Exception:
687 686 log.exception('Exception while updating lab settings')
688 687 h.flash(_('Error occurred during updating labs settings'),
689 688 category='error')
690 689 else:
691 690 Session().commit()
692 691 SettingsModel().invalidate_settings_cache()
693 692 h.flash(_('Updated Labs settings'), category='success')
694 693 raise HTTPFound(h.route_path('admin_settings_labs'))
695 694
696 695 data = render('rhodecode:templates/admin/settings/settings.mako',
697 696 self._get_template_context(c), self.request)
698 697 html = formencode.htmlfill.render(
699 698 data,
700 699 defaults=self._form_defaults(),
701 700 encoding="UTF-8",
702 701 force_defaults=False
703 702 )
704 703 return Response(html)
705 704
706 705
707 706 # :param key: name of the setting including the 'rhodecode_' prefix
708 707 # :param type: the RhodeCodeSetting type to use.
709 708 # :param group: the i18ned group in which we should dispaly this setting
710 709 # :param label: the i18ned label we should display for this setting
711 710 # :param help: the i18ned help we should dispaly for this setting
712 711 LabSetting = collections.namedtuple(
713 712 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
714 713
715 714
716 715 # This list has to be kept in sync with the form
717 716 # rhodecode.model.forms.LabsSettingsForm.
718 717 _LAB_SETTINGS = [
719 718
720 719 ]
@@ -1,56 +1,56 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24
25 25 from rhodecode.apps._base import BaseAppView
26 26 from rhodecode.apps.svn_support.utils import generate_mod_dav_svn_config
27 27 from rhodecode.lib.auth import (
28 28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class AdminSvnConfigView(BaseAppView):
34 34
35 35 @LoginRequired()
36 36 @HasPermissionAllDecorator('hg.admin')
37 37 @CSRFRequired()
38 38 def vcs_svn_generate_config(self):
39 39 _ = self.request.translate
40 40 try:
41 41 file_path = generate_mod_dav_svn_config(self.request.registry)
42 42 msg = {
43 43 'message': _('Apache configuration for Subversion generated at `{}`.').format(file_path),
44 44 'level': 'success',
45 45 }
46 46 except Exception:
47 47 log.exception(
48 48 'Exception while generating the Apache '
49 49 'configuration for Subversion.')
50 50 msg = {
51 51 'message': _('Failed to generate the Apache configuration for Subversion.'),
52 52 'level': 'error',
53 53 }
54 54
55 55 data = {'message': msg}
56 56 return data
@@ -1,234 +1,234 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import urllib.request, urllib.error, urllib.parse
23 23 import os
24 24
25 25 import rhodecode
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.apps._base.navigation import navigation_list
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 30 from rhodecode.lib.utils2 import str2bool
31 31 from rhodecode.lib import system_info
32 32 from rhodecode.model.update import UpdateModel
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class AdminSystemInfoSettingsView(BaseAppView):
38 38 def load_default_context(self):
39 39 c = self._get_local_tmpl_context()
40 40 return c
41 41
42 42 def get_env_data(self):
43 43 black_list = [
44 44 'NIX_LDFLAGS',
45 45 'NIX_CFLAGS_COMPILE',
46 46 'propagatedBuildInputs',
47 47 'propagatedNativeBuildInputs',
48 48 'postInstall',
49 49 'buildInputs',
50 50 'buildPhase',
51 51 'preShellHook',
52 52 'preShellHook',
53 53 'preCheck',
54 54 'preBuild',
55 55 'postShellHook',
56 56 'postFixup',
57 57 'postCheck',
58 58 'nativeBuildInputs',
59 59 'installPhase',
60 60 'installCheckPhase',
61 61 'checkPhase',
62 62 'configurePhase',
63 63 'shellHook'
64 64 ]
65 65 secret_list = [
66 66 'RHODECODE_USER_PASS'
67 67 ]
68 68
69 69 for k, v in sorted(os.environ.items()):
70 70 if k in black_list:
71 71 continue
72 72 if k in secret_list:
73 73 v = '*****'
74 74 yield k, v
75 75
76 76 @LoginRequired()
77 77 @HasPermissionAllDecorator('hg.admin')
78 78 def settings_system_info(self):
79 79 _ = self.request.translate
80 80 c = self.load_default_context()
81 81
82 82 c.active = 'system'
83 83 c.navlist = navigation_list(self.request)
84 84
85 85 # TODO(marcink), figure out how to allow only selected users to do this
86 86 c.allowed_to_snapshot = self._rhodecode_user.admin
87 87
88 88 snapshot = str2bool(self.request.params.get('snapshot'))
89 89
90 90 c.rhodecode_update_url = UpdateModel().get_update_url()
91 91 c.env_data = self.get_env_data()
92 92 server_info = system_info.get_system_info(self.request.environ)
93 93
94 94 for key, val in server_info.items():
95 95 setattr(c, key, val)
96 96
97 97 def val(name, subkey='human_value'):
98 98 return server_info[name][subkey]
99 99
100 100 def state(name):
101 101 return server_info[name]['state']
102 102
103 103 def val2(name):
104 104 val = server_info[name]['human_value']
105 105 state = server_info[name]['state']
106 106 return val, state
107 107
108 108 update_info_msg = _('Note: please make sure this server can '
109 109 'access `${url}` for the update link to work',
110 110 mapping=dict(url=c.rhodecode_update_url))
111 111 version = UpdateModel().get_stored_version()
112 112 is_outdated = UpdateModel().is_outdated(
113 113 rhodecode.__version__, version)
114 114 update_state = {
115 115 'type': 'warning',
116 116 'message': 'New version available: {}'.format(version)
117 117 } \
118 118 if is_outdated else {}
119 119 c.data_items = [
120 120 # update info
121 121 (_('Update info'), h.literal(
122 122 '<span class="link" id="check_for_update" >%s.</span>' % (
123 123 _('Check for updates')) +
124 124 '<br/> <span >%s.</span>' % (update_info_msg)
125 125 ), ''),
126 126
127 127 # RhodeCode specific
128 128 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
129 129 (_('Latest version'), version, update_state),
130 130 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
131 131 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
132 132 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
133 133 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
134 134 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
135 135 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
136 136 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
137 137 ('', '', ''), # spacer
138 138
139 139 # Database
140 140 (_('Database'), val('database')['url'], state('database')),
141 141 (_('Database version'), val('database')['version'], state('database')),
142 142 ('', '', ''), # spacer
143 143
144 144 # Platform/Python
145 145 (_('Platform'), val('platform')['name'], state('platform')),
146 146 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
147 147 (_('Lang'), val('locale'), state('locale')),
148 148 (_('Python version'), val('python')['version'], state('python')),
149 149 (_('Python path'), val('python')['executable'], state('python')),
150 150 ('', '', ''), # spacer
151 151
152 152 # Systems stats
153 153 (_('CPU'), val('cpu')['text'], state('cpu')),
154 154 (_('Load'), val('load')['text'], state('load')),
155 155 (_('Memory'), val('memory')['text'], state('memory')),
156 156 (_('Uptime'), val('uptime')['text'], state('uptime')),
157 157 ('', '', ''), # spacer
158 158
159 159 # ulimit
160 160 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
161 161
162 162 # Repo storage
163 163 (_('Storage location'), val('storage')['path'], state('storage')),
164 164 (_('Storage info'), val('storage')['text'], state('storage')),
165 165 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
166 166
167 167 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
168 168 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
169 169
170 170 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
171 171 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
172 172
173 173 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
174 174 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
175 175
176 176 (_('Search info'), val('search')['text'], state('search')),
177 177 (_('Search location'), val('search')['location'], state('search')),
178 178 ('', '', ''), # spacer
179 179
180 180 # VCS specific
181 181 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
182 182 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
183 183 (_('GIT'), val('git'), state('git')),
184 184 (_('HG'), val('hg'), state('hg')),
185 185 (_('SVN'), val('svn'), state('svn')),
186 186
187 187 ]
188 188
189 189 c.vcsserver_data_items = [
190 190 (k, v) for k,v in (val('vcs_server_config') or {}).items()
191 191 ]
192 192
193 193 if snapshot:
194 194 if c.allowed_to_snapshot:
195 195 c.data_items.pop(0) # remove server info
196 196 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
197 197 else:
198 198 h.flash('You are not allowed to do this', category='warning')
199 199 return self._get_template_context(c)
200 200
201 201 @LoginRequired()
202 202 @HasPermissionAllDecorator('hg.admin')
203 203 def settings_system_info_check_update(self):
204 204 _ = self.request.translate
205 205 c = self.load_default_context()
206 206
207 207 update_url = UpdateModel().get_update_url()
208 208
209 209 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
210 210 try:
211 211 data = UpdateModel().get_update_data(update_url)
212 212 except urllib.error.URLError as e:
213 213 log.exception("Exception contacting upgrade server")
214 214 self.request.override_renderer = 'string'
215 215 return _err('Failed to contact upgrade server: %r' % e)
216 216 except ValueError as e:
217 217 log.exception("Bad data sent from update server")
218 218 self.request.override_renderer = 'string'
219 219 return _err('Bad data sent from update server')
220 220
221 221 latest = data['versions'][0]
222 222
223 223 c.update_url = update_url
224 224 c.latest_data = latest
225 225 c.latest_ver = latest['version']
226 226 c.cur_ver = rhodecode.__version__
227 227 c.should_upgrade = False
228 228
229 229 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
230 230 if is_oudated:
231 231 c.should_upgrade = True
232 232 c.important_notices = latest['general']
233 233 UpdateModel().store_version(latest['version'])
234 234 return self._get_template_context(c)
@@ -1,253 +1,253 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27
28 28 from pyramid.response import Response
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
35 35 from rhodecode.lib import helpers as h, audit_logger
36 36 from rhodecode.lib.utils2 import safe_unicode
37 37
38 38 from rhodecode.model.forms import UserGroupForm
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.scm import UserGroupList
41 41 from rhodecode.model.db import (
42 42 or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.model.user_group import UserGroupModel
45 45 from rhodecode.model.db import true
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class AdminUserGroupsView(BaseAppView, DataGridAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54 PermissionModel().set_global_permission_choices(
55 55 c, gettext_translator=self.request.translate)
56 56 return c
57 57
58 58 # permission check in data loading of
59 59 # `user_groups_list_data` via UserGroupList
60 60 @LoginRequired()
61 61 @NotAnonymous()
62 62 def user_groups_list(self):
63 63 c = self.load_default_context()
64 64 return self._get_template_context(c)
65 65
66 66 # permission check inside
67 67 @LoginRequired()
68 68 @NotAnonymous()
69 69 def user_groups_list_data(self):
70 70 self.load_default_context()
71 71 column_map = {
72 72 'active': 'users_group_active',
73 73 'description': 'user_group_description',
74 74 'members': 'members_total',
75 75 'owner': 'user_username',
76 76 'sync': 'group_data'
77 77 }
78 78 draw, start, limit = self._extract_chunk(self.request)
79 79 search_q, order_by, order_dir = self._extract_ordering(
80 80 self.request, column_map=column_map)
81 81
82 82 _render = self.request.get_partial_renderer(
83 83 'rhodecode:templates/data_table/_dt_elements.mako')
84 84
85 85 def user_group_name(user_group_name):
86 86 return _render("user_group_name", user_group_name)
87 87
88 88 def user_group_actions(user_group_id, user_group_name):
89 89 return _render("user_group_actions", user_group_id, user_group_name)
90 90
91 91 def user_profile(username):
92 92 return _render('user_profile', username)
93 93
94 94 _perms = ['usergroup.admin']
95 95 allowed_ids = [-1] + self._rhodecode_user.user_group_acl_ids_from_stack(_perms)
96 96
97 97 user_groups_data_total_count = UserGroup.query()\
98 98 .filter(or_(
99 99 # generate multiple IN to fix limitation problems
100 100 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
101 101 ))\
102 102 .count()
103 103
104 104 user_groups_data_total_inactive_count = UserGroup.query()\
105 105 .filter(or_(
106 106 # generate multiple IN to fix limitation problems
107 107 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
108 108 ))\
109 109 .filter(UserGroup.users_group_active != true()).count()
110 110
111 111 member_count = count(UserGroupMember.user_id)
112 112 base_q = Session.query(
113 113 UserGroup.users_group_name,
114 114 UserGroup.user_group_description,
115 115 UserGroup.users_group_active,
116 116 UserGroup.users_group_id,
117 117 UserGroup.group_data,
118 118 User,
119 119 member_count.label('member_count')
120 120 ) \
121 121 .filter(or_(
122 122 # generate multiple IN to fix limitation problems
123 123 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
124 124 )) \
125 125 .outerjoin(UserGroupMember, UserGroupMember.users_group_id == UserGroup.users_group_id) \
126 126 .join(User, User.user_id == UserGroup.user_id) \
127 127 .group_by(UserGroup, User)
128 128
129 129 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
130 130
131 131 if search_q:
132 132 like_expression = u'%{}%'.format(safe_unicode(search_q))
133 133 base_q = base_q.filter(or_(
134 134 UserGroup.users_group_name.ilike(like_expression),
135 135 ))
136 136 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
137 137
138 138 user_groups_data_total_filtered_count = base_q.count()
139 139 user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
140 140
141 141 sort_defined = False
142 142 if order_by == 'members_total':
143 143 sort_col = member_count
144 144 sort_defined = True
145 145 elif order_by == 'user_username':
146 146 sort_col = User.username
147 147 else:
148 148 sort_col = getattr(UserGroup, order_by, None)
149 149
150 150 if sort_defined or sort_col:
151 151 if order_dir == 'asc':
152 152 sort_col = sort_col.asc()
153 153 else:
154 154 sort_col = sort_col.desc()
155 155
156 156 base_q = base_q.order_by(sort_col)
157 157 base_q = base_q.offset(start).limit(limit)
158 158
159 159 # authenticated access to user groups
160 160 auth_user_group_list = base_q.all()
161 161
162 162 user_groups_data = []
163 163 for user_gr in auth_user_group_list:
164 164 row = {
165 165 "users_group_name": user_group_name(user_gr.users_group_name),
166 166 "description": h.escape(user_gr.user_group_description),
167 167 "members": user_gr.member_count,
168 168 # NOTE(marcink): because of advanced query we
169 169 # need to load it like that
170 170 "sync": UserGroup._load_sync(
171 171 UserGroup._load_group_data(user_gr.group_data)),
172 172 "active": h.bool2icon(user_gr.users_group_active),
173 173 "owner": user_profile(user_gr.User.username),
174 174 "action": user_group_actions(
175 175 user_gr.users_group_id, user_gr.users_group_name)
176 176 }
177 177 user_groups_data.append(row)
178 178
179 179 data = ({
180 180 'draw': draw,
181 181 'data': user_groups_data,
182 182 'recordsTotal': user_groups_data_total_count,
183 183 'recordsTotalInactive': user_groups_data_total_inactive_count,
184 184 'recordsFiltered': user_groups_data_total_filtered_count,
185 185 'recordsFilteredInactive': user_groups_data_total_filtered_inactive_count,
186 186 })
187 187
188 188 return data
189 189
190 190 @LoginRequired()
191 191 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
192 192 def user_groups_new(self):
193 193 c = self.load_default_context()
194 194 return self._get_template_context(c)
195 195
196 196 @LoginRequired()
197 197 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
198 198 @CSRFRequired()
199 199 def user_groups_create(self):
200 200 _ = self.request.translate
201 201 c = self.load_default_context()
202 202 users_group_form = UserGroupForm(self.request.translate)()
203 203
204 204 user_group_name = self.request.POST.get('users_group_name')
205 205 try:
206 206 form_result = users_group_form.to_python(dict(self.request.POST))
207 207 user_group = UserGroupModel().create(
208 208 name=form_result['users_group_name'],
209 209 description=form_result['user_group_description'],
210 210 owner=self._rhodecode_user.user_id,
211 211 active=form_result['users_group_active'])
212 212 Session().flush()
213 213 creation_data = user_group.get_api_data()
214 214 user_group_name = form_result['users_group_name']
215 215
216 216 audit_logger.store_web(
217 217 'user_group.create', action_data={'data': creation_data},
218 218 user=self._rhodecode_user)
219 219
220 220 user_group_link = h.link_to(
221 221 h.escape(user_group_name),
222 222 h.route_path(
223 223 'edit_user_group', user_group_id=user_group.users_group_id))
224 224 h.flash(h.literal(_('Created user group %(user_group_link)s')
225 225 % {'user_group_link': user_group_link}),
226 226 category='success')
227 227 Session().commit()
228 228 user_group_id = user_group.users_group_id
229 229 except formencode.Invalid as errors:
230 230
231 231 data = render(
232 232 'rhodecode:templates/admin/user_groups/user_group_add.mako',
233 233 self._get_template_context(c), self.request)
234 234 html = formencode.htmlfill.render(
235 235 data,
236 236 defaults=errors.value,
237 237 errors=errors.unpack_errors() or {},
238 238 prefix_error=False,
239 239 encoding="UTF-8",
240 240 force_defaults=False
241 241 )
242 242 return Response(html)
243 243
244 244 except Exception:
245 245 log.exception("Exception creating user group")
246 246 h.flash(_('Error occurred during creation of user group %s') \
247 247 % user_group_name, category='error')
248 248 raise HTTPFound(h.route_path('user_groups_new'))
249 249
250 250 PermissionModel().trigger_permission_flush()
251 251
252 252 raise HTTPFound(
253 253 h.route_path('edit_user_group', user_group_id=user_group_id))
@@ -1,1322 +1,1322 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
34 34 from rhodecode.authentication.plugins import auth_rhodecode
35 35 from rhodecode.events import trigger
36 36 from rhodecode.model.db import true, UserNotice
37 37
38 38 from rhodecode.lib import audit_logger, rc_cache, auth
39 39 from rhodecode.lib.exceptions import (
40 40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
42 42 UserOwnsArtifactsException, DefaultUserException)
43 43 from rhodecode.lib import ext_json
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 46 from rhodecode.lib import helpers as h
47 47 from rhodecode.lib.helpers import SqlPage
48 48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
49 49 from rhodecode.model.auth_token import AuthTokenModel
50 50 from rhodecode.model.forms import (
51 51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 52 UserExtraEmailForm, UserExtraIpForm)
53 53 from rhodecode.model.permission import PermissionModel
54 54 from rhodecode.model.repo_group import RepoGroupModel
55 55 from rhodecode.model.ssh_key import SshKeyModel
56 56 from rhodecode.model.user import UserModel
57 57 from rhodecode.model.user_group import UserGroupModel
58 58 from rhodecode.model.db import (
59 59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 60 UserApiKeys, UserSshKeys, RepoGroup)
61 61 from rhodecode.model.meta import Session
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 class AdminUsersView(BaseAppView, DataGridAppView):
67 67
68 68 def load_default_context(self):
69 69 c = self._get_local_tmpl_context()
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @HasPermissionAllDecorator('hg.admin')
74 74 def users_list(self):
75 75 c = self.load_default_context()
76 76 return self._get_template_context(c)
77 77
78 78 @LoginRequired()
79 79 @HasPermissionAllDecorator('hg.admin')
80 80 def users_list_data(self):
81 81 self.load_default_context()
82 82 column_map = {
83 83 'first_name': 'name',
84 84 'last_name': 'lastname',
85 85 }
86 86 draw, start, limit = self._extract_chunk(self.request)
87 87 search_q, order_by, order_dir = self._extract_ordering(
88 88 self.request, column_map=column_map)
89 89 _render = self.request.get_partial_renderer(
90 90 'rhodecode:templates/data_table/_dt_elements.mako')
91 91
92 92 def user_actions(user_id, username):
93 93 return _render("user_actions", user_id, username)
94 94
95 95 users_data_total_count = User.query()\
96 96 .filter(User.username != User.DEFAULT_USER) \
97 97 .count()
98 98
99 99 users_data_total_inactive_count = User.query()\
100 100 .filter(User.username != User.DEFAULT_USER) \
101 101 .filter(User.active != true())\
102 102 .count()
103 103
104 104 # json generate
105 105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
106 106 base_inactive_q = base_q.filter(User.active != true())
107 107
108 108 if search_q:
109 109 like_expression = '%{}%'.format(safe_unicode(search_q))
110 110 base_q = base_q.filter(or_(
111 111 User.username.ilike(like_expression),
112 112 User._email.ilike(like_expression),
113 113 User.name.ilike(like_expression),
114 114 User.lastname.ilike(like_expression),
115 115 ))
116 116 base_inactive_q = base_q.filter(User.active != true())
117 117
118 118 users_data_total_filtered_count = base_q.count()
119 119 users_data_total_filtered_inactive_count = base_inactive_q.count()
120 120
121 121 sort_col = getattr(User, order_by, None)
122 122 if sort_col:
123 123 if order_dir == 'asc':
124 124 # handle null values properly to order by NULL last
125 125 if order_by in ['last_activity']:
126 126 sort_col = coalesce(sort_col, datetime.date.max)
127 127 sort_col = sort_col.asc()
128 128 else:
129 129 # handle null values properly to order by NULL last
130 130 if order_by in ['last_activity']:
131 131 sort_col = coalesce(sort_col, datetime.date.min)
132 132 sort_col = sort_col.desc()
133 133
134 134 base_q = base_q.order_by(sort_col)
135 135 base_q = base_q.offset(start).limit(limit)
136 136
137 137 users_list = base_q.all()
138 138
139 139 users_data = []
140 140 for user in users_list:
141 141 users_data.append({
142 142 "username": h.gravatar_with_user(self.request, user.username),
143 143 "email": user.email,
144 144 "first_name": user.first_name,
145 145 "last_name": user.last_name,
146 146 "last_login": h.format_date(user.last_login),
147 147 "last_activity": h.format_date(user.last_activity),
148 148 "active": h.bool2icon(user.active),
149 149 "active_raw": user.active,
150 150 "admin": h.bool2icon(user.admin),
151 151 "extern_type": user.extern_type,
152 152 "extern_name": user.extern_name,
153 153 "action": user_actions(user.user_id, user.username),
154 154 })
155 155 data = ({
156 156 'draw': draw,
157 157 'data': users_data,
158 158 'recordsTotal': users_data_total_count,
159 159 'recordsFiltered': users_data_total_filtered_count,
160 160 'recordsTotalInactive': users_data_total_inactive_count,
161 161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
162 162 })
163 163
164 164 return data
165 165
166 166 def _set_personal_repo_group_template_vars(self, c_obj):
167 167 DummyUser = AttributeDict({
168 168 'username': '${username}',
169 169 'user_id': '${user_id}',
170 170 })
171 171 c_obj.default_create_repo_group = RepoGroupModel() \
172 172 .get_default_create_personal_repo_group()
173 173 c_obj.personal_repo_group_name = RepoGroupModel() \
174 174 .get_personal_group_name(DummyUser)
175 175
176 176 @LoginRequired()
177 177 @HasPermissionAllDecorator('hg.admin')
178 178 def users_new(self):
179 179 _ = self.request.translate
180 180 c = self.load_default_context()
181 181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
182 182 self._set_personal_repo_group_template_vars(c)
183 183 return self._get_template_context(c)
184 184
185 185 @LoginRequired()
186 186 @HasPermissionAllDecorator('hg.admin')
187 187 @CSRFRequired()
188 188 def users_create(self):
189 189 _ = self.request.translate
190 190 c = self.load_default_context()
191 191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 192 user_model = UserModel()
193 193 user_form = UserForm(self.request.translate)()
194 194 try:
195 195 form_result = user_form.to_python(dict(self.request.POST))
196 196 user = user_model.create(form_result)
197 197 Session().flush()
198 198 creation_data = user.get_api_data()
199 199 username = form_result['username']
200 200
201 201 audit_logger.store_web(
202 202 'user.create', action_data={'data': creation_data},
203 203 user=c.rhodecode_user)
204 204
205 205 user_link = h.link_to(
206 206 h.escape(username),
207 207 h.route_path('user_edit', user_id=user.user_id))
208 208 h.flash(h.literal(_('Created user %(user_link)s')
209 209 % {'user_link': user_link}), category='success')
210 210 Session().commit()
211 211 except formencode.Invalid as errors:
212 212 self._set_personal_repo_group_template_vars(c)
213 213 data = render(
214 214 'rhodecode:templates/admin/users/user_add.mako',
215 215 self._get_template_context(c), self.request)
216 216 html = formencode.htmlfill.render(
217 217 data,
218 218 defaults=errors.value,
219 219 errors=errors.unpack_errors() or {},
220 220 prefix_error=False,
221 221 encoding="UTF-8",
222 222 force_defaults=False
223 223 )
224 224 return Response(html)
225 225 except UserCreationError as e:
226 226 h.flash(safe_unicode(e), 'error')
227 227 except Exception:
228 228 log.exception("Exception creation of user")
229 229 h.flash(_('Error occurred during creation of user %s')
230 230 % self.request.POST.get('username'), category='error')
231 231 raise HTTPFound(h.route_path('users'))
232 232
233 233
234 234 class UsersView(UserAppView):
235 235 ALLOW_SCOPED_TOKENS = False
236 236 """
237 237 This view has alternative version inside EE, if modified please take a look
238 238 in there as well.
239 239 """
240 240
241 241 def get_auth_plugins(self):
242 242 valid_plugins = []
243 243 authn_registry = get_authn_registry(self.request.registry)
244 244 for plugin in authn_registry.get_plugins_for_authentication():
245 245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
246 246 valid_plugins.append(plugin)
247 247 elif plugin.name == 'rhodecode':
248 248 valid_plugins.append(plugin)
249 249
250 250 # extend our choices if user has set a bound plugin which isn't enabled at the
251 251 # moment
252 252 extern_type = self.db_user.extern_type
253 253 if extern_type not in [x.uid for x in valid_plugins]:
254 254 try:
255 255 plugin = authn_registry.get_plugin_by_uid(extern_type)
256 256 if plugin:
257 257 valid_plugins.append(plugin)
258 258
259 259 except Exception:
260 260 log.exception(
261 261 'Could not extend user plugins with `{}`'.format(extern_type))
262 262 return valid_plugins
263 263
264 264 def load_default_context(self):
265 265 req = self.request
266 266
267 267 c = self._get_local_tmpl_context()
268 268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
269 269 c.allowed_languages = [
270 270 ('en', 'English (en)'),
271 271 ('de', 'German (de)'),
272 272 ('fr', 'French (fr)'),
273 273 ('it', 'Italian (it)'),
274 274 ('ja', 'Japanese (ja)'),
275 275 ('pl', 'Polish (pl)'),
276 276 ('pt', 'Portuguese (pt)'),
277 277 ('ru', 'Russian (ru)'),
278 278 ('zh', 'Chinese (zh)'),
279 279 ]
280 280
281 281 c.allowed_extern_types = [
282 282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
283 283 ]
284 284 perms = req.registry.settings.get('available_permissions')
285 285 if not perms:
286 286 # inject info about available permissions
287 287 auth.set_available_permissions(req.registry.settings)
288 288
289 289 c.available_permissions = req.registry.settings['available_permissions']
290 290 PermissionModel().set_global_permission_choices(
291 291 c, gettext_translator=req.translate)
292 292
293 293 return c
294 294
295 295 @LoginRequired()
296 296 @HasPermissionAllDecorator('hg.admin')
297 297 @CSRFRequired()
298 298 def user_update(self):
299 299 _ = self.request.translate
300 300 c = self.load_default_context()
301 301
302 302 user_id = self.db_user_id
303 303 c.user = self.db_user
304 304
305 305 c.active = 'profile'
306 306 c.extern_type = c.user.extern_type
307 307 c.extern_name = c.user.extern_name
308 308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
309 309 available_languages = [x[0] for x in c.allowed_languages]
310 310 _form = UserForm(self.request.translate, edit=True,
311 311 available_languages=available_languages,
312 312 old_data={'user_id': user_id,
313 313 'email': c.user.email})()
314 314
315 315 c.edit_mode = self.request.POST.get('edit') == '1'
316 316 form_result = {}
317 317 old_values = c.user.get_api_data()
318 318 try:
319 319 form_result = _form.to_python(dict(self.request.POST))
320 320 skip_attrs = ['extern_name']
321 321 # TODO: plugin should define if username can be updated
322 322
323 323 if c.extern_type != "rhodecode" and not c.edit_mode:
324 324 # forbid updating username for external accounts
325 325 skip_attrs.append('username')
326 326
327 327 UserModel().update_user(
328 328 user_id, skip_attrs=skip_attrs, **form_result)
329 329
330 330 audit_logger.store_web(
331 331 'user.edit', action_data={'old_data': old_values},
332 332 user=c.rhodecode_user)
333 333
334 334 Session().commit()
335 335 h.flash(_('User updated successfully'), category='success')
336 336 except formencode.Invalid as errors:
337 337 data = render(
338 338 'rhodecode:templates/admin/users/user_edit.mako',
339 339 self._get_template_context(c), self.request)
340 340 html = formencode.htmlfill.render(
341 341 data,
342 342 defaults=errors.value,
343 343 errors=errors.unpack_errors() or {},
344 344 prefix_error=False,
345 345 encoding="UTF-8",
346 346 force_defaults=False
347 347 )
348 348 return Response(html)
349 349 except UserCreationError as e:
350 350 h.flash(safe_unicode(e), 'error')
351 351 except Exception:
352 352 log.exception("Exception updating user")
353 353 h.flash(_('Error occurred during update of user %s')
354 354 % form_result.get('username'), category='error')
355 355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
356 356
357 357 @LoginRequired()
358 358 @HasPermissionAllDecorator('hg.admin')
359 359 @CSRFRequired()
360 360 def user_delete(self):
361 361 _ = self.request.translate
362 362 c = self.load_default_context()
363 363 c.user = self.db_user
364 364
365 365 _repos = c.user.repositories
366 366 _repo_groups = c.user.repository_groups
367 367 _user_groups = c.user.user_groups
368 368 _pull_requests = c.user.user_pull_requests
369 369 _artifacts = c.user.artifacts
370 370
371 371 handle_repos = None
372 372 handle_repo_groups = None
373 373 handle_user_groups = None
374 374 handle_pull_requests = None
375 375 handle_artifacts = None
376 376
377 377 # calls for flash of handle based on handle case detach or delete
378 378 def set_handle_flash_repos():
379 379 handle = handle_repos
380 380 if handle == 'detach':
381 381 h.flash(_('Detached %s repositories') % len(_repos),
382 382 category='success')
383 383 elif handle == 'delete':
384 384 h.flash(_('Deleted %s repositories') % len(_repos),
385 385 category='success')
386 386
387 387 def set_handle_flash_repo_groups():
388 388 handle = handle_repo_groups
389 389 if handle == 'detach':
390 390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
391 391 category='success')
392 392 elif handle == 'delete':
393 393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
394 394 category='success')
395 395
396 396 def set_handle_flash_user_groups():
397 397 handle = handle_user_groups
398 398 if handle == 'detach':
399 399 h.flash(_('Detached %s user groups') % len(_user_groups),
400 400 category='success')
401 401 elif handle == 'delete':
402 402 h.flash(_('Deleted %s user groups') % len(_user_groups),
403 403 category='success')
404 404
405 405 def set_handle_flash_pull_requests():
406 406 handle = handle_pull_requests
407 407 if handle == 'detach':
408 408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
409 409 category='success')
410 410 elif handle == 'delete':
411 411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
412 412 category='success')
413 413
414 414 def set_handle_flash_artifacts():
415 415 handle = handle_artifacts
416 416 if handle == 'detach':
417 417 h.flash(_('Detached %s artifacts') % len(_artifacts),
418 418 category='success')
419 419 elif handle == 'delete':
420 420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
421 421 category='success')
422 422
423 423 handle_user = User.get_first_super_admin()
424 424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
425 425 if handle_user_id:
426 426 # NOTE(marcink): we get new owner for objects...
427 427 handle_user = User.get_or_404(handle_user_id)
428 428
429 429 if _repos and self.request.POST.get('user_repos'):
430 430 handle_repos = self.request.POST['user_repos']
431 431
432 432 if _repo_groups and self.request.POST.get('user_repo_groups'):
433 433 handle_repo_groups = self.request.POST['user_repo_groups']
434 434
435 435 if _user_groups and self.request.POST.get('user_user_groups'):
436 436 handle_user_groups = self.request.POST['user_user_groups']
437 437
438 438 if _pull_requests and self.request.POST.get('user_pull_requests'):
439 439 handle_pull_requests = self.request.POST['user_pull_requests']
440 440
441 441 if _artifacts and self.request.POST.get('user_artifacts'):
442 442 handle_artifacts = self.request.POST['user_artifacts']
443 443
444 444 old_values = c.user.get_api_data()
445 445
446 446 try:
447 447
448 448 UserModel().delete(
449 449 c.user,
450 450 handle_repos=handle_repos,
451 451 handle_repo_groups=handle_repo_groups,
452 452 handle_user_groups=handle_user_groups,
453 453 handle_pull_requests=handle_pull_requests,
454 454 handle_artifacts=handle_artifacts,
455 455 handle_new_owner=handle_user
456 456 )
457 457
458 458 audit_logger.store_web(
459 459 'user.delete', action_data={'old_data': old_values},
460 460 user=c.rhodecode_user)
461 461
462 462 Session().commit()
463 463 set_handle_flash_repos()
464 464 set_handle_flash_repo_groups()
465 465 set_handle_flash_user_groups()
466 466 set_handle_flash_pull_requests()
467 467 set_handle_flash_artifacts()
468 468 username = h.escape(old_values['username'])
469 469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
470 470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
471 471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
472 472 UserOwnsArtifactsException, DefaultUserException) as e:
473 473 h.flash(e, category='warning')
474 474 except Exception:
475 475 log.exception("Exception during deletion of user")
476 476 h.flash(_('An error occurred during deletion of user'),
477 477 category='error')
478 478 raise HTTPFound(h.route_path('users'))
479 479
480 480 @LoginRequired()
481 481 @HasPermissionAllDecorator('hg.admin')
482 482 def user_edit(self):
483 483 _ = self.request.translate
484 484 c = self.load_default_context()
485 485 c.user = self.db_user
486 486
487 487 c.active = 'profile'
488 488 c.extern_type = c.user.extern_type
489 489 c.extern_name = c.user.extern_name
490 490 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
491 491 c.edit_mode = self.request.GET.get('edit') == '1'
492 492
493 493 defaults = c.user.get_dict()
494 494 defaults.update({'language': c.user.user_data.get('language')})
495 495
496 496 data = render(
497 497 'rhodecode:templates/admin/users/user_edit.mako',
498 498 self._get_template_context(c), self.request)
499 499 html = formencode.htmlfill.render(
500 500 data,
501 501 defaults=defaults,
502 502 encoding="UTF-8",
503 503 force_defaults=False
504 504 )
505 505 return Response(html)
506 506
507 507 @LoginRequired()
508 508 @HasPermissionAllDecorator('hg.admin')
509 509 def user_edit_advanced(self):
510 510 _ = self.request.translate
511 511 c = self.load_default_context()
512 512
513 513 user_id = self.db_user_id
514 514 c.user = self.db_user
515 515
516 516 c.detach_user = User.get_first_super_admin()
517 517 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
518 518 if detach_user_id:
519 519 c.detach_user = User.get_or_404(detach_user_id)
520 520
521 521 c.active = 'advanced'
522 522 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
523 523 c.personal_repo_group_name = RepoGroupModel()\
524 524 .get_personal_group_name(c.user)
525 525
526 526 c.user_to_review_rules = sorted(
527 527 (x.user for x in c.user.user_review_rules),
528 528 key=lambda u: u.username.lower())
529 529
530 530 defaults = c.user.get_dict()
531 531
532 532 # Interim workaround if the user participated on any pull requests as a
533 533 # reviewer.
534 534 has_review = len(c.user.reviewer_pull_requests)
535 535 c.can_delete_user = not has_review
536 536 c.can_delete_user_message = ''
537 537 inactive_link = h.link_to(
538 538 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
539 539 if has_review == 1:
540 540 c.can_delete_user_message = h.literal(_(
541 541 'The user participates as reviewer in {} pull request and '
542 542 'cannot be deleted. \nYou can set the user to '
543 543 '"{}" instead of deleting it.').format(
544 544 has_review, inactive_link))
545 545 elif has_review:
546 546 c.can_delete_user_message = h.literal(_(
547 547 'The user participates as reviewer in {} pull requests and '
548 548 'cannot be deleted. \nYou can set the user to '
549 549 '"{}" instead of deleting it.').format(
550 550 has_review, inactive_link))
551 551
552 552 data = render(
553 553 'rhodecode:templates/admin/users/user_edit.mako',
554 554 self._get_template_context(c), self.request)
555 555 html = formencode.htmlfill.render(
556 556 data,
557 557 defaults=defaults,
558 558 encoding="UTF-8",
559 559 force_defaults=False
560 560 )
561 561 return Response(html)
562 562
563 563 @LoginRequired()
564 564 @HasPermissionAllDecorator('hg.admin')
565 565 def user_edit_global_perms(self):
566 566 _ = self.request.translate
567 567 c = self.load_default_context()
568 568 c.user = self.db_user
569 569
570 570 c.active = 'global_perms'
571 571
572 572 c.default_user = User.get_default_user()
573 573 defaults = c.user.get_dict()
574 574 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
575 575 defaults.update(c.default_user.get_default_perms())
576 576 defaults.update(c.user.get_default_perms())
577 577
578 578 data = render(
579 579 'rhodecode:templates/admin/users/user_edit.mako',
580 580 self._get_template_context(c), self.request)
581 581 html = formencode.htmlfill.render(
582 582 data,
583 583 defaults=defaults,
584 584 encoding="UTF-8",
585 585 force_defaults=False
586 586 )
587 587 return Response(html)
588 588
589 589 @LoginRequired()
590 590 @HasPermissionAllDecorator('hg.admin')
591 591 @CSRFRequired()
592 592 def user_edit_global_perms_update(self):
593 593 _ = self.request.translate
594 594 c = self.load_default_context()
595 595
596 596 user_id = self.db_user_id
597 597 c.user = self.db_user
598 598
599 599 c.active = 'global_perms'
600 600 try:
601 601 # first stage that verifies the checkbox
602 602 _form = UserIndividualPermissionsForm(self.request.translate)
603 603 form_result = _form.to_python(dict(self.request.POST))
604 604 inherit_perms = form_result['inherit_default_permissions']
605 605 c.user.inherit_default_permissions = inherit_perms
606 606 Session().add(c.user)
607 607
608 608 if not inherit_perms:
609 609 # only update the individual ones if we un check the flag
610 610 _form = UserPermissionsForm(
611 611 self.request.translate,
612 612 [x[0] for x in c.repo_create_choices],
613 613 [x[0] for x in c.repo_create_on_write_choices],
614 614 [x[0] for x in c.repo_group_create_choices],
615 615 [x[0] for x in c.user_group_create_choices],
616 616 [x[0] for x in c.fork_choices],
617 617 [x[0] for x in c.inherit_default_permission_choices])()
618 618
619 619 form_result = _form.to_python(dict(self.request.POST))
620 620 form_result.update({'perm_user_id': c.user.user_id})
621 621
622 622 PermissionModel().update_user_permissions(form_result)
623 623
624 624 # TODO(marcink): implement global permissions
625 625 # audit_log.store_web('user.edit.permissions')
626 626
627 627 Session().commit()
628 628
629 629 h.flash(_('User global permissions updated successfully'),
630 630 category='success')
631 631
632 632 except formencode.Invalid as errors:
633 633 data = render(
634 634 'rhodecode:templates/admin/users/user_edit.mako',
635 635 self._get_template_context(c), self.request)
636 636 html = formencode.htmlfill.render(
637 637 data,
638 638 defaults=errors.value,
639 639 errors=errors.unpack_errors() or {},
640 640 prefix_error=False,
641 641 encoding="UTF-8",
642 642 force_defaults=False
643 643 )
644 644 return Response(html)
645 645 except Exception:
646 646 log.exception("Exception during permissions saving")
647 647 h.flash(_('An error occurred during permissions saving'),
648 648 category='error')
649 649
650 650 affected_user_ids = [user_id]
651 651 PermissionModel().trigger_permission_flush(affected_user_ids)
652 652 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
653 653
654 654 @LoginRequired()
655 655 @HasPermissionAllDecorator('hg.admin')
656 656 @CSRFRequired()
657 657 def user_enable_force_password_reset(self):
658 658 _ = self.request.translate
659 659 c = self.load_default_context()
660 660
661 661 user_id = self.db_user_id
662 662 c.user = self.db_user
663 663
664 664 try:
665 665 c.user.update_userdata(force_password_change=True)
666 666
667 667 msg = _('Force password change enabled for user')
668 668 audit_logger.store_web('user.edit.password_reset.enabled',
669 669 user=c.rhodecode_user)
670 670
671 671 Session().commit()
672 672 h.flash(msg, category='success')
673 673 except Exception:
674 674 log.exception("Exception during password reset for user")
675 675 h.flash(_('An error occurred during password reset for user'),
676 676 category='error')
677 677
678 678 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
679 679
680 680 @LoginRequired()
681 681 @HasPermissionAllDecorator('hg.admin')
682 682 @CSRFRequired()
683 683 def user_disable_force_password_reset(self):
684 684 _ = self.request.translate
685 685 c = self.load_default_context()
686 686
687 687 user_id = self.db_user_id
688 688 c.user = self.db_user
689 689
690 690 try:
691 691 c.user.update_userdata(force_password_change=False)
692 692
693 693 msg = _('Force password change disabled for user')
694 694 audit_logger.store_web(
695 695 'user.edit.password_reset.disabled',
696 696 user=c.rhodecode_user)
697 697
698 698 Session().commit()
699 699 h.flash(msg, category='success')
700 700 except Exception:
701 701 log.exception("Exception during password reset for user")
702 702 h.flash(_('An error occurred during password reset for user'),
703 703 category='error')
704 704
705 705 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
706 706
707 707 @LoginRequired()
708 708 @HasPermissionAllDecorator('hg.admin')
709 709 @CSRFRequired()
710 710 def user_notice_dismiss(self):
711 711 _ = self.request.translate
712 712 c = self.load_default_context()
713 713
714 714 user_id = self.db_user_id
715 715 c.user = self.db_user
716 716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
717 717 notice = UserNotice().query()\
718 718 .filter(UserNotice.user_id == user_id)\
719 719 .filter(UserNotice.user_notice_id == user_notice_id)\
720 720 .scalar()
721 721 read = False
722 722 if notice:
723 723 notice.notice_read = True
724 724 Session().add(notice)
725 725 Session().commit()
726 726 read = True
727 727
728 728 return {'notice': user_notice_id, 'read': read}
729 729
730 730 @LoginRequired()
731 731 @HasPermissionAllDecorator('hg.admin')
732 732 @CSRFRequired()
733 733 def user_create_personal_repo_group(self):
734 734 """
735 735 Create personal repository group for this user
736 736 """
737 737 from rhodecode.model.repo_group import RepoGroupModel
738 738
739 739 _ = self.request.translate
740 740 c = self.load_default_context()
741 741
742 742 user_id = self.db_user_id
743 743 c.user = self.db_user
744 744
745 745 personal_repo_group = RepoGroup.get_user_personal_repo_group(
746 746 c.user.user_id)
747 747 if personal_repo_group:
748 748 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
749 749
750 750 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
751 751 named_personal_group = RepoGroup.get_by_group_name(
752 752 personal_repo_group_name)
753 753 try:
754 754
755 755 if named_personal_group and named_personal_group.user_id == c.user.user_id:
756 756 # migrate the same named group, and mark it as personal
757 757 named_personal_group.personal = True
758 758 Session().add(named_personal_group)
759 759 Session().commit()
760 760 msg = _('Linked repository group `%s` as personal' % (
761 761 personal_repo_group_name,))
762 762 h.flash(msg, category='success')
763 763 elif not named_personal_group:
764 764 RepoGroupModel().create_personal_repo_group(c.user)
765 765
766 766 msg = _('Created repository group `%s`' % (
767 767 personal_repo_group_name,))
768 768 h.flash(msg, category='success')
769 769 else:
770 770 msg = _('Repository group `%s` is already taken' % (
771 771 personal_repo_group_name,))
772 772 h.flash(msg, category='warning')
773 773 except Exception:
774 774 log.exception("Exception during repository group creation")
775 775 msg = _(
776 776 'An error occurred during repository group creation for user')
777 777 h.flash(msg, category='error')
778 778 Session().rollback()
779 779
780 780 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
781 781
782 782 @LoginRequired()
783 783 @HasPermissionAllDecorator('hg.admin')
784 784 def auth_tokens(self):
785 785 _ = self.request.translate
786 786 c = self.load_default_context()
787 787 c.user = self.db_user
788 788
789 789 c.active = 'auth_tokens'
790 790
791 791 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
792 792 c.role_values = [
793 793 (x, AuthTokenModel.cls._get_role_name(x))
794 794 for x in AuthTokenModel.cls.ROLES]
795 795 c.role_options = [(c.role_values, _("Role"))]
796 796 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
797 797 c.user.user_id, show_expired=True)
798 798 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
799 799 return self._get_template_context(c)
800 800
801 801 @LoginRequired()
802 802 @HasPermissionAllDecorator('hg.admin')
803 803 def auth_tokens_view(self):
804 804 _ = self.request.translate
805 805 c = self.load_default_context()
806 806 c.user = self.db_user
807 807
808 808 auth_token_id = self.request.POST.get('auth_token_id')
809 809
810 810 if auth_token_id:
811 811 token = UserApiKeys.get_or_404(auth_token_id)
812 812
813 813 return {
814 814 'auth_token': token.api_key
815 815 }
816 816
817 817 def maybe_attach_token_scope(self, token):
818 818 # implemented in EE edition
819 819 pass
820 820
821 821 @LoginRequired()
822 822 @HasPermissionAllDecorator('hg.admin')
823 823 @CSRFRequired()
824 824 def auth_tokens_add(self):
825 825 _ = self.request.translate
826 826 c = self.load_default_context()
827 827
828 828 user_id = self.db_user_id
829 829 c.user = self.db_user
830 830
831 831 user_data = c.user.get_api_data()
832 832 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
833 833 description = self.request.POST.get('description')
834 834 role = self.request.POST.get('role')
835 835
836 836 token = UserModel().add_auth_token(
837 837 user=c.user.user_id,
838 838 lifetime_minutes=lifetime, role=role, description=description,
839 839 scope_callback=self.maybe_attach_token_scope)
840 840 token_data = token.get_api_data()
841 841
842 842 audit_logger.store_web(
843 843 'user.edit.token.add', action_data={
844 844 'data': {'token': token_data, 'user': user_data}},
845 845 user=self._rhodecode_user, )
846 846 Session().commit()
847 847
848 848 h.flash(_("Auth token successfully created"), category='success')
849 849 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
850 850
851 851 @LoginRequired()
852 852 @HasPermissionAllDecorator('hg.admin')
853 853 @CSRFRequired()
854 854 def auth_tokens_delete(self):
855 855 _ = self.request.translate
856 856 c = self.load_default_context()
857 857
858 858 user_id = self.db_user_id
859 859 c.user = self.db_user
860 860
861 861 user_data = c.user.get_api_data()
862 862
863 863 del_auth_token = self.request.POST.get('del_auth_token')
864 864
865 865 if del_auth_token:
866 866 token = UserApiKeys.get_or_404(del_auth_token)
867 867 token_data = token.get_api_data()
868 868
869 869 AuthTokenModel().delete(del_auth_token, c.user.user_id)
870 870 audit_logger.store_web(
871 871 'user.edit.token.delete', action_data={
872 872 'data': {'token': token_data, 'user': user_data}},
873 873 user=self._rhodecode_user,)
874 874 Session().commit()
875 875 h.flash(_("Auth token successfully deleted"), category='success')
876 876
877 877 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
878 878
879 879 @LoginRequired()
880 880 @HasPermissionAllDecorator('hg.admin')
881 881 def ssh_keys(self):
882 882 _ = self.request.translate
883 883 c = self.load_default_context()
884 884 c.user = self.db_user
885 885
886 886 c.active = 'ssh_keys'
887 887 c.default_key = self.request.GET.get('default_key')
888 888 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
889 889 return self._get_template_context(c)
890 890
891 891 @LoginRequired()
892 892 @HasPermissionAllDecorator('hg.admin')
893 893 def ssh_keys_generate_keypair(self):
894 894 _ = self.request.translate
895 895 c = self.load_default_context()
896 896
897 897 c.user = self.db_user
898 898
899 899 c.active = 'ssh_keys_generate'
900 900 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
901 901 private_format = self.request.GET.get('private_format') \
902 902 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
903 903 c.private, c.public = SshKeyModel().generate_keypair(
904 904 comment=comment, private_format=private_format)
905 905
906 906 return self._get_template_context(c)
907 907
908 908 @LoginRequired()
909 909 @HasPermissionAllDecorator('hg.admin')
910 910 @CSRFRequired()
911 911 def ssh_keys_add(self):
912 912 _ = self.request.translate
913 913 c = self.load_default_context()
914 914
915 915 user_id = self.db_user_id
916 916 c.user = self.db_user
917 917
918 918 user_data = c.user.get_api_data()
919 919 key_data = self.request.POST.get('key_data')
920 920 description = self.request.POST.get('description')
921 921
922 922 fingerprint = 'unknown'
923 923 try:
924 924 if not key_data:
925 925 raise ValueError('Please add a valid public key')
926 926
927 927 key = SshKeyModel().parse_key(key_data.strip())
928 928 fingerprint = key.hash_md5()
929 929
930 930 ssh_key = SshKeyModel().create(
931 931 c.user.user_id, fingerprint, key.keydata, description)
932 932 ssh_key_data = ssh_key.get_api_data()
933 933
934 934 audit_logger.store_web(
935 935 'user.edit.ssh_key.add', action_data={
936 936 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
937 937 user=self._rhodecode_user, )
938 938 Session().commit()
939 939
940 940 # Trigger an event on change of keys.
941 941 trigger(SshKeyFileChangeEvent(), self.request.registry)
942 942
943 943 h.flash(_("Ssh Key successfully created"), category='success')
944 944
945 945 except IntegrityError:
946 946 log.exception("Exception during ssh key saving")
947 947 err = 'Such key with fingerprint `{}` already exists, ' \
948 948 'please use a different one'.format(fingerprint)
949 949 h.flash(_('An error occurred during ssh key saving: {}').format(err),
950 950 category='error')
951 951 except Exception as e:
952 952 log.exception("Exception during ssh key saving")
953 953 h.flash(_('An error occurred during ssh key saving: {}').format(e),
954 954 category='error')
955 955
956 956 return HTTPFound(
957 957 h.route_path('edit_user_ssh_keys', user_id=user_id))
958 958
959 959 @LoginRequired()
960 960 @HasPermissionAllDecorator('hg.admin')
961 961 @CSRFRequired()
962 962 def ssh_keys_delete(self):
963 963 _ = self.request.translate
964 964 c = self.load_default_context()
965 965
966 966 user_id = self.db_user_id
967 967 c.user = self.db_user
968 968
969 969 user_data = c.user.get_api_data()
970 970
971 971 del_ssh_key = self.request.POST.get('del_ssh_key')
972 972
973 973 if del_ssh_key:
974 974 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
975 975 ssh_key_data = ssh_key.get_api_data()
976 976
977 977 SshKeyModel().delete(del_ssh_key, c.user.user_id)
978 978 audit_logger.store_web(
979 979 'user.edit.ssh_key.delete', action_data={
980 980 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
981 981 user=self._rhodecode_user,)
982 982 Session().commit()
983 983 # Trigger an event on change of keys.
984 984 trigger(SshKeyFileChangeEvent(), self.request.registry)
985 985 h.flash(_("Ssh key successfully deleted"), category='success')
986 986
987 987 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
988 988
989 989 @LoginRequired()
990 990 @HasPermissionAllDecorator('hg.admin')
991 991 def emails(self):
992 992 _ = self.request.translate
993 993 c = self.load_default_context()
994 994 c.user = self.db_user
995 995
996 996 c.active = 'emails'
997 997 c.user_email_map = UserEmailMap.query() \
998 998 .filter(UserEmailMap.user == c.user).all()
999 999
1000 1000 return self._get_template_context(c)
1001 1001
1002 1002 @LoginRequired()
1003 1003 @HasPermissionAllDecorator('hg.admin')
1004 1004 @CSRFRequired()
1005 1005 def emails_add(self):
1006 1006 _ = self.request.translate
1007 1007 c = self.load_default_context()
1008 1008
1009 1009 user_id = self.db_user_id
1010 1010 c.user = self.db_user
1011 1011
1012 1012 email = self.request.POST.get('new_email')
1013 1013 user_data = c.user.get_api_data()
1014 1014 try:
1015 1015
1016 1016 form = UserExtraEmailForm(self.request.translate)()
1017 1017 data = form.to_python({'email': email})
1018 1018 email = data['email']
1019 1019
1020 1020 UserModel().add_extra_email(c.user.user_id, email)
1021 1021 audit_logger.store_web(
1022 1022 'user.edit.email.add',
1023 1023 action_data={'email': email, 'user': user_data},
1024 1024 user=self._rhodecode_user)
1025 1025 Session().commit()
1026 1026 h.flash(_("Added new email address `%s` for user account") % email,
1027 1027 category='success')
1028 1028 except formencode.Invalid as error:
1029 1029 msg = error.unpack_errors()['email']
1030 1030 h.flash(h.escape(msg), category='error')
1031 1031 except IntegrityError:
1032 1032 log.warning("Email %s already exists", email)
1033 1033 h.flash(_('Email `{}` is already registered for another user.').format(email),
1034 1034 category='error')
1035 1035 except Exception:
1036 1036 log.exception("Exception during email saving")
1037 1037 h.flash(_('An error occurred during email saving'),
1038 1038 category='error')
1039 1039 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1040 1040
1041 1041 @LoginRequired()
1042 1042 @HasPermissionAllDecorator('hg.admin')
1043 1043 @CSRFRequired()
1044 1044 def emails_delete(self):
1045 1045 _ = self.request.translate
1046 1046 c = self.load_default_context()
1047 1047
1048 1048 user_id = self.db_user_id
1049 1049 c.user = self.db_user
1050 1050
1051 1051 email_id = self.request.POST.get('del_email_id')
1052 1052 user_model = UserModel()
1053 1053
1054 1054 email = UserEmailMap.query().get(email_id).email
1055 1055 user_data = c.user.get_api_data()
1056 1056 user_model.delete_extra_email(c.user.user_id, email_id)
1057 1057 audit_logger.store_web(
1058 1058 'user.edit.email.delete',
1059 1059 action_data={'email': email, 'user': user_data},
1060 1060 user=self._rhodecode_user)
1061 1061 Session().commit()
1062 1062 h.flash(_("Removed email address from user account"),
1063 1063 category='success')
1064 1064 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1065 1065
1066 1066 @LoginRequired()
1067 1067 @HasPermissionAllDecorator('hg.admin')
1068 1068 def ips(self):
1069 1069 _ = self.request.translate
1070 1070 c = self.load_default_context()
1071 1071 c.user = self.db_user
1072 1072
1073 1073 c.active = 'ips'
1074 1074 c.user_ip_map = UserIpMap.query() \
1075 1075 .filter(UserIpMap.user == c.user).all()
1076 1076
1077 1077 c.inherit_default_ips = c.user.inherit_default_permissions
1078 1078 c.default_user_ip_map = UserIpMap.query() \
1079 1079 .filter(UserIpMap.user == User.get_default_user()).all()
1080 1080
1081 1081 return self._get_template_context(c)
1082 1082
1083 1083 @LoginRequired()
1084 1084 @HasPermissionAllDecorator('hg.admin')
1085 1085 @CSRFRequired()
1086 1086 # NOTE(marcink): this view is allowed for default users, as we can
1087 1087 # edit their IP white list
1088 1088 def ips_add(self):
1089 1089 _ = self.request.translate
1090 1090 c = self.load_default_context()
1091 1091
1092 1092 user_id = self.db_user_id
1093 1093 c.user = self.db_user
1094 1094
1095 1095 user_model = UserModel()
1096 1096 desc = self.request.POST.get('description')
1097 1097 try:
1098 1098 ip_list = user_model.parse_ip_range(
1099 1099 self.request.POST.get('new_ip'))
1100 1100 except Exception as e:
1101 1101 ip_list = []
1102 1102 log.exception("Exception during ip saving")
1103 1103 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1104 1104 category='error')
1105 1105 added = []
1106 1106 user_data = c.user.get_api_data()
1107 1107 for ip in ip_list:
1108 1108 try:
1109 1109 form = UserExtraIpForm(self.request.translate)()
1110 1110 data = form.to_python({'ip': ip})
1111 1111 ip = data['ip']
1112 1112
1113 1113 user_model.add_extra_ip(c.user.user_id, ip, desc)
1114 1114 audit_logger.store_web(
1115 1115 'user.edit.ip.add',
1116 1116 action_data={'ip': ip, 'user': user_data},
1117 1117 user=self._rhodecode_user)
1118 1118 Session().commit()
1119 1119 added.append(ip)
1120 1120 except formencode.Invalid as error:
1121 1121 msg = error.unpack_errors()['ip']
1122 1122 h.flash(msg, category='error')
1123 1123 except Exception:
1124 1124 log.exception("Exception during ip saving")
1125 1125 h.flash(_('An error occurred during ip saving'),
1126 1126 category='error')
1127 1127 if added:
1128 1128 h.flash(
1129 1129 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1130 1130 category='success')
1131 1131 if 'default_user' in self.request.POST:
1132 1132 # case for editing global IP list we do it for 'DEFAULT' user
1133 1133 raise HTTPFound(h.route_path('admin_permissions_ips'))
1134 1134 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1135 1135
1136 1136 @LoginRequired()
1137 1137 @HasPermissionAllDecorator('hg.admin')
1138 1138 @CSRFRequired()
1139 1139 # NOTE(marcink): this view is allowed for default users, as we can
1140 1140 # edit their IP white list
1141 1141 def ips_delete(self):
1142 1142 _ = self.request.translate
1143 1143 c = self.load_default_context()
1144 1144
1145 1145 user_id = self.db_user_id
1146 1146 c.user = self.db_user
1147 1147
1148 1148 ip_id = self.request.POST.get('del_ip_id')
1149 1149 user_model = UserModel()
1150 1150 user_data = c.user.get_api_data()
1151 1151 ip = UserIpMap.query().get(ip_id).ip_addr
1152 1152 user_model.delete_extra_ip(c.user.user_id, ip_id)
1153 1153 audit_logger.store_web(
1154 1154 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1155 1155 user=self._rhodecode_user)
1156 1156 Session().commit()
1157 1157 h.flash(_("Removed ip address from user whitelist"), category='success')
1158 1158
1159 1159 if 'default_user' in self.request.POST:
1160 1160 # case for editing global IP list we do it for 'DEFAULT' user
1161 1161 raise HTTPFound(h.route_path('admin_permissions_ips'))
1162 1162 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1163 1163
1164 1164 @LoginRequired()
1165 1165 @HasPermissionAllDecorator('hg.admin')
1166 1166 def groups_management(self):
1167 1167 c = self.load_default_context()
1168 1168 c.user = self.db_user
1169 1169 c.data = c.user.group_member
1170 1170
1171 1171 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1172 1172 for group in c.user.group_member]
1173 1173 c.groups = ext_json.str_json(groups)
1174 1174 c.active = 'groups'
1175 1175
1176 1176 return self._get_template_context(c)
1177 1177
1178 1178 @LoginRequired()
1179 1179 @HasPermissionAllDecorator('hg.admin')
1180 1180 @CSRFRequired()
1181 1181 def groups_management_updates(self):
1182 1182 _ = self.request.translate
1183 1183 c = self.load_default_context()
1184 1184
1185 1185 user_id = self.db_user_id
1186 1186 c.user = self.db_user
1187 1187
1188 1188 user_groups = set(self.request.POST.getall('users_group_id'))
1189 1189 user_groups_objects = []
1190 1190
1191 1191 for ugid in user_groups:
1192 1192 user_groups_objects.append(
1193 1193 UserGroupModel().get_group(safe_int(ugid)))
1194 1194 user_group_model = UserGroupModel()
1195 1195 added_to_groups, removed_from_groups = \
1196 1196 user_group_model.change_groups(c.user, user_groups_objects)
1197 1197
1198 1198 user_data = c.user.get_api_data()
1199 1199 for user_group_id in added_to_groups:
1200 1200 user_group = UserGroup.get(user_group_id)
1201 1201 old_values = user_group.get_api_data()
1202 1202 audit_logger.store_web(
1203 1203 'user_group.edit.member.add',
1204 1204 action_data={'user': user_data, 'old_data': old_values},
1205 1205 user=self._rhodecode_user)
1206 1206
1207 1207 for user_group_id in removed_from_groups:
1208 1208 user_group = UserGroup.get(user_group_id)
1209 1209 old_values = user_group.get_api_data()
1210 1210 audit_logger.store_web(
1211 1211 'user_group.edit.member.delete',
1212 1212 action_data={'user': user_data, 'old_data': old_values},
1213 1213 user=self._rhodecode_user)
1214 1214
1215 1215 Session().commit()
1216 1216 c.active = 'user_groups_management'
1217 1217 h.flash(_("Groups successfully changed"), category='success')
1218 1218
1219 1219 return HTTPFound(h.route_path(
1220 1220 'edit_user_groups_management', user_id=user_id))
1221 1221
1222 1222 @LoginRequired()
1223 1223 @HasPermissionAllDecorator('hg.admin')
1224 1224 def user_audit_logs(self):
1225 1225 _ = self.request.translate
1226 1226 c = self.load_default_context()
1227 1227 c.user = self.db_user
1228 1228
1229 1229 c.active = 'audit'
1230 1230
1231 1231 p = safe_int(self.request.GET.get('page', 1), 1)
1232 1232
1233 1233 filter_term = self.request.GET.get('filter')
1234 1234 user_log = UserModel().get_user_log(c.user, filter_term)
1235 1235
1236 1236 def url_generator(page_num):
1237 1237 query_params = {
1238 1238 'page': page_num
1239 1239 }
1240 1240 if filter_term:
1241 1241 query_params['filter'] = filter_term
1242 1242 return self.request.current_route_path(_query=query_params)
1243 1243
1244 1244 c.audit_logs = SqlPage(
1245 1245 user_log, page=p, items_per_page=10, url_maker=url_generator)
1246 1246 c.filter_term = filter_term
1247 1247 return self._get_template_context(c)
1248 1248
1249 1249 @LoginRequired()
1250 1250 @HasPermissionAllDecorator('hg.admin')
1251 1251 def user_audit_logs_download(self):
1252 1252 _ = self.request.translate
1253 1253 c = self.load_default_context()
1254 1254 c.user = self.db_user
1255 1255
1256 1256 user_log = UserModel().get_user_log(c.user, filter_term=None)
1257 1257
1258 1258 audit_log_data = {}
1259 1259 for entry in user_log:
1260 1260 audit_log_data[entry.user_log_id] = entry.get_dict()
1261 1261
1262 1262 response = Response(ext_json.formatted_str_json(audit_log_data))
1263 1263 response.content_disposition = f'attachment; filename=user_{c.user.user_id}_audit_logs.json'
1264 1264 response.content_type = 'application/json'
1265 1265
1266 1266 return response
1267 1267
1268 1268 @LoginRequired()
1269 1269 @HasPermissionAllDecorator('hg.admin')
1270 1270 def user_perms_summary(self):
1271 1271 _ = self.request.translate
1272 1272 c = self.load_default_context()
1273 1273 c.user = self.db_user
1274 1274
1275 1275 c.active = 'perms_summary'
1276 1276 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1277 1277
1278 1278 return self._get_template_context(c)
1279 1279
1280 1280 @LoginRequired()
1281 1281 @HasPermissionAllDecorator('hg.admin')
1282 1282 def user_perms_summary_json(self):
1283 1283 self.load_default_context()
1284 1284 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1285 1285
1286 1286 return perm_user.permissions
1287 1287
1288 1288 @LoginRequired()
1289 1289 @HasPermissionAllDecorator('hg.admin')
1290 1290 def user_caches(self):
1291 1291 _ = self.request.translate
1292 1292 c = self.load_default_context()
1293 1293 c.user = self.db_user
1294 1294
1295 1295 c.active = 'caches'
1296 1296 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1297 1297
1298 1298 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1299 1299 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1300 1300 c.backend = c.region.backend
1301 1301 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1302 1302
1303 1303 return self._get_template_context(c)
1304 1304
1305 1305 @LoginRequired()
1306 1306 @HasPermissionAllDecorator('hg.admin')
1307 1307 @CSRFRequired()
1308 1308 def user_caches_update(self):
1309 1309 _ = self.request.translate
1310 1310 c = self.load_default_context()
1311 1311 c.user = self.db_user
1312 1312
1313 1313 c.active = 'caches'
1314 1314 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1315 1315
1316 1316 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1317 1317 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1318 1318
1319 1319 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1320 1320
1321 1321 return HTTPFound(h.route_path(
1322 1322 'edit_user_caches', user_id=c.user.user_id))
@@ -1,107 +1,106 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import os
22 21
23 22 from pyramid.events import ApplicationCreated
24 23 from pyramid.settings import asbool
25 24
26 25 from rhodecode.apps._base import ADMIN_PREFIX
27 26 from rhodecode.lib.ext_json import json
28 27 from rhodecode.lib.str_utils import safe_str
29 28
30 29
31 30 def url_gen(request):
32 31 registry = request.registry
33 32 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
34 33 ws_url = registry.settings.get('channelstream.ws_url', '')
35 34 proxy_url = request.route_url('channelstream_proxy')
36 35 urls = {
37 36 'connect': request.route_path('channelstream_connect'),
38 37 'subscribe': request.route_path('channelstream_subscribe'),
39 38 'longpoll': longpoll_url or proxy_url,
40 39 'ws': ws_url or proxy_url.replace('http', 'ws')
41 40 }
42 41 return safe_str(json.dumps(urls))
43 42
44 43
45 44 PLUGIN_DEFINITION = {
46 45 'name': 'channelstream',
47 46 'config': {
48 47 'javascript': [],
49 48 'css': [],
50 49 'template_hooks': {
51 50 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako'
52 51 },
53 52 'url_gen': url_gen,
54 53 'static': None,
55 54 'enabled': False,
56 55 'server': '',
57 56 'secret': ''
58 57 }
59 58 }
60 59
61 60
62 61 def maybe_create_history_store(event):
63 62 # create plugin history location
64 63 settings = event.app.registry.settings
65 64 history_dir = settings.get('channelstream.history.location', '')
66 65 if history_dir and not os.path.exists(history_dir):
67 66 os.makedirs(history_dir, 0o750)
68 67
69 68
70 69 def includeme(config):
71 70 from rhodecode.apps.channelstream.views import ChannelstreamView
72 71
73 72 settings = config.registry.settings
74 73 PLUGIN_DEFINITION['config']['enabled'] = asbool(
75 74 settings.get('channelstream.enabled'))
76 75 PLUGIN_DEFINITION['config']['server'] = settings.get(
77 76 'channelstream.server', '')
78 77 PLUGIN_DEFINITION['config']['secret'] = settings.get(
79 78 'channelstream.secret', '')
80 79 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
81 80 'channelstream.history.location', '')
82 81 config.register_rhodecode_plugin(
83 82 PLUGIN_DEFINITION['name'],
84 83 PLUGIN_DEFINITION['config']
85 84 )
86 85 config.add_subscriber(maybe_create_history_store, ApplicationCreated)
87 86
88 87 config.add_route(
89 88 name='channelstream_connect',
90 89 pattern=ADMIN_PREFIX + '/channelstream/connect')
91 90 config.add_view(
92 91 ChannelstreamView,
93 92 attr='channelstream_connect',
94 93 route_name='channelstream_connect', renderer='json_ext')
95 94
96 95 config.add_route(
97 96 name='channelstream_subscribe',
98 97 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
99 98 config.add_view(
100 99 ChannelstreamView,
101 100 attr='channelstream_subscribe',
102 101 route_name='channelstream_subscribe', renderer='json_ext')
103 102
104 103 config.add_route(
105 104 name='channelstream_proxy',
106 105 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
107 106
@@ -1,185 +1,184 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import logging
22 21 import uuid
23 22
24 23
25 24 from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
26 25
27 26 from rhodecode.apps._base import BaseAppView
28 27 from rhodecode.lib.channelstream import (
29 28 channelstream_request, get_channelstream_server_url,
30 29 ChannelstreamConnectionException,
31 30 ChannelstreamPermissionException,
32 31 check_channel_permissions,
33 32 get_connection_validators,
34 33 get_user_data,
35 34 parse_channels_info,
36 35 update_history_from_logs,
37 36 USER_STATE_PUBLIC_KEYS)
38 37
39 38 from rhodecode.lib.auth import NotAnonymous
40 39
41 40 log = logging.getLogger(__name__)
42 41
43 42
44 43 class ChannelstreamView(BaseAppView):
45 44
46 45 def load_default_context(self):
47 46 c = self._get_local_tmpl_context()
48 47 self.channelstream_config = \
49 48 self.request.registry.rhodecode_plugins['channelstream']
50 49 if not self.channelstream_config.get('enabled'):
51 50 log.warning('Channelstream plugin is disabled')
52 51 raise HTTPBadRequest()
53 52
54 53 return c
55 54
56 55 @NotAnonymous()
57 56 def channelstream_connect(self):
58 57 """ handle authorization of users trying to connect """
59 58
60 59 self.load_default_context()
61 60 try:
62 61 json_body = self.request.json_body
63 62 except Exception:
64 63 log.exception('Failed to decode json from request')
65 64 raise HTTPBadRequest()
66 65
67 66 try:
68 67 channels = check_channel_permissions(
69 68 json_body.get('channels'),
70 69 get_connection_validators(self.request.registry))
71 70 except ChannelstreamPermissionException:
72 71 log.error('Incorrect permissions for requested channels')
73 72 raise HTTPForbidden()
74 73
75 74 user = self._rhodecode_user
76 75 if user.user_id:
77 76 user_data = get_user_data(user.user_id)
78 77 else:
79 78 user_data = {
80 79 'id': None,
81 80 'username': None,
82 81 'first_name': None,
83 82 'last_name': None,
84 83 'icon_link': None,
85 84 'display_name': None,
86 85 'display_link': None,
87 86 }
88 87
89 88 #user_data['permissions'] = self._rhodecode_user.permissions_safe
90 89
91 90 payload = {
92 91 'username': user.username,
93 92 'user_state': user_data,
94 93 'conn_id': str(uuid.uuid4()),
95 94 'channels': channels,
96 95 'channel_configs': {},
97 96 'state_public_keys': USER_STATE_PUBLIC_KEYS,
98 97 'info': {
99 98 'exclude_channels': ['broadcast']
100 99 }
101 100 }
102 101 filtered_channels = [channel for channel in channels
103 102 if channel != 'broadcast']
104 103 for channel in filtered_channels:
105 104 payload['channel_configs'][channel] = {
106 105 'notify_presence': True,
107 106 'history_size': 100,
108 107 'store_history': True,
109 108 'broadcast_presence_with_user_lists': True
110 109 }
111 110 # connect user to server
112 111 channelstream_url = get_channelstream_server_url(
113 112 self.channelstream_config, '/connect')
114 113 try:
115 114 connect_result = channelstream_request(
116 115 self.channelstream_config, payload, '/connect')
117 116 except ChannelstreamConnectionException:
118 117 log.exception(
119 118 'Channelstream service at {} is down'.format(channelstream_url))
120 119 return HTTPBadGateway()
121 120
122 121 channel_info = connect_result.get('channels_info')
123 122 if not channel_info:
124 123 raise HTTPBadRequest()
125 124
126 125 connect_result['channels'] = channels
127 126 connect_result['channels_info'] = parse_channels_info(
128 127 channel_info, include_channel_info=filtered_channels)
129 128 update_history_from_logs(self.channelstream_config,
130 129 filtered_channels, connect_result)
131 130 return connect_result
132 131
133 132 @NotAnonymous()
134 133 def channelstream_subscribe(self):
135 134 """ can be used to subscribe specific connection to other channels """
136 135 self.load_default_context()
137 136 try:
138 137 json_body = self.request.json_body
139 138 except Exception:
140 139 log.exception('Failed to decode json from request')
141 140 raise HTTPBadRequest()
142 141 try:
143 142 channels = check_channel_permissions(
144 143 json_body.get('channels'),
145 144 get_connection_validators(self.request.registry))
146 145 except ChannelstreamPermissionException:
147 146 log.error('Incorrect permissions for requested channels')
148 147 raise HTTPForbidden()
149 148 payload = {'conn_id': json_body.get('conn_id', ''),
150 149 'channels': channels,
151 150 'channel_configs': {},
152 151 'info': {
153 152 'exclude_channels': ['broadcast']}
154 153 }
155 154 filtered_channels = [chan for chan in channels if chan != 'broadcast']
156 155 for channel in filtered_channels:
157 156 payload['channel_configs'][channel] = {
158 157 'notify_presence': True,
159 158 'history_size': 100,
160 159 'store_history': True,
161 160 'broadcast_presence_with_user_lists': True
162 161 }
163 162
164 163 channelstream_url = get_channelstream_server_url(
165 164 self.channelstream_config, '/subscribe')
166 165 try:
167 166 connect_result = channelstream_request(
168 167 self.channelstream_config, payload, '/subscribe')
169 168 except ChannelstreamConnectionException:
170 169 log.exception(
171 170 'Channelstream service at {} is down'.format(channelstream_url))
172 171 return HTTPBadGateway()
173 172
174 173 channel_info = connect_result.get('channels_info')
175 174 if not channel_info:
176 175 raise HTTPBadRequest()
177 176
178 177 # include_channel_info will limit history only to new channel
179 178 # to not overwrite histories on other channels in client
180 179 connect_result['channels_info'] = parse_channels_info(
181 180 channel_info,
182 181 include_channel_info=filtered_channels)
183 182 update_history_from_logs(
184 183 self.channelstream_config, filtered_channels, connect_result)
185 184 return connect_result
@@ -1,81 +1,81 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import ADMIN_PREFIX
21 21 from rhodecode.lib.utils2 import str2bool
22 22
23 23
24 24 class DebugStylePredicate(object):
25 25 def __init__(self, val, config):
26 26 self.val = val
27 27
28 28 def text(self):
29 29 return f'debug style route = {self.val}'
30 30
31 31 phash = text
32 32
33 33 def __call__(self, info, request):
34 34 return str2bool(request.registry.settings.get('debug_style'))
35 35
36 36
37 37 def includeme(config):
38 38 from rhodecode.apps.debug_style.views import DebugStyleView
39 39
40 40 config.add_route_predicate(
41 41 'debug_style', DebugStylePredicate)
42 42
43 43 config.add_route(
44 44 name='debug_style_home',
45 45 pattern=ADMIN_PREFIX + '/debug_style',
46 46 debug_style=True)
47 47 config.add_view(
48 48 DebugStyleView,
49 49 attr='index',
50 50 route_name='debug_style_home', request_method='GET',
51 51 renderer=None)
52 52
53 53 config.add_route(
54 54 name='debug_style_email',
55 55 pattern=ADMIN_PREFIX + '/debug_style/email/{email_id}',
56 56 debug_style=True)
57 57 config.add_view(
58 58 DebugStyleView,
59 59 attr='render_email',
60 60 route_name='debug_style_email', request_method='GET',
61 61 renderer=None)
62 62
63 63 config.add_route(
64 64 name='debug_style_email_plain_rendered',
65 65 pattern=ADMIN_PREFIX + '/debug_style/email-rendered/{email_id}',
66 66 debug_style=True)
67 67 config.add_view(
68 68 DebugStyleView,
69 69 attr='render_email',
70 70 route_name='debug_style_email_plain_rendered', request_method='GET',
71 71 renderer=None)
72 72
73 73 config.add_route(
74 74 name='debug_style_template',
75 75 pattern=ADMIN_PREFIX + '/debug_style/t/{t_path}',
76 76 debug_style=True)
77 77 config.add_view(
78 78 DebugStyleView,
79 79 attr='template',
80 80 route_name='debug_style_template', request_method='GET',
81 81 renderer=None)
@@ -1,476 +1,476 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 23 import datetime
24 24
25 25 from pyramid.renderers import render_to_response
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.lib.celerylib import run_task, tasks
28 28 from rhodecode.lib.utils2 import AttributeDict
29 29 from rhodecode.model.db import User
30 30 from rhodecode.model.notification import EmailNotificationModel
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class DebugStyleView(BaseAppView):
36 36
37 37 def load_default_context(self):
38 38 c = self._get_local_tmpl_context()
39 39 return c
40 40
41 41 def index(self):
42 42 c = self.load_default_context()
43 43 c.active = 'index'
44 44
45 45 return render_to_response(
46 46 'debug_style/index.html', self._get_template_context(c),
47 47 request=self.request)
48 48
49 49 def render_email(self):
50 50 c = self.load_default_context()
51 51 email_id = self.request.matchdict['email_id']
52 52 c.active = 'emails'
53 53
54 54 pr = AttributeDict(
55 55 pull_request_id=123,
56 56 title='digital_ocean: fix redis, elastic search start on boot, '
57 57 'fix fd limits on supervisor, set postgres 11 version',
58 58 description='''
59 59 Check if we should use full-topic or mini-topic.
60 60
61 61 - full topic produces some problems with merge states etc
62 62 - server-mini-topic needs probably tweeks.
63 63 ''',
64 64 repo_name='foobar',
65 65 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
66 66 target_ref_parts=AttributeDict(type='branch', name='master'),
67 67 )
68 68
69 69 target_repo = AttributeDict(repo_name='repo_group/target_repo')
70 70 source_repo = AttributeDict(repo_name='repo_group/source_repo')
71 71 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
72 72 # file/commit changes for PR update
73 73 commit_changes = AttributeDict({
74 74 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
75 75 'removed': ['eeeeeeeeeee'],
76 76 })
77 77
78 78 file_changes = AttributeDict({
79 79 'added': ['a/file1.md', 'file2.py'],
80 80 'modified': ['b/modified_file.rst'],
81 81 'removed': ['.idea'],
82 82 })
83 83
84 84 exc_traceback = {
85 85 'exc_utc_date': '2020-03-26T12:54:50.683281',
86 86 'exc_id': 139638856342656,
87 87 'exc_timestamp': '1585227290.683288',
88 88 'version': 'v1',
89 89 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
90 90 'exc_type': 'AttributeError'
91 91 }
92 92
93 93 email_kwargs = {
94 94 'test': {},
95 95
96 96 'message': {
97 97 'body': 'message body !'
98 98 },
99 99
100 100 'email_test': {
101 101 'user': user,
102 102 'date': datetime.datetime.now(),
103 103 },
104 104
105 105 'update_available': {
106 106 'current_ver': '4.23.0',
107 107 'latest_ver': '4.24.0',
108 108 },
109 109
110 110 'exception': {
111 111 'email_prefix': '[RHODECODE ERROR]',
112 112 'exc_id': exc_traceback['exc_id'],
113 113 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
114 114 'exc_type_name': 'NameError',
115 115 'exc_traceback': exc_traceback,
116 116 },
117 117
118 118 'password_reset': {
119 119 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
120 120
121 121 'user': user,
122 122 'date': datetime.datetime.now(),
123 123 'email': 'test@rhodecode.com',
124 124 'first_admin_email': User.get_first_super_admin().email
125 125 },
126 126
127 127 'password_reset_confirmation': {
128 128 'new_password': 'new-password-example',
129 129 'user': user,
130 130 'date': datetime.datetime.now(),
131 131 'email': 'test@rhodecode.com',
132 132 'first_admin_email': User.get_first_super_admin().email
133 133 },
134 134
135 135 'registration': {
136 136 'user': user,
137 137 'date': datetime.datetime.now(),
138 138 },
139 139
140 140 'pull_request_comment': {
141 141 'user': user,
142 142
143 143 'status_change': None,
144 144 'status_change_type': None,
145 145
146 146 'pull_request': pr,
147 147 'pull_request_commits': [],
148 148
149 149 'pull_request_target_repo': target_repo,
150 150 'pull_request_target_repo_url': 'http://target-repo/url',
151 151
152 152 'pull_request_source_repo': source_repo,
153 153 'pull_request_source_repo_url': 'http://source-repo/url',
154 154
155 155 'pull_request_url': 'http://localhost/pr1',
156 156 'pr_comment_url': 'http://comment-url',
157 157 'pr_comment_reply_url': 'http://comment-url#reply',
158 158
159 159 'comment_file': None,
160 160 'comment_line': None,
161 161 'comment_type': 'note',
162 162 'comment_body': 'This is my comment body. *I like !*',
163 163 'comment_id': 2048,
164 164 'renderer_type': 'markdown',
165 165 'mention': True,
166 166
167 167 },
168 168
169 169 'pull_request_comment+status': {
170 170 'user': user,
171 171
172 172 'status_change': 'approved',
173 173 'status_change_type': 'approved',
174 174
175 175 'pull_request': pr,
176 176 'pull_request_commits': [],
177 177
178 178 'pull_request_target_repo': target_repo,
179 179 'pull_request_target_repo_url': 'http://target-repo/url',
180 180
181 181 'pull_request_source_repo': source_repo,
182 182 'pull_request_source_repo_url': 'http://source-repo/url',
183 183
184 184 'pull_request_url': 'http://localhost/pr1',
185 185 'pr_comment_url': 'http://comment-url',
186 186 'pr_comment_reply_url': 'http://comment-url#reply',
187 187
188 188 'comment_type': 'todo',
189 189 'comment_file': None,
190 190 'comment_line': None,
191 191 'comment_body': '''
192 192 I think something like this would be better
193 193
194 194 ```py
195 195 // markdown renderer
196 196
197 197 def db():
198 198 global connection
199 199 return connection
200 200
201 201 ```
202 202
203 203 ''',
204 204 'comment_id': 2048,
205 205 'renderer_type': 'markdown',
206 206 'mention': True,
207 207
208 208 },
209 209
210 210 'pull_request_comment+file': {
211 211 'user': user,
212 212
213 213 'status_change': None,
214 214 'status_change_type': None,
215 215
216 216 'pull_request': pr,
217 217 'pull_request_commits': [],
218 218
219 219 'pull_request_target_repo': target_repo,
220 220 'pull_request_target_repo_url': 'http://target-repo/url',
221 221
222 222 'pull_request_source_repo': source_repo,
223 223 'pull_request_source_repo_url': 'http://source-repo/url',
224 224
225 225 'pull_request_url': 'http://localhost/pr1',
226 226
227 227 'pr_comment_url': 'http://comment-url',
228 228 'pr_comment_reply_url': 'http://comment-url#reply',
229 229
230 230 'comment_file': 'rhodecode/model/get_flow_commits',
231 231 'comment_line': 'o1210',
232 232 'comment_type': 'todo',
233 233 'comment_body': '''
234 234 I like this !
235 235
236 236 But please check this code
237 237
238 238 .. code-block:: javascript
239 239
240 240 // THIS IS RST CODE
241 241
242 242 this.createResolutionComment = function(commentId) {
243 243 // hide the trigger text
244 244 $('#resolve-comment-{0}'.format(commentId)).hide();
245 245
246 246 var comment = $('#comment-'+commentId);
247 247 var commentData = comment.data();
248 248 if (commentData.commentInline) {
249 249 this.createComment(comment, f_path, line_no, commentId)
250 250 } else {
251 251 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
252 252 }
253 253
254 254 return false;
255 255 };
256 256
257 257 This should work better !
258 258 ''',
259 259 'comment_id': 2048,
260 260 'renderer_type': 'rst',
261 261 'mention': True,
262 262
263 263 },
264 264
265 265 'pull_request_update': {
266 266 'updating_user': user,
267 267
268 268 'status_change': None,
269 269 'status_change_type': None,
270 270
271 271 'pull_request': pr,
272 272 'pull_request_commits': [],
273 273
274 274 'pull_request_target_repo': target_repo,
275 275 'pull_request_target_repo_url': 'http://target-repo/url',
276 276
277 277 'pull_request_source_repo': source_repo,
278 278 'pull_request_source_repo_url': 'http://source-repo/url',
279 279
280 280 'pull_request_url': 'http://localhost/pr1',
281 281
282 282 # update comment links
283 283 'pr_comment_url': 'http://comment-url',
284 284 'pr_comment_reply_url': 'http://comment-url#reply',
285 285 'ancestor_commit_id': 'f39bd443',
286 286 'added_commits': commit_changes.added,
287 287 'removed_commits': commit_changes.removed,
288 288 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
289 289 'added_files': file_changes.added,
290 290 'modified_files': file_changes.modified,
291 291 'removed_files': file_changes.removed,
292 292 },
293 293
294 294 'cs_comment': {
295 295 'user': user,
296 296 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
297 297 'status_change': None,
298 298 'status_change_type': None,
299 299
300 300 'commit_target_repo_url': 'http://foo.example.com/#comment1',
301 301 'repo_name': 'test-repo',
302 302 'comment_type': 'note',
303 303 'comment_file': None,
304 304 'comment_line': None,
305 305 'commit_comment_url': 'http://comment-url',
306 306 'commit_comment_reply_url': 'http://comment-url#reply',
307 307 'comment_body': 'This is my comment body. *I like !*',
308 308 'comment_id': 2048,
309 309 'renderer_type': 'markdown',
310 310 'mention': True,
311 311 },
312 312
313 313 'cs_comment+status': {
314 314 'user': user,
315 315 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
316 316 'status_change': 'approved',
317 317 'status_change_type': 'approved',
318 318
319 319 'commit_target_repo_url': 'http://foo.example.com/#comment1',
320 320 'repo_name': 'test-repo',
321 321 'comment_type': 'note',
322 322 'comment_file': None,
323 323 'comment_line': None,
324 324 'commit_comment_url': 'http://comment-url',
325 325 'commit_comment_reply_url': 'http://comment-url#reply',
326 326 'comment_body': '''
327 327 Hello **world**
328 328
329 329 This is a multiline comment :)
330 330
331 331 - list
332 332 - list2
333 333 ''',
334 334 'comment_id': 2048,
335 335 'renderer_type': 'markdown',
336 336 'mention': True,
337 337 },
338 338
339 339 'cs_comment+file': {
340 340 'user': user,
341 341 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
342 342 'status_change': None,
343 343 'status_change_type': None,
344 344
345 345 'commit_target_repo_url': 'http://foo.example.com/#comment1',
346 346 'repo_name': 'test-repo',
347 347
348 348 'comment_type': 'note',
349 349 'comment_file': 'test-file.py',
350 350 'comment_line': 'n100',
351 351
352 352 'commit_comment_url': 'http://comment-url',
353 353 'commit_comment_reply_url': 'http://comment-url#reply',
354 354 'comment_body': 'This is my comment body. *I like !*',
355 355 'comment_id': 2048,
356 356 'renderer_type': 'markdown',
357 357 'mention': True,
358 358 },
359 359
360 360 'pull_request': {
361 361 'user': user,
362 362 'pull_request': pr,
363 363 'pull_request_commits': [
364 364 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
365 365 my-account: moved email closer to profile as it's similar data just moved outside.
366 366 '''),
367 367 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
368 368 users: description edit fixes
369 369
370 370 - tests
371 371 - added metatags info
372 372 '''),
373 373 ],
374 374
375 375 'pull_request_target_repo': target_repo,
376 376 'pull_request_target_repo_url': 'http://target-repo/url',
377 377
378 378 'pull_request_source_repo': source_repo,
379 379 'pull_request_source_repo_url': 'http://source-repo/url',
380 380
381 381 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
382 382 'user_role': 'reviewer',
383 383 },
384 384
385 385 'pull_request+reviewer_role': {
386 386 'user': user,
387 387 'pull_request': pr,
388 388 'pull_request_commits': [
389 389 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
390 390 my-account: moved email closer to profile as it's similar data just moved outside.
391 391 '''),
392 392 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
393 393 users: description edit fixes
394 394
395 395 - tests
396 396 - added metatags info
397 397 '''),
398 398 ],
399 399
400 400 'pull_request_target_repo': target_repo,
401 401 'pull_request_target_repo_url': 'http://target-repo/url',
402 402
403 403 'pull_request_source_repo': source_repo,
404 404 'pull_request_source_repo_url': 'http://source-repo/url',
405 405
406 406 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
407 407 'user_role': 'reviewer',
408 408 },
409 409
410 410 'pull_request+observer_role': {
411 411 'user': user,
412 412 'pull_request': pr,
413 413 'pull_request_commits': [
414 414 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
415 415 my-account: moved email closer to profile as it's similar data just moved outside.
416 416 '''),
417 417 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
418 418 users: description edit fixes
419 419
420 420 - tests
421 421 - added metatags info
422 422 '''),
423 423 ],
424 424
425 425 'pull_request_target_repo': target_repo,
426 426 'pull_request_target_repo_url': 'http://target-repo/url',
427 427
428 428 'pull_request_source_repo': source_repo,
429 429 'pull_request_source_repo_url': 'http://source-repo/url',
430 430
431 431 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
432 432 'user_role': 'observer'
433 433 }
434 434 }
435 435
436 436 template_type = email_id.split('+')[0]
437 437 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
438 438 template_type, **email_kwargs.get(email_id, {}))
439 439
440 440 test_email = self.request.GET.get('email')
441 441 if test_email:
442 442 recipients = [test_email]
443 443 run_task(tasks.send_email, recipients, c.subject,
444 444 c.email_body_plaintext, c.email_body)
445 445
446 446 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
447 447 template = 'debug_style/email_plain_rendered.mako'
448 448 else:
449 449 template = 'debug_style/email.mako'
450 450 return render_to_response(
451 451 template, self._get_template_context(c),
452 452 request=self.request)
453 453
454 454 def template(self):
455 455 t_path = self.request.matchdict['t_path']
456 456 c = self.load_default_context()
457 457 c.active = os.path.splitext(t_path)[0]
458 458 c.came_from = ''
459 459 # NOTE(marcink): extend the email types with variations based on data sets
460 460 c.email_types = {
461 461 'cs_comment+file': {},
462 462 'cs_comment+status': {},
463 463
464 464 'pull_request_comment+file': {},
465 465 'pull_request_comment+status': {},
466 466
467 467 'pull_request_update': {},
468 468
469 469 'pull_request+reviewer_role': {},
470 470 'pull_request+observer_role': {},
471 471 }
472 472 c.email_types.update(EmailNotificationModel.email_types)
473 473
474 474 return render_to_response(
475 475 'debug_style/' + t_path, self._get_template_context(c),
476 476 request=self.request)
@@ -1,68 +1,68 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import os
21 21 from rhodecode.apps.file_store import config_keys
22 22 from rhodecode.config.settings_maker import SettingsMaker
23 23
24 24
25 25 def _sanitize_settings_and_apply_defaults(settings):
26 26 """
27 27 Set defaults, convert to python types and validate settings.
28 28 """
29 29 settings_maker = SettingsMaker(settings)
30 30
31 31 settings_maker.make_setting(config_keys.enabled, True, parser='bool')
32 32 settings_maker.make_setting(config_keys.backend, 'local')
33 33
34 34 default_store = os.path.join(os.path.dirname(settings['__file__']), 'upload_store')
35 35 settings_maker.make_setting(config_keys.store_path, default_store)
36 36
37 37 settings_maker.env_expand()
38 38
39 39
40 40 def includeme(config):
41 41 from rhodecode.apps.file_store.views import FileStoreView
42 42
43 43 settings = config.registry.settings
44 44 _sanitize_settings_and_apply_defaults(settings)
45 45
46 46 config.add_route(
47 47 name='upload_file',
48 48 pattern='/_file_store/upload')
49 49 config.add_view(
50 50 FileStoreView,
51 51 attr='upload_file',
52 52 route_name='upload_file', request_method='POST', renderer='json_ext')
53 53
54 54 config.add_route(
55 55 name='download_file',
56 56 pattern='/_file_store/download/{fid:.*}')
57 57 config.add_view(
58 58 FileStoreView,
59 59 attr='download_file',
60 60 route_name='download_file')
61 61
62 62 config.add_route(
63 63 name='download_file_by_token',
64 64 pattern='/_file_store/token-download/{_auth_token}/{fid:.*}')
65 65 config.add_view(
66 66 FileStoreView,
67 67 attr='download_file_by_token',
68 68 route_name='download_file_by_token')
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,270 +1,270 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import time
23 23 import errno
24 24 import hashlib
25 25
26 26 from rhodecode.lib.ext_json import json
27 27 from rhodecode.apps.file_store import utils
28 28 from rhodecode.apps.file_store.extensions import resolve_extensions
29 29 from rhodecode.apps.file_store.exceptions import (
30 30 FileNotAllowedException, FileOverSizeException)
31 31
32 32 METADATA_VER = 'v1'
33 33
34 34
35 35 def safe_make_dirs(dir_path):
36 36 if not os.path.exists(dir_path):
37 37 try:
38 38 os.makedirs(dir_path)
39 39 except OSError as e:
40 40 if e.errno != errno.EEXIST:
41 41 raise
42 42 return
43 43
44 44
45 45 class LocalFileStorage(object):
46 46
47 47 @classmethod
48 48 def apply_counter(cls, counter, filename):
49 49 name_counted = '%d-%s' % (counter, filename)
50 50 return name_counted
51 51
52 52 @classmethod
53 53 def resolve_name(cls, name, directory):
54 54 """
55 55 Resolves a unique name and the correct path. If a filename
56 56 for that path already exists then a numeric prefix with values > 0 will be
57 57 added, for example test.jpg -> 1-test.jpg etc. initially file would have 0 prefix.
58 58
59 59 :param name: base name of file
60 60 :param directory: absolute directory path
61 61 """
62 62
63 63 counter = 0
64 64 while True:
65 65 name_counted = cls.apply_counter(counter, name)
66 66
67 67 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
68 68 sub_store = cls._sub_store_from_filename(name_counted)
69 69 sub_store_path = os.path.join(directory, sub_store)
70 70 safe_make_dirs(sub_store_path)
71 71
72 72 path = os.path.join(sub_store_path, name_counted)
73 73 if not os.path.exists(path):
74 74 return name_counted, path
75 75 counter += 1
76 76
77 77 @classmethod
78 78 def _sub_store_from_filename(cls, filename):
79 79 return filename[:2]
80 80
81 81 @classmethod
82 82 def calculate_path_hash(cls, file_path):
83 83 """
84 84 Efficient calculation of file_path sha256 sum
85 85
86 86 :param file_path:
87 87 :return: sha256sum
88 88 """
89 89 digest = hashlib.sha256()
90 90 with open(file_path, 'rb') as f:
91 91 for chunk in iter(lambda: f.read(1024 * 100), b""):
92 92 digest.update(chunk)
93 93
94 94 return digest.hexdigest()
95 95
96 96 def __init__(self, base_path, extension_groups=None):
97 97
98 98 """
99 99 Local file storage
100 100
101 101 :param base_path: the absolute base path where uploads are stored
102 102 :param extension_groups: extensions string
103 103 """
104 104
105 105 extension_groups = extension_groups or ['any']
106 106 self.base_path = base_path
107 107 self.extensions = resolve_extensions([], groups=extension_groups)
108 108
109 109 def __repr__(self):
110 110 return '{}@{}'.format(self.__class__, self.base_path)
111 111
112 112 def store_path(self, filename):
113 113 """
114 114 Returns absolute file path of the filename, joined to the
115 115 base_path.
116 116
117 117 :param filename: base name of file
118 118 """
119 119 prefix_dir = ''
120 120 if '/' in filename:
121 121 prefix_dir, filename = filename.split('/')
122 122 sub_store = self._sub_store_from_filename(filename)
123 123 else:
124 124 sub_store = self._sub_store_from_filename(filename)
125 125 return os.path.join(self.base_path, prefix_dir, sub_store, filename)
126 126
127 127 def delete(self, filename):
128 128 """
129 129 Deletes the filename. Filename is resolved with the
130 130 absolute path based on base_path. If file does not exist,
131 131 returns **False**, otherwise **True**
132 132
133 133 :param filename: base name of file
134 134 """
135 135 if self.exists(filename):
136 136 os.remove(self.store_path(filename))
137 137 return True
138 138 return False
139 139
140 140 def exists(self, filename):
141 141 """
142 142 Checks if file exists. Resolves filename's absolute
143 143 path based on base_path.
144 144
145 145 :param filename: file_uid name of file, e.g 0-f62b2b2d-9708-4079-a071-ec3f958448d4.svg
146 146 """
147 147 return os.path.exists(self.store_path(filename))
148 148
149 149 def filename_allowed(self, filename, extensions=None):
150 150 """Checks if a filename has an allowed extension
151 151
152 152 :param filename: base name of file
153 153 :param extensions: iterable of extensions (or self.extensions)
154 154 """
155 155 _, ext = os.path.splitext(filename)
156 156 return self.extension_allowed(ext, extensions)
157 157
158 158 def extension_allowed(self, ext, extensions=None):
159 159 """
160 160 Checks if an extension is permitted. Both e.g. ".jpg" and
161 161 "jpg" can be passed in. Extension lookup is case-insensitive.
162 162
163 163 :param ext: extension to check
164 164 :param extensions: iterable of extensions to validate against (or self.extensions)
165 165 """
166 166 def normalize_ext(_ext):
167 167 if _ext.startswith('.'):
168 168 _ext = _ext[1:]
169 169 return _ext.lower()
170 170
171 171 extensions = extensions or self.extensions
172 172 if not extensions:
173 173 return True
174 174
175 175 ext = normalize_ext(ext)
176 176
177 177 return ext in [normalize_ext(x) for x in extensions]
178 178
179 179 def save_file(self, file_obj, filename, directory=None, extensions=None,
180 180 extra_metadata=None, max_filesize=None, randomized_name=True, **kwargs):
181 181 """
182 182 Saves a file object to the uploads location.
183 183 Returns the resolved filename, i.e. the directory +
184 184 the (randomized/incremented) base name.
185 185
186 186 :param file_obj: **cgi.FieldStorage** object (or similar)
187 187 :param filename: original filename
188 188 :param directory: relative path of sub-directory
189 189 :param extensions: iterable of allowed extensions, if not default
190 190 :param max_filesize: maximum size of file that should be allowed
191 191 :param randomized_name: generate random generated UID or fixed based on the filename
192 192 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
193 193
194 194 """
195 195
196 196 extensions = extensions or self.extensions
197 197
198 198 if not self.filename_allowed(filename, extensions):
199 199 raise FileNotAllowedException()
200 200
201 201 if directory:
202 202 dest_directory = os.path.join(self.base_path, directory)
203 203 else:
204 204 dest_directory = self.base_path
205 205
206 206 safe_make_dirs(dest_directory)
207 207
208 208 uid_filename = utils.uid_filename(filename, randomized=randomized_name)
209 209
210 210 # resolve also produces special sub-dir for file optimized store
211 211 filename, path = self.resolve_name(uid_filename, dest_directory)
212 212 stored_file_dir = os.path.dirname(path)
213 213
214 214 no_body_seek = kwargs.pop('no_body_seek', False)
215 215 if no_body_seek:
216 216 pass
217 217 else:
218 218 file_obj.seek(0)
219 219
220 220 with open(path, "wb") as dest:
221 221 length = 256 * 1024
222 222 while 1:
223 223 buf = file_obj.read(length)
224 224 if not buf:
225 225 break
226 226 dest.write(buf)
227 227
228 228 metadata = {}
229 229 if extra_metadata:
230 230 metadata = extra_metadata
231 231
232 232 size = os.stat(path).st_size
233 233
234 234 if max_filesize and size > max_filesize:
235 235 # free up the copied file, and raise exc
236 236 os.remove(path)
237 237 raise FileOverSizeException()
238 238
239 239 file_hash = self.calculate_path_hash(path)
240 240
241 241 metadata.update({
242 242 "filename": filename,
243 243 "size": size,
244 244 "time": time.time(),
245 245 "sha256": file_hash,
246 246 "meta_ver": METADATA_VER
247 247 })
248 248
249 249 filename_meta = filename + '.meta'
250 250 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
251 251 dest_meta.write(json.dumps(metadata))
252 252
253 253 if directory:
254 254 filename = os.path.join(directory, filename)
255 255
256 256 return filename, metadata
257 257
258 258 def get_metadata(self, filename, ignore_missing=False):
259 259 """
260 260 Reads JSON stored metadata for a file
261 261
262 262 :param filename:
263 263 :return:
264 264 """
265 265 filename = self.store_path(filename)
266 266 filename_meta = filename + '.meta'
267 267 if ignore_missing and not os.path.isfile(filename_meta):
268 268 return {}
269 269 with open(filename_meta, "rb") as source_meta:
270 270 return json.loads(source_meta.read())
@@ -1,27 +1,27 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 # Definition of setting keys used to configure this module. Defined here to
23 23 # avoid repetition of keys throughout the module.
24 24
25 25 enabled = 'file_store.enabled'
26 26 backend = 'file_store.backend'
27 27 store_path = 'file_store.storage_path'
@@ -1,31 +1,31 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 class FileNotAllowedException(Exception):
23 23 """
24 24 Thrown if file does not have an allowed extension.
25 25 """
26 26
27 27
28 28 class FileOverSizeException(Exception):
29 29 """
30 30 Thrown if file is over the set limit.
31 31 """
@@ -1,66 +1,66 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 ANY = []
23 23 TEXT_EXT = ['txt', 'md', 'rst', 'log']
24 24 DOCUMENTS_EXT = ['pdf', 'rtf', 'odf', 'ods', 'gnumeric', 'abw', 'doc', 'docx', 'xls', 'xlsx']
25 25 IMAGES_EXT = ['jpg', 'jpe', 'jpeg', 'png', 'gif', 'svg', 'bmp', 'tiff']
26 26 AUDIO_EXT = ['wav', 'mp3', 'aac', 'ogg', 'oga', 'flac']
27 27 VIDEO_EXT = ['mpeg', '3gp', 'avi', 'divx', 'dvr', 'flv', 'mp4', 'wmv']
28 28 DATA_EXT = ['csv', 'ini', 'json', 'plist', 'xml', 'yaml', 'yml']
29 29 SCRIPTS_EXT = ['js', 'php', 'pl', 'py', 'rb', 'sh', 'go', 'c', 'h']
30 30 ARCHIVES_EXT = ['gz', 'bz2', 'zip', 'tar', 'tgz', 'txz', '7z']
31 31 EXECUTABLES_EXT = ['so', 'exe', 'dll']
32 32
33 33
34 34 DEFAULT = DOCUMENTS_EXT + TEXT_EXT + IMAGES_EXT + DATA_EXT
35 35
36 36 GROUPS = dict((
37 37 ('any', ANY),
38 38 ('text', TEXT_EXT),
39 39 ('documents', DOCUMENTS_EXT),
40 40 ('images', IMAGES_EXT),
41 41 ('audio', AUDIO_EXT),
42 42 ('video', VIDEO_EXT),
43 43 ('data', DATA_EXT),
44 44 ('scripts', SCRIPTS_EXT),
45 45 ('archives', ARCHIVES_EXT),
46 46 ('executables', EXECUTABLES_EXT),
47 47 ('default', DEFAULT),
48 48 ))
49 49
50 50
51 51 def resolve_extensions(extensions, groups=None):
52 52 """
53 53 Calculate allowed extensions based on a list of extensions provided, and optional
54 54 groups of extensions from the available lists.
55 55
56 56 :param extensions: a list of extensions e.g ['py', 'txt']
57 57 :param groups: additionally groups to extend the extensions.
58 58 """
59 59 groups = groups or []
60 60 valid_exts = set([x.lower() for x in extensions])
61 61
62 62 for group in groups:
63 63 if group in GROUPS:
64 64 valid_exts.update(GROUPS[group])
65 65
66 66 return valid_exts
@@ -1,20 +1,20 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
@@ -1,261 +1,260 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19 import os
21 20 import pytest
22 21
23 22 from rhodecode.lib.ext_json import json
24 23 from rhodecode.model.auth_token import AuthTokenModel
25 24 from rhodecode.model.db import Session, FileStore, Repository, User
26 25 from rhodecode.tests import TestController
27 26 from rhodecode.apps.file_store import utils, config_keys
28 27
29 28
30 29 def route_path(name, params=None, **kwargs):
31 30 import urllib.request, urllib.parse, urllib.error
32 31
33 32 base_url = {
34 33 'upload_file': '/_file_store/upload',
35 34 'download_file': '/_file_store/download/{fid}',
36 35 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
37 36
38 37 }[name].format(**kwargs)
39 38
40 39 if params:
41 40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 41 return base_url
43 42
44 43
45 44 class TestFileStoreViews(TestController):
46 45
47 46 @pytest.mark.parametrize("fid, content, exists", [
48 47 ('abcde-0.jpg', "xxxxx", True),
49 48 ('abcde-0.exe', "1234567", True),
50 49 ('abcde-0.jpg', "xxxxx", False),
51 50 ])
52 51 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
53 52 user = self.log_user()
54 53 user_id = user['user_id']
55 54 repo_id = user_util.create_repo().repo_id
56 55 store_path = self.app._pyramid_settings[config_keys.store_path]
57 56 store_uid = fid
58 57
59 58 if exists:
60 59 status = 200
61 60 store = utils.get_file_storage({config_keys.store_path: store_path})
62 61 filesystem_file = os.path.join(str(tmpdir), fid)
63 62 with open(filesystem_file, 'wb') as f:
64 63 f.write(content)
65 64
66 65 with open(filesystem_file, 'rb') as f:
67 66 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
68 67
69 68 entry = FileStore.create(
70 69 file_uid=store_uid, filename=metadata["filename"],
71 70 file_hash=metadata["sha256"], file_size=metadata["size"],
72 71 file_display_name='file_display_name',
73 72 file_description='repo artifact `{}`'.format(metadata["filename"]),
74 73 check_acl=True, user_id=user_id,
75 74 scope_repo_id=repo_id
76 75 )
77 76 Session().add(entry)
78 77 Session().commit()
79 78
80 79 else:
81 80 status = 404
82 81
83 82 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
84 83
85 84 if exists:
86 85 assert response.text == content
87 86 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
88 87 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
89 88 assert os.path.exists(metadata_file)
90 89 with open(metadata_file, 'rb') as f:
91 90 json_data = json.loads(f.read())
92 91
93 92 assert json_data
94 93 assert 'size' in json_data
95 94
96 95 def test_upload_files_without_content_to_store(self):
97 96 self.log_user()
98 97 response = self.app.post(
99 98 route_path('upload_file'),
100 99 params={'csrf_token': self.csrf_token},
101 100 status=200)
102 101
103 102 assert response.json == {
104 103 u'error': u'store_file data field is missing',
105 104 u'access_path': None,
106 105 u'store_fid': None}
107 106
108 107 def test_upload_files_bogus_content_to_store(self):
109 108 self.log_user()
110 109 response = self.app.post(
111 110 route_path('upload_file'),
112 111 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
113 112 status=200)
114 113
115 114 assert response.json == {
116 115 u'error': u'filename cannot be read from the data field',
117 116 u'access_path': None,
118 117 u'store_fid': None}
119 118
120 119 def test_upload_content_to_store(self):
121 120 self.log_user()
122 121 response = self.app.post(
123 122 route_path('upload_file'),
124 123 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
125 124 params={'csrf_token': self.csrf_token},
126 125 status=200)
127 126
128 127 assert response.json['store_fid']
129 128
130 129 @pytest.fixture()
131 130 def create_artifact_factory(self, tmpdir):
132 131 def factory(user_id, content):
133 132 store_path = self.app._pyramid_settings[config_keys.store_path]
134 133 store = utils.get_file_storage({config_keys.store_path: store_path})
135 134 fid = 'example.txt'
136 135
137 136 filesystem_file = os.path.join(str(tmpdir), fid)
138 137 with open(filesystem_file, 'wb') as f:
139 138 f.write(content)
140 139
141 140 with open(filesystem_file, 'rb') as f:
142 141 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
143 142
144 143 entry = FileStore.create(
145 144 file_uid=store_uid, filename=metadata["filename"],
146 145 file_hash=metadata["sha256"], file_size=metadata["size"],
147 146 file_display_name='file_display_name',
148 147 file_description='repo artifact `{}`'.format(metadata["filename"]),
149 148 check_acl=True, user_id=user_id,
150 149 )
151 150 Session().add(entry)
152 151 Session().commit()
153 152 return entry
154 153 return factory
155 154
156 155 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
157 156 user = self.log_user()
158 157 user_id = user['user_id']
159 158 content = 'HELLO MY NAME IS ARTIFACT !'
160 159
161 160 artifact = create_artifact_factory(user_id, content)
162 161 file_uid = artifact.file_uid
163 162 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
164 163 assert response.text == content
165 164
166 165 # log-in to new user and test download again
167 166 user = user_util.create_user(password='qweqwe')
168 167 self.log_user(user.username, 'qweqwe')
169 168 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
170 169 assert response.text == content
171 170
172 171 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
173 172 user = self.log_user()
174 173 user_id = user['user_id']
175 174 content = 'HELLO MY NAME IS ARTIFACT !'
176 175
177 176 artifact = create_artifact_factory(user_id, content)
178 177 # bind to repo
179 178 repo = user_util.create_repo()
180 179 repo_id = repo.repo_id
181 180 artifact.scope_repo_id = repo_id
182 181 Session().add(artifact)
183 182 Session().commit()
184 183
185 184 file_uid = artifact.file_uid
186 185 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
187 186 assert response.text == content
188 187
189 188 # log-in to new user and test download again
190 189 user = user_util.create_user(password='qweqwe')
191 190 self.log_user(user.username, 'qweqwe')
192 191 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
193 192 assert response.text == content
194 193
195 194 # forbid user the rights to repo
196 195 repo = Repository.get(repo_id)
197 196 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
198 197 self.app.get(route_path('download_file', fid=file_uid), status=404)
199 198
200 199 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
201 200 user = self.log_user()
202 201 user_id = user['user_id']
203 202 content = 'HELLO MY NAME IS ARTIFACT !'
204 203
205 204 artifact = create_artifact_factory(user_id, content)
206 205 # bind to user
207 206 user = user_util.create_user(password='qweqwe')
208 207
209 208 artifact.scope_user_id = user.user_id
210 209 Session().add(artifact)
211 210 Session().commit()
212 211
213 212 # artifact creator doesn't have access since it's bind to another user
214 213 file_uid = artifact.file_uid
215 214 self.app.get(route_path('download_file', fid=file_uid), status=404)
216 215
217 216 # log-in to new user and test download again, should be ok since we're bind to this artifact
218 217 self.log_user(user.username, 'qweqwe')
219 218 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
220 219 assert response.text == content
221 220
222 221 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
223 222 user_id = User.get_first_super_admin().user_id
224 223 content = 'HELLO MY NAME IS ARTIFACT !'
225 224
226 225 artifact = create_artifact_factory(user_id, content)
227 226 # bind to repo
228 227 repo = user_util.create_repo()
229 228 repo_id = repo.repo_id
230 229 artifact.scope_repo_id = repo_id
231 230 Session().add(artifact)
232 231 Session().commit()
233 232
234 233 file_uid = artifact.file_uid
235 234 self.app.get(route_path('download_file_by_token',
236 235 _auth_token='bogus', fid=file_uid), status=302)
237 236
238 237 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
239 238 user = User.get_first_super_admin()
240 239 AuthTokenModel().create(user, 'test artifact token',
241 240 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
242 241
243 242 user = User.get_first_super_admin()
244 243 artifact_token = user.artifact_token
245 244
246 245 user_id = User.get_first_super_admin().user_id
247 246 content = 'HELLO MY NAME IS ARTIFACT !'
248 247
249 248 artifact = create_artifact_factory(user_id, content)
250 249 # bind to repo
251 250 repo = user_util.create_repo()
252 251 repo_id = repo.repo_id
253 252 artifact.scope_repo_id = repo_id
254 253 Session().add(artifact)
255 254 Session().commit()
256 255
257 256 file_uid = artifact.file_uid
258 257 response = self.app.get(
259 258 route_path('download_file_by_token',
260 259 _auth_token=artifact_token, fid=file_uid), status=200)
261 260 assert response.text == content
@@ -1,57 +1,57 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import io
22 22 import uuid
23 23 import pathlib
24 24
25 25
26 26 def get_file_storage(settings):
27 27 from rhodecode.apps.file_store.backends.local_store import LocalFileStorage
28 28 from rhodecode.apps.file_store import config_keys
29 29 store_path = settings.get(config_keys.store_path)
30 30 return LocalFileStorage(base_path=store_path)
31 31
32 32
33 33 def splitext(filename):
34 34 ext = ''.join(pathlib.Path(filename).suffixes)
35 35 return filename, ext
36 36
37 37
38 38 def uid_filename(filename, randomized=True):
39 39 """
40 40 Generates a randomized or stable (uuid) filename,
41 41 preserving the original extension.
42 42
43 43 :param filename: the original filename
44 44 :param randomized: define if filename should be stable (sha1 based) or randomized
45 45 """
46 46
47 47 _, ext = splitext(filename)
48 48 if randomized:
49 49 uid = uuid.uuid4()
50 50 else:
51 51 hash_key = '{}.{}'.format(filename, 'store')
52 52 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
53 53 return str(uid) + ext.lower()
54 54
55 55
56 56 def bytes_to_file_obj(bytes_data):
57 57 return io.StringIO(bytes_data)
@@ -1,202 +1,202 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import logging
21 21
22 22
23 23 from pyramid.response import FileResponse
24 24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.apps.file_store import utils
28 28 from rhodecode.apps.file_store.exceptions import (
29 29 FileNotAllowedException, FileOverSizeException)
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib.auth import (
34 34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
35 35 LoginRequired)
36 36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
37 37 from rhodecode.model.db import Session, FileStore, UserApiKeys
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class FileStoreView(BaseAppView):
43 43 upload_key = 'store_file'
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47 self.storage = utils.get_file_storage(self.request.registry.settings)
48 48 return c
49 49
50 50 def _guess_type(self, file_name):
51 51 """
52 52 Our own type guesser for mimetypes using the rich DB
53 53 """
54 54 if not hasattr(self, 'db'):
55 55 self.db = get_mimetypes_db()
56 56 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
57 57 return _content_type, _encoding
58 58
59 59 def _serve_file(self, file_uid):
60 60 if not self.storage.exists(file_uid):
61 61 store_path = self.storage.store_path(file_uid)
62 62 log.debug('File with FID:%s not found in the store under `%s`',
63 63 file_uid, store_path)
64 64 raise HTTPNotFound()
65 65
66 66 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
67 67 if not db_obj:
68 68 raise HTTPNotFound()
69 69
70 70 # private upload for user
71 71 if db_obj.check_acl and db_obj.scope_user_id:
72 72 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
73 73 db_obj.scope_user_id)
74 74 user = db_obj.user
75 75 if self._rhodecode_db_user.user_id != user.user_id:
76 76 log.warning('Access to file store object forbidden')
77 77 raise HTTPNotFound()
78 78
79 79 # scoped to repository permissions
80 80 if db_obj.check_acl and db_obj.scope_repo_id:
81 81 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
82 82 db_obj.scope_repo_id)
83 83 repo = db_obj.repo
84 84 perm_set = ['repository.read', 'repository.write', 'repository.admin']
85 85 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
86 86 if not has_perm:
87 87 log.warning('Access to file store object `%s` forbidden', file_uid)
88 88 raise HTTPNotFound()
89 89
90 90 # scoped to repository group permissions
91 91 if db_obj.check_acl and db_obj.scope_repo_group_id:
92 92 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
93 93 db_obj.scope_repo_group_id)
94 94 repo_group = db_obj.repo_group
95 95 perm_set = ['group.read', 'group.write', 'group.admin']
96 96 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
97 97 if not has_perm:
98 98 log.warning('Access to file store object `%s` forbidden', file_uid)
99 99 raise HTTPNotFound()
100 100
101 101 FileStore.bump_access_counter(file_uid)
102 102
103 103 file_path = self.storage.store_path(file_uid)
104 104 content_type = 'application/octet-stream'
105 105 content_encoding = None
106 106
107 107 _content_type, _encoding = self._guess_type(file_path)
108 108 if _content_type:
109 109 content_type = _content_type
110 110
111 111 # For file store we don't submit any session data, this logic tells the
112 112 # Session lib to skip it
113 113 setattr(self.request, '_file_response', True)
114 114 response = FileResponse(
115 115 file_path, request=self.request,
116 116 content_type=content_type, content_encoding=content_encoding)
117 117
118 118 file_name = db_obj.file_display_name
119 119
120 120 response.headers["Content-Disposition"] = (
121 121 'attachment; filename="{}"'.format(str(file_name))
122 122 )
123 123 response.headers["X-RC-Artifact-Id"] = str(db_obj.file_store_id)
124 124 response.headers["X-RC-Artifact-Desc"] = str(db_obj.file_description)
125 125 response.headers["X-RC-Artifact-Sha256"] = str(db_obj.file_hash)
126 126 return response
127 127
128 128 @LoginRequired()
129 129 @NotAnonymous()
130 130 @CSRFRequired()
131 131 def upload_file(self):
132 132 self.load_default_context()
133 133 file_obj = self.request.POST.get(self.upload_key)
134 134
135 135 if file_obj is None:
136 136 return {'store_fid': None,
137 137 'access_path': None,
138 138 'error': '{} data field is missing'.format(self.upload_key)}
139 139
140 140 if not hasattr(file_obj, 'filename'):
141 141 return {'store_fid': None,
142 142 'access_path': None,
143 143 'error': 'filename cannot be read from the data field'}
144 144
145 145 filename = file_obj.filename
146 146
147 147 metadata = {
148 148 'user_uploaded': {'username': self._rhodecode_user.username,
149 149 'user_id': self._rhodecode_user.user_id,
150 150 'ip': self._rhodecode_user.ip_addr}}
151 151 try:
152 152 store_uid, metadata = self.storage.save_file(
153 153 file_obj.file, filename, extra_metadata=metadata)
154 154 except FileNotAllowedException:
155 155 return {'store_fid': None,
156 156 'access_path': None,
157 157 'error': 'File {} is not allowed.'.format(filename)}
158 158
159 159 except FileOverSizeException:
160 160 return {'store_fid': None,
161 161 'access_path': None,
162 162 'error': 'File {} is exceeding allowed limit.'.format(filename)}
163 163
164 164 try:
165 165 entry = FileStore.create(
166 166 file_uid=store_uid, filename=metadata["filename"],
167 167 file_hash=metadata["sha256"], file_size=metadata["size"],
168 168 file_description=u'upload attachment',
169 169 check_acl=False, user_id=self._rhodecode_user.user_id
170 170 )
171 171 Session().add(entry)
172 172 Session().commit()
173 173 log.debug('Stored upload in DB as %s', entry)
174 174 except Exception:
175 175 log.exception('Failed to store file %s', filename)
176 176 return {'store_fid': None,
177 177 'access_path': None,
178 178 'error': 'File {} failed to store in DB.'.format(filename)}
179 179
180 180 return {'store_fid': store_uid,
181 181 'access_path': h.route_path('download_file', fid=store_uid)}
182 182
183 183 # ACL is checked by scopes, if no scope the file is accessible to all
184 184 def download_file(self):
185 185 self.load_default_context()
186 186 file_uid = self.request.matchdict['fid']
187 187 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
188 188 return self._serve_file(file_uid)
189 189
190 190 # in addition to @LoginRequired ACL is checked by scopes
191 191 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
192 192 @NotAnonymous()
193 193 def download_file_by_token(self):
194 194 """
195 195 Special view that allows to access the download file by special URL that
196 196 is stored inside the URL.
197 197
198 198 http://example.com/_file_store/token-download/TOKEN/FILE_UID
199 199 """
200 200 self.load_default_context()
201 201 file_uid = self.request.matchdict['fid']
202 202 return self._serve_file(file_uid)
@@ -1,120 +1,120 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import ADMIN_PREFIX
21 21
22 22
23 23 def admin_routes(config):
24 24 from rhodecode.apps.gist.views import GistView
25 25
26 26 config.add_route(
27 27 name='gists_show', pattern='/gists')
28 28 config.add_view(
29 29 GistView,
30 30 attr='gist_show_all',
31 31 route_name='gists_show', request_method='GET',
32 32 renderer='rhodecode:templates/admin/gists/gist_index.mako')
33 33
34 34 config.add_route(
35 35 name='gists_new', pattern='/gists/new')
36 36 config.add_view(
37 37 GistView,
38 38 attr='gist_new',
39 39 route_name='gists_new', request_method='GET',
40 40 renderer='rhodecode:templates/admin/gists/gist_new.mako')
41 41
42 42 config.add_route(
43 43 name='gists_create', pattern='/gists/create')
44 44 config.add_view(
45 45 GistView,
46 46 attr='gist_create',
47 47 route_name='gists_create', request_method='POST',
48 48 renderer='rhodecode:templates/admin/gists/gist_new.mako')
49 49
50 50 config.add_route(
51 51 name='gist_show', pattern='/gists/{gist_id}')
52 52 config.add_view(
53 53 GistView,
54 54 attr='gist_show',
55 55 route_name='gist_show', request_method='GET',
56 56 renderer='rhodecode:templates/admin/gists/gist_show.mako')
57 57
58 58 config.add_route(
59 59 name='gist_show_rev',
60 60 pattern='/gists/{gist_id}/rev/{revision}')
61 61
62 62 config.add_view(
63 63 GistView,
64 64 attr='gist_show',
65 65 route_name='gist_show_rev', request_method='GET',
66 66 renderer='rhodecode:templates/admin/gists/gist_show.mako')
67 67
68 68 config.add_route(
69 69 name='gist_show_formatted',
70 70 pattern='/gists/{gist_id}/rev/{revision}/{format}')
71 71 config.add_view(
72 72 GistView,
73 73 attr='gist_show',
74 74 route_name='gist_show_formatted', request_method='GET',
75 75 renderer=None)
76 76
77 77 config.add_route(
78 78 name='gist_show_formatted_path',
79 79 pattern='/gists/{gist_id}/rev/{revision}/{format}/{f_path:.*}')
80 80 config.add_view(
81 81 GistView,
82 82 attr='gist_show',
83 83 route_name='gist_show_formatted_path', request_method='GET',
84 84 renderer=None)
85 85
86 86 config.add_route(
87 87 name='gist_delete', pattern='/gists/{gist_id}/delete')
88 88 config.add_view(
89 89 GistView,
90 90 attr='gist_delete',
91 91 route_name='gist_delete', request_method='POST')
92 92
93 93 config.add_route(
94 94 name='gist_edit', pattern='/gists/{gist_id}/edit')
95 95 config.add_view(
96 96 GistView,
97 97 attr='gist_edit',
98 98 route_name='gist_edit', request_method='GET',
99 99 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
100 100
101 101 config.add_route(
102 102 name='gist_update', pattern='/gists/{gist_id}/update')
103 103 config.add_view(
104 104 GistView,
105 105 attr='gist_update',
106 106 route_name='gist_update', request_method='POST',
107 107 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
108 108
109 109 config.add_route(
110 110 name='gist_edit_check_revision',
111 111 pattern='/gists/{gist_id}/edit/check_revision')
112 112 config.add_view(
113 113 GistView,
114 114 attr='gist_edit_check_revision',
115 115 route_name='gist_edit_check_revision', request_method='GET',
116 116 renderer='json_ext')
117 117
118 118
119 119 def includeme(config):
120 120 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
@@ -1,20 +1,20 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
@@ -1,391 +1,390 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import mock
22 21 import pytest
23 22
24 23 from rhodecode.lib import helpers as h
25 24 from rhodecode.model.db import User, Gist
26 25 from rhodecode.model.gist import GistModel
27 26 from rhodecode.model.meta import Session
28 27 from rhodecode.tests import (
29 28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
30 29 TestController, assert_session_flash)
31 30
32 31
33 32 def route_path(name, params=None, **kwargs):
34 33 import urllib.request, urllib.parse, urllib.error
35 34 from rhodecode.apps._base import ADMIN_PREFIX
36 35
37 36 base_url = {
38 37 'gists_show': ADMIN_PREFIX + '/gists',
39 38 'gists_new': ADMIN_PREFIX + '/gists/new',
40 39 'gists_create': ADMIN_PREFIX + '/gists/create',
41 40 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
42 41 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
43 42 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
44 43 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
45 44 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
46 45 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
47 46 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
48 47 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
49 48
50 49 }[name].format(**kwargs)
51 50
52 51 if params:
53 52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 53 return base_url
55 54
56 55
57 56 class GistUtility(object):
58 57
59 58 def __init__(self):
60 59 self._gist_ids = []
61 60
62 61 def __call__(
63 62 self, f_name, content='some gist', lifetime=-1,
64 63 description='gist-desc', gist_type='public',
65 64 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
66 65 gist_mapping = {
67 66 f_name: {'content': content}
68 67 }
69 68 user = User.get_by_username(owner)
70 69 gist = GistModel().create(
71 70 description, owner=user, gist_mapping=gist_mapping,
72 71 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
73 72 Session().commit()
74 73 self._gist_ids.append(gist.gist_id)
75 74 return gist
76 75
77 76 def cleanup(self):
78 77 for gist_id in self._gist_ids:
79 78 gist = Gist.get(gist_id)
80 79 if gist:
81 80 Session().delete(gist)
82 81
83 82 Session().commit()
84 83
85 84
86 85 @pytest.fixture()
87 86 def create_gist(request):
88 87 gist_utility = GistUtility()
89 88 request.addfinalizer(gist_utility.cleanup)
90 89 return gist_utility
91 90
92 91
93 92 class TestGistsController(TestController):
94 93
95 94 def test_index_empty(self, create_gist):
96 95 self.log_user()
97 96 response = self.app.get(route_path('gists_show'))
98 97 response.mustcontain('data: [],')
99 98
100 99 def test_index(self, create_gist):
101 100 self.log_user()
102 101 g1 = create_gist('gist1')
103 102 g2 = create_gist('gist2', lifetime=1400)
104 103 g3 = create_gist('gist3', description='gist3-desc')
105 104 g4 = create_gist('gist4', gist_type='private').gist_access_id
106 105 response = self.app.get(route_path('gists_show'))
107 106
108 107 response.mustcontain(g1.gist_access_id)
109 108 response.mustcontain(g2.gist_access_id)
110 109 response.mustcontain(g3.gist_access_id)
111 110 response.mustcontain('gist3-desc')
112 111 response.mustcontain(no=[g4])
113 112
114 113 # Expiration information should be visible
115 114 expires_tag = '%s' % h.age_component(
116 115 h.time_to_utcdatetime(g2.gist_expires))
117 116 response.mustcontain(expires_tag.replace('"', '\\"'))
118 117
119 118 def test_index_private_gists(self, create_gist):
120 119 self.log_user()
121 120 gist = create_gist('gist5', gist_type='private')
122 121 response = self.app.get(route_path('gists_show', params=dict(private=1)))
123 122
124 123 # and privates
125 124 response.mustcontain(gist.gist_access_id)
126 125
127 126 def test_index_show_all(self, create_gist):
128 127 self.log_user()
129 128 create_gist('gist1')
130 129 create_gist('gist2', lifetime=1400)
131 130 create_gist('gist3', description='gist3-desc')
132 131 create_gist('gist4', gist_type='private')
133 132
134 133 response = self.app.get(route_path('gists_show', params=dict(all=1)))
135 134
136 135 assert len(GistModel.get_all()) == 4
137 136 # and privates
138 137 for gist in GistModel.get_all():
139 138 response.mustcontain(gist.gist_access_id)
140 139
141 140 def test_index_show_all_hidden_from_regular(self, create_gist):
142 141 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
143 142 create_gist('gist2', gist_type='private')
144 143 create_gist('gist3', gist_type='private')
145 144 create_gist('gist4', gist_type='private')
146 145
147 146 response = self.app.get(route_path('gists_show', params=dict(all=1)))
148 147
149 148 assert len(GistModel.get_all()) == 3
150 149 # since we don't have access to private in this view, we
151 150 # should see nothing
152 151 for gist in GistModel.get_all():
153 152 response.mustcontain(no=[gist.gist_access_id])
154 153
155 154 def test_create(self):
156 155 self.log_user()
157 156 response = self.app.post(
158 157 route_path('gists_create'),
159 158 params={'lifetime': -1,
160 159 'content': 'gist test',
161 160 'filename': 'foo',
162 161 'gist_type': 'public',
163 162 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
164 163 'csrf_token': self.csrf_token},
165 164 status=302)
166 165 response = response.follow()
167 166 response.mustcontain('added file: foo')
168 167 response.mustcontain('gist test')
169 168
170 169 def test_create_with_path_with_dirs(self):
171 170 self.log_user()
172 171 response = self.app.post(
173 172 route_path('gists_create'),
174 173 params={'lifetime': -1,
175 174 'content': 'gist test',
176 175 'filename': '/home/foo',
177 176 'gist_type': 'public',
178 177 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
179 178 'csrf_token': self.csrf_token},
180 179 status=200)
181 180 response.mustcontain('Filename /home/foo cannot be inside a directory')
182 181
183 182 def test_access_expired_gist(self, create_gist):
184 183 self.log_user()
185 184 gist = create_gist('never-see-me')
186 185 gist.gist_expires = 0 # 1970
187 186 Session().add(gist)
188 187 Session().commit()
189 188
190 189 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
191 190 status=404)
192 191
193 192 def test_create_private(self):
194 193 self.log_user()
195 194 response = self.app.post(
196 195 route_path('gists_create'),
197 196 params={'lifetime': -1,
198 197 'content': 'private gist test',
199 198 'filename': 'private-foo',
200 199 'gist_type': 'private',
201 200 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
202 201 'csrf_token': self.csrf_token},
203 202 status=302)
204 203 response = response.follow()
205 204 response.mustcontain('added file: private-foo<')
206 205 response.mustcontain('private gist test')
207 206 response.mustcontain('Private Gist')
208 207 # Make sure private gists are not indexed by robots
209 208 response.mustcontain(
210 209 '<meta name="robots" content="noindex, nofollow">')
211 210
212 211 def test_create_private_acl_private(self):
213 212 self.log_user()
214 213 response = self.app.post(
215 214 route_path('gists_create'),
216 215 params={'lifetime': -1,
217 216 'content': 'private gist test',
218 217 'filename': 'private-foo',
219 218 'gist_type': 'private',
220 219 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
221 220 'csrf_token': self.csrf_token},
222 221 status=302)
223 222 response = response.follow()
224 223 response.mustcontain('added file: private-foo<')
225 224 response.mustcontain('private gist test')
226 225 response.mustcontain('Private Gist')
227 226 # Make sure private gists are not indexed by robots
228 227 response.mustcontain(
229 228 '<meta name="robots" content="noindex, nofollow">')
230 229
231 230 def test_create_with_description(self):
232 231 self.log_user()
233 232 response = self.app.post(
234 233 route_path('gists_create'),
235 234 params={'lifetime': -1,
236 235 'content': 'gist test',
237 236 'filename': 'foo-desc',
238 237 'description': 'gist-desc',
239 238 'gist_type': 'public',
240 239 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
241 240 'csrf_token': self.csrf_token},
242 241 status=302)
243 242 response = response.follow()
244 243 response.mustcontain('added file: foo-desc')
245 244 response.mustcontain('gist test')
246 245 response.mustcontain('gist-desc')
247 246
248 247 def test_create_public_with_anonymous_access(self):
249 248 self.log_user()
250 249 params = {
251 250 'lifetime': -1,
252 251 'content': 'gist test',
253 252 'filename': 'foo-desc',
254 253 'description': 'gist-desc',
255 254 'gist_type': 'public',
256 255 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
257 256 'csrf_token': self.csrf_token
258 257 }
259 258 response = self.app.post(
260 259 route_path('gists_create'), params=params, status=302)
261 260 self.logout_user()
262 261 response = response.follow()
263 262 response.mustcontain('added file: foo-desc')
264 263 response.mustcontain('gist test')
265 264 response.mustcontain('gist-desc')
266 265
267 266 def test_new(self):
268 267 self.log_user()
269 268 self.app.get(route_path('gists_new'))
270 269
271 270 def test_delete(self, create_gist):
272 271 self.log_user()
273 272 gist = create_gist('delete-me')
274 273 response = self.app.post(
275 274 route_path('gist_delete', gist_id=gist.gist_id),
276 275 params={'csrf_token': self.csrf_token})
277 276 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
278 277
279 278 def test_delete_normal_user_his_gist(self, create_gist):
280 279 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
281 280 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
282 281
283 282 response = self.app.post(
284 283 route_path('gist_delete', gist_id=gist.gist_id),
285 284 params={'csrf_token': self.csrf_token})
286 285 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
287 286
288 287 def test_delete_normal_user_not_his_own_gist(self, create_gist):
289 288 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
290 289 gist = create_gist('delete-me-2')
291 290
292 291 self.app.post(
293 292 route_path('gist_delete', gist_id=gist.gist_id),
294 293 params={'csrf_token': self.csrf_token}, status=404)
295 294
296 295 def test_show(self, create_gist):
297 296 gist = create_gist('gist-show-me')
298 297 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
299 298
300 299 response.mustcontain('added file: gist-show-me<')
301 300
302 301 assert_response = response.assert_response()
303 302 assert_response.element_equals_to(
304 303 'div.rc-user span.user',
305 304 '<a href="/_profiles/test_admin">test_admin</a>')
306 305
307 306 response.mustcontain('gist-desc')
308 307
309 308 def test_show_without_hg(self, create_gist):
310 309 with mock.patch(
311 310 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
312 311 gist = create_gist('gist-show-me-again')
313 312 self.app.get(
314 313 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
315 314
316 315 def test_show_acl_private(self, create_gist):
317 316 gist = create_gist('gist-show-me-only-when-im-logged-in',
318 317 acl_level=Gist.ACL_LEVEL_PRIVATE)
319 318 self.app.get(
320 319 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
321 320
322 321 # now we log-in we should see thi gist
323 322 self.log_user()
324 323 response = self.app.get(
325 324 route_path('gist_show', gist_id=gist.gist_access_id))
326 325 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
327 326
328 327 assert_response = response.assert_response()
329 328 assert_response.element_equals_to(
330 329 'div.rc-user span.user',
331 330 '<a href="/_profiles/test_admin">test_admin</a>')
332 331 response.mustcontain('gist-desc')
333 332
334 333 def test_show_as_raw(self, create_gist):
335 334 gist = create_gist('gist-show-me', content='GIST CONTENT')
336 335 response = self.app.get(
337 336 route_path('gist_show_formatted',
338 337 gist_id=gist.gist_access_id, revision='tip',
339 338 format='raw'))
340 339 assert response.text == 'GIST CONTENT'
341 340
342 341 def test_show_as_raw_individual_file(self, create_gist):
343 342 gist = create_gist('gist-show-me-raw', content='GIST BODY')
344 343 response = self.app.get(
345 344 route_path('gist_show_formatted_path',
346 345 gist_id=gist.gist_access_id, format='raw',
347 346 revision='tip', f_path='gist-show-me-raw'))
348 347 assert response.text == 'GIST BODY'
349 348
350 349 def test_edit_page(self, create_gist):
351 350 self.log_user()
352 351 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
353 352 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
354 353 response.mustcontain('GIST EDIT BODY')
355 354
356 355 def test_edit_page_non_logged_user(self, create_gist):
357 356 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
358 357 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
359 358 status=302)
360 359
361 360 def test_edit_normal_user_his_gist(self, create_gist):
362 361 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
363 362 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
364 363 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
365 364 status=200))
366 365
367 366 def test_edit_normal_user_not_his_own_gist(self, create_gist):
368 367 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
369 368 gist = create_gist('delete-me')
370 369 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
371 370 status=404)
372 371
373 372 def test_user_first_name_is_escaped(self, user_util, create_gist):
374 373 xss_atack_string = '"><script>alert(\'First Name\')</script>'
375 374 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
376 375 password = 'test'
377 376 user = user_util.create_user(
378 377 firstname=xss_atack_string, password=password)
379 378 create_gist('gist', gist_type='public', owner=user.username)
380 379 response = self.app.get(route_path('gists_show'))
381 380 response.mustcontain(xss_escaped_string)
382 381
383 382 def test_user_last_name_is_escaped(self, user_util, create_gist):
384 383 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
385 384 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
386 385 password = 'test'
387 386 user = user_util.create_user(
388 387 lastname=xss_atack_string, password=password)
389 388 create_gist('gist', gist_type='public', owner=user.username)
390 389 response = self.app.get(route_path('gists_show'))
391 390 response.mustcontain(xss_escaped_string)
@@ -1,378 +1,378 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2013-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound, HTTPBadRequest
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode.apps._base import BaseAppView
33 33 from rhodecode.lib import helpers as h, ext_json
34 34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 35 from rhodecode.lib.utils2 import time_to_datetime
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 38 from rhodecode.model.gist import GistModel
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.db import Gist, User, or_
41 41 from rhodecode.model import validation_schema
42 42 from rhodecode.model.validation_schema.schemas import gist_schema
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class GistView(BaseAppView):
49 49
50 50 def load_default_context(self):
51 51 _ = self.request.translate
52 52 c = self._get_local_tmpl_context()
53 53 c.user = c.auth_user.get_instance()
54 54
55 55 c.lifetime_values = [
56 56 (-1, _('forever')),
57 57 (5, _('5 minutes')),
58 58 (60, _('1 hour')),
59 59 (60 * 24, _('1 day')),
60 60 (60 * 24 * 30, _('1 month')),
61 61 ]
62 62
63 63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 64 c.acl_options = [
65 65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 67 ]
68 68
69 69 return c
70 70
71 71 @LoginRequired()
72 72 def gist_show_all(self):
73 73 c = self.load_default_context()
74 74
75 75 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
76 76 c.show_private = self.request.GET.get('private') and not_default_user
77 77 c.show_public = self.request.GET.get('public') and not_default_user
78 78 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
79 79
80 80 gists = _gists = Gist().query()\
81 81 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
82 82 .order_by(Gist.created_on.desc())
83 83
84 84 c.active = 'public'
85 85 # MY private
86 86 if c.show_private and not c.show_public:
87 87 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
88 88 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
89 89 c.active = 'my_private'
90 90 # MY public
91 91 elif c.show_public and not c.show_private:
92 92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
93 93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
94 94 c.active = 'my_public'
95 95 # MY public+private
96 96 elif c.show_private and c.show_public:
97 97 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
98 98 Gist.gist_type == Gist.GIST_PRIVATE))\
99 99 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
100 100 c.active = 'my_all'
101 101 # Show all by super-admin
102 102 elif c.show_all:
103 103 c.active = 'all'
104 104 gists = _gists
105 105
106 106 # default show ALL public gists
107 107 if not c.show_public and not c.show_private and not c.show_all:
108 108 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
109 109 c.active = 'public'
110 110
111 111 _render = self.request.get_partial_renderer(
112 112 'rhodecode:templates/data_table/_dt_elements.mako')
113 113
114 114 data = []
115 115
116 116 for gist in gists:
117 117 data.append({
118 118 'created_on': _render('gist_created', gist.created_on),
119 119 'created_on_raw': gist.created_on,
120 120 'type': _render('gist_type', gist.gist_type),
121 121 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
122 122 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
123 123 'author_raw': h.escape(gist.owner.full_contact),
124 124 'expires': _render('gist_expires', gist.gist_expires),
125 125 'description': _render('gist_description', gist.gist_description)
126 126 })
127 127 c.data = ext_json.str_json(data)
128 128
129 129 return self._get_template_context(c)
130 130
131 131 @LoginRequired()
132 132 @NotAnonymous()
133 133 def gist_new(self):
134 134 c = self.load_default_context()
135 135 return self._get_template_context(c)
136 136
137 137 @LoginRequired()
138 138 @NotAnonymous()
139 139 @CSRFRequired()
140 140 def gist_create(self):
141 141 _ = self.request.translate
142 142 c = self.load_default_context()
143 143
144 144 data = dict(self.request.POST)
145 145
146 146 filename = data.pop('filename', '') or Gist.DEFAULT_FILENAME
147 147
148 148 data['nodes'] = [{
149 149 'filename': filename,
150 150 'content': data.pop('content', ''),
151 151 'mimetype': data.pop('mimetype', None) # None is autodetect
152 152 }]
153 153
154 154 schema = gist_schema.GistSchema().bind(
155 155 lifetime_options=[x[0] for x in c.lifetime_values])
156 156
157 157 try:
158 158
159 159 schema_data = schema.deserialize(data)
160 160
161 161 # convert to safer format with just KEYs so we sure no duplicates
162 162 schema_data['nodes'] = gist_schema.sequence_to_nodes(schema_data['nodes'])
163 163
164 164 gist = GistModel().create(
165 165 gist_id=schema_data['gistid'], # custom access id not real ID
166 166 description=schema_data['description'],
167 167 owner=self._rhodecode_user.user_id,
168 168 gist_mapping=schema_data['nodes'],
169 169 gist_type=schema_data['gist_type'],
170 170 lifetime=schema_data['lifetime'],
171 171 gist_acl_level=schema_data['gist_acl_level']
172 172 )
173 173 Session().commit()
174 174 new_gist_id = gist.gist_access_id
175 175 except validation_schema.Invalid as errors:
176 176 defaults = data
177 177 errors = errors.asdict()
178 178
179 179 if 'nodes.0.content' in errors:
180 180 errors['content'] = errors['nodes.0.content']
181 181 del errors['nodes.0.content']
182 182 if 'nodes.0.filename' in errors:
183 183 errors['filename'] = errors['nodes.0.filename']
184 184 del errors['nodes.0.filename']
185 185
186 186 data = render('rhodecode:templates/admin/gists/gist_new.mako',
187 187 self._get_template_context(c), self.request)
188 188 html = formencode.htmlfill.render(
189 189 data,
190 190 defaults=defaults,
191 191 errors=errors,
192 192 prefix_error=False,
193 193 encoding="UTF-8",
194 194 force_defaults=False
195 195 )
196 196 return Response(html)
197 197
198 198 except Exception:
199 199 log.exception("Exception while trying to create a gist")
200 200 h.flash(_('Error occurred during gist creation'), category='error')
201 201 raise HTTPFound(h.route_url('gists_new'))
202 202 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
203 203
204 204 @LoginRequired()
205 205 @NotAnonymous()
206 206 @CSRFRequired()
207 207 def gist_delete(self):
208 208 _ = self.request.translate
209 209 gist_id = self.request.matchdict['gist_id']
210 210
211 211 c = self.load_default_context()
212 212 c.gist = Gist.get_or_404(gist_id)
213 213
214 214 owner = c.gist.gist_owner == self._rhodecode_user.user_id
215 215 if not (h.HasPermissionAny('hg.admin')() or owner):
216 216 log.warning('Deletion of Gist was forbidden '
217 217 'by unauthorized user: `%s`', self._rhodecode_user)
218 218 raise HTTPNotFound()
219 219
220 220 GistModel().delete(c.gist)
221 221 Session().commit()
222 222 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
223 223
224 224 raise HTTPFound(h.route_url('gists_show'))
225 225
226 226 def _get_gist(self, gist_id):
227 227
228 228 gist = Gist.get_or_404(gist_id)
229 229
230 230 # Check if this gist is expired
231 231 if gist.gist_expires != -1:
232 232 if time.time() > gist.gist_expires:
233 233 log.error(
234 234 'Gist expired at %s', time_to_datetime(gist.gist_expires))
235 235 raise HTTPNotFound()
236 236
237 237 # check if this gist requires a login
238 238 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
239 239 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
240 240 log.error("Anonymous user %s tried to access protected gist `%s`",
241 241 self._rhodecode_user, gist_id)
242 242 raise HTTPNotFound()
243 243 return gist
244 244
245 245 @LoginRequired()
246 246 def gist_show(self):
247 247 gist_id = self.request.matchdict['gist_id']
248 248
249 249 # TODO(marcink): expose those via matching dict
250 250 revision = self.request.matchdict.get('revision', 'tip')
251 251 f_path = self.request.matchdict.get('f_path', None)
252 252 return_format = self.request.matchdict.get('format')
253 253
254 254 c = self.load_default_context()
255 255 c.gist = self._get_gist(gist_id)
256 256 c.render = not self.request.GET.get('no-render', False)
257 257
258 258 try:
259 259 c.file_last_commit, c.files = GistModel().get_gist_files(
260 260 gist_id, revision=revision)
261 261 except VCSError:
262 262 log.exception("Exception in gist show")
263 263 raise HTTPNotFound()
264 264
265 265 if return_format == 'raw':
266 266 content = '\n\n'.join([f.content for f in c.files
267 267 if (f_path is None or f.path == f_path)])
268 268 response = Response(content)
269 269 response.content_type = 'text/plain'
270 270 return response
271 271 elif return_format:
272 272 raise HTTPBadRequest()
273 273
274 274 return self._get_template_context(c)
275 275
276 276 @LoginRequired()
277 277 @NotAnonymous()
278 278 def gist_edit(self):
279 279 _ = self.request.translate
280 280 gist_id = self.request.matchdict['gist_id']
281 281 c = self.load_default_context()
282 282 c.gist = self._get_gist(gist_id)
283 283
284 284 owner = c.gist.gist_owner == self._rhodecode_user.user_id
285 285 if not (h.HasPermissionAny('hg.admin')() or owner):
286 286 raise HTTPNotFound()
287 287
288 288 try:
289 289 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
290 290 except VCSError:
291 291 log.exception("Exception in gist edit")
292 292 raise HTTPNotFound()
293 293
294 294 if c.gist.gist_expires == -1:
295 295 expiry = _('never')
296 296 else:
297 297 # this cannot use timeago, since it's used in select2 as a value
298 298 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
299 299
300 300 c.lifetime_values.append(
301 301 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
302 302 )
303 303
304 304 return self._get_template_context(c)
305 305
306 306 @LoginRequired()
307 307 @NotAnonymous()
308 308 @CSRFRequired()
309 309 def gist_update(self):
310 310 _ = self.request.translate
311 311 gist_id = self.request.matchdict['gist_id']
312 312 c = self.load_default_context()
313 313 c.gist = self._get_gist(gist_id)
314 314
315 315 owner = c.gist.gist_owner == self._rhodecode_user.user_id
316 316 if not (h.HasPermissionAny('hg.admin')() or owner):
317 317 raise HTTPNotFound()
318 318
319 319 data = peppercorn.parse(self.request.POST.items())
320 320
321 321 schema = gist_schema.GistSchema()
322 322 schema = schema.bind(
323 323 # '0' is special value to leave lifetime untouched
324 324 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
325 325 )
326 326
327 327 try:
328 328 schema_data = schema.deserialize(data)
329 329 # convert to safer format with just KEYs so we sure no duplicates
330 330 schema_data['nodes'] = gist_schema.sequence_to_nodes(
331 331 schema_data['nodes'])
332 332
333 333 GistModel().update(
334 334 gist=c.gist,
335 335 description=schema_data['description'],
336 336 owner=c.gist.owner,
337 337 gist_mapping=schema_data['nodes'],
338 338 lifetime=schema_data['lifetime'],
339 339 gist_acl_level=schema_data['gist_acl_level']
340 340 )
341 341
342 342 Session().commit()
343 343 h.flash(_('Successfully updated gist content'), category='success')
344 344 except NodeNotChangedError:
345 345 # raised if nothing was changed in repo itself. We anyway then
346 346 # store only DB stuff for gist
347 347 Session().commit()
348 348 h.flash(_('Successfully updated gist data'), category='success')
349 349 except validation_schema.Invalid as errors:
350 350 errors = h.escape(errors.asdict())
351 351 h.flash(_('Error occurred during update of gist {}: {}').format(
352 352 gist_id, errors), category='error')
353 353 except Exception:
354 354 log.exception("Exception in gist edit")
355 355 h.flash(_('Error occurred during update of gist %s') % gist_id,
356 356 category='error')
357 357
358 358 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
359 359
360 360 @LoginRequired()
361 361 @NotAnonymous()
362 362 def gist_edit_check_revision(self):
363 363 _ = self.request.translate
364 364 gist_id = self.request.matchdict['gist_id']
365 365 c = self.load_default_context()
366 366 c.gist = self._get_gist(gist_id)
367 367
368 368 last_rev = c.gist.scm_instance().get_commit()
369 369 success = True
370 370 revision = self.request.GET.get('revision')
371 371
372 372 if revision != last_rev.raw_id:
373 373 log.error('Last revision %s is different then submitted %s',
374 374 revision, last_rev)
375 375 # our gist has newer version than we
376 376 success = False
377 377
378 378 return {'success': success}
@@ -1,147 +1,147 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.config import routing_links
21 21
22 22
23 23 class VCSCallPredicate(object):
24 24 def __init__(self, val, config):
25 25 self.val = val
26 26
27 27 def text(self):
28 28 return 'vcs_call route = %s' % self.val
29 29
30 30 phash = text
31 31
32 32 def __call__(self, info, request):
33 33 if hasattr(request, 'vcs_call'):
34 34 # skip vcs calls
35 35 return False
36 36
37 37 return True
38 38
39 39
40 40 def includeme(config):
41 41 from rhodecode.apps.home.views import HomeView
42 42
43 43 config.add_route_predicate(
44 44 'skip_vcs_call', VCSCallPredicate)
45 45
46 46 config.add_route(
47 47 name='home',
48 48 pattern='/')
49 49 config.add_view(
50 50 HomeView,
51 51 attr='main_page',
52 52 route_name='home', request_method='GET',
53 53 renderer='rhodecode:templates/index.mako')
54 54
55 55 config.add_route(
56 56 name='main_page_repos_data',
57 57 pattern='/_home_repos')
58 58 config.add_view(
59 59 HomeView,
60 60 attr='main_page_repos_data',
61 61 route_name='main_page_repos_data',
62 62 request_method='GET', renderer='json_ext', xhr=True)
63 63
64 64 config.add_route(
65 65 name='main_page_repo_groups_data',
66 66 pattern='/_home_repo_groups')
67 67 config.add_view(
68 68 HomeView,
69 69 attr='main_page_repo_groups_data',
70 70 route_name='main_page_repo_groups_data',
71 71 request_method='GET', renderer='json_ext', xhr=True)
72 72
73 73 config.add_route(
74 74 name='user_autocomplete_data',
75 75 pattern='/_users')
76 76 config.add_view(
77 77 HomeView,
78 78 attr='user_autocomplete_data',
79 79 route_name='user_autocomplete_data', request_method='GET',
80 80 renderer='json_ext', xhr=True)
81 81
82 82 config.add_route(
83 83 name='user_group_autocomplete_data',
84 84 pattern='/_user_groups')
85 85 config.add_view(
86 86 HomeView,
87 87 attr='user_group_autocomplete_data',
88 88 route_name='user_group_autocomplete_data', request_method='GET',
89 89 renderer='json_ext', xhr=True)
90 90
91 91 config.add_route(
92 92 name='repo_list_data',
93 93 pattern='/_repos')
94 94 config.add_view(
95 95 HomeView,
96 96 attr='repo_list_data',
97 97 route_name='repo_list_data', request_method='GET',
98 98 renderer='json_ext', xhr=True)
99 99
100 100 config.add_route(
101 101 name='repo_group_list_data',
102 102 pattern='/_repo_groups')
103 103 config.add_view(
104 104 HomeView,
105 105 attr='repo_group_list_data',
106 106 route_name='repo_group_list_data', request_method='GET',
107 107 renderer='json_ext', xhr=True)
108 108
109 109 config.add_route(
110 110 name='goto_switcher_data',
111 111 pattern='/_goto_data')
112 112 config.add_view(
113 113 HomeView,
114 114 attr='goto_switcher_data',
115 115 route_name='goto_switcher_data', request_method='GET',
116 116 renderer='json_ext', xhr=True)
117 117
118 118 config.add_route(
119 119 name='markup_preview',
120 120 pattern='/_markup_preview')
121 121 config.add_view(
122 122 HomeView,
123 123 attr='markup_preview',
124 124 route_name='markup_preview', request_method='POST',
125 125 renderer='string', xhr=True)
126 126
127 127 config.add_route(
128 128 name='file_preview',
129 129 pattern='/_file_preview')
130 130 config.add_view(
131 131 HomeView,
132 132 attr='file_preview',
133 133 route_name='file_preview', request_method='POST',
134 134 renderer='string', xhr=True)
135 135
136 136 config.add_route(
137 137 name='store_user_session_value',
138 138 pattern='/_store_session_attr')
139 139 config.add_view(
140 140 HomeView,
141 141 attr='store_user_session_attr',
142 142 route_name='store_user_session_value', request_method='POST',
143 143 renderer='string', xhr=True)
144 144
145 145 # register our static links via redirection mechanism
146 146 routing_links.connect_redirection_links(config)
147 147
@@ -1,69 +1,69 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 def assert_and_get_main_filter_content(result):
23 23 repos = []
24 24 groups = []
25 25 commits = []
26 26 users = []
27 27 for data_item in result:
28 28 assert data_item['id']
29 29 assert data_item['value']
30 30 assert data_item['value_display']
31 31 assert data_item['url']
32 32
33 33 if data_item['type'] == 'search':
34 34 display_val = data_item['value_display']
35 35 if data_item['id'] == -1:
36 36 assert 'File content search for:' in display_val, display_val
37 37 elif data_item['id'] == -2:
38 38 assert 'Commit search for:' in display_val, display_val
39 39 else:
40 40 assert False, 'No Proper ID returned {}'.format(data_item['id'])
41 41
42 42 elif data_item['type'] == 'repo':
43 43 repos.append(data_item)
44 44 elif data_item['type'] == 'repo_group':
45 45 groups.append(data_item)
46 46 elif data_item['type'] == 'user':
47 47 users.append(data_item)
48 48 elif data_item['type'] == 'commit':
49 49 commits.append(data_item)
50 50 else:
51 51 raise Exception('invalid type `%s`' % data_item['type'])
52 52
53 53 return repos, groups, users, commits
54 54
55 55
56 56 def assert_and_get_repo_list_content(result):
57 57 repos = []
58 58 for data in result:
59 59 for data_item in data['children']:
60 60 assert data_item['id']
61 61 assert data_item['text']
62 62 assert data_item['url']
63 63
64 64 if data_item['type'] == 'repo':
65 65 repos.append(data_item)
66 66 else:
67 67 raise Exception('invalid type %s' % data_item['type'])
68 68
69 69 return repos
@@ -1,180 +1,181 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 import json
22
23 21 import pytest
24 22
25 23 from . import assert_and_get_main_filter_content
26 24 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
27 25 from rhodecode.tests.fixture import Fixture
28 26
29 27 from rhodecode.lib.utils import map_groups
28 from rhodecode.lib.ext_json import json
30 29 from rhodecode.model.repo import RepoModel
31 30 from rhodecode.model.repo_group import RepoGroupModel
32 31 from rhodecode.model.db import Session, Repository, RepoGroup
33 32
34 33 fixture = Fixture()
35 34
36 35
37 36 def route_path(name, params=None, **kwargs):
38 import urllib.request, urllib.parse, urllib.error
37 import urllib.request
38 import urllib.parse
39 import urllib.error
39 40
40 41 base_url = {
41 42 'goto_switcher_data': '/_goto_data',
42 43 }[name].format(**kwargs)
43 44
44 45 if params:
45 46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 47 return base_url
47 48
48 49
49 50 class TestGotoSwitcherData(TestController):
50 51
51 52 required_repos_with_groups = [
52 53 'abc',
53 54 'abc-fork',
54 55 'forks/abcd',
55 56 'abcd',
56 57 'abcde',
57 58 'a/abc',
58 59 'aa/abc',
59 60 'aaa/abc',
60 61 'aaaa/abc',
61 62 'repos_abc/aaa/abc',
62 63 'abc_repos/abc',
63 64 'abc_repos/abcd',
64 65 'xxx/xyz',
65 66 'forked-abc/a/abc'
66 67 ]
67 68
68 69 @pytest.fixture(autouse=True, scope='class')
69 70 def prepare(self, request, baseapp):
70 71 for repo_and_group in self.required_repos_with_groups:
71 72 # create structure of groups and return the last group
72 73
73 74 repo_group = map_groups(repo_and_group)
74 75
75 76 RepoModel()._create_repo(
76 77 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
77 78 repo_group=getattr(repo_group, 'group_id', None))
78 79
79 80 Session().commit()
80 81
81 82 request.addfinalizer(self.cleanup)
82 83
83 84 def cleanup(self):
84 85 # first delete all repos
85 86 for repo_and_groups in self.required_repos_with_groups:
86 87 repo = Repository.get_by_repo_name(repo_and_groups)
87 88 if repo:
88 89 RepoModel().delete(repo)
89 90 Session().commit()
90 91
91 92 # then delete all empty groups
92 93 for repo_and_groups in self.required_repos_with_groups:
93 94 if '/' in repo_and_groups:
94 95 r_group = repo_and_groups.rsplit('/', 1)[0]
95 96 repo_group = RepoGroup.get_by_group_name(r_group)
96 97 if not repo_group:
97 98 continue
98 99 parents = repo_group.parents
99 100 RepoGroupModel().delete(repo_group, force_delete=True)
100 101 Session().commit()
101 102
102 103 for el in reversed(parents):
103 104 RepoGroupModel().delete(el, force_delete=True)
104 105 Session().commit()
105 106
106 107 def test_empty_query(self, xhr_header):
107 108 self.log_user()
108 109
109 110 response = self.app.get(
110 111 route_path('goto_switcher_data'),
111 112 extra_environ=xhr_header, status=200)
112 113 result = json.loads(response.body)['suggestions']
113 114
114 115 assert result == []
115 116
116 117 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
117 118 self.log_user()
118 119
119 120 response = self.app.get(
120 121 route_path('goto_switcher_data'),
121 122 params={'query': 'abc'},
122 123 extra_environ=xhr_header, status=200)
123 124 result = json.loads(response.body)['suggestions']
124 125
125 126 repos, groups, users, commits = assert_and_get_main_filter_content(result)
126 127
127 128 assert len(repos) == 13
128 129 assert len(groups) == 5
129 130 assert len(users) == 0
130 131 assert len(commits) == 0
131 132
132 133 def test_returns_list_of_users_filtered(self, xhr_header):
133 134 self.log_user()
134 135
135 136 response = self.app.get(
136 137 route_path('goto_switcher_data'),
137 138 params={'query': 'user:admin'},
138 139 extra_environ=xhr_header, status=200)
139 140 result = json.loads(response.body)['suggestions']
140 141
141 142 repos, groups, users, commits = assert_and_get_main_filter_content(result)
142 143
143 144 assert len(repos) == 0
144 145 assert len(groups) == 0
145 146 assert len(users) == 1
146 147 assert len(commits) == 0
147 148
148 149 def test_returns_list_of_commits_filtered(self, xhr_header):
149 150 self.log_user()
150 151
151 152 response = self.app.get(
152 153 route_path('goto_switcher_data'),
153 154 params={'query': 'commit:e8'},
154 155 extra_environ=xhr_header, status=200)
155 156 result = json.loads(response.body)['suggestions']
156 157
157 158 repos, groups, users, commits = assert_and_get_main_filter_content(result)
158 159
159 160 assert len(repos) == 0
160 161 assert len(groups) == 0
161 162 assert len(users) == 0
162 163 assert len(commits) == 5
163 164
164 165 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
165 166 self.log_user()
166 167
167 168 response = self.app.get(
168 169 route_path('goto_switcher_data'),
169 170 params={'query': 'abc'},
170 171 extra_environ=xhr_header, status=200)
171 172 result = json.loads(response.body)['suggestions']
172 173
173 174 repos, groups, users, commits = assert_and_get_main_filter_content(result)
174 175
175 176 test_repos = [x['value_display'] for x in repos[:4]]
176 177 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
177 178
178 179 test_groups = [x['value_display'] for x in groups[:4]]
179 180 assert ['abc_repos', 'repos_abc',
180 181 'forked-abc', 'forked-abc/a'] == test_groups
@@ -1,95 +1,95 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import json
22 22
23 23 from . import assert_and_get_repo_list_content
24 24 from rhodecode.tests import TestController
25 25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.model.db import Repository
27 27
28 28 fixture = Fixture()
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 32 import urllib.request, urllib.parse, urllib.error
33 33
34 34 base_url = {
35 35 'repo_list_data': '/_repos',
36 36 }[name].format(**kwargs)
37 37
38 38 if params:
39 39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 40 return base_url
41 41
42 42
43 43 class TestRepoListData(TestController):
44 44
45 45 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 46 self.log_user()
47 47
48 48 response = self.app.get(
49 49 route_path('repo_list_data'),
50 50 extra_environ=xhr_header, status=200)
51 51 result = json.loads(response.body)['results']
52 52
53 53 repos = assert_and_get_repo_list_content(result)
54 54
55 55 assert len(repos) == len(Repository.get_all())
56 56
57 57 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
58 58 self.log_user()
59 59
60 60 response = self.app.get(
61 61 route_path('repo_list_data'),
62 62 params={'query': 'vcs_test_git'},
63 63 extra_environ=xhr_header, status=200)
64 64 result = json.loads(response.body)['results']
65 65
66 66 repos = assert_and_get_repo_list_content(result)
67 67
68 68 assert len(repos) == len(Repository.query().filter(
69 69 Repository.repo_name.ilike('%vcs_test_git%')).all())
70 70
71 71 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
72 72 self.log_user()
73 73
74 74 response = self.app.get(
75 75 route_path('repo_list_data'),
76 76 params={'query': 'vcs_test_git', 'repo_type': 'git'},
77 77 extra_environ=xhr_header, status=200)
78 78 result = json.loads(response.body)['results']
79 79
80 80 repos = assert_and_get_repo_list_content(result)
81 81
82 82 assert len(repos) == len(Repository.query().filter(
83 83 Repository.repo_name.ilike('%vcs_test_git%')).all())
84 84
85 85 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
86 86 self.log_user()
87 87 response = self.app.get(
88 88 route_path('repo_list_data'),
89 89 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
90 90 extra_environ=xhr_header, status=200)
91 91 result = json.loads(response.body)['results']
92 92
93 93 repos = assert_and_get_repo_list_content(result)
94 94
95 95 assert len(repos) == 0
@@ -1,112 +1,112 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import json
22 22 import pytest
23 23
24 24 from rhodecode.tests import TestController
25 25 from rhodecode.tests.fixture import Fixture
26 26
27 27
28 28 fixture = Fixture()
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 32 import urllib.request, urllib.parse, urllib.error
33 33
34 34 base_url = {
35 35 'user_autocomplete_data': '/_users',
36 36 'user_group_autocomplete_data': '/_user_groups'
37 37 }[name].format(**kwargs)
38 38
39 39 if params:
40 40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 41 return base_url
42 42
43 43
44 44 class TestUserAutocompleteData(TestController):
45 45
46 46 def test_returns_list_of_users(self, user_util, xhr_header):
47 47 self.log_user()
48 48 user = user_util.create_user(active=True)
49 49 user_name = user.username
50 50 response = self.app.get(
51 51 route_path('user_autocomplete_data'),
52 52 extra_environ=xhr_header, status=200)
53 53 result = json.loads(response.body)
54 54 values = [suggestion['value'] for suggestion in result['suggestions']]
55 55 assert user_name in values
56 56
57 57 def test_returns_inactive_users_when_active_flag_sent(
58 58 self, user_util, xhr_header):
59 59 self.log_user()
60 60 user = user_util.create_user(active=False)
61 61 user_name = user.username
62 62
63 63 response = self.app.get(
64 64 route_path('user_autocomplete_data',
65 65 params=dict(user_groups='true', active='0')),
66 66 extra_environ=xhr_header, status=200)
67 67 result = json.loads(response.body)
68 68 values = [suggestion['value'] for suggestion in result['suggestions']]
69 69 assert user_name in values
70 70
71 71 response = self.app.get(
72 72 route_path('user_autocomplete_data',
73 73 params=dict(user_groups='true', active='1')),
74 74 extra_environ=xhr_header, status=200)
75 75 result = json.loads(response.body)
76 76 values = [suggestion['value'] for suggestion in result['suggestions']]
77 77 assert user_name not in values
78 78
79 79 def test_returns_groups_when_user_groups_flag_sent(
80 80 self, user_util, xhr_header):
81 81 self.log_user()
82 82 group = user_util.create_user_group(user_groups_active=True)
83 83 group_name = group.users_group_name
84 84 response = self.app.get(
85 85 route_path('user_autocomplete_data',
86 86 params=dict(user_groups='true')),
87 87 extra_environ=xhr_header, status=200)
88 88 result = json.loads(response.body)
89 89 values = [suggestion['value'] for suggestion in result['suggestions']]
90 90 assert group_name in values
91 91
92 92 @pytest.mark.parametrize('query, count', [
93 93 ('hello1', 0),
94 94 ('dev', 2),
95 95 ])
96 96 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
97 97 query, count):
98 98 self.log_user()
99 99
100 100 user_util._test_name = 'dev-test'
101 101 user_util.create_user()
102 102
103 103 user_util._test_name = 'dev-group-test'
104 104 user_util.create_user_group()
105 105
106 106 response = self.app.get(
107 107 route_path('user_autocomplete_data',
108 108 params=dict(user_groups='true', query=query)),
109 109 extra_environ=xhr_header, status=200)
110 110
111 111 result = json.loads(response.body)
112 112 assert len(result['suggestions']) == count
@@ -1,117 +1,116 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2016-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19 # -*- coding: utf-8 -*-
21 20
22 21 # Copyright (C) 2016-2020 RhodeCode GmbH
23 22 #
24 23 # This program is free software: you can redistribute it and/or modify
25 24 # it under the terms of the GNU Affero General Public License, version 3
26 25 # (only), as published by the Free Software Foundation.
27 26 #
28 27 # This program is distributed in the hope that it will be useful,
29 28 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 29 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 30 # GNU General Public License for more details.
32 31 #
33 32 # You should have received a copy of the GNU Affero General Public License
34 33 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 34 #
36 35 # This program is dual-licensed. If you wish to learn more about the
37 36 # RhodeCode Enterprise Edition, including its added features, Support services,
38 37 # and proprietary license terms, please see https://rhodecode.com/licenses/
39 38
40 39 import json
41 40
42 41 import pytest
43 42
44 43 from rhodecode.tests import TestController
45 44 from rhodecode.tests.fixture import Fixture
46 45
47 46
48 47 fixture = Fixture()
49 48
50 49
51 50 def route_path(name, params=None, **kwargs):
52 51 import urllib.request, urllib.parse, urllib.error
53 52
54 53 base_url = {
55 54 'user_autocomplete_data': '/_users',
56 55 'user_group_autocomplete_data': '/_user_groups'
57 56 }[name].format(**kwargs)
58 57
59 58 if params:
60 59 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
61 60 return base_url
62 61
63 62
64 63 class TestUserGroupAutocompleteData(TestController):
65 64
66 65 def test_returns_list_of_user_groups(self, user_util, xhr_header):
67 66 self.log_user()
68 67 user_group = user_util.create_user_group(active=True)
69 68 user_group_name = user_group.users_group_name
70 69 response = self.app.get(
71 70 route_path('user_group_autocomplete_data'),
72 71 extra_environ=xhr_header, status=200)
73 72 result = json.loads(response.body)
74 73 values = [suggestion['value'] for suggestion in result['suggestions']]
75 74 assert user_group_name in values
76 75
77 76 def test_returns_inactive_user_groups_when_active_flag_sent(
78 77 self, user_util, xhr_header):
79 78 self.log_user()
80 79 user_group = user_util.create_user_group(active=False)
81 80 user_group_name = user_group.users_group_name
82 81
83 82 response = self.app.get(
84 83 route_path('user_group_autocomplete_data',
85 84 params=dict(active='0')),
86 85 extra_environ=xhr_header, status=200)
87 86 result = json.loads(response.body)
88 87 values = [suggestion['value'] for suggestion in result['suggestions']]
89 88 assert user_group_name in values
90 89
91 90 response = self.app.get(
92 91 route_path('user_group_autocomplete_data',
93 92 params=dict(active='1')),
94 93 extra_environ=xhr_header, status=200)
95 94 result = json.loads(response.body)
96 95 values = [suggestion['value'] for suggestion in result['suggestions']]
97 96 assert user_group_name not in values
98 97
99 98 @pytest.mark.parametrize('query, count', [
100 99 ('hello1', 0),
101 100 ('dev', 1),
102 101 ])
103 102 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
104 103 self.log_user()
105 104
106 105 user_util._test_name = 'dev-test'
107 106 user_util.create_user_group()
108 107
109 108 response = self.app.get(
110 109 route_path('user_group_autocomplete_data',
111 110 params=dict(user_groups='true',
112 111 query=query)),
113 112 extra_environ=xhr_header, status=200)
114 113
115 114 result = json.loads(response.body)
116 115
117 116 assert len(result['suggestions']) == count
@@ -1,179 +1,178 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20
22 21 import pytest
23 22
24 23 import rhodecode
25 24 from rhodecode.model.db import Repository, RepoGroup, User
26 25 from rhodecode.model.meta import Session
27 26 from rhodecode.model.repo import RepoModel
28 27 from rhodecode.model.repo_group import RepoGroupModel
29 28 from rhodecode.model.settings import SettingsModel
30 29 from rhodecode.tests import TestController
31 30 from rhodecode.tests.fixture import Fixture
32 31 from rhodecode.lib import helpers as h
33 32
34 33 fixture = Fixture()
35 34
36 35
37 36 def route_path(name, **kwargs):
38 37 return {
39 38 'home': '/',
40 39 'main_page_repos_data': '/_home_repos',
41 40 'main_page_repo_groups_data': '/_home_repo_groups',
42 41 'repo_group_home': '/{repo_group_name}'
43 42 }[name].format(**kwargs)
44 43
45 44
46 45 class TestHomeController(TestController):
47 46
48 47 def test_index(self):
49 48 self.log_user()
50 49 response = self.app.get(route_path('home'))
51 50 # if global permission is set
52 51 response.mustcontain('New Repository')
53 52
54 53 def test_index_grid_repos(self, xhr_header):
55 54 self.log_user()
56 55 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
57 56 # search for objects inside the JavaScript JSON
58 57 for obj in Repository.getAll():
59 58 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
60 59
61 60 def test_index_grid_repo_groups(self, xhr_header):
62 61 self.log_user()
63 62 response = self.app.get(route_path('main_page_repo_groups_data'),
64 63 extra_environ=xhr_header,)
65 64
66 65 # search for objects inside the JavaScript JSON
67 66 for obj in RepoGroup.getAll():
68 67 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
69 68
70 69 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
71 70 user = user_util.create_user(password='qweqwe')
72 71 group_ok = user_util.create_repo_group(owner=user)
73 72 group_id_ok = group_ok.group_id
74 73
75 74 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
76 75 group_id_forbidden = group_forbidden.group_id
77 76
78 77 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
79 78 self.log_user(user.username, 'qweqwe')
80 79
81 80 self.app.get(route_path('main_page_repo_groups_data'),
82 81 extra_environ=xhr_header,
83 82 params={'repo_group_id': group_id_ok}, status=200)
84 83
85 84 self.app.get(route_path('main_page_repo_groups_data'),
86 85 extra_environ=xhr_header,
87 86 params={'repo_group_id': group_id_forbidden}, status=404)
88 87
89 88 def test_index_contains_statics_with_ver(self):
90 89 from rhodecode.lib.base import calculate_version_hash
91 90
92 91 self.log_user()
93 92 response = self.app.get(route_path('home'))
94 93
95 94 rhodecode_version_hash = calculate_version_hash(
96 95 {'beaker.session.secret': 'test-rc-uytcxaz'})
97 96 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
98 97 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
99 98
100 99 def test_index_contains_backend_specific_details(self, backend, xhr_header):
101 100 self.log_user()
102 101 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
103 102 tip = backend.repo.get_commit().raw_id
104 103
105 104 # html in javascript variable:
106 105 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
107 106 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
108 107
109 108 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
110 109 response.mustcontain("""Added a symlink""")
111 110
112 111 def test_index_with_anonymous_access_disabled(self):
113 112 with fixture.anon_access(False):
114 113 response = self.app.get(route_path('home'), status=302)
115 114 assert 'login' in response.location
116 115
117 116 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
118 117 group_id = 918123
119 118 self.app.get(
120 119 route_path('main_page_repo_groups_data'),
121 120 params={'repo_group_id': group_id},
122 121 status=404, extra_environ=xhr_header)
123 122
124 123 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
125 124 gr = user_util.create_repo_group()
126 125 repo = user_util.create_repo(parent=gr)
127 126 repo_name = repo.repo_name
128 127 group_id = gr.group_id
129 128
130 129 response = self.app.get(route_path(
131 130 'repo_group_home', repo_group_name=gr.group_name))
132 131 response.mustcontain('d.repo_group_id = {}'.format(group_id))
133 132
134 133 response = self.app.get(
135 134 route_path('main_page_repos_data'),
136 135 params={'repo_group_id': group_id},
137 136 extra_environ=xhr_header,)
138 137 response.mustcontain(repo_name)
139 138
140 139 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
141 140 gr = user_util.create_repo_group()
142 141 repo = user_util.create_repo(parent=gr)
143 142 repo_name = repo.repo_name
144 143 group_id = gr.group_id
145 144
146 145 response = self.app.get(route_path(
147 146 'repo_group_home', repo_group_name=gr.group_name+'/'))
148 147 response.mustcontain('d.repo_group_id = {}'.format(group_id))
149 148
150 149 response = self.app.get(
151 150 route_path('main_page_repos_data'),
152 151 params={'repo_group_id': group_id},
153 152 extra_environ=xhr_header, )
154 153 response.mustcontain(repo_name)
155 154
156 155 @pytest.mark.parametrize("name, state", [
157 156 ('Disabled', False),
158 157 ('Enabled', True),
159 158 ])
160 159 def test_index_show_version(self, autologin_user, name, state):
161 160 version_string = 'RhodeCode %s' % rhodecode.__version__
162 161
163 162 sett = SettingsModel().create_or_update_setting(
164 163 'show_version', state, 'bool')
165 164 Session().add(sett)
166 165 Session().commit()
167 166 SettingsModel().invalidate_settings_cache()
168 167
169 168 response = self.app.get(route_path('home'))
170 169 if state is True:
171 170 response.mustcontain(version_string)
172 171 if state is False:
173 172 response.mustcontain(no=[version_string])
174 173
175 174 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
176 175 response = self.app.get(route_path('home'))
177 176 assert_response = response.assert_response()
178 177 element = assert_response.get_element('.logout [name=csrf_token]')
179 178 assert element.value == csrf_token
@@ -1,856 +1,856 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound
26 26
27 27 from rhodecode.apps._base import BaseAppView, DataGridAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
31 31 HasRepoGroupPermissionAny, AuthUser)
32 32 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
33 33 from rhodecode.lib.index import searcher_from_config
34 34 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int, safe_str
35 35 from rhodecode.lib.vcs.nodes import FileNode
36 36 from rhodecode.model.db import (
37 37 func, true, or_, case, cast, in_filter_generator, String, Session,
38 38 Repository, RepoGroup, User, UserGroup, PullRequest)
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.repo_group import RepoGroupModel
41 41 from rhodecode.model.user import UserModel
42 42 from rhodecode.model.user_group import UserGroupModel
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class HomeView(BaseAppView, DataGridAppView):
48 48
49 49 def load_default_context(self):
50 50 c = self._get_local_tmpl_context()
51 51 c.user = c.auth_user.get_instance()
52 52 return c
53 53
54 54 @LoginRequired()
55 55 def user_autocomplete_data(self):
56 56 self.load_default_context()
57 57 query = self.request.GET.get('query')
58 58 active = str2bool(self.request.GET.get('active') or True)
59 59 include_groups = str2bool(self.request.GET.get('user_groups'))
60 60 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
61 61 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
62 62
63 63 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
64 64 query, active, include_groups)
65 65
66 66 _users = UserModel().get_users(
67 67 name_contains=query, only_active=active)
68 68
69 69 def maybe_skip_default_user(usr):
70 70 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
71 71 return False
72 72 return True
73 73 _users = filter(maybe_skip_default_user, _users)
74 74
75 75 if include_groups:
76 76 # extend with user groups
77 77 _user_groups = UserGroupModel().get_user_groups(
78 78 name_contains=query, only_active=active,
79 79 expand_groups=expand_groups)
80 80 _users = _users + _user_groups
81 81
82 82 return {'suggestions': _users}
83 83
84 84 @LoginRequired()
85 85 @NotAnonymous()
86 86 def user_group_autocomplete_data(self):
87 87 self.load_default_context()
88 88 query = self.request.GET.get('query')
89 89 active = str2bool(self.request.GET.get('active') or True)
90 90 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
91 91
92 92 log.debug('generating user group list, query:%s, active:%s',
93 93 query, active)
94 94
95 95 _user_groups = UserGroupModel().get_user_groups(
96 96 name_contains=query, only_active=active,
97 97 expand_groups=expand_groups)
98 98 _user_groups = _user_groups
99 99
100 100 return {'suggestions': _user_groups}
101 101
102 102 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
103 103 org_query = name_contains
104 104 allowed_ids = self._rhodecode_user.repo_acl_ids(
105 105 ['repository.read', 'repository.write', 'repository.admin'],
106 106 cache=True, name_filter=name_contains) or [-1]
107 107
108 108 query = Session().query(
109 109 Repository.repo_name,
110 110 Repository.repo_id,
111 111 Repository.repo_type,
112 112 Repository.private,
113 113 )\
114 114 .filter(Repository.archived.isnot(true()))\
115 115 .filter(or_(
116 116 # generate multiple IN to fix limitation problems
117 117 *in_filter_generator(Repository.repo_id, allowed_ids)
118 118 ))
119 119
120 120 query = query.order_by(case(
121 121 [
122 122 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
123 123 ],
124 124 ))
125 125 query = query.order_by(func.length(Repository.repo_name))
126 126 query = query.order_by(Repository.repo_name)
127 127
128 128 if repo_type:
129 129 query = query.filter(Repository.repo_type == repo_type)
130 130
131 131 if name_contains:
132 132 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
133 133 query = query.filter(
134 134 Repository.repo_name.ilike(ilike_expression))
135 135 query = query.limit(limit)
136 136
137 137 acl_iter = query
138 138
139 139 return [
140 140 {
141 141 'id': obj.repo_name,
142 142 'value': org_query,
143 143 'value_display': obj.repo_name,
144 144 'text': obj.repo_name,
145 145 'type': 'repo',
146 146 'repo_id': obj.repo_id,
147 147 'repo_type': obj.repo_type,
148 148 'private': obj.private,
149 149 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
150 150 }
151 151 for obj in acl_iter]
152 152
153 153 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
154 154 org_query = name_contains
155 155 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
156 156 ['group.read', 'group.write', 'group.admin'],
157 157 cache=True, name_filter=name_contains) or [-1]
158 158
159 159 query = Session().query(
160 160 RepoGroup.group_id,
161 161 RepoGroup.group_name,
162 162 )\
163 163 .filter(or_(
164 164 # generate multiple IN to fix limitation problems
165 165 *in_filter_generator(RepoGroup.group_id, allowed_ids)
166 166 ))
167 167
168 168 query = query.order_by(case(
169 169 [
170 170 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
171 171 ],
172 172 ))
173 173 query = query.order_by(func.length(RepoGroup.group_name))
174 174 query = query.order_by(RepoGroup.group_name)
175 175
176 176 if name_contains:
177 177 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
178 178 query = query.filter(
179 179 RepoGroup.group_name.ilike(ilike_expression))
180 180 query = query.limit(limit)
181 181
182 182 acl_iter = query
183 183
184 184 return [
185 185 {
186 186 'id': obj.group_name,
187 187 'value': org_query,
188 188 'value_display': obj.group_name,
189 189 'text': obj.group_name,
190 190 'type': 'repo_group',
191 191 'repo_group_id': obj.group_id,
192 192 'url': h.route_path(
193 193 'repo_group_home', repo_group_name=obj.group_name)
194 194 }
195 195 for obj in acl_iter]
196 196
197 197 def _get_user_list(self, name_contains=None, limit=20):
198 198 org_query = name_contains
199 199 if not name_contains:
200 200 return [], False
201 201
202 202 # TODO(marcink): should all logged in users be allowed to search others?
203 203 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
204 204 if not allowed_user_search:
205 205 return [], False
206 206
207 207 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
208 208 if len(name_contains) != 1:
209 209 return [], False
210 210
211 211 name_contains = name_contains[0]
212 212
213 213 query = User.query()\
214 214 .order_by(func.length(User.username))\
215 215 .order_by(User.username) \
216 216 .filter(User.username != User.DEFAULT_USER)
217 217
218 218 if name_contains:
219 219 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
220 220 query = query.filter(
221 221 User.username.ilike(ilike_expression))
222 222 query = query.limit(limit)
223 223
224 224 acl_iter = query
225 225
226 226 return [
227 227 {
228 228 'id': obj.user_id,
229 229 'value': org_query,
230 230 'value_display': 'user: `{}`'.format(obj.username),
231 231 'type': 'user',
232 232 'icon_link': h.gravatar_url(obj.email, 30),
233 233 'url': h.route_path(
234 234 'user_profile', username=obj.username)
235 235 }
236 236 for obj in acl_iter], True
237 237
238 238 def _get_user_groups_list(self, name_contains=None, limit=20):
239 239 org_query = name_contains
240 240 if not name_contains:
241 241 return [], False
242 242
243 243 # TODO(marcink): should all logged in users be allowed to search others?
244 244 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
245 245 if not allowed_user_search:
246 246 return [], False
247 247
248 248 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
249 249 if len(name_contains) != 1:
250 250 return [], False
251 251
252 252 name_contains = name_contains[0]
253 253
254 254 query = UserGroup.query()\
255 255 .order_by(func.length(UserGroup.users_group_name))\
256 256 .order_by(UserGroup.users_group_name)
257 257
258 258 if name_contains:
259 259 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
260 260 query = query.filter(
261 261 UserGroup.users_group_name.ilike(ilike_expression))
262 262 query = query.limit(limit)
263 263
264 264 acl_iter = query
265 265
266 266 return [
267 267 {
268 268 'id': obj.users_group_id,
269 269 'value': org_query,
270 270 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
271 271 'type': 'user_group',
272 272 'url': h.route_path(
273 273 'user_group_profile', user_group_name=obj.users_group_name)
274 274 }
275 275 for obj in acl_iter], True
276 276
277 277 def _get_pull_request_list(self, name_contains=None, limit=20):
278 278 org_query = name_contains
279 279 if not name_contains:
280 280 return [], False
281 281
282 282 # TODO(marcink): should all logged in users be allowed to search others?
283 283 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
284 284 if not allowed_user_search:
285 285 return [], False
286 286
287 287 name_contains = re.compile('(?:pr:[ ]?)(.+)').findall(name_contains)
288 288 if len(name_contains) != 1:
289 289 return [], False
290 290
291 291 name_contains = name_contains[0]
292 292
293 293 allowed_ids = self._rhodecode_user.repo_acl_ids(
294 294 ['repository.read', 'repository.write', 'repository.admin'],
295 295 cache=True) or [-1]
296 296
297 297 query = Session().query(
298 298 PullRequest.pull_request_id,
299 299 PullRequest.title,
300 300 )
301 301 query = query.join(Repository, Repository.repo_id == PullRequest.target_repo_id)
302 302
303 303 query = query.filter(or_(
304 304 # generate multiple IN to fix limitation problems
305 305 *in_filter_generator(Repository.repo_id, allowed_ids)
306 306 ))
307 307
308 308 query = query.order_by(PullRequest.pull_request_id)
309 309
310 310 if name_contains:
311 311 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
312 312 query = query.filter(or_(
313 313 cast(PullRequest.pull_request_id, String).ilike(ilike_expression),
314 314 PullRequest.title.ilike(ilike_expression),
315 315 PullRequest.description.ilike(ilike_expression),
316 316 ))
317 317
318 318 query = query.limit(limit)
319 319
320 320 acl_iter = query
321 321
322 322 return [
323 323 {
324 324 'id': obj.pull_request_id,
325 325 'value': org_query,
326 326 'value_display': 'pull request: `!{} - {}`'.format(
327 327 obj.pull_request_id, safe_str(obj.title[:50])),
328 328 'type': 'pull_request',
329 329 'url': h.route_path('pull_requests_global', pull_request_id=obj.pull_request_id)
330 330 }
331 331 for obj in acl_iter], True
332 332
333 333 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
334 334 repo_name = repo_group_name = None
335 335 if repo:
336 336 repo_name = repo.repo_name
337 337 if repo_group:
338 338 repo_group_name = repo_group.group_name
339 339
340 340 org_query = query
341 341 if not query or len(query) < 3 or not searcher:
342 342 return [], False
343 343
344 344 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
345 345
346 346 if len(commit_hashes) != 1:
347 347 return [], False
348 348
349 349 commit_hash = commit_hashes[0]
350 350
351 351 result = searcher.search(
352 352 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
353 353 repo_name, repo_group_name, raise_on_exc=False)
354 354
355 355 commits = []
356 356 for entry in result['results']:
357 357 repo_data = {
358 358 'repository_id': entry.get('repository_id'),
359 359 'repository_type': entry.get('repo_type'),
360 360 'repository_name': entry.get('repository'),
361 361 }
362 362
363 363 commit_entry = {
364 364 'id': entry['commit_id'],
365 365 'value': org_query,
366 366 'value_display': '`{}` commit: {}'.format(
367 367 entry['repository'], entry['commit_id']),
368 368 'type': 'commit',
369 369 'repo': entry['repository'],
370 370 'repo_data': repo_data,
371 371
372 372 'url': h.route_path(
373 373 'repo_commit',
374 374 repo_name=entry['repository'], commit_id=entry['commit_id'])
375 375 }
376 376
377 377 commits.append(commit_entry)
378 378 return commits, True
379 379
380 380 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
381 381 repo_name = repo_group_name = None
382 382 if repo:
383 383 repo_name = repo.repo_name
384 384 if repo_group:
385 385 repo_group_name = repo_group.group_name
386 386
387 387 org_query = query
388 388 if not query or len(query) < 3 or not searcher:
389 389 return [], False
390 390
391 391 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
392 392 if len(paths_re) != 1:
393 393 return [], False
394 394
395 395 file_path = paths_re[0]
396 396
397 397 search_path = searcher.escape_specials(file_path)
398 398 result = searcher.search(
399 399 'file.raw:*{}*'.format(search_path), 'path', auth_user,
400 400 repo_name, repo_group_name, raise_on_exc=False)
401 401
402 402 files = []
403 403 for entry in result['results']:
404 404 repo_data = {
405 405 'repository_id': entry.get('repository_id'),
406 406 'repository_type': entry.get('repo_type'),
407 407 'repository_name': entry.get('repository'),
408 408 }
409 409
410 410 file_entry = {
411 411 'id': entry['commit_id'],
412 412 'value': org_query,
413 413 'value_display': '`{}` file: {}'.format(
414 414 entry['repository'], entry['file']),
415 415 'type': 'file',
416 416 'repo': entry['repository'],
417 417 'repo_data': repo_data,
418 418
419 419 'url': h.route_path(
420 420 'repo_files',
421 421 repo_name=entry['repository'], commit_id=entry['commit_id'],
422 422 f_path=entry['file'])
423 423 }
424 424
425 425 files.append(file_entry)
426 426 return files, True
427 427
428 428 @LoginRequired()
429 429 def repo_list_data(self):
430 430 _ = self.request.translate
431 431 self.load_default_context()
432 432
433 433 query = self.request.GET.get('query')
434 434 repo_type = self.request.GET.get('repo_type')
435 435 log.debug('generating repo list, query:%s, repo_type:%s',
436 436 query, repo_type)
437 437
438 438 res = []
439 439 repos = self._get_repo_list(query, repo_type=repo_type)
440 440 if repos:
441 441 res.append({
442 442 'text': _('Repositories'),
443 443 'children': repos
444 444 })
445 445
446 446 data = {
447 447 'more': False,
448 448 'results': res
449 449 }
450 450 return data
451 451
452 452 @LoginRequired()
453 453 def repo_group_list_data(self):
454 454 _ = self.request.translate
455 455 self.load_default_context()
456 456
457 457 query = self.request.GET.get('query')
458 458
459 459 log.debug('generating repo group list, query:%s',
460 460 query)
461 461
462 462 res = []
463 463 repo_groups = self._get_repo_group_list(query)
464 464 if repo_groups:
465 465 res.append({
466 466 'text': _('Repository Groups'),
467 467 'children': repo_groups
468 468 })
469 469
470 470 data = {
471 471 'more': False,
472 472 'results': res
473 473 }
474 474 return data
475 475
476 476 def _get_default_search_queries(self, search_context, searcher, query):
477 477 if not searcher:
478 478 return []
479 479
480 480 is_es_6 = searcher.is_es_6
481 481
482 482 queries = []
483 483 repo_group_name, repo_name, repo_context = None, None, None
484 484
485 485 # repo group context
486 486 if search_context.get('search_context[repo_group_name]'):
487 487 repo_group_name = search_context.get('search_context[repo_group_name]')
488 488 if search_context.get('search_context[repo_name]'):
489 489 repo_name = search_context.get('search_context[repo_name]')
490 490 repo_context = search_context.get('search_context[repo_view_type]')
491 491
492 492 if is_es_6 and repo_name:
493 493 # files
494 494 def query_modifier():
495 495 qry = query
496 496 return {'q': qry, 'type': 'content'}
497 497
498 498 label = u'File content search for `{}`'.format(h.escape(query))
499 499 file_qry = {
500 500 'id': -10,
501 501 'value': query,
502 502 'value_display': label,
503 503 'value_icon': '<i class="icon-code"></i>',
504 504 'type': 'search',
505 505 'subtype': 'repo',
506 506 'url': h.route_path('search_repo',
507 507 repo_name=repo_name,
508 508 _query=query_modifier())
509 509 }
510 510
511 511 # commits
512 512 def query_modifier():
513 513 qry = query
514 514 return {'q': qry, 'type': 'commit'}
515 515
516 516 label = u'Commit search for `{}`'.format(h.escape(query))
517 517 commit_qry = {
518 518 'id': -20,
519 519 'value': query,
520 520 'value_display': label,
521 521 'value_icon': '<i class="icon-history"></i>',
522 522 'type': 'search',
523 523 'subtype': 'repo',
524 524 'url': h.route_path('search_repo',
525 525 repo_name=repo_name,
526 526 _query=query_modifier())
527 527 }
528 528
529 529 if repo_context in ['commit', 'commits']:
530 530 queries.extend([commit_qry, file_qry])
531 531 elif repo_context in ['files', 'summary']:
532 532 queries.extend([file_qry, commit_qry])
533 533 else:
534 534 queries.extend([commit_qry, file_qry])
535 535
536 536 elif is_es_6 and repo_group_name:
537 537 # files
538 538 def query_modifier():
539 539 qry = query
540 540 return {'q': qry, 'type': 'content'}
541 541
542 542 label = u'File content search for `{}`'.format(query)
543 543 file_qry = {
544 544 'id': -30,
545 545 'value': query,
546 546 'value_display': label,
547 547 'value_icon': '<i class="icon-code"></i>',
548 548 'type': 'search',
549 549 'subtype': 'repo_group',
550 550 'url': h.route_path('search_repo_group',
551 551 repo_group_name=repo_group_name,
552 552 _query=query_modifier())
553 553 }
554 554
555 555 # commits
556 556 def query_modifier():
557 557 qry = query
558 558 return {'q': qry, 'type': 'commit'}
559 559
560 560 label = u'Commit search for `{}`'.format(query)
561 561 commit_qry = {
562 562 'id': -40,
563 563 'value': query,
564 564 'value_display': label,
565 565 'value_icon': '<i class="icon-history"></i>',
566 566 'type': 'search',
567 567 'subtype': 'repo_group',
568 568 'url': h.route_path('search_repo_group',
569 569 repo_group_name=repo_group_name,
570 570 _query=query_modifier())
571 571 }
572 572
573 573 if repo_context in ['commit', 'commits']:
574 574 queries.extend([commit_qry, file_qry])
575 575 elif repo_context in ['files', 'summary']:
576 576 queries.extend([file_qry, commit_qry])
577 577 else:
578 578 queries.extend([commit_qry, file_qry])
579 579
580 580 # Global, not scoped
581 581 if not queries:
582 582 queries.append(
583 583 {
584 584 'id': -1,
585 585 'value': query,
586 586 'value_display': u'File content search for: `{}`'.format(query),
587 587 'value_icon': '<i class="icon-code"></i>',
588 588 'type': 'search',
589 589 'subtype': 'global',
590 590 'url': h.route_path('search',
591 591 _query={'q': query, 'type': 'content'})
592 592 })
593 593 queries.append(
594 594 {
595 595 'id': -2,
596 596 'value': query,
597 597 'value_display': u'Commit search for: `{}`'.format(query),
598 598 'value_icon': '<i class="icon-history"></i>',
599 599 'type': 'search',
600 600 'subtype': 'global',
601 601 'url': h.route_path('search',
602 602 _query={'q': query, 'type': 'commit'})
603 603 })
604 604
605 605 return queries
606 606
607 607 @LoginRequired()
608 608 def goto_switcher_data(self):
609 609 c = self.load_default_context()
610 610
611 611 _ = self.request.translate
612 612
613 613 query = self.request.GET.get('query')
614 614 log.debug('generating main filter data, query %s', query)
615 615
616 616 res = []
617 617 if not query:
618 618 return {'suggestions': res}
619 619
620 620 def no_match(name):
621 621 return {
622 622 'id': -1,
623 623 'value': "",
624 624 'value_display': name,
625 625 'type': 'text',
626 626 'url': ""
627 627 }
628 628 searcher = searcher_from_config(self.request.registry.settings)
629 629 has_specialized_search = False
630 630
631 631 # set repo context
632 632 repo = None
633 633 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
634 634 if repo_id:
635 635 repo = Repository.get(repo_id)
636 636
637 637 # set group context
638 638 repo_group = None
639 639 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
640 640 if repo_group_id:
641 641 repo_group = RepoGroup.get(repo_group_id)
642 642 prefix_match = False
643 643
644 644 # user: type search
645 645 if not prefix_match:
646 646 users, prefix_match = self._get_user_list(query)
647 647 if users:
648 648 has_specialized_search = True
649 649 for serialized_user in users:
650 650 res.append(serialized_user)
651 651 elif prefix_match:
652 652 has_specialized_search = True
653 653 res.append(no_match('No matching users found'))
654 654
655 655 # user_group: type search
656 656 if not prefix_match:
657 657 user_groups, prefix_match = self._get_user_groups_list(query)
658 658 if user_groups:
659 659 has_specialized_search = True
660 660 for serialized_user_group in user_groups:
661 661 res.append(serialized_user_group)
662 662 elif prefix_match:
663 663 has_specialized_search = True
664 664 res.append(no_match('No matching user groups found'))
665 665
666 666 # pr: type search
667 667 if not prefix_match:
668 668 pull_requests, prefix_match = self._get_pull_request_list(query)
669 669 if pull_requests:
670 670 has_specialized_search = True
671 671 for serialized_pull_request in pull_requests:
672 672 res.append(serialized_pull_request)
673 673 elif prefix_match:
674 674 has_specialized_search = True
675 675 res.append(no_match('No matching pull requests found'))
676 676
677 677 # FTS commit: type search
678 678 if not prefix_match:
679 679 commits, prefix_match = self._get_hash_commit_list(
680 680 c.auth_user, searcher, query, repo, repo_group)
681 681 if commits:
682 682 has_specialized_search = True
683 683 unique_repos = collections.OrderedDict()
684 684 for commit in commits:
685 685 repo_name = commit['repo']
686 686 unique_repos.setdefault(repo_name, []).append(commit)
687 687
688 688 for _repo, commits in unique_repos.items():
689 689 for commit in commits:
690 690 res.append(commit)
691 691 elif prefix_match:
692 692 has_specialized_search = True
693 693 res.append(no_match('No matching commits found'))
694 694
695 695 # FTS file: type search
696 696 if not prefix_match:
697 697 paths, prefix_match = self._get_path_list(
698 698 c.auth_user, searcher, query, repo, repo_group)
699 699 if paths:
700 700 has_specialized_search = True
701 701 unique_repos = collections.OrderedDict()
702 702 for path in paths:
703 703 repo_name = path['repo']
704 704 unique_repos.setdefault(repo_name, []).append(path)
705 705
706 706 for repo, paths in unique_repos.items():
707 707 for path in paths:
708 708 res.append(path)
709 709 elif prefix_match:
710 710 has_specialized_search = True
711 711 res.append(no_match('No matching files found'))
712 712
713 713 # main suggestions
714 714 if not has_specialized_search:
715 715 repo_group_name = ''
716 716 if repo_group:
717 717 repo_group_name = repo_group.group_name
718 718
719 719 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
720 720 res.append(_q)
721 721
722 722 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
723 723 for serialized_repo_group in repo_groups:
724 724 res.append(serialized_repo_group)
725 725
726 726 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
727 727 for serialized_repo in repos:
728 728 res.append(serialized_repo)
729 729
730 730 if not repos and not repo_groups:
731 731 res.append(no_match('No matches found'))
732 732
733 733 return {'suggestions': res}
734 734
735 735 @LoginRequired()
736 736 def main_page(self):
737 737 c = self.load_default_context()
738 738 c.repo_group = None
739 739 return self._get_template_context(c)
740 740
741 741 def _main_page_repo_groups_data(self, repo_group_id):
742 742 column_map = {
743 743 'name': 'group_name_hash',
744 744 'desc': 'group_description',
745 745 'last_change': 'updated_on',
746 746 'owner': 'user_username',
747 747 }
748 748 draw, start, limit = self._extract_chunk(self.request)
749 749 search_q, order_by, order_dir = self._extract_ordering(
750 750 self.request, column_map=column_map)
751 751 return RepoGroupModel().get_repo_groups_data_table(
752 752 draw, start, limit,
753 753 search_q, order_by, order_dir,
754 754 self._rhodecode_user, repo_group_id)
755 755
756 756 def _main_page_repos_data(self, repo_group_id):
757 757 column_map = {
758 758 'name': 'repo_name',
759 759 'desc': 'description',
760 760 'last_change': 'updated_on',
761 761 'owner': 'user_username',
762 762 }
763 763 draw, start, limit = self._extract_chunk(self.request)
764 764 search_q, order_by, order_dir = self._extract_ordering(
765 765 self.request, column_map=column_map)
766 766 return RepoModel().get_repos_data_table(
767 767 draw, start, limit,
768 768 search_q, order_by, order_dir,
769 769 self._rhodecode_user, repo_group_id)
770 770
771 771 @LoginRequired()
772 772 def main_page_repo_groups_data(self):
773 773 self.load_default_context()
774 774 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
775 775
776 776 if repo_group_id:
777 777 group = RepoGroup.get_or_404(repo_group_id)
778 778 _perms = AuthUser.repo_group_read_perms
779 779 if not HasRepoGroupPermissionAny(*_perms)(
780 780 group.group_name, 'user is allowed to list repo group children'):
781 781 raise HTTPNotFound()
782 782
783 783 return self._main_page_repo_groups_data(repo_group_id)
784 784
785 785 @LoginRequired()
786 786 def main_page_repos_data(self):
787 787 self.load_default_context()
788 788 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
789 789
790 790 if repo_group_id:
791 791 group = RepoGroup.get_or_404(repo_group_id)
792 792 _perms = AuthUser.repo_group_read_perms
793 793 if not HasRepoGroupPermissionAny(*_perms)(
794 794 group.group_name, 'user is allowed to list repo group children'):
795 795 raise HTTPNotFound()
796 796
797 797 return self._main_page_repos_data(repo_group_id)
798 798
799 799 @LoginRequired()
800 800 @HasRepoGroupPermissionAnyDecorator(*AuthUser.repo_group_read_perms)
801 801 def repo_group_main_page(self):
802 802 c = self.load_default_context()
803 803 c.repo_group = self.request.db_repo_group
804 804 return self._get_template_context(c)
805 805
806 806 @LoginRequired()
807 807 @CSRFRequired()
808 808 def markup_preview(self):
809 809 # Technically a CSRF token is not needed as no state changes with this
810 810 # call. However, as this is a POST is better to have it, so automated
811 811 # tools don't flag it as potential CSRF.
812 812 # Post is required because the payload could be bigger than the maximum
813 813 # allowed by GET.
814 814
815 815 text = self.request.POST.get('text')
816 816 renderer = self.request.POST.get('renderer') or 'rst'
817 817 if text:
818 818 return h.render(text, renderer=renderer, mentions=True)
819 819 return ''
820 820
821 821 @LoginRequired()
822 822 @CSRFRequired()
823 823 def file_preview(self):
824 824 # Technically a CSRF token is not needed as no state changes with this
825 825 # call. However, as this is a POST is better to have it, so automated
826 826 # tools don't flag it as potential CSRF.
827 827 # Post is required because the payload could be bigger than the maximum
828 828 # allowed by GET.
829 829
830 830 text = self.request.POST.get('text')
831 831 file_path = self.request.POST.get('file_path')
832 832
833 833 renderer = h.renderer_from_filename(file_path)
834 834
835 835 if renderer:
836 836 return h.render(text, renderer=renderer, mentions=True)
837 837 else:
838 838 self.load_default_context()
839 839 _render = self.request.get_partial_renderer(
840 840 'rhodecode:templates/files/file_content.mako')
841 841
842 842 lines = filenode_as_lines_tokens(FileNode(file_path, text))
843 843
844 844 return _render('render_lines', lines)
845 845
846 846 @LoginRequired()
847 847 @CSRFRequired()
848 848 def store_user_session_attr(self):
849 849 key = self.request.POST.get('key')
850 850 val = self.request.POST.get('val')
851 851
852 852 existing_value = self.request.session.get(key)
853 853 if existing_value != val:
854 854 self.request.session[key] = val
855 855
856 856 return 'stored:{}:{}'.format(key, val)
@@ -1,67 +1,67 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2018-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 def includeme(config):
23 23 from rhodecode.apps.hovercards.views import HoverCardsView, HoverCardsRepoView
24 24 config.add_route(
25 25 name='hovercard_user',
26 26 pattern='/_hovercard/user/{user_id}')
27 27 config.add_view(
28 28 HoverCardsView,
29 29 attr='hovercard_user',
30 30 route_name='hovercard_user', request_method='GET', xhr=True,
31 31 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
32 32
33 33 config.add_route(
34 34 name='hovercard_username',
35 35 pattern='/_hovercard/username/{username}')
36 36 config.add_view(
37 37 HoverCardsView,
38 38 attr='hovercard_username',
39 39 route_name='hovercard_username', request_method='GET', xhr=True,
40 40 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
41 41
42 42 config.add_route(
43 43 name='hovercard_user_group',
44 44 pattern='/_hovercard/user_group/{user_group_id}')
45 45 config.add_view(
46 46 HoverCardsView,
47 47 attr='hovercard_user_group',
48 48 route_name='hovercard_user_group', request_method='GET', xhr=True,
49 49 renderer='rhodecode:templates/hovercards/hovercard_user_group.mako')
50 50
51 51 config.add_route(
52 52 name='hovercard_pull_request',
53 53 pattern='/_hovercard/pull_request/{pull_request_id}')
54 54 config.add_view(
55 55 HoverCardsView,
56 56 attr='hovercard_pull_request',
57 57 route_name='hovercard_pull_request', request_method='GET', xhr=True,
58 58 renderer='rhodecode:templates/hovercards/hovercard_pull_request.mako')
59 59
60 60 config.add_route(
61 61 name='hovercard_repo_commit',
62 62 pattern='/_hovercard/commit/{repo_name:.*?[^/]}/{commit_id}', repo_route=True)
63 63 config.add_view(
64 64 HoverCardsRepoView,
65 65 attr='hovercard_repo_commit',
66 66 route_name='hovercard_repo_commit', request_method='GET', xhr=True,
67 67 renderer='rhodecode:templates/hovercards/hovercard_repo_commit.mako')
@@ -1,108 +1,108 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound
26 26
27 27
28 28 from rhodecode.apps._base import BaseAppView, RepoAppView
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
32 32 HasRepoPermissionAnyDecorator)
33 33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
34 34 from rhodecode.lib.index import searcher_from_config
35 35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, EmptyRepositoryError
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39 from rhodecode.model.db import (
40 40 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup, PullRequest)
41 41 from rhodecode.model.repo import RepoModel
42 42 from rhodecode.model.repo_group import RepoGroupModel
43 43 from rhodecode.model.scm import RepoGroupList, RepoList
44 44 from rhodecode.model.user import UserModel
45 45 from rhodecode.model.user_group import UserGroupModel
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class HoverCardsView(BaseAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54 return c
55 55
56 56 @LoginRequired()
57 57 def hovercard_user(self):
58 58 c = self.load_default_context()
59 59 user_id = self.request.matchdict['user_id']
60 60 c.user = User.get_or_404(user_id)
61 61 return self._get_template_context(c)
62 62
63 63 @LoginRequired()
64 64 def hovercard_username(self):
65 65 c = self.load_default_context()
66 66 username = self.request.matchdict['username']
67 67 c.user = User.get_by_username(username)
68 68 if not c.user:
69 69 raise HTTPNotFound()
70 70
71 71 return self._get_template_context(c)
72 72
73 73 @LoginRequired()
74 74 def hovercard_user_group(self):
75 75 c = self.load_default_context()
76 76 user_group_id = self.request.matchdict['user_group_id']
77 77 c.user_group = UserGroup.get_or_404(user_group_id)
78 78 return self._get_template_context(c)
79 79
80 80 @LoginRequired()
81 81 def hovercard_pull_request(self):
82 82 c = self.load_default_context()
83 83 c.pull_request = PullRequest.get_or_404(
84 84 self.request.matchdict['pull_request_id'])
85 85 perms = ['repository.read', 'repository.write', 'repository.admin']
86 86 c.can_view_pr = h.HasRepoPermissionAny(*perms)(
87 87 c.pull_request.target_repo.repo_name)
88 88 return self._get_template_context(c)
89 89
90 90
91 91 class HoverCardsRepoView(RepoAppView):
92 92 def load_default_context(self):
93 93 c = self._get_local_tmpl_context()
94 94 return c
95 95
96 96 @LoginRequired()
97 97 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin')
98 98 def hovercard_repo_commit(self):
99 99 c = self.load_default_context()
100 100 commit_id = self.request.matchdict['commit_id']
101 101 pre_load = ['author', 'branch', 'date', 'message']
102 102 try:
103 103 c.commit = self.rhodecode_vcs_repo.get_commit(
104 104 commit_id=commit_id, pre_load=pre_load)
105 105 except (CommitDoesNotExistError, EmptyRepositoryError):
106 106 raise HTTPNotFound()
107 107
108 108 return self._get_template_context(c)
@@ -1,102 +1,102 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def admin_routes(config):
26 26 from rhodecode.apps.journal.views import JournalView
27 27
28 28 config.add_route(
29 29 name='journal', pattern='/journal')
30 30 config.add_view(
31 31 JournalView,
32 32 attr='journal',
33 33 route_name='journal', request_method='GET',
34 34 renderer=None)
35 35
36 36 config.add_route(
37 37 name='journal_rss', pattern='/journal/rss')
38 38 config.add_view(
39 39 JournalView,
40 40 attr='journal_rss',
41 41 route_name='journal_rss', request_method='GET',
42 42 renderer=None)
43 43
44 44 config.add_route(
45 45 name='journal_atom', pattern='/journal/atom')
46 46 config.add_view(
47 47 JournalView,
48 48 attr='journal_atom',
49 49 route_name='journal_atom', request_method='GET',
50 50 renderer=None)
51 51
52 52 config.add_route(
53 53 name='journal_public', pattern='/public_journal')
54 54 config.add_view(
55 55 JournalView,
56 56 attr='journal_public',
57 57 route_name='journal_public', request_method='GET',
58 58 renderer=None)
59 59
60 60 config.add_route(
61 61 name='journal_public_atom', pattern='/public_journal/atom')
62 62 config.add_view(
63 63 JournalView,
64 64 attr='journal_public_atom',
65 65 route_name='journal_public_atom', request_method='GET',
66 66 renderer=None)
67 67
68 68 config.add_route(
69 69 name='journal_public_atom_old', pattern='/public_journal_atom')
70 70 config.add_view(
71 71 JournalView,
72 72 attr='journal_public_atom',
73 73 route_name='journal_public_atom_old', request_method='GET',
74 74 renderer=None)
75 75
76 76 config.add_route(
77 77 name='journal_public_rss', pattern='/public_journal/rss')
78 78 config.add_view(
79 79 JournalView,
80 80 attr='journal_public_rss',
81 81 route_name='journal_public_rss', request_method='GET',
82 82 renderer=None)
83 83
84 84 config.add_route(
85 85 name='journal_public_rss_old', pattern='/public_journal_rss')
86 86 config.add_view(
87 87 JournalView,
88 88 attr='journal_public_rss',
89 89 route_name='journal_public_rss_old', request_method='GET',
90 90 renderer=None)
91 91
92 92 config.add_route(
93 93 name='toggle_following', pattern='/toggle_following')
94 94 config.add_view(
95 95 JournalView,
96 96 attr='toggle_following',
97 97 route_name='toggle_following', request_method='POST',
98 98 renderer='json_ext')
99 99
100 100
101 101 def includeme(config):
102 102 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,106 +1,105 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import datetime
22 21
23 22 import pytest
24 23
25 24 from rhodecode.apps._base import ADMIN_PREFIX
26 25 from rhodecode.tests import TestController
27 26 from rhodecode.model.db import UserFollowing, Repository
28 27
29 28
30 29 def route_path(name, params=None, **kwargs):
31 30 import urllib.request, urllib.parse, urllib.error
32 31
33 32 base_url = {
34 33 'journal': ADMIN_PREFIX + '/journal',
35 34 'journal_rss': ADMIN_PREFIX + '/journal/rss',
36 35 'journal_atom': ADMIN_PREFIX + '/journal/atom',
37 36 'journal_public': ADMIN_PREFIX + '/public_journal',
38 37 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
39 38 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
40 39 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
41 40 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
42 41 'toggle_following': ADMIN_PREFIX + '/toggle_following',
43 42 }[name].format(**kwargs)
44 43
45 44 if params:
46 45 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 46 return base_url
48 47
49 48
50 49 class TestJournalViews(TestController):
51 50
52 51 def test_journal(self):
53 52 self.log_user()
54 53 response = self.app.get(route_path('journal'))
55 54 # response.mustcontain(
56 55 # """<div class="journal_day">%s</div>""" % datetime.date.today())
57 56
58 57 @pytest.mark.parametrize("feed_type, content_type", [
59 58 ('rss', "application/rss+xml"),
60 59 ('atom', "application/atom+xml")
61 60 ])
62 61 def test_journal_feed(self, feed_type, content_type):
63 62 self.log_user()
64 63 response = self.app.get(
65 64 route_path(
66 65 'journal_{}'.format(feed_type)),
67 66 status=200)
68 67
69 68 assert response.content_type == content_type
70 69
71 70 def test_toggle_following_repository(self, backend):
72 71 user = self.log_user()
73 72 repo = Repository.get_by_repo_name(backend.repo_name)
74 73 repo_id = repo.repo_id
75 74 self.app.post(
76 75 route_path('toggle_following'), {'follows_repo_id': repo_id,
77 76 'csrf_token': self.csrf_token})
78 77
79 78 followings = UserFollowing.query()\
80 79 .filter(UserFollowing.user_id == user['user_id'])\
81 80 .filter(UserFollowing.follows_repo_id == repo_id).all()
82 81
83 82 assert len(followings) == 0
84 83
85 84 self.app.post(
86 85 route_path('toggle_following'), {'follows_repo_id': repo_id,
87 86 'csrf_token': self.csrf_token})
88 87
89 88 followings = UserFollowing.query()\
90 89 .filter(UserFollowing.user_id == user['user_id'])\
91 90 .filter(UserFollowing.follows_repo_id == repo_id).all()
92 91
93 92 assert len(followings) == 1
94 93
95 94 @pytest.mark.parametrize("feed_type, content_type", [
96 95 ('rss', "application/rss+xml"),
97 96 ('atom', "application/atom+xml")
98 97 ])
99 98 def test_public_journal_feed(self, feed_type, content_type):
100 99 self.log_user()
101 100 response = self.app.get(
102 101 route_path(
103 102 'journal_public_{}'.format(feed_type)),
104 103 status=200)
105 104
106 105 assert response.content_type == content_type
@@ -1,364 +1,363 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import logging
22 21 import itertools
23 22
24 23 from pyramid.httpexceptions import HTTPBadRequest
25 24 from pyramid.response import Response
26 25 from pyramid.renderers import render
27 26
28 27 from rhodecode.apps._base import BaseAppView
29 28 from rhodecode.model.db import (
30 29 or_, joinedload, Repository, UserLog, UserFollowing, User, UserApiKeys)
31 30 from rhodecode.model.meta import Session
32 31 import rhodecode.lib.helpers as h
33 32 from rhodecode.lib.helpers import SqlPage
34 33 from rhodecode.lib.user_log_filter import user_log_filter
35 34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, HasRepoPermissionAny
36 35 from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe
37 36 from rhodecode.lib.feedgenerator.feedgenerator import Atom1Feed, Rss201rev2Feed
38 37 from rhodecode.model.scm import ScmModel
39 38
40 39 log = logging.getLogger(__name__)
41 40
42 41
43 42 class JournalView(BaseAppView):
44 43
45 44 def load_default_context(self):
46 45 c = self._get_local_tmpl_context(include_app_defaults=True)
47 46
48 47 self._load_defaults(c.rhodecode_name)
49 48
50 49 # TODO(marcink): what is this, why we need a global register ?
51 50 c.search_term = self.request.GET.get('filter') or ''
52 51 return c
53 52
54 53 def _get_config(self, rhodecode_name):
55 54 import rhodecode
56 55 config = rhodecode.CONFIG
57 56
58 57 return {
59 58 'language': 'en-us',
60 59 'feed_ttl': '5', # TTL of feed,
61 60 'feed_items_per_page':
62 61 safe_int(config.get('rss_items_per_page', 20)),
63 62 'rhodecode_name': rhodecode_name
64 63 }
65 64
66 65 def _load_defaults(self, rhodecode_name):
67 66 config = self._get_config(rhodecode_name)
68 67 # common values for feeds
69 68 self.language = config["language"]
70 69 self.ttl = config["feed_ttl"]
71 70 self.feed_items_per_page = config['feed_items_per_page']
72 71 self.rhodecode_name = config['rhodecode_name']
73 72
74 73 def _get_daily_aggregate(self, journal):
75 74 groups = []
76 75 for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
77 76 user_group = []
78 77 # groupby username if it's a present value, else
79 78 # fallback to journal username
80 79 for _, g2 in itertools.groupby(
81 80 list(g), lambda x: x.user.username if x.user else x.username):
82 81 l = list(g2)
83 82 user_group.append((l[0].user, l))
84 83
85 84 groups.append((k, user_group,))
86 85
87 86 return groups
88 87
89 88 def _get_journal_data(self, following_repos, search_term):
90 89 repo_ids = [x.follows_repository.repo_id for x in following_repos
91 90 if x.follows_repository is not None]
92 91 user_ids = [x.follows_user.user_id for x in following_repos
93 92 if x.follows_user is not None]
94 93
95 94 filtering_criterion = None
96 95
97 96 if repo_ids and user_ids:
98 97 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
99 98 UserLog.user_id.in_(user_ids))
100 99 if repo_ids and not user_ids:
101 100 filtering_criterion = UserLog.repository_id.in_(repo_ids)
102 101 if not repo_ids and user_ids:
103 102 filtering_criterion = UserLog.user_id.in_(user_ids)
104 103 if filtering_criterion is not None:
105 104 journal = Session().query(UserLog)\
106 105 .options(joinedload(UserLog.user))\
107 106 .options(joinedload(UserLog.repository))
108 107 # filter
109 108 try:
110 109 journal = user_log_filter(journal, search_term)
111 110 except Exception:
112 111 # we want this to crash for now
113 112 raise
114 113 journal = journal.filter(filtering_criterion)\
115 114 .order_by(UserLog.action_date.desc())
116 115 else:
117 116 journal = []
118 117
119 118 return journal
120 119
121 120 def feed_uid(self, entry_id):
122 121 return '{}:{}'.format('journal', md5_safe(entry_id))
123 122
124 123 def _atom_feed(self, repos, search_term, public=True):
125 124 _ = self.request.translate
126 125 journal = self._get_journal_data(repos, search_term)
127 126 if public:
128 127 _link = h.route_url('journal_public_atom')
129 128 _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
130 129 'atom feed')
131 130 else:
132 131 _link = h.route_url('journal_atom')
133 132 _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
134 133
135 134 feed = Atom1Feed(
136 135 title=_desc, link=_link, description=_desc,
137 136 language=self.language, ttl=self.ttl)
138 137
139 138 for entry in journal[:self.feed_items_per_page]:
140 139 user = entry.user
141 140 if user is None:
142 141 # fix deleted users
143 142 user = AttributeDict({'short_contact': entry.username,
144 143 'email': '',
145 144 'full_contact': ''})
146 145 action, action_extra, ico = h.action_parser(
147 146 self.request, entry, feed=True)
148 147 title = "%s - %s %s" % (user.short_contact, action(),
149 148 entry.repository.repo_name)
150 149 desc = action_extra()
151 150 _url = h.route_url('home')
152 151 if entry.repository is not None:
153 152 _url = h.route_url('repo_commits',
154 153 repo_name=entry.repository.repo_name)
155 154
156 155 feed.add_item(
157 156 unique_id=self.feed_uid(entry.user_log_id),
158 157 title=title,
159 158 pubdate=entry.action_date,
160 159 link=_url,
161 160 author_email=user.email,
162 161 author_name=user.full_contact,
163 162 description=desc)
164 163
165 164 response = Response(feed.writeString('utf-8'))
166 165 response.content_type = feed.content_type
167 166 return response
168 167
169 168 def _rss_feed(self, repos, search_term, public=True):
170 169 _ = self.request.translate
171 170 journal = self._get_journal_data(repos, search_term)
172 171 if public:
173 172 _link = h.route_url('journal_public_atom')
174 173 _desc = '%s %s %s' % (
175 174 self.rhodecode_name, _('public journal'), 'rss feed')
176 175 else:
177 176 _link = h.route_url('journal_atom')
178 177 _desc = '%s %s %s' % (
179 178 self.rhodecode_name, _('journal'), 'rss feed')
180 179
181 180 feed = Rss201rev2Feed(
182 181 title=_desc, link=_link, description=_desc,
183 182 language=self.language, ttl=self.ttl)
184 183
185 184 for entry in journal[:self.feed_items_per_page]:
186 185 user = entry.user
187 186 if user is None:
188 187 # fix deleted users
189 188 user = AttributeDict({'short_contact': entry.username,
190 189 'email': '',
191 190 'full_contact': ''})
192 191 action, action_extra, ico = h.action_parser(
193 192 self.request, entry, feed=True)
194 193 title = "%s - %s %s" % (user.short_contact, action(),
195 194 entry.repository.repo_name)
196 195 desc = action_extra()
197 196 _url = h.route_url('home')
198 197 if entry.repository is not None:
199 198 _url = h.route_url('repo_commits',
200 199 repo_name=entry.repository.repo_name)
201 200
202 201 feed.add_item(
203 202 unique_id=self.feed_uid(entry.user_log_id),
204 203 title=title,
205 204 pubdate=entry.action_date,
206 205 link=_url,
207 206 author_email=user.email,
208 207 author_name=user.full_contact,
209 208 description=desc)
210 209
211 210 response = Response(feed.writeString('utf-8'))
212 211 response.content_type = feed.content_type
213 212 return response
214 213
215 214 @LoginRequired()
216 215 @NotAnonymous()
217 216 def journal(self):
218 217 c = self.load_default_context()
219 218
220 219 p = safe_int(self.request.GET.get('page', 1), 1)
221 220 c.user = User.get(self._rhodecode_user.user_id)
222 221 following = Session().query(UserFollowing)\
223 222 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
224 223 .options(joinedload(UserFollowing.follows_repository))\
225 224 .all()
226 225
227 226 journal = self._get_journal_data(following, c.search_term)
228 227
229 228 def url_generator(page_num):
230 229 query_params = {
231 230 'page': page_num,
232 231 'filter': c.search_term
233 232 }
234 233 return self.request.current_route_path(_query=query_params)
235 234
236 235 c.journal_pager = SqlPage(
237 236 journal, page=p, items_per_page=20, url_maker=url_generator)
238 237 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
239 238
240 239 c.journal_data = render(
241 240 'rhodecode:templates/journal/journal_data.mako',
242 241 self._get_template_context(c), self.request)
243 242
244 243 if self.request.is_xhr:
245 244 return Response(c.journal_data)
246 245
247 246 html = render(
248 247 'rhodecode:templates/journal/journal.mako',
249 248 self._get_template_context(c), self.request)
250 249 return Response(html)
251 250
252 251 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
253 252 @NotAnonymous()
254 253 def journal_atom(self):
255 254 """
256 255 Produce an atom-1.0 feed via feedgenerator module
257 256 """
258 257 c = self.load_default_context()
259 258 following_repos = Session().query(UserFollowing)\
260 259 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
261 260 .options(joinedload(UserFollowing.follows_repository))\
262 261 .all()
263 262 return self._atom_feed(following_repos, c.search_term, public=False)
264 263
265 264 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
266 265 @NotAnonymous()
267 266 def journal_rss(self):
268 267 """
269 268 Produce an rss feed via feedgenerator module
270 269 """
271 270 c = self.load_default_context()
272 271 following_repos = Session().query(UserFollowing)\
273 272 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
274 273 .options(joinedload(UserFollowing.follows_repository))\
275 274 .all()
276 275 return self._rss_feed(following_repos, c.search_term, public=False)
277 276
278 277 @LoginRequired()
279 278 def journal_public(self):
280 279 c = self.load_default_context()
281 280 # Return a rendered template
282 281 p = safe_int(self.request.GET.get('page', 1), 1)
283 282
284 283 c.following = Session().query(UserFollowing)\
285 284 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
286 285 .options(joinedload(UserFollowing.follows_repository))\
287 286 .all()
288 287
289 288 journal = self._get_journal_data(c.following, c.search_term)
290 289
291 290 def url_generator(page_num):
292 291 query_params = {
293 292 'page': page_num
294 293 }
295 294 return self.request.current_route_path(_query=query_params)
296 295
297 296 c.journal_pager = SqlPage(
298 297 journal, page=p, items_per_page=20, url_maker=url_generator)
299 298 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
300 299
301 300 c.journal_data = render(
302 301 'rhodecode:templates/journal/journal_data.mako',
303 302 self._get_template_context(c), self.request)
304 303
305 304 if self.request.is_xhr:
306 305 return Response(c.journal_data)
307 306
308 307 html = render(
309 308 'rhodecode:templates/journal/public_journal.mako',
310 309 self._get_template_context(c), self.request)
311 310 return Response(html)
312 311
313 312 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
314 313 def journal_public_atom(self):
315 314 """
316 315 Produce an atom-1.0 feed via feedgenerator module
317 316 """
318 317 c = self.load_default_context()
319 318 following_repos = Session().query(UserFollowing)\
320 319 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
321 320 .options(joinedload(UserFollowing.follows_repository))\
322 321 .all()
323 322
324 323 return self._atom_feed(following_repos, c.search_term)
325 324
326 325 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
327 326 def journal_public_rss(self):
328 327 """
329 328 Produce an rss2 feed via feedgenerator module
330 329 """
331 330 c = self.load_default_context()
332 331 following_repos = Session().query(UserFollowing)\
333 332 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
334 333 .options(joinedload(UserFollowing.follows_repository))\
335 334 .all()
336 335
337 336 return self._rss_feed(following_repos, c.search_term)
338 337
339 338 @LoginRequired()
340 339 @NotAnonymous()
341 340 @CSRFRequired()
342 341 def toggle_following(self):
343 342 user_id = self.request.POST.get('follows_user_id')
344 343 if user_id:
345 344 try:
346 345 ScmModel().toggle_following_user(user_id, self._rhodecode_user.user_id)
347 346 Session().commit()
348 347 return 'ok'
349 348 except Exception:
350 349 raise HTTPBadRequest()
351 350
352 351 repo_id = self.request.POST.get('follows_repo_id')
353 352 repo = Repository.get_or_404(repo_id)
354 353 perm_set = ['repository.read', 'repository.write', 'repository.admin']
355 354 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'RepoWatch check')
356 355 if repo and has_perm:
357 356 try:
358 357 ScmModel().toggle_following_repo(repo_id, self._rhodecode_user.user_id)
359 358 Session().commit()
360 359 return 'ok'
361 360 except Exception:
362 361 raise HTTPBadRequest()
363 362
364 363 raise HTTPBadRequest()
@@ -1,79 +1,79 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def includeme(config):
26 26 from rhodecode.apps.login.views import LoginView
27 27
28 28 config.add_route(
29 29 name='login',
30 30 pattern=ADMIN_PREFIX + '/login')
31 31 config.add_view(
32 32 LoginView,
33 33 attr='login',
34 34 route_name='login', request_method='GET',
35 35 renderer='rhodecode:templates/login.mako')
36 36 config.add_view(
37 37 LoginView,
38 38 attr='login_post',
39 39 route_name='login', request_method='POST',
40 40 renderer='rhodecode:templates/login.mako')
41 41
42 42 config.add_route(
43 43 name='logout',
44 44 pattern=ADMIN_PREFIX + '/logout')
45 45 config.add_view(
46 46 LoginView,
47 47 attr='logout',
48 48 route_name='logout', request_method='POST')
49 49
50 50 config.add_route(
51 51 name='register',
52 52 pattern=ADMIN_PREFIX + '/register')
53 53 config.add_view(
54 54 LoginView,
55 55 attr='register',
56 56 route_name='register', request_method='GET',
57 57 renderer='rhodecode:templates/register.mako')
58 58 config.add_view(
59 59 LoginView,
60 60 attr='register_post',
61 61 route_name='register', request_method='POST',
62 62 renderer='rhodecode:templates/register.mako')
63 63
64 64 config.add_route(
65 65 name='reset_password',
66 66 pattern=ADMIN_PREFIX + '/password_reset')
67 67 config.add_view(
68 68 LoginView,
69 69 attr='password_reset',
70 70 route_name='reset_password', request_method=('GET', 'POST'),
71 71 renderer='rhodecode:templates/password_reset.mako')
72 72
73 73 config.add_route(
74 74 name='reset_password_confirmation',
75 75 pattern=ADMIN_PREFIX + '/password_reset_confirmation')
76 76 config.add_view(
77 77 LoginView,
78 78 attr='password_reset_confirmation',
79 79 route_name='reset_password_confirmation', request_method='GET')
@@ -1,580 +1,579 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import urllib.parse
22 21
23 22 import mock
24 23 import pytest
25 24
26 25 from rhodecode.tests import (
27 26 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 27 no_newline_id_generator)
29 28 from rhodecode.tests.fixture import Fixture
30 29 from rhodecode.lib.auth import check_password
31 30 from rhodecode.lib import helpers as h
32 31 from rhodecode.model.auth_token import AuthTokenModel
33 32 from rhodecode.model.db import User, Notification, UserApiKeys
34 33 from rhodecode.model.meta import Session
35 34
36 35 fixture = Fixture()
37 36
38 37 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39 38
40 39
41 40 def route_path(name, params=None, **kwargs):
42 41 import urllib.request, urllib.parse, urllib.error
43 42 from rhodecode.apps._base import ADMIN_PREFIX
44 43
45 44 base_url = {
46 45 'login': ADMIN_PREFIX + '/login',
47 46 'logout': ADMIN_PREFIX + '/logout',
48 47 'register': ADMIN_PREFIX + '/register',
49 48 'reset_password':
50 49 ADMIN_PREFIX + '/password_reset',
51 50 'reset_password_confirmation':
52 51 ADMIN_PREFIX + '/password_reset_confirmation',
53 52
54 53 'admin_permissions_application':
55 54 ADMIN_PREFIX + '/permissions/application',
56 55 'admin_permissions_application_update':
57 56 ADMIN_PREFIX + '/permissions/application/update',
58 57
59 58 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60 59
61 60 }[name].format(**kwargs)
62 61
63 62 if params:
64 63 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
65 64 return base_url
66 65
67 66
68 67 @pytest.mark.usefixtures('app')
69 68 class TestLoginController(object):
70 69 destroy_users = set()
71 70
72 71 @classmethod
73 72 def teardown_class(cls):
74 73 fixture.destroy_users(cls.destroy_users)
75 74
76 75 def teardown_method(self, method):
77 76 for n in Notification.query().all():
78 77 Session().delete(n)
79 78
80 79 Session().commit()
81 80 assert Notification.query().all() == []
82 81
83 82 def test_index(self):
84 83 response = self.app.get(route_path('login'))
85 84 assert response.status == '200 OK'
86 85 # Test response...
87 86
88 87 def test_login_admin_ok(self):
89 88 response = self.app.post(route_path('login'),
90 89 {'username': 'test_admin',
91 90 'password': 'test12'}, status=302)
92 91 response = response.follow()
93 92 session = response.get_session_from_response()
94 93 username = session['rhodecode_user'].get('username')
95 94 assert username == 'test_admin'
96 95 response.mustcontain('logout')
97 96
98 97 def test_login_regular_ok(self):
99 98 response = self.app.post(route_path('login'),
100 99 {'username': 'test_regular',
101 100 'password': 'test12'}, status=302)
102 101
103 102 response = response.follow()
104 103 session = response.get_session_from_response()
105 104 username = session['rhodecode_user'].get('username')
106 105 assert username == 'test_regular'
107 106 response.mustcontain('logout')
108 107
109 108 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 109 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 110 with fixture.auth_restriction(self.app._pyramid_registry,
112 111 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 112 response = self.app.post(route_path('login'),
114 113 {'username': 'test_regular',
115 114 'password': 'test12'})
116 115
117 116 response.mustcontain('invalid user name')
118 117 response.mustcontain('invalid password')
119 118
120 119 def test_login_regular_forbidden_when_scope_restriction(self):
121 120 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 121 with fixture.scope_restriction(self.app._pyramid_registry,
123 122 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
124 123 response = self.app.post(route_path('login'),
125 124 {'username': 'test_regular',
126 125 'password': 'test12'})
127 126
128 127 response.mustcontain('invalid user name')
129 128 response.mustcontain('invalid password')
130 129
131 130 def test_login_ok_came_from(self):
132 131 test_came_from = '/_admin/users?branch=stable'
133 132 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
134 133 response = self.app.post(
135 134 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
136 135
137 136 assert 'branch=stable' in response.location
138 137 response = response.follow()
139 138
140 139 assert response.status == '200 OK'
141 140 response.mustcontain('Users administration')
142 141
143 142 def test_redirect_to_login_with_get_args(self):
144 143 with fixture.anon_access(False):
145 144 kwargs = {'branch': 'stable'}
146 145 response = self.app.get(
147 146 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
148 147 status=302)
149 148
150 149 response_query = urllib.parse.parse_qsl(response.location)
151 150 assert 'branch=stable' in response_query[0][1]
152 151
153 152 def test_login_form_with_get_args(self):
154 153 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
155 154 response = self.app.get(_url)
156 155 assert 'branch%3Dstable' in response.form.action
157 156
158 157 @pytest.mark.parametrize("url_came_from", [
159 158 'data:text/html,<script>window.alert("xss")</script>',
160 159 'mailto:test@rhodecode.org',
161 160 'file:///etc/passwd',
162 161 'ftp://some.ftp.server',
163 162 'http://other.domain',
164 163 '/\r\nX-Forwarded-Host: http://example.org',
165 164 ], ids=no_newline_id_generator)
166 165 def test_login_bad_came_froms(self, url_came_from):
167 166 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
168 167 response = self.app.post(
169 168 _url,
170 169 {'username': 'test_admin', 'password': 'test12'})
171 170 assert response.status == '302 Found'
172 171 response = response.follow()
173 172 assert response.status == '200 OK'
174 173 assert response.request.path == '/'
175 174
176 175 def test_login_short_password(self):
177 176 response = self.app.post(route_path('login'),
178 177 {'username': 'test_admin',
179 178 'password': 'as'})
180 179 assert response.status == '200 OK'
181 180
182 181 response.mustcontain('Enter 3 characters or more')
183 182
184 183 def test_login_wrong_non_ascii_password(self, user_regular):
185 184 response = self.app.post(
186 185 route_path('login'),
187 186 {'username': user_regular.username,
188 187 'password': u'invalid-non-asci\xe4'.encode('utf8')})
189 188
190 189 response.mustcontain('invalid user name')
191 190 response.mustcontain('invalid password')
192 191
193 192 def test_login_with_non_ascii_password(self, user_util):
194 193 password = u'valid-non-ascii\xe4'
195 194 user = user_util.create_user(password=password)
196 195 response = self.app.post(
197 196 route_path('login'),
198 197 {'username': user.username,
199 198 'password': password})
200 199 assert response.status_code == 302
201 200
202 201 def test_login_wrong_username_password(self):
203 202 response = self.app.post(route_path('login'),
204 203 {'username': 'error',
205 204 'password': 'test12'})
206 205
207 206 response.mustcontain('invalid user name')
208 207 response.mustcontain('invalid password')
209 208
210 209 def test_login_admin_ok_password_migration(self, real_crypto_backend):
211 210 from rhodecode.lib import auth
212 211
213 212 # create new user, with sha256 password
214 213 temp_user = 'test_admin_sha256'
215 214 user = fixture.create_user(temp_user)
216 215 user.password = auth._RhodeCodeCryptoSha256().hash_create(
217 216 b'test123')
218 217 Session().add(user)
219 218 Session().commit()
220 219 self.destroy_users.add(temp_user)
221 220 response = self.app.post(route_path('login'),
222 221 {'username': temp_user,
223 222 'password': 'test123'}, status=302)
224 223
225 224 response = response.follow()
226 225 session = response.get_session_from_response()
227 226 username = session['rhodecode_user'].get('username')
228 227 assert username == temp_user
229 228 response.mustcontain('logout')
230 229
231 230 # new password should be bcrypted, after log-in and transfer
232 231 user = User.get_by_username(temp_user)
233 232 assert user.password.startswith('$')
234 233
235 234 # REGISTRATIONS
236 235 def test_register(self):
237 236 response = self.app.get(route_path('register'))
238 237 response.mustcontain('Create an Account')
239 238
240 239 def test_register_err_same_username(self):
241 240 uname = 'test_admin'
242 241 response = self.app.post(
243 242 route_path('register'),
244 243 {
245 244 'username': uname,
246 245 'password': 'test12',
247 246 'password_confirmation': 'test12',
248 247 'email': 'goodmail@domain.com',
249 248 'firstname': 'test',
250 249 'lastname': 'test'
251 250 }
252 251 )
253 252
254 253 assertr = response.assert_response()
255 254 msg = 'Username "%(username)s" already exists'
256 255 msg = msg % {'username': uname}
257 256 assertr.element_contains('#username+.error-message', msg)
258 257
259 258 def test_register_err_same_email(self):
260 259 response = self.app.post(
261 260 route_path('register'),
262 261 {
263 262 'username': 'test_admin_0',
264 263 'password': 'test12',
265 264 'password_confirmation': 'test12',
266 265 'email': 'test_admin@mail.com',
267 266 'firstname': 'test',
268 267 'lastname': 'test'
269 268 }
270 269 )
271 270
272 271 assertr = response.assert_response()
273 272 msg = u'This e-mail address is already taken'
274 273 assertr.element_contains('#email+.error-message', msg)
275 274
276 275 def test_register_err_same_email_case_sensitive(self):
277 276 response = self.app.post(
278 277 route_path('register'),
279 278 {
280 279 'username': 'test_admin_1',
281 280 'password': 'test12',
282 281 'password_confirmation': 'test12',
283 282 'email': 'TesT_Admin@mail.COM',
284 283 'firstname': 'test',
285 284 'lastname': 'test'
286 285 }
287 286 )
288 287 assertr = response.assert_response()
289 288 msg = u'This e-mail address is already taken'
290 289 assertr.element_contains('#email+.error-message', msg)
291 290
292 291 def test_register_err_wrong_data(self):
293 292 response = self.app.post(
294 293 route_path('register'),
295 294 {
296 295 'username': 'xs',
297 296 'password': 'test',
298 297 'password_confirmation': 'test',
299 298 'email': 'goodmailm',
300 299 'firstname': 'test',
301 300 'lastname': 'test'
302 301 }
303 302 )
304 303 assert response.status == '200 OK'
305 304 response.mustcontain('An email address must contain a single @')
306 305 response.mustcontain('Enter a value 6 characters long or more')
307 306
308 307 def test_register_err_username(self):
309 308 response = self.app.post(
310 309 route_path('register'),
311 310 {
312 311 'username': 'error user',
313 312 'password': 'test12',
314 313 'password_confirmation': 'test12',
315 314 'email': 'goodmailm',
316 315 'firstname': 'test',
317 316 'lastname': 'test'
318 317 }
319 318 )
320 319
321 320 response.mustcontain('An email address must contain a single @')
322 321 response.mustcontain(
323 322 'Username may only contain '
324 323 'alphanumeric characters underscores, '
325 324 'periods or dashes and must begin with '
326 325 'alphanumeric character')
327 326
328 327 def test_register_err_case_sensitive(self):
329 328 usr = 'Test_Admin'
330 329 response = self.app.post(
331 330 route_path('register'),
332 331 {
333 332 'username': usr,
334 333 'password': 'test12',
335 334 'password_confirmation': 'test12',
336 335 'email': 'goodmailm',
337 336 'firstname': 'test',
338 337 'lastname': 'test'
339 338 }
340 339 )
341 340
342 341 assertr = response.assert_response()
343 342 msg = u'Username "%(username)s" already exists'
344 343 msg = msg % {'username': usr}
345 344 assertr.element_contains('#username+.error-message', msg)
346 345
347 346 def test_register_special_chars(self):
348 347 response = self.app.post(
349 348 route_path('register'),
350 349 {
351 350 'username': 'xxxaxn',
352 351 'password': 'ąćźżąśśśś',
353 352 'password_confirmation': 'ąćźżąśśśś',
354 353 'email': 'goodmailm@test.plx',
355 354 'firstname': 'test',
356 355 'lastname': 'test'
357 356 }
358 357 )
359 358
360 359 msg = u'Invalid characters (non-ascii) in password'
361 360 response.mustcontain(msg)
362 361
363 362 def test_register_password_mismatch(self):
364 363 response = self.app.post(
365 364 route_path('register'),
366 365 {
367 366 'username': 'xs',
368 367 'password': '123qwe',
369 368 'password_confirmation': 'qwe123',
370 369 'email': 'goodmailm@test.plxa',
371 370 'firstname': 'test',
372 371 'lastname': 'test'
373 372 }
374 373 )
375 374 msg = u'Passwords do not match'
376 375 response.mustcontain(msg)
377 376
378 377 def test_register_ok(self):
379 378 username = 'test_regular4'
380 379 password = 'qweqwe'
381 380 email = 'marcin@test.com'
382 381 name = 'testname'
383 382 lastname = 'testlastname'
384 383
385 384 # this initializes a session
386 385 response = self.app.get(route_path('register'))
387 386 response.mustcontain('Create an Account')
388 387
389 388
390 389 response = self.app.post(
391 390 route_path('register'),
392 391 {
393 392 'username': username,
394 393 'password': password,
395 394 'password_confirmation': password,
396 395 'email': email,
397 396 'firstname': name,
398 397 'lastname': lastname,
399 398 'admin': True
400 399 },
401 400 status=302
402 401 ) # This should be overridden
403 402
404 403 assert_session_flash(
405 404 response, 'You have successfully registered with RhodeCode. You can log-in now.')
406 405
407 406 ret = Session().query(User).filter(
408 407 User.username == 'test_regular4').one()
409 408 assert ret.username == username
410 409 assert check_password(password, ret.password)
411 410 assert ret.email == email
412 411 assert ret.name == name
413 412 assert ret.lastname == lastname
414 413 assert ret.auth_tokens is not None
415 414 assert not ret.admin
416 415
417 416 def test_forgot_password_wrong_mail(self):
418 417 bad_email = 'marcin@wrongmail.org'
419 418 # this initializes a session
420 419 self.app.get(route_path('reset_password'))
421 420
422 421 response = self.app.post(
423 422 route_path('reset_password'), {'email': bad_email, }
424 423 )
425 424 assert_session_flash(response,
426 425 'If such email exists, a password reset link was sent to it.')
427 426
428 427 def test_forgot_password(self, user_util):
429 428 # this initializes a session
430 429 self.app.get(route_path('reset_password'))
431 430
432 431 user = user_util.create_user()
433 432 user_id = user.user_id
434 433 email = user.email
435 434
436 435 response = self.app.post(route_path('reset_password'), {'email': email, })
437 436
438 437 assert_session_flash(response,
439 438 'If such email exists, a password reset link was sent to it.')
440 439
441 440 # BAD KEY
442 441 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
443 442 response = self.app.get(confirm_url, status=302)
444 443 assert response.location.endswith(route_path('reset_password'))
445 444 assert_session_flash(response, 'Given reset token is invalid')
446 445
447 446 response.follow() # cleanup flash
448 447
449 448 # GOOD KEY
450 449 key = UserApiKeys.query()\
451 450 .filter(UserApiKeys.user_id == user_id)\
452 451 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
453 452 .first()
454 453
455 454 assert key
456 455
457 456 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
458 457 response = self.app.get(confirm_url)
459 458 assert response.status == '302 Found'
460 459 assert response.location.endswith(route_path('login'))
461 460
462 461 assert_session_flash(
463 462 response,
464 463 'Your password reset was successful, '
465 464 'a new password has been sent to your email')
466 465
467 466 response.follow()
468 467
469 468 def _get_api_whitelist(self, values=None):
470 469 config = {'api_access_controllers_whitelist': values or []}
471 470 return config
472 471
473 472 @pytest.mark.parametrize("test_name, auth_token", [
474 473 ('none', None),
475 474 ('empty_string', ''),
476 475 ('fake_number', '123456'),
477 476 ('proper_auth_token', None)
478 477 ])
479 478 def test_access_not_whitelisted_page_via_auth_token(
480 479 self, test_name, auth_token, user_admin):
481 480
482 481 whitelist = self._get_api_whitelist([])
483 482 with mock.patch.dict('rhodecode.CONFIG', whitelist):
484 483 assert [] == whitelist['api_access_controllers_whitelist']
485 484 if test_name == 'proper_auth_token':
486 485 # use builtin if api_key is None
487 486 auth_token = user_admin.api_key
488 487
489 488 with fixture.anon_access(False):
490 489 self.app.get(
491 490 route_path('repo_commit_raw',
492 491 repo_name=HG_REPO, commit_id='tip',
493 492 params=dict(api_key=auth_token)),
494 493 status=302)
495 494
496 495 @pytest.mark.parametrize("test_name, auth_token, code", [
497 496 ('none', None, 302),
498 497 ('empty_string', '', 302),
499 498 ('fake_number', '123456', 302),
500 499 ('proper_auth_token', None, 200)
501 500 ])
502 501 def test_access_whitelisted_page_via_auth_token(
503 502 self, test_name, auth_token, code, user_admin):
504 503
505 504 whitelist = self._get_api_whitelist(whitelist_view)
506 505
507 506 with mock.patch.dict('rhodecode.CONFIG', whitelist):
508 507 assert whitelist_view == whitelist['api_access_controllers_whitelist']
509 508
510 509 if test_name == 'proper_auth_token':
511 510 auth_token = user_admin.api_key
512 511 assert auth_token
513 512
514 513 with fixture.anon_access(False):
515 514 self.app.get(
516 515 route_path('repo_commit_raw',
517 516 repo_name=HG_REPO, commit_id='tip',
518 517 params=dict(api_key=auth_token)),
519 518 status=code)
520 519
521 520 @pytest.mark.parametrize("test_name, auth_token, code", [
522 521 ('proper_auth_token', None, 200),
523 522 ('wrong_auth_token', '123456', 302),
524 523 ])
525 524 def test_access_whitelisted_page_via_auth_token_bound_to_token(
526 525 self, test_name, auth_token, code, user_admin):
527 526
528 527 expected_token = auth_token
529 528 if test_name == 'proper_auth_token':
530 529 auth_token = user_admin.api_key
531 530 expected_token = auth_token
532 531 assert auth_token
533 532
534 533 whitelist = self._get_api_whitelist([
535 534 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
536 535
537 536 with mock.patch.dict('rhodecode.CONFIG', whitelist):
538 537
539 538 with fixture.anon_access(False):
540 539 self.app.get(
541 540 route_path('repo_commit_raw',
542 541 repo_name=HG_REPO, commit_id='tip',
543 542 params=dict(api_key=auth_token)),
544 543 status=code)
545 544
546 545 def test_access_page_via_extra_auth_token(self):
547 546 whitelist = self._get_api_whitelist(whitelist_view)
548 547 with mock.patch.dict('rhodecode.CONFIG', whitelist):
549 548 assert whitelist_view == \
550 549 whitelist['api_access_controllers_whitelist']
551 550
552 551 new_auth_token = AuthTokenModel().create(
553 552 TEST_USER_ADMIN_LOGIN, 'test')
554 553 Session().commit()
555 554 with fixture.anon_access(False):
556 555 self.app.get(
557 556 route_path('repo_commit_raw',
558 557 repo_name=HG_REPO, commit_id='tip',
559 558 params=dict(api_key=new_auth_token.api_key)),
560 559 status=200)
561 560
562 561 def test_access_page_via_expired_auth_token(self):
563 562 whitelist = self._get_api_whitelist(whitelist_view)
564 563 with mock.patch.dict('rhodecode.CONFIG', whitelist):
565 564 assert whitelist_view == \
566 565 whitelist['api_access_controllers_whitelist']
567 566
568 567 new_auth_token = AuthTokenModel().create(
569 568 TEST_USER_ADMIN_LOGIN, 'test')
570 569 Session().commit()
571 570 # patch the api key and make it expired
572 571 new_auth_token.expires = 0
573 572 Session().add(new_auth_token)
574 573 Session().commit()
575 574 with fixture.anon_access(False):
576 575 self.app.get(
577 576 route_path('repo_commit_raw',
578 577 repo_name=HG_REPO, commit_id='tip',
579 578 params=dict(api_key=new_auth_token.api_key)),
580 579 status=302)
@@ -1,118 +1,117 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.lib import helpers as h
24 23 from rhodecode.tests import (
25 24 TestController, clear_cache_regions,
26 25 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
27 26 from rhodecode.tests.fixture import Fixture
28 27 from rhodecode.tests.utils import AssertResponse
29 28
30 29 fixture = Fixture()
31 30
32 31
33 32 def route_path(name, params=None, **kwargs):
34 33 import urllib.request, urllib.parse, urllib.error
35 34 from rhodecode.apps._base import ADMIN_PREFIX
36 35
37 36 base_url = {
38 37 'login': ADMIN_PREFIX + '/login',
39 38 'logout': ADMIN_PREFIX + '/logout',
40 39 'register': ADMIN_PREFIX + '/register',
41 40 'reset_password':
42 41 ADMIN_PREFIX + '/password_reset',
43 42 'reset_password_confirmation':
44 43 ADMIN_PREFIX + '/password_reset_confirmation',
45 44
46 45 'admin_permissions_application':
47 46 ADMIN_PREFIX + '/permissions/application',
48 47 'admin_permissions_application_update':
49 48 ADMIN_PREFIX + '/permissions/application/update',
50 49 }[name].format(**kwargs)
51 50
52 51 if params:
53 52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 53 return base_url
55 54
56 55
57 56 class TestPasswordReset(TestController):
58 57
59 58 @pytest.mark.parametrize(
60 59 'pwd_reset_setting, show_link, show_reset', [
61 60 ('hg.password_reset.enabled', True, True),
62 61 ('hg.password_reset.hidden', False, True),
63 62 ('hg.password_reset.disabled', False, False),
64 63 ])
65 64 def test_password_reset_settings(
66 65 self, pwd_reset_setting, show_link, show_reset):
67 66 clear_cache_regions()
68 67 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
69 68 params = {
70 69 'csrf_token': self.csrf_token,
71 70 'anonymous': 'True',
72 71 'default_register': 'hg.register.auto_activate',
73 72 'default_register_message': '',
74 73 'default_password_reset': pwd_reset_setting,
75 74 'default_extern_activate': 'hg.extern_activate.auto',
76 75 }
77 76 resp = self.app.post(
78 77 route_path('admin_permissions_application_update'), params=params)
79 78 self.logout_user()
80 79
81 80 login_page = self.app.get(route_path('login'))
82 81 asr_login = AssertResponse(login_page)
83 82
84 83 if show_link:
85 84 asr_login.one_element_exists('a.pwd_reset')
86 85 else:
87 86 asr_login.no_element_exists('a.pwd_reset')
88 87
89 88 response = self.app.get(route_path('reset_password'))
90 89
91 90 assert_response = response.assert_response()
92 91 if show_reset:
93 92 response.mustcontain('Send password reset email')
94 93 assert_response.one_element_exists('#email')
95 94 assert_response.one_element_exists('#send')
96 95 else:
97 96 response.mustcontain('Password reset is disabled.')
98 97 assert_response.no_element_exists('#email')
99 98 assert_response.no_element_exists('#send')
100 99
101 100 def test_password_form_disabled(self):
102 101 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
103 102 params = {
104 103 'csrf_token': self.csrf_token,
105 104 'anonymous': 'True',
106 105 'default_register': 'hg.register.auto_activate',
107 106 'default_register_message': '',
108 107 'default_password_reset': 'hg.password_reset.disabled',
109 108 'default_extern_activate': 'hg.extern_activate.auto',
110 109 }
111 110 self.app.post(route_path('admin_permissions_application_update'), params=params)
112 111 self.logout_user()
113 112
114 113 response = self.app.post(
115 114 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
116 115 )
117 116 response = response.follow()
118 117 response.mustcontain('Password reset is disabled.')
@@ -1,133 +1,133 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import mock
23 23 import pytest
24 24
25 25 from rhodecode.apps._base import ADMIN_PREFIX
26 26 from rhodecode.apps.login.views import LoginView, CaptchaData
27 27 from rhodecode.model.settings import SettingsModel
28 28 from rhodecode.lib.utils2 import AttributeDict
29 29 from rhodecode.tests.utils import AssertResponse
30 30
31 31
32 32 class RhodeCodeSetting(object):
33 33 def __init__(self, name, value):
34 34 self.name = name
35 35 self.value = value
36 36
37 37 def __enter__(self):
38 38 from rhodecode.model.settings import SettingsModel
39 39 model = SettingsModel()
40 40 self.old_setting = model.get_setting_by_name(self.name)
41 41 model.create_or_update_setting(name=self.name, val=self.value)
42 42 return self
43 43
44 44 def __exit__(self, exc_type, exc_val, exc_tb):
45 45 model = SettingsModel()
46 46 if self.old_setting:
47 47 model.create_or_update_setting(
48 48 name=self.name, val=self.old_setting.app_settings_value)
49 49 else:
50 50 model.create_or_update_setting(name=self.name)
51 51
52 52
53 53 class TestRegisterCaptcha(object):
54 54
55 55 @pytest.mark.parametrize('private_key, public_key, expected', [
56 56 ('', '', CaptchaData(False, '', '')),
57 57 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
58 58 ('privkey', '', CaptchaData(True, 'privkey', '')),
59 59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
60 60 ])
61 61 def test_get_captcha_data(self, private_key, public_key, expected,
62 62 request_stub, user_util):
63 63 request_stub.user = user_util.create_user().AuthUser()
64 64 request_stub.matched_route = AttributeDict({'name': 'login'})
65 65 login_view = LoginView(mock.Mock(), request_stub)
66 66
67 67 with RhodeCodeSetting('captcha_private_key', private_key):
68 68 with RhodeCodeSetting('captcha_public_key', public_key):
69 69 captcha = login_view._get_captcha_data()
70 70 assert captcha == expected
71 71
72 72 @pytest.mark.parametrize('active', [False, True])
73 73 @mock.patch.object(LoginView, '_get_captcha_data')
74 74 def test_private_key_does_not_leak_to_html(
75 75 self, m_get_captcha_data, active, app):
76 76 captcha = CaptchaData(
77 77 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
78 78 m_get_captcha_data.return_value = captcha
79 79
80 80 response = app.get(ADMIN_PREFIX + '/register')
81 81 assert 'PRIVATE_KEY' not in response
82 82
83 83 @pytest.mark.parametrize('active', [False, True])
84 84 @mock.patch.object(LoginView, '_get_captcha_data')
85 85 def test_register_view_renders_captcha(
86 86 self, m_get_captcha_data, active, app):
87 87 captcha = CaptchaData(
88 88 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
89 89 m_get_captcha_data.return_value = captcha
90 90
91 91 response = app.get(ADMIN_PREFIX + '/register')
92 92
93 93 assertr = response.assert_response()
94 94 if active:
95 95 assertr.one_element_exists('#recaptcha_field')
96 96 else:
97 97 assertr.no_element_exists('#recaptcha_field')
98 98
99 99 @pytest.mark.parametrize('valid', [False, True])
100 100 @mock.patch.object(LoginView, 'validate_captcha')
101 101 @mock.patch.object(LoginView, '_get_captcha_data')
102 102 def test_register_with_active_captcha(
103 103 self, m_get_captcha_data, m_validate_captcha, valid, app, csrf_token):
104 104 captcha = CaptchaData(
105 105 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
106 106 m_get_captcha_data.return_value = captcha
107 107 m_response = mock.Mock()
108 108 m_response.is_valid = valid
109 109 m_validate_captcha.return_value = valid, 'ok'
110 110
111 111 params = {
112 112 'csrf_token': csrf_token,
113 113 'email': 'pytest@example.com',
114 114 'firstname': 'pytest-firstname',
115 115 'lastname': 'pytest-lastname',
116 116 'password': 'secret',
117 117 'password_confirmation': 'secret',
118 118 'username': 'pytest',
119 119 }
120 120 response = app.post(ADMIN_PREFIX + '/register', params=params)
121 121
122 122 if valid:
123 123 # If we provided a valid captcha input we expect a successful
124 124 # registration and redirect to the login page.
125 125 assert response.status_int == 302
126 126 assert 'location' in response.headers
127 127 assert ADMIN_PREFIX + '/login' in response.headers['location']
128 128 else:
129 129 # If captche input is invalid we expect to stay on the registration
130 130 # page with an error message displayed.
131 131 assertr = response.assert_response()
132 132 assert response.status_int == 200
133 133 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
@@ -1,470 +1,471 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import collections
23 23 import datetime
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import logging
27 27 import urllib.parse
28 28 import requests
29 29
30 30 from pyramid.httpexceptions import HTTPFound
31 31
32 32
33 33 from rhodecode.apps._base import BaseAppView
34 34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 35 from rhodecode.authentication.plugins import auth_rhodecode
36 36 from rhodecode.events import UserRegistered, trigger
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.auth import (
40 40 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
41 41 from rhodecode.lib.base import get_ip_addr
42 42 from rhodecode.lib.exceptions import UserCreationError
43 43 from rhodecode.lib.utils2 import safe_str
44 44 from rhodecode.model.db import User, UserApiKeys
45 45 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.auth_token import AuthTokenModel
48 48 from rhodecode.model.settings import SettingsModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.translation import _
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55 CaptchaData = collections.namedtuple(
56 56 'CaptchaData', 'active, private_key, public_key')
57 57
58 58
59 59 def store_user_in_session(session, username, remember=False):
60 60 user = User.get_by_username(username, case_insensitive=True)
61 61 auth_user = AuthUser(user.user_id)
62 62 auth_user.set_authenticated()
63 63 cs = auth_user.get_cookie_store()
64 64 session['rhodecode_user'] = cs
65 65 user.update_lastlogin()
66 66 Session().commit()
67 67
68 68 # If they want to be remembered, update the cookie
69 69 if remember:
70 70 _year = (datetime.datetime.now() +
71 71 datetime.timedelta(seconds=60 * 60 * 24 * 365))
72 72 session._set_cookie_expires(_year)
73 73
74 74 session.save()
75 75
76 76 safe_cs = cs.copy()
77 77 safe_cs['password'] = '****'
78 78 log.info('user %s is now authenticated and stored in '
79 79 'session, session attrs %s', username, safe_cs)
80 80
81 81 # dumps session attrs back to cookie
82 82 session._update_cookie_out()
83 83 # we set new cookie
84 84 headers = None
85 85 if session.request['set_cookie']:
86 86 # send set-cookie headers back to response to update cookie
87 87 headers = [('Set-Cookie', session.request['cookie_out'])]
88 88 return headers
89 89
90 90
91 91 def get_came_from(request):
92 92 came_from = safe_str(request.GET.get('came_from', ''))
93 93 parsed = urllib.parse.urlparse(came_from)
94
94 95 allowed_schemes = ['http', 'https']
95 96 default_came_from = h.route_path('home')
96 97 if parsed.scheme and parsed.scheme not in allowed_schemes:
97 98 log.error('Suspicious URL scheme detected %s for url %s',
98 99 parsed.scheme, parsed)
99 100 came_from = default_came_from
100 101 elif parsed.netloc and request.host != parsed.netloc:
101 102 log.error('Suspicious NETLOC detected %s for url %s server url '
102 103 'is: %s', parsed.netloc, parsed, request.host)
103 104 came_from = default_came_from
104 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
105 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
105 106 log.error('Header injection detected `%s` for url %s server url ',
106 107 parsed.path, parsed)
107 108 came_from = default_came_from
108 109
109 110 return came_from or default_came_from
110 111
111 112
112 113 class LoginView(BaseAppView):
113 114
114 115 def load_default_context(self):
115 116 c = self._get_local_tmpl_context()
116 117 c.came_from = get_came_from(self.request)
117 118 return c
118 119
119 120 def _get_captcha_data(self):
120 121 settings = SettingsModel().get_all_settings()
121 122 private_key = settings.get('rhodecode_captcha_private_key')
122 123 public_key = settings.get('rhodecode_captcha_public_key')
123 124 active = bool(private_key)
124 125 return CaptchaData(
125 126 active=active, private_key=private_key, public_key=public_key)
126 127
127 128 def validate_captcha(self, private_key):
128 129
129 130 captcha_rs = self.request.POST.get('g-recaptcha-response')
130 131 url = "https://www.google.com/recaptcha/api/siteverify"
131 132 params = {
132 133 'secret': private_key,
133 134 'response': captcha_rs,
134 135 'remoteip': get_ip_addr(self.request.environ)
135 136 }
136 137 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
137 138 verify_rs = verify_rs.json()
138 139 captcha_status = verify_rs.get('success', False)
139 140 captcha_errors = verify_rs.get('error-codes', [])
140 141 if not isinstance(captcha_errors, list):
141 142 captcha_errors = [captcha_errors]
142 143 captcha_errors = ', '.join(captcha_errors)
143 144 captcha_message = ''
144 145 if captcha_status is False:
145 146 captcha_message = "Bad captcha. Errors: {}".format(
146 147 captcha_errors)
147 148
148 149 return captcha_status, captcha_message
149 150
150 151 def login(self):
151 152 c = self.load_default_context()
152 153 auth_user = self._rhodecode_user
153 154
154 155 # redirect if already logged in
155 156 if (auth_user.is_authenticated and
156 157 not auth_user.is_default and auth_user.ip_allowed):
157 158 raise HTTPFound(c.came_from)
158 159
159 160 # check if we use headers plugin, and try to login using it.
160 161 try:
161 162 log.debug('Running PRE-AUTH for headers based authentication')
162 163 auth_info = authenticate(
163 164 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
164 165 if auth_info:
165 166 headers = store_user_in_session(
166 167 self.session, auth_info.get('username'))
167 168 raise HTTPFound(c.came_from, headers=headers)
168 169 except UserCreationError as e:
169 170 log.error(e)
170 171 h.flash(e, category='error')
171 172
172 173 return self._get_template_context(c)
173 174
174 175 def login_post(self):
175 176 c = self.load_default_context()
176 177
177 178 login_form = LoginForm(self.request.translate)()
178 179
179 180 try:
180 181 self.session.invalidate()
181 182 form_result = login_form.to_python(self.request.POST)
182 183 # form checks for username/password, now we're authenticated
183 184 headers = store_user_in_session(
184 185 self.session,
185 186 username=form_result['username'],
186 187 remember=form_result['remember'])
187 188 log.debug('Redirecting to "%s" after login.', c.came_from)
188 189
189 190 audit_user = audit_logger.UserWrap(
190 191 username=self.request.POST.get('username'),
191 192 ip_addr=self.request.remote_addr)
192 193 action_data = {'user_agent': self.request.user_agent}
193 194 audit_logger.store_web(
194 195 'user.login.success', action_data=action_data,
195 196 user=audit_user, commit=True)
196 197
197 198 raise HTTPFound(c.came_from, headers=headers)
198 199 except formencode.Invalid as errors:
199 200 defaults = errors.value
200 201 # remove password from filling in form again
201 202 defaults.pop('password', None)
202 203 render_ctx = {
203 204 'errors': errors.error_dict,
204 205 'defaults': defaults,
205 206 }
206 207
207 208 audit_user = audit_logger.UserWrap(
208 209 username=self.request.POST.get('username'),
209 210 ip_addr=self.request.remote_addr)
210 211 action_data = {'user_agent': self.request.user_agent}
211 212 audit_logger.store_web(
212 213 'user.login.failure', action_data=action_data,
213 214 user=audit_user, commit=True)
214 215 return self._get_template_context(c, **render_ctx)
215 216
216 217 except UserCreationError as e:
217 218 # headers auth or other auth functions that create users on
218 219 # the fly can throw this exception signaling that there's issue
219 220 # with user creation, explanation should be provided in
220 221 # Exception itself
221 222 h.flash(e, category='error')
222 223 return self._get_template_context(c)
223 224
224 225 @CSRFRequired()
225 226 def logout(self):
226 227 auth_user = self._rhodecode_user
227 228 log.info('Deleting session for user: `%s`', auth_user)
228 229
229 230 action_data = {'user_agent': self.request.user_agent}
230 231 audit_logger.store_web(
231 232 'user.logout', action_data=action_data,
232 233 user=auth_user, commit=True)
233 234 self.session.delete()
234 235 return HTTPFound(h.route_path('home'))
235 236
236 237 @HasPermissionAnyDecorator(
237 238 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
238 239 def register(self, defaults=None, errors=None):
239 240 c = self.load_default_context()
240 241 defaults = defaults or {}
241 242 errors = errors or {}
242 243
243 244 settings = SettingsModel().get_all_settings()
244 245 register_message = settings.get('rhodecode_register_message') or ''
245 246 captcha = self._get_captcha_data()
246 247 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
247 248 .AuthUser().permissions['global']
248 249
249 250 render_ctx = self._get_template_context(c)
250 251 render_ctx.update({
251 252 'defaults': defaults,
252 253 'errors': errors,
253 254 'auto_active': auto_active,
254 255 'captcha_active': captcha.active,
255 256 'captcha_public_key': captcha.public_key,
256 257 'register_message': register_message,
257 258 })
258 259 return render_ctx
259 260
260 261 @HasPermissionAnyDecorator(
261 262 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
262 263 def register_post(self):
263 264 from rhodecode.authentication.plugins import auth_rhodecode
264 265
265 266 self.load_default_context()
266 267 captcha = self._get_captcha_data()
267 268 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
268 269 .AuthUser().permissions['global']
269 270
270 271 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
271 272 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
272 273
273 274 register_form = RegisterForm(self.request.translate)()
274 275 try:
275 276
276 277 form_result = register_form.to_python(self.request.POST)
277 278 form_result['active'] = auto_active
278 279 external_identity = self.request.POST.get('external_identity')
279 280
280 281 if external_identity:
281 282 extern_name = external_identity
282 283 extern_type = external_identity
283 284
284 285 if captcha.active:
285 286 captcha_status, captcha_message = self.validate_captcha(
286 287 captcha.private_key)
287 288
288 289 if not captcha_status:
289 290 _value = form_result
290 291 _msg = _('Bad captcha')
291 292 error_dict = {'recaptcha_field': captcha_message}
292 293 raise formencode.Invalid(
293 294 _msg, _value, None, error_dict=error_dict)
294 295
295 296 new_user = UserModel().create_registration(
296 297 form_result, extern_name=extern_name, extern_type=extern_type)
297 298
298 299 action_data = {'data': new_user.get_api_data(),
299 300 'user_agent': self.request.user_agent}
300 301
301 302 if external_identity:
302 303 action_data['external_identity'] = external_identity
303 304
304 305 audit_user = audit_logger.UserWrap(
305 306 username=new_user.username,
306 307 user_id=new_user.user_id,
307 308 ip_addr=self.request.remote_addr)
308 309
309 310 audit_logger.store_web(
310 311 'user.register', action_data=action_data,
311 312 user=audit_user)
312 313
313 314 event = UserRegistered(user=new_user, session=self.session)
314 315 trigger(event)
315 316 h.flash(
316 317 _('You have successfully registered with RhodeCode. You can log-in now.'),
317 318 category='success')
318 319 if external_identity:
319 320 h.flash(
320 321 _('Please use the {identity} button to log-in').format(
321 322 identity=external_identity),
322 323 category='success')
323 324 Session().commit()
324 325
325 326 redirect_ro = self.request.route_path('login')
326 327 raise HTTPFound(redirect_ro)
327 328
328 329 except formencode.Invalid as errors:
329 330 errors.value.pop('password', None)
330 331 errors.value.pop('password_confirmation', None)
331 332 return self.register(
332 333 defaults=errors.value, errors=errors.error_dict)
333 334
334 335 except UserCreationError as e:
335 336 # container auth or other auth functions that create users on
336 337 # the fly can throw this exception signaling that there's issue
337 338 # with user creation, explanation should be provided in
338 339 # Exception itself
339 340 h.flash(e, category='error')
340 341 return self.register()
341 342
342 343 def password_reset(self):
343 344 c = self.load_default_context()
344 345 captcha = self._get_captcha_data()
345 346
346 347 template_context = {
347 348 'captcha_active': captcha.active,
348 349 'captcha_public_key': captcha.public_key,
349 350 'defaults': {},
350 351 'errors': {},
351 352 }
352 353
353 354 # always send implicit message to prevent from discovery of
354 355 # matching emails
355 356 msg = _('If such email exists, a password reset link was sent to it.')
356 357
357 358 def default_response():
358 359 log.debug('faking response on invalid password reset')
359 360 # make this take 2s, to prevent brute forcing.
360 361 time.sleep(2)
361 362 h.flash(msg, category='success')
362 363 return HTTPFound(self.request.route_path('reset_password'))
363 364
364 365 if self.request.POST:
365 366 if h.HasPermissionAny('hg.password_reset.disabled')():
366 367 _email = self.request.POST.get('email', '')
367 368 log.error('Failed attempt to reset password for `%s`.', _email)
368 369 h.flash(_('Password reset has been disabled.'), category='error')
369 370 return HTTPFound(self.request.route_path('reset_password'))
370 371
371 372 password_reset_form = PasswordResetForm(self.request.translate)()
372 373 description = u'Generated token for password reset from {}'.format(
373 374 datetime.datetime.now().isoformat())
374 375
375 376 try:
376 377 form_result = password_reset_form.to_python(
377 378 self.request.POST)
378 379 user_email = form_result['email']
379 380
380 381 if captcha.active:
381 382 captcha_status, captcha_message = self.validate_captcha(
382 383 captcha.private_key)
383 384
384 385 if not captcha_status:
385 386 _value = form_result
386 387 _msg = _('Bad captcha')
387 388 error_dict = {'recaptcha_field': captcha_message}
388 389 raise formencode.Invalid(
389 390 _msg, _value, None, error_dict=error_dict)
390 391
391 392 # Generate reset URL and send mail.
392 393 user = User.get_by_email(user_email)
393 394
394 395 # only allow rhodecode based users to reset their password
395 396 # external auth shouldn't allow password reset
396 397 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
397 398 log.warning('User %s with external type `%s` tried a password reset. '
398 399 'This try was rejected', user, user.extern_type)
399 400 return default_response()
400 401
401 402 # generate password reset token that expires in 10 minutes
402 403 reset_token = UserModel().add_auth_token(
403 404 user=user, lifetime_minutes=10,
404 405 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
405 406 description=description)
406 407 Session().commit()
407 408
408 409 log.debug('Successfully created password recovery token')
409 410 password_reset_url = self.request.route_url(
410 411 'reset_password_confirmation',
411 412 _query={'key': reset_token.api_key})
412 413 UserModel().reset_password_link(
413 414 form_result, password_reset_url)
414 415
415 416 action_data = {'email': user_email,
416 417 'user_agent': self.request.user_agent}
417 418 audit_logger.store_web(
418 419 'user.password.reset_request', action_data=action_data,
419 420 user=self._rhodecode_user, commit=True)
420 421
421 422 return default_response()
422 423
423 424 except formencode.Invalid as errors:
424 425 template_context.update({
425 426 'defaults': errors.value,
426 427 'errors': errors.error_dict,
427 428 })
428 429 if not self.request.POST.get('email'):
429 430 # case of empty email, we want to report that
430 431 return self._get_template_context(c, **template_context)
431 432
432 433 if 'recaptcha_field' in errors.error_dict:
433 434 # case of failed captcha
434 435 return self._get_template_context(c, **template_context)
435 436
436 437 return default_response()
437 438
438 439 return self._get_template_context(c, **template_context)
439 440
440 441 def password_reset_confirmation(self):
441 442 self.load_default_context()
442 443 if self.request.GET and self.request.GET.get('key'):
443 444 # make this take 2s, to prevent brute forcing.
444 445 time.sleep(2)
445 446
446 447 token = AuthTokenModel().get_auth_token(
447 448 self.request.GET.get('key'))
448 449
449 450 # verify token is the correct role
450 451 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
451 452 log.debug('Got token with role:%s expected is %s',
452 453 getattr(token, 'role', 'EMPTY_TOKEN'),
453 454 UserApiKeys.ROLE_PASSWORD_RESET)
454 455 h.flash(
455 456 _('Given reset token is invalid'), category='error')
456 457 return HTTPFound(self.request.route_path('reset_password'))
457 458
458 459 try:
459 460 owner = token.user
460 461 data = {'email': owner.email, 'token': token.api_key}
461 462 UserModel().reset_password(data)
462 463 h.flash(
463 464 _('Your password reset was successful, '
464 465 'a new password has been sent to your email'),
465 466 category='success')
466 467 except Exception as e:
467 468 log.error(e)
468 469 return HTTPFound(self.request.route_path('reset_password'))
469 470
470 471 return HTTPFound(self.request.route_path('login'))
@@ -1,333 +1,333 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def includeme(config):
26 26 from rhodecode.apps.my_account.views.my_account import MyAccountView
27 27 from rhodecode.apps.my_account.views.my_account_notifications import MyAccountNotificationsView
28 28 from rhodecode.apps.my_account.views.my_account_ssh_keys import MyAccountSshKeysView
29 29
30 30 config.add_route(
31 31 name='my_account_profile',
32 32 pattern=ADMIN_PREFIX + '/my_account/profile')
33 33 config.add_view(
34 34 MyAccountView,
35 35 attr='my_account_profile',
36 36 route_name='my_account_profile', request_method='GET',
37 37 renderer='rhodecode:templates/admin/my_account/my_account.mako')
38 38
39 39 # my account edit details
40 40 config.add_route(
41 41 name='my_account_edit',
42 42 pattern=ADMIN_PREFIX + '/my_account/edit')
43 43 config.add_view(
44 44 MyAccountView,
45 45 attr='my_account_edit',
46 46 route_name='my_account_edit',
47 47 request_method='GET',
48 48 renderer='rhodecode:templates/admin/my_account/my_account.mako')
49 49
50 50 config.add_route(
51 51 name='my_account_update',
52 52 pattern=ADMIN_PREFIX + '/my_account/update')
53 53 config.add_view(
54 54 MyAccountView,
55 55 attr='my_account_update',
56 56 route_name='my_account_update',
57 57 request_method='POST',
58 58 renderer='rhodecode:templates/admin/my_account/my_account.mako')
59 59
60 60 # my account password
61 61 config.add_route(
62 62 name='my_account_password',
63 63 pattern=ADMIN_PREFIX + '/my_account/password')
64 64 config.add_view(
65 65 MyAccountView,
66 66 attr='my_account_password',
67 67 route_name='my_account_password', request_method='GET',
68 68 renderer='rhodecode:templates/admin/my_account/my_account.mako')
69 69
70 70 config.add_route(
71 71 name='my_account_password_update',
72 72 pattern=ADMIN_PREFIX + '/my_account/password/update')
73 73 config.add_view(
74 74 MyAccountView,
75 75 attr='my_account_password_update',
76 76 route_name='my_account_password_update', request_method='POST',
77 77 renderer='rhodecode:templates/admin/my_account/my_account.mako')
78 78
79 79 # my account tokens
80 80 config.add_route(
81 81 name='my_account_auth_tokens',
82 82 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
83 83 config.add_view(
84 84 MyAccountView,
85 85 attr='my_account_auth_tokens',
86 86 route_name='my_account_auth_tokens', request_method='GET',
87 87 renderer='rhodecode:templates/admin/my_account/my_account.mako')
88 88
89 89 config.add_route(
90 90 name='my_account_auth_tokens_view',
91 91 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/view')
92 92 config.add_view(
93 93 MyAccountView,
94 94 attr='my_account_auth_tokens_view',
95 95 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
96 96 renderer='json_ext')
97 97
98 98 config.add_route(
99 99 name='my_account_auth_tokens_add',
100 100 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')
101 101 config.add_view(
102 102 MyAccountView,
103 103 attr='my_account_auth_tokens_add',
104 104 route_name='my_account_auth_tokens_add', request_method='POST')
105 105
106 106 config.add_route(
107 107 name='my_account_auth_tokens_delete',
108 108 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
109 109 config.add_view(
110 110 MyAccountView,
111 111 attr='my_account_auth_tokens_delete',
112 112 route_name='my_account_auth_tokens_delete', request_method='POST')
113 113
114 114 # my account ssh keys
115 115 config.add_route(
116 116 name='my_account_ssh_keys',
117 117 pattern=ADMIN_PREFIX + '/my_account/ssh_keys')
118 118 config.add_view(
119 119 MyAccountSshKeysView,
120 120 attr='my_account_ssh_keys',
121 121 route_name='my_account_ssh_keys', request_method='GET',
122 122 renderer='rhodecode:templates/admin/my_account/my_account.mako')
123 123
124 124 config.add_route(
125 125 name='my_account_ssh_keys_generate',
126 126 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/generate')
127 127 config.add_view(
128 128 MyAccountSshKeysView,
129 129 attr='ssh_keys_generate_keypair',
130 130 route_name='my_account_ssh_keys_generate', request_method='GET',
131 131 renderer='rhodecode:templates/admin/my_account/my_account.mako')
132 132
133 133 config.add_route(
134 134 name='my_account_ssh_keys_add',
135 135 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/new')
136 136 config.add_view(
137 137 MyAccountSshKeysView,
138 138 attr='my_account_ssh_keys_add',
139 139 route_name='my_account_ssh_keys_add', request_method='POST',)
140 140
141 141 config.add_route(
142 142 name='my_account_ssh_keys_delete',
143 143 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/delete')
144 144 config.add_view(
145 145 MyAccountSshKeysView,
146 146 attr='my_account_ssh_keys_delete',
147 147 route_name='my_account_ssh_keys_delete', request_method='POST')
148 148
149 149 # my account user group membership
150 150 config.add_route(
151 151 name='my_account_user_group_membership',
152 152 pattern=ADMIN_PREFIX + '/my_account/user_group_membership')
153 153 config.add_view(
154 154 MyAccountView,
155 155 attr='my_account_user_group_membership',
156 156 route_name='my_account_user_group_membership',
157 157 request_method='GET',
158 158 renderer='rhodecode:templates/admin/my_account/my_account.mako')
159 159
160 160 # my account emails
161 161 config.add_route(
162 162 name='my_account_emails',
163 163 pattern=ADMIN_PREFIX + '/my_account/emails')
164 164 config.add_view(
165 165 MyAccountView,
166 166 attr='my_account_emails',
167 167 route_name='my_account_emails', request_method='GET',
168 168 renderer='rhodecode:templates/admin/my_account/my_account.mako')
169 169
170 170 config.add_route(
171 171 name='my_account_emails_add',
172 172 pattern=ADMIN_PREFIX + '/my_account/emails/new')
173 173 config.add_view(
174 174 MyAccountView,
175 175 attr='my_account_emails_add',
176 176 route_name='my_account_emails_add', request_method='POST',
177 177 renderer='rhodecode:templates/admin/my_account/my_account.mako')
178 178
179 179 config.add_route(
180 180 name='my_account_emails_delete',
181 181 pattern=ADMIN_PREFIX + '/my_account/emails/delete')
182 182 config.add_view(
183 183 MyAccountView,
184 184 attr='my_account_emails_delete',
185 185 route_name='my_account_emails_delete', request_method='POST')
186 186
187 187 config.add_route(
188 188 name='my_account_repos',
189 189 pattern=ADMIN_PREFIX + '/my_account/repos')
190 190 config.add_view(
191 191 MyAccountView,
192 192 attr='my_account_repos',
193 193 route_name='my_account_repos', request_method='GET',
194 194 renderer='rhodecode:templates/admin/my_account/my_account.mako')
195 195
196 196 config.add_route(
197 197 name='my_account_watched',
198 198 pattern=ADMIN_PREFIX + '/my_account/watched')
199 199 config.add_view(
200 200 MyAccountView,
201 201 attr='my_account_watched',
202 202 route_name='my_account_watched', request_method='GET',
203 203 renderer='rhodecode:templates/admin/my_account/my_account.mako')
204 204
205 205 config.add_route(
206 206 name='my_account_bookmarks',
207 207 pattern=ADMIN_PREFIX + '/my_account/bookmarks')
208 208 config.add_view(
209 209 MyAccountView,
210 210 attr='my_account_bookmarks',
211 211 route_name='my_account_bookmarks', request_method='GET',
212 212 renderer='rhodecode:templates/admin/my_account/my_account.mako')
213 213
214 214 config.add_route(
215 215 name='my_account_bookmarks_update',
216 216 pattern=ADMIN_PREFIX + '/my_account/bookmarks/update')
217 217 config.add_view(
218 218 MyAccountView,
219 219 attr='my_account_bookmarks_update',
220 220 route_name='my_account_bookmarks_update', request_method='POST')
221 221
222 222 config.add_route(
223 223 name='my_account_goto_bookmark',
224 224 pattern=ADMIN_PREFIX + '/my_account/bookmark/{bookmark_id}')
225 225 config.add_view(
226 226 MyAccountView,
227 227 attr='my_account_goto_bookmark',
228 228 route_name='my_account_goto_bookmark', request_method='GET',
229 229 renderer='rhodecode:templates/admin/my_account/my_account.mako')
230 230
231 231 config.add_route(
232 232 name='my_account_perms',
233 233 pattern=ADMIN_PREFIX + '/my_account/perms')
234 234 config.add_view(
235 235 MyAccountView,
236 236 attr='my_account_perms',
237 237 route_name='my_account_perms', request_method='GET',
238 238 renderer='rhodecode:templates/admin/my_account/my_account.mako')
239 239
240 240 config.add_route(
241 241 name='my_account_notifications',
242 242 pattern=ADMIN_PREFIX + '/my_account/notifications')
243 243 config.add_view(
244 244 MyAccountView,
245 245 attr='my_notifications',
246 246 route_name='my_account_notifications', request_method='GET',
247 247 renderer='rhodecode:templates/admin/my_account/my_account.mako')
248 248
249 249 config.add_route(
250 250 name='my_account_notifications_toggle_visibility',
251 251 pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
252 252 config.add_view(
253 253 MyAccountView,
254 254 attr='my_notifications_toggle_visibility',
255 255 route_name='my_account_notifications_toggle_visibility',
256 256 request_method='POST', renderer='json_ext')
257 257
258 258 # my account pull requests
259 259 config.add_route(
260 260 name='my_account_pullrequests',
261 261 pattern=ADMIN_PREFIX + '/my_account/pull_requests')
262 262 config.add_view(
263 263 MyAccountView,
264 264 attr='my_account_pullrequests',
265 265 route_name='my_account_pullrequests',
266 266 request_method='GET',
267 267 renderer='rhodecode:templates/admin/my_account/my_account.mako')
268 268
269 269 config.add_route(
270 270 name='my_account_pullrequests_data',
271 271 pattern=ADMIN_PREFIX + '/my_account/pull_requests/data')
272 272 config.add_view(
273 273 MyAccountView,
274 274 attr='my_account_pullrequests_data',
275 275 route_name='my_account_pullrequests_data',
276 276 request_method='GET', renderer='json_ext')
277 277
278 278 # channelstream test
279 279 config.add_route(
280 280 name='my_account_notifications_test_channelstream',
281 281 pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
282 282 config.add_view(
283 283 MyAccountView,
284 284 attr='my_account_notifications_test_channelstream',
285 285 route_name='my_account_notifications_test_channelstream',
286 286 request_method='POST', renderer='json_ext')
287 287
288 288 # notifications
289 289 config.add_route(
290 290 name='notifications_show_all',
291 291 pattern=ADMIN_PREFIX + '/notifications')
292 292 config.add_view(
293 293 MyAccountNotificationsView,
294 294 attr='notifications_show_all',
295 295 route_name='notifications_show_all', request_method='GET',
296 296 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
297 297
298 298 # notifications
299 299 config.add_route(
300 300 name='notifications_mark_all_read',
301 301 pattern=ADMIN_PREFIX + '/notifications_mark_all_read')
302 302 config.add_view(
303 303 MyAccountNotificationsView,
304 304 attr='notifications_mark_all_read',
305 305 route_name='notifications_mark_all_read', request_method='POST',
306 306 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
307 307
308 308 config.add_route(
309 309 name='notifications_show',
310 310 pattern=ADMIN_PREFIX + '/notifications/{notification_id}')
311 311 config.add_view(
312 312 MyAccountNotificationsView,
313 313 attr='notifications_show',
314 314 route_name='notifications_show', request_method='GET',
315 315 renderer='rhodecode:templates/admin/notifications/notifications_show.mako')
316 316
317 317 config.add_route(
318 318 name='notifications_update',
319 319 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update')
320 320 config.add_view(
321 321 MyAccountNotificationsView,
322 322 attr='notification_update',
323 323 route_name='notifications_update', request_method='POST',
324 324 renderer='json_ext')
325 325
326 326 config.add_route(
327 327 name='notifications_delete',
328 328 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/delete')
329 329 config.add_view(
330 330 MyAccountNotificationsView,
331 331 attr='notification_delete',
332 332 route_name='notifications_delete', request_method='POST',
333 333 renderer='json_ext')
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,111 +1,110 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.apps._base import ADMIN_PREFIX
24 23 from rhodecode.model.db import User
25 24 from rhodecode.tests import (
26 25 TestController, route_path_generator, assert_session_flash)
27 26 from rhodecode.tests.fixture import Fixture
28 27 from rhodecode.tests.utils import AssertResponse
29 28
30 29 fixture = Fixture()
31 30
32 31
33 32 def route_path(name, params=None, **kwargs):
34 33 url_defs = {
35 34 'my_account_auth_tokens':
36 35 ADMIN_PREFIX + '/my_account/auth_tokens',
37 36 'my_account_auth_tokens_add':
38 37 ADMIN_PREFIX + '/my_account/auth_tokens/new',
39 38 'my_account_auth_tokens_delete':
40 39 ADMIN_PREFIX + '/my_account/auth_tokens/delete',
41 40 }
42 41 return route_path_generator(url_defs, name=name, params=params, **kwargs)
43 42
44 43
45 44 class TestMyAccountAuthTokens(TestController):
46 45
47 46 def test_my_account_auth_tokens(self):
48 47 usr = self.log_user('test_regular2', 'test12')
49 48 user = User.get(usr['user_id'])
50 49 response = self.app.get(route_path('my_account_auth_tokens'))
51 50 for token in user.auth_tokens:
52 51 response.mustcontain(token[:4])
53 52 response.mustcontain('never')
54 53
55 54 def test_my_account_add_auth_tokens_wrong_csrf(self, user_util):
56 55 user = user_util.create_user(password='qweqwe')
57 56 self.log_user(user.username, 'qweqwe')
58 57
59 58 self.app.post(
60 59 route_path('my_account_auth_tokens_add'),
61 60 {'description': 'desc', 'lifetime': -1}, status=403)
62 61
63 62 @pytest.mark.parametrize("desc, lifetime", [
64 63 ('forever', -1),
65 64 ('5mins', 60*5),
66 65 ('30days', 60*60*24*30),
67 66 ])
68 67 def test_my_account_add_auth_tokens(self, desc, lifetime, user_util):
69 68 user = user_util.create_user(password='qweqwe')
70 69 user_id = user.user_id
71 70 self.log_user(user.username, 'qweqwe')
72 71
73 72 response = self.app.post(
74 73 route_path('my_account_auth_tokens_add'),
75 74 {'description': desc, 'lifetime': lifetime,
76 75 'csrf_token': self.csrf_token})
77 76 assert_session_flash(response, 'Auth token successfully created')
78 77
79 78 response = response.follow()
80 79 user = User.get(user_id)
81 80 for auth_token in user.auth_tokens:
82 81 response.mustcontain(auth_token[:4])
83 82
84 83 def test_my_account_delete_auth_token(self, user_util):
85 84 user = user_util.create_user(password='qweqwe')
86 85 user_id = user.user_id
87 86 self.log_user(user.username, 'qweqwe')
88 87
89 88 user = User.get(user_id)
90 89 keys = user.get_auth_tokens()
91 90 assert 2 == len(keys)
92 91
93 92 response = self.app.post(
94 93 route_path('my_account_auth_tokens_add'),
95 94 {'description': 'desc', 'lifetime': -1,
96 95 'csrf_token': self.csrf_token})
97 96 assert_session_flash(response, 'Auth token successfully created')
98 97 response.follow()
99 98
100 99 user = User.get(user_id)
101 100 keys = user.get_auth_tokens()
102 101 assert 3 == len(keys)
103 102
104 103 response = self.app.post(
105 104 route_path('my_account_auth_tokens_delete'),
106 105 {'del_auth_token': keys[0].user_api_key_id, 'csrf_token': self.csrf_token})
107 106 assert_session_flash(response, 'Auth token successfully deleted')
108 107
109 108 user = User.get(user_id)
110 109 keys = user.auth_tokens
111 110 assert 2 == len(keys)
@@ -1,208 +1,210 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 # -*- coding: utf-8 -*-
20
21 21
22 22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 23 #
24 24 # This program is free software: you can redistribute it and/or modify
25 25 # it under the terms of the GNU Affero General Public License, version 3
26 26 # (only), as published by the Free Software Foundation.
27 27 #
28 28 # This program is distributed in the hope that it will be useful,
29 29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 31 # GNU General Public License for more details.
32 32 #
33 33 # You should have received a copy of the GNU Affero General Public License
34 34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 35 #
36 36 # This program is dual-licensed. If you wish to learn more about the
37 37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39 39
40 40 import pytest
41 41
42 42 from rhodecode.model.db import User
43 43 from rhodecode.tests import TestController, assert_session_flash
44 44 from rhodecode.lib import helpers as h
45 45
46 46
47 47 def route_path(name, params=None, **kwargs):
48 import urllib.request, urllib.parse, urllib.error
48 import urllib.request
49 import urllib.parse
50 import urllib.error
49 51 from rhodecode.apps._base import ADMIN_PREFIX
50 52
51 53 base_url = {
52 54 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 55 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 56 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 57 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 58 }[name].format(**kwargs)
57 59
58 60 if params:
59 61 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
60 62 return base_url
61 63
62 64
63 65 class TestMyAccountEdit(TestController):
64 66
65 67 def test_my_account_edit(self):
66 68 self.log_user()
67 69 response = self.app.get(route_path('my_account_edit'))
68 70
69 71 response.mustcontain('value="test_admin')
70 72
71 73 @pytest.mark.backends("git", "hg")
72 74 def test_my_account_my_pullrequests(self, pr_util):
73 75 self.log_user()
74 76 response = self.app.get(route_path('my_account_pullrequests'))
75 77 response.mustcontain('There are currently no open pull '
76 78 'requests requiring your participation.')
77 79
78 80 @pytest.mark.backends("git", "hg")
79 81 @pytest.mark.parametrize('params, expected_title', [
80 82 ({'closed': 1}, 'Closed'),
81 83 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 84 ])
83 85 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
84 86 self.log_user()
85 87 response = self.app.get(route_path('my_account_pullrequests_data'),
86 88 extra_environ=xhr_header)
87 89 assert response.json == {
88 90 u'data': [], u'draw': None,
89 91 u'recordsFiltered': 0, u'recordsTotal': 0}
90 92
91 93 pr = pr_util.create_pull_request(title='TestMyAccountPR')
92 94 expected = {
93 95 'author_raw': 'RhodeCode Admin',
94 96 'name_raw': pr.pull_request_id
95 97 }
96 98 response = self.app.get(route_path('my_account_pullrequests_data'),
97 99 extra_environ=xhr_header)
98 100 assert response.json['recordsTotal'] == 1
99 101 assert response.json['data'][0]['author_raw'] == expected['author_raw']
100 102
101 103 assert response.json['data'][0]['author_raw'] == expected['author_raw']
102 104 assert response.json['data'][0]['name_raw'] == expected['name_raw']
103 105
104 106 @pytest.mark.parametrize(
105 107 "name, attrs", [
106 108 ('firstname', {'firstname': 'new_username'}),
107 109 ('lastname', {'lastname': 'new_username'}),
108 110 ('admin', {'admin': True}),
109 111 ('admin', {'admin': False}),
110 112 ('extern_type', {'extern_type': 'ldap'}),
111 113 ('extern_type', {'extern_type': None}),
112 114 # ('extern_name', {'extern_name': 'test'}),
113 115 # ('extern_name', {'extern_name': None}),
114 116 ('active', {'active': False}),
115 117 ('active', {'active': True}),
116 118 ('email', {'email': u'some@email.com'}),
117 119 ])
118 120 def test_my_account_update(self, name, attrs, user_util):
119 121 usr = user_util.create_user(password='qweqwe')
120 122 params = usr.get_api_data() # current user data
121 123 user_id = usr.user_id
122 124 self.log_user(
123 125 username=usr.username, password='qweqwe')
124 126
125 127 params.update({'password_confirmation': ''})
126 128 params.update({'new_password': ''})
127 129 params.update({'extern_type': u'rhodecode'})
128 130 params.update({'extern_name': u'rhodecode'})
129 131 params.update({'csrf_token': self.csrf_token})
130 132
131 133 params.update(attrs)
132 134 # my account page cannot set language param yet, only for admins
133 135 del params['language']
134 136 if name == 'email':
135 137 uem = user_util.create_additional_user_email(usr, attrs['email'])
136 138 email_before = User.get(user_id).email
137 139
138 140 response = self.app.post(route_path('my_account_update'), params)
139 141
140 142 assert_session_flash(
141 143 response, 'Your account was updated successfully')
142 144
143 145 del params['csrf_token']
144 146
145 147 updated_user = User.get(user_id)
146 148 updated_params = updated_user.get_api_data()
147 149 updated_params.update({'password_confirmation': ''})
148 150 updated_params.update({'new_password': ''})
149 151
150 152 params['last_login'] = updated_params['last_login']
151 153 params['last_activity'] = updated_params['last_activity']
152 154 # my account page cannot set language param yet, only for admins
153 155 # but we get this info from API anyway
154 156 params['language'] = updated_params['language']
155 157
156 158 if name == 'email':
157 159 params['emails'] = [attrs['email'], email_before]
158 160 if name == 'extern_type':
159 161 # cannot update this via form, expected value is original one
160 162 params['extern_type'] = "rhodecode"
161 163 if name == 'extern_name':
162 164 # cannot update this via form, expected value is original one
163 165 params['extern_name'] = str(user_id)
164 166 if name == 'active':
165 167 # my account cannot deactivate account
166 168 params['active'] = True
167 169 if name == 'admin':
168 170 # my account cannot make you an admin !
169 171 params['admin'] = False
170 172
171 173 assert params == updated_params
172 174
173 175 def test_my_account_update_err_email_not_exists_in_emails(self):
174 176 self.log_user()
175 177
176 178 new_email = 'test_regular@mail.com' # not in emails
177 179 params = {
178 180 'username': 'test_admin',
179 181 'new_password': 'test12',
180 182 'password_confirmation': 'test122',
181 183 'firstname': 'NewName',
182 184 'lastname': 'NewLastname',
183 185 'email': new_email,
184 186 'csrf_token': self.csrf_token,
185 187 }
186 188
187 189 response = self.app.post(route_path('my_account_update'),
188 190 params=params)
189 191
190 192 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
191 193
192 194 def test_my_account_update_bad_email_address(self):
193 195 self.log_user('test_regular2', 'test12')
194 196
195 197 new_email = 'newmail.pl'
196 198 params = {
197 199 'username': 'test_admin',
198 200 'new_password': 'test12',
199 201 'password_confirmation': 'test122',
200 202 'firstname': 'NewName',
201 203 'lastname': 'NewLastname',
202 204 'email': new_email,
203 205 'csrf_token': self.csrf_token,
204 206 }
205 207 response = self.app.post(route_path('my_account_update'),
206 208 params=params)
207 209
208 210 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
@@ -1,77 +1,76 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.apps._base import ADMIN_PREFIX
24 23 from rhodecode.model.db import User, UserEmailMap
25 24 from rhodecode.tests import (
26 25 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
27 26 assert_session_flash, TEST_USER_REGULAR_PASS)
28 27 from rhodecode.tests.fixture import Fixture
29 28
30 29 fixture = Fixture()
31 30
32 31
33 32 def route_path(name, **kwargs):
34 33 return {
35 34 'my_account_emails':
36 35 ADMIN_PREFIX + '/my_account/emails',
37 36 'my_account_emails_add':
38 37 ADMIN_PREFIX + '/my_account/emails/new',
39 38 'my_account_emails_delete':
40 39 ADMIN_PREFIX + '/my_account/emails/delete',
41 40 }[name].format(**kwargs)
42 41
43 42
44 43 class TestMyAccountEmails(TestController):
45 44 def test_my_account_my_emails(self):
46 45 self.log_user()
47 46 response = self.app.get(route_path('my_account_emails'))
48 47 response.mustcontain('No additional emails specified')
49 48
50 49 def test_my_account_my_emails_add_remove(self):
51 50 self.log_user()
52 51 response = self.app.get(route_path('my_account_emails'))
53 52 response.mustcontain('No additional emails specified')
54 53
55 54 response = self.app.post(route_path('my_account_emails_add'),
56 55 {'email': 'foo@barz.com',
57 56 'current_password': TEST_USER_REGULAR_PASS,
58 57 'csrf_token': self.csrf_token})
59 58
60 59 response = self.app.get(route_path('my_account_emails'))
61 60
62 61 email_id = UserEmailMap.query().filter(
63 62 UserEmailMap.user == User.get_by_username(
64 63 TEST_USER_ADMIN_LOGIN)).filter(
65 64 UserEmailMap.email == 'foo@barz.com').one().email_id
66 65
67 66 response.mustcontain('foo@barz.com')
68 67 response.mustcontain('<input id="del_email_id" name="del_email_id" '
69 68 'type="hidden" value="%s" />' % email_id)
70 69
71 70 response = self.app.post(
72 71 route_path('my_account_emails_delete'), {
73 72 'del_email_id': email_id,
74 73 'csrf_token': self.csrf_token})
75 74 assert_session_flash(response, 'Email successfully deleted')
76 75 response = self.app.get(route_path('my_account_emails'))
77 76 response.mustcontain('No additional emails specified')
@@ -1,206 +1,205 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.apps._base import ADMIN_PREFIX
24 23 from rhodecode.tests import (
25 24 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
26 25 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
27 26 from rhodecode.tests.fixture import Fixture
28 27
29 28 from rhodecode.model.db import Notification, User
30 29 from rhodecode.model.user import UserModel
31 30 from rhodecode.model.notification import NotificationModel
32 31 from rhodecode.model.meta import Session
33 32
34 33 fixture = Fixture()
35 34
36 35
37 36 def route_path(name, params=None, **kwargs):
38 37 import urllib.request, urllib.parse, urllib.error
39 38 from rhodecode.apps._base import ADMIN_PREFIX
40 39
41 40 base_url = {
42 41 'notifications_show_all': ADMIN_PREFIX + '/notifications',
43 42 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
44 43 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
45 44 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
46 45 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
47 46
48 47 }[name].format(**kwargs)
49 48
50 49 if params:
51 50 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 51 return base_url
53 52
54 53
55 54 class TestNotificationsController(TestController):
56 55
57 56 def teardown_method(self, method):
58 57 for n in Notification.query().all():
59 58 inst = Notification.get(n.notification_id)
60 59 Session().delete(inst)
61 60 Session().commit()
62 61
63 62 def test_mark_all_read(self, user_util):
64 63 user = user_util.create_user(password='qweqwe')
65 64 self.log_user(user.username, 'qweqwe')
66 65
67 66 self.app.post(
68 67 route_path('notifications_mark_all_read'), status=302,
69 68 params={'csrf_token': self.csrf_token}
70 69 )
71 70
72 71 def test_show_all(self, user_util):
73 72 user = user_util.create_user(password='qweqwe')
74 73 user_id = user.user_id
75 74 self.log_user(user.username, 'qweqwe')
76 75
77 76 response = self.app.get(
78 77 route_path('notifications_show_all', params={'type': 'all'}))
79 78 response.mustcontain(
80 79 '<div class="table">No notifications here yet</div>')
81 80
82 81 notification = NotificationModel().create(
83 82 created_by=user_id, notification_subject=u'test_notification_1',
84 83 notification_body=u'notification_1', recipients=[user_id])
85 84 Session().commit()
86 85 notification_id = notification.notification_id
87 86
88 87 response = self.app.get(route_path('notifications_show_all',
89 88 params={'type': 'all'}))
90 89 response.mustcontain('id="notification_%s"' % notification_id)
91 90
92 91 def test_show_unread(self, user_util):
93 92 user = user_util.create_user(password='qweqwe')
94 93 user_id = user.user_id
95 94 self.log_user(user.username, 'qweqwe')
96 95
97 96 response = self.app.get(route_path('notifications_show_all'))
98 97 response.mustcontain(
99 98 '<div class="table">No notifications here yet</div>')
100 99
101 100 notification = NotificationModel().create(
102 101 created_by=user_id, notification_subject=u'test_notification_1',
103 102 notification_body=u'notification_1', recipients=[user_id])
104 103
105 104 # mark the USER notification as unread
106 105 user_notification = NotificationModel().get_user_notification(
107 106 user_id, notification)
108 107 user_notification.read = False
109 108
110 109 Session().commit()
111 110 notification_id = notification.notification_id
112 111
113 112 response = self.app.get(route_path('notifications_show_all'))
114 113 response.mustcontain('id="notification_%s"' % notification_id)
115 114 response.mustcontain('<div class="desc unread')
116 115
117 116 @pytest.mark.parametrize('user,password', [
118 117 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
119 118 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
120 119 ])
121 120 def test_delete(self, user, password, user_util):
122 121 self.log_user(user, password)
123 122 cur_user = self._get_logged_user()
124 123
125 124 u1 = user_util.create_user()
126 125 u2 = user_util.create_user()
127 126
128 127 # make notifications
129 128 notification = NotificationModel().create(
130 129 created_by=cur_user, notification_subject=u'test',
131 130 notification_body=u'hi there', recipients=[cur_user, u1, u2])
132 131 Session().commit()
133 132 u1 = User.get(u1.user_id)
134 133 u2 = User.get(u2.user_id)
135 134
136 135 # check DB
137 136 get_notif = lambda un: [x.notification for x in un]
138 137 assert get_notif(cur_user.notifications) == [notification]
139 138 assert get_notif(u1.notifications) == [notification]
140 139 assert get_notif(u2.notifications) == [notification]
141 140 cur_usr_id = cur_user.user_id
142 141
143 142 response = self.app.post(
144 143 route_path('notifications_delete',
145 144 notification_id=notification.notification_id),
146 145 params={'csrf_token': self.csrf_token})
147 146 assert response.json == 'ok'
148 147
149 148 cur_user = User.get(cur_usr_id)
150 149 assert cur_user.notifications == []
151 150
152 151 @pytest.mark.parametrize('user,password', [
153 152 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
154 153 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
155 154 ])
156 155 def test_show(self, user, password, user_util):
157 156 self.log_user(user, password)
158 157 cur_user = self._get_logged_user()
159 158 u1 = user_util.create_user()
160 159 u2 = user_util.create_user()
161 160
162 161 subject = u'test'
163 162 notif_body = u'hi there'
164 163 notification = NotificationModel().create(
165 164 created_by=cur_user, notification_subject=subject,
166 165 notification_body=notif_body, recipients=[cur_user, u1, u2])
167 166 Session().commit()
168 167
169 168 response = self.app.get(
170 169 route_path('notifications_show',
171 170 notification_id=notification.notification_id))
172 171
173 172 response.mustcontain(subject)
174 173 response.mustcontain(notif_body)
175 174
176 175 @pytest.mark.parametrize('user,password', [
177 176 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
178 177 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
179 178 ])
180 179 def test_update(self, user, password, user_util):
181 180 self.log_user(user, password)
182 181 cur_user = self._get_logged_user()
183 182 u1 = user_util.create_user()
184 183 u2 = user_util.create_user()
185 184
186 185 # make notifications
187 186 recipients = [cur_user, u1, u2]
188 187 notification = NotificationModel().create(
189 188 created_by=cur_user, notification_subject=u'test',
190 189 notification_body=u'hi there', recipients=recipients)
191 190 Session().commit()
192 191
193 192 for u_obj in recipients:
194 193 # if it's current user, he has his message already read
195 194 read = u_obj.username == user
196 195 assert len(u_obj.notifications) == 1
197 196 assert u_obj.notifications[0].read == read
198 197
199 198 response = self.app.post(
200 199 route_path('notifications_update',
201 200 notification_id=notification.notification_id),
202 201 params={'csrf_token': self.csrf_token})
203 202 assert response.json == 'ok'
204 203
205 204 cur_user = self._get_logged_user()
206 205 assert True is cur_user.notifications[0].read
@@ -1,144 +1,143 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21 import mock
23 22
24 23 from rhodecode.apps._base import ADMIN_PREFIX
25 24 from rhodecode.lib import helpers as h
26 25 from rhodecode.lib.auth import check_password
27 26 from rhodecode.model.meta import Session
28 27 from rhodecode.model.user import UserModel
29 28 from rhodecode.tests import assert_session_flash
30 29 from rhodecode.tests.fixture import Fixture, TestController, error_function
31 30
32 31 fixture = Fixture()
33 32
34 33
35 34 def route_path(name, **kwargs):
36 35 return {
37 36 'home': '/',
38 37 'my_account_password':
39 38 ADMIN_PREFIX + '/my_account/password',
40 39 'my_account_password_update':
41 40 ADMIN_PREFIX + '/my_account/password/update',
42 41 }[name].format(**kwargs)
43 42
44 43
45 44 test_user_1 = 'testme'
46 45 test_user_1_password = '0jd83nHNS/d23n'
47 46
48 47
49 48 class TestMyAccountPassword(TestController):
50 49 def test_valid_change_password(self, user_util):
51 50 new_password = 'my_new_valid_password'
52 51 user = user_util.create_user(password=test_user_1_password)
53 52 self.log_user(user.username, test_user_1_password)
54 53
55 54 form_data = [
56 55 ('current_password', test_user_1_password),
57 56 ('__start__', 'new_password:mapping'),
58 57 ('new_password', new_password),
59 58 ('new_password-confirm', new_password),
60 59 ('__end__', 'new_password:mapping'),
61 60 ('csrf_token', self.csrf_token),
62 61 ]
63 62 response = self.app.post(
64 63 route_path('my_account_password_update'), form_data).follow()
65 64 assert 'Successfully updated password' in response
66 65
67 66 # check_password depends on user being in session
68 67 Session().add(user)
69 68 try:
70 69 assert check_password(new_password, user.password)
71 70 finally:
72 71 Session().expunge(user)
73 72
74 73 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
75 74 ('', 'abcdef123', 'abcdef123'),
76 75 ('wrong_pw', 'abcdef123', 'abcdef123'),
77 76 (test_user_1_password, test_user_1_password, test_user_1_password),
78 77 (test_user_1_password, '', ''),
79 78 (test_user_1_password, 'abcdef123', ''),
80 79 (test_user_1_password, '', 'abcdef123'),
81 80 (test_user_1_password, 'not_the', 'same_pw'),
82 81 (test_user_1_password, 'short', 'short'),
83 82 ])
84 83 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
85 84 user_util):
86 85 user = user_util.create_user(password=test_user_1_password)
87 86 self.log_user(user.username, test_user_1_password)
88 87
89 88 form_data = [
90 89 ('current_password', current_pw),
91 90 ('__start__', 'new_password:mapping'),
92 91 ('new_password', new_pw),
93 92 ('new_password-confirm', confirm_pw),
94 93 ('__end__', 'new_password:mapping'),
95 94 ('csrf_token', self.csrf_token),
96 95 ]
97 96 response = self.app.post(
98 97 route_path('my_account_password_update'), form_data)
99 98
100 99 assert_response = response.assert_response()
101 100 assert assert_response.get_elements('.error-block')
102 101
103 102 @mock.patch.object(UserModel, 'update_user', error_function)
104 103 def test_invalid_change_password_exception(self, user_util):
105 104 user = user_util.create_user(password=test_user_1_password)
106 105 self.log_user(user.username, test_user_1_password)
107 106
108 107 form_data = [
109 108 ('current_password', test_user_1_password),
110 109 ('__start__', 'new_password:mapping'),
111 110 ('new_password', '123456'),
112 111 ('new_password-confirm', '123456'),
113 112 ('__end__', 'new_password:mapping'),
114 113 ('csrf_token', self.csrf_token),
115 114 ]
116 115 response = self.app.post(
117 116 route_path('my_account_password_update'), form_data)
118 117 assert_session_flash(
119 118 response, 'Error occurred during update of user password')
120 119
121 120 def test_password_is_updated_in_session_on_password_change(self, user_util):
122 121 old_password = 'abcdef123'
123 122 new_password = 'abcdef124'
124 123
125 124 user = user_util.create_user(password=old_password)
126 125 session = self.log_user(user.username, old_password)
127 126 old_password_hash = session['password']
128 127
129 128 form_data = [
130 129 ('current_password', old_password),
131 130 ('__start__', 'new_password:mapping'),
132 131 ('new_password', new_password),
133 132 ('new_password-confirm', new_password),
134 133 ('__end__', 'new_password:mapping'),
135 134 ('csrf_token', self.csrf_token),
136 135 ]
137 136 self.app.post(
138 137 route_path('my_account_password_update'), form_data)
139 138
140 139 response = self.app.get(route_path('home'))
141 140 session = response.get_session_from_response()
142 141 new_password_hash = session['rhodecode_user']['password']
143 142
144 143 assert old_password_hash != new_password_hash No newline at end of file
@@ -1,55 +1,54 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.apps._base import ADMIN_PREFIX
24 23 from rhodecode.tests import (
25 24 TestController, TEST_USER_ADMIN_LOGIN,
26 25 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
27 26 from rhodecode.tests.fixture import Fixture
28 27
29 28 fixture = Fixture()
30 29
31 30
32 31 def route_path(name, **kwargs):
33 32 return {
34 33 'my_account':
35 34 ADMIN_PREFIX + '/my_account/profile',
36 35 }[name].format(**kwargs)
37 36
38 37
39 38 class TestMyAccountProfile(TestController):
40 39
41 40 def test_my_account(self):
42 41 self.log_user()
43 42 response = self.app.get(route_path('my_account'))
44 43
45 44 response.mustcontain(TEST_USER_ADMIN_LOGIN)
46 45 response.mustcontain('href="/_admin/my_account/edit"')
47 46 response.mustcontain('Photo')
48 47
49 48 def test_my_account_regular_user(self):
50 49 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
51 50 response = self.app.get(route_path('my_account'))
52 51
53 52 response.mustcontain(TEST_USER_REGULAR_LOGIN)
54 53 response.mustcontain('href="/_admin/my_account/edit"')
55 54 response.mustcontain('Photo')
@@ -1,75 +1,74 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.apps._base import ADMIN_PREFIX
24 23 from rhodecode.model.db import User, UserEmailMap, Repository, UserFollowing
25 24 from rhodecode.tests import (
26 25 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
27 26 assert_session_flash)
28 27 from rhodecode.tests.fixture import Fixture
29 28
30 29 fixture = Fixture()
31 30
32 31
33 32 def route_path(name, **kwargs):
34 33 return {
35 34 'my_account_repos':
36 35 ADMIN_PREFIX + '/my_account/repos',
37 36 'my_account_watched':
38 37 ADMIN_PREFIX + '/my_account/watched',
39 38 'my_account_perms':
40 39 ADMIN_PREFIX + '/my_account/perms',
41 40 'my_account_notifications':
42 41 ADMIN_PREFIX + '/my_account/notifications',
43 42 }[name].format(**kwargs)
44 43
45 44
46 45 class TestMyAccountSimpleViews(TestController):
47 46
48 47 def test_my_account_my_repos(self, autologin_user):
49 48 response = self.app.get(route_path('my_account_repos'))
50 49 repos = Repository.query().filter(
51 50 Repository.user == User.get_by_username(
52 51 TEST_USER_ADMIN_LOGIN)).all()
53 52 for repo in repos:
54 53 response.mustcontain(f'"name_raw":"{repo.repo_name}"')
55 54
56 55 def test_my_account_my_watched(self, autologin_user):
57 56 response = self.app.get(route_path('my_account_watched'))
58 57
59 58 repos = UserFollowing.query().filter(
60 59 UserFollowing.user == User.get_by_username(
61 60 TEST_USER_ADMIN_LOGIN)).all()
62 61 for repo in repos:
63 62 response.mustcontain(f'"name_raw":"{repo.follows_repository.repo_name}"')
64 63
65 64 def test_my_account_perms(self, autologin_user):
66 65 response = self.app.get(route_path('my_account_perms'))
67 66 assert_response = response.assert_response()
68 67 assert assert_response.get_elements('.perm_tag.none')
69 68 assert assert_response.get_elements('.perm_tag.read')
70 69 assert assert_response.get_elements('.perm_tag.write')
71 70 assert assert_response.get_elements('.perm_tag.admin')
72 71
73 72 def test_my_account_notifications(self, autologin_user):
74 73 response = self.app.get(route_path('my_account_notifications'))
75 74 response.mustcontain('Test flash message')
@@ -1,163 +1,162 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.model.db import User, UserSshKeys
24 23
25 24 from rhodecode.tests import TestController, assert_session_flash
26 25 from rhodecode.tests.fixture import Fixture
27 26
28 27 fixture = Fixture()
29 28
30 29
31 30 def route_path(name, params=None, **kwargs):
32 31 import urllib.request, urllib.parse, urllib.error
33 32 from rhodecode.apps._base import ADMIN_PREFIX
34 33
35 34 base_url = {
36 35 'my_account_ssh_keys':
37 36 ADMIN_PREFIX + '/my_account/ssh_keys',
38 37 'my_account_ssh_keys_generate':
39 38 ADMIN_PREFIX + '/my_account/ssh_keys/generate',
40 39 'my_account_ssh_keys_add':
41 40 ADMIN_PREFIX + '/my_account/ssh_keys/new',
42 41 'my_account_ssh_keys_delete':
43 42 ADMIN_PREFIX + '/my_account/ssh_keys/delete',
44 43 }[name].format(**kwargs)
45 44
46 45 if params:
47 46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 47 return base_url
49 48
50 49
51 50 class TestMyAccountSshKeysView(TestController):
52 51 INVALID_KEY = """\
53 52 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
54 53 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
55 54 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
56 55 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
57 56 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
58 57 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
59 58 your_email@example.com
60 59 """
61 60 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
62 61 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
63 62 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
64 63 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
65 64 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
66 65 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
67 66 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
68 67 'your_email@example.com'
69 68 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
70 69
71 70 def test_add_ssh_key_error(self, user_util):
72 71 user = user_util.create_user(password='qweqwe')
73 72 self.log_user(user.username, 'qweqwe')
74 73
75 74 key_data = self.INVALID_KEY
76 75
77 76 desc = 'MY SSH KEY'
78 77 response = self.app.post(
79 78 route_path('my_account_ssh_keys_add'),
80 79 {'description': desc, 'key_data': key_data,
81 80 'csrf_token': self.csrf_token})
82 81 assert_session_flash(response, 'An error occurred during ssh '
83 82 'key saving: Unable to decode the key')
84 83
85 84 def test_ssh_key_duplicate(self, user_util):
86 85 user = user_util.create_user(password='qweqwe')
87 86 self.log_user(user.username, 'qweqwe')
88 87 key_data = self.VALID_KEY
89 88
90 89 desc = 'MY SSH KEY'
91 90 response = self.app.post(
92 91 route_path('my_account_ssh_keys_add'),
93 92 {'description': desc, 'key_data': key_data,
94 93 'csrf_token': self.csrf_token})
95 94 assert_session_flash(response, 'Ssh Key successfully created')
96 95 response.follow() # flush session flash
97 96
98 97 # add the same key AGAIN
99 98 desc = 'MY SSH KEY'
100 99 response = self.app.post(
101 100 route_path('my_account_ssh_keys_add'),
102 101 {'description': desc, 'key_data': key_data,
103 102 'csrf_token': self.csrf_token})
104 103
105 104 err = 'Such key with fingerprint `{}` already exists, ' \
106 105 'please use a different one'.format(self.FINGERPRINT)
107 106 assert_session_flash(response, 'An error occurred during ssh key '
108 107 'saving: {}'.format(err))
109 108
110 109 def test_add_ssh_key(self, user_util):
111 110 user = user_util.create_user(password='qweqwe')
112 111 self.log_user(user.username, 'qweqwe')
113 112
114 113 key_data = self.VALID_KEY
115 114
116 115 desc = 'MY SSH KEY'
117 116 response = self.app.post(
118 117 route_path('my_account_ssh_keys_add'),
119 118 {'description': desc, 'key_data': key_data,
120 119 'csrf_token': self.csrf_token})
121 120 assert_session_flash(response, 'Ssh Key successfully created')
122 121
123 122 response = response.follow()
124 123 response.mustcontain(desc)
125 124
126 125 def test_delete_ssh_key(self, user_util):
127 126 user = user_util.create_user(password='qweqwe')
128 127 user_id = user.user_id
129 128 self.log_user(user.username, 'qweqwe')
130 129
131 130 key_data = self.VALID_KEY
132 131
133 132 desc = 'MY SSH KEY'
134 133 response = self.app.post(
135 134 route_path('my_account_ssh_keys_add'),
136 135 {'description': desc, 'key_data': key_data,
137 136 'csrf_token': self.csrf_token})
138 137 assert_session_flash(response, 'Ssh Key successfully created')
139 138 response = response.follow() # flush the Session flash
140 139
141 140 # now delete our key
142 141 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
143 142 assert 1 == len(keys)
144 143
145 144 response = self.app.post(
146 145 route_path('my_account_ssh_keys_delete'),
147 146 {'del_ssh_key': keys[0].ssh_key_id,
148 147 'csrf_token': self.csrf_token})
149 148
150 149 assert_session_flash(response, 'Ssh key successfully deleted')
151 150 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
152 151 assert 0 == len(keys)
153 152
154 153 def test_generate_keypair(self, user_util):
155 154 user = user_util.create_user(password='qweqwe')
156 155 self.log_user(user.username, 'qweqwe')
157 156
158 157 response = self.app.get(
159 158 route_path('my_account_ssh_keys_generate'))
160 159
161 160 response.mustcontain('Private key')
162 161 response.mustcontain('Public key')
163 162 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,783 +1,783 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 29
30 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 31 from rhodecode import forms
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib import ext_json
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, NotAnonymous, CSRFRequired,
37 37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 38 from rhodecode.lib.channelstream import (
39 39 channelstream_request, ChannelstreamException)
40 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 IntegrityError, or_, in_filter_generator,
45 45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.pull_request import PullRequestModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.user_group import UserGroupModel
51 51 from rhodecode.model.validation_schema.schemas import user_schema
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class MyAccountView(BaseAppView, DataGridAppView):
57 57 ALLOW_SCOPED_TOKENS = False
58 58 """
59 59 This view has alternative version inside EE, if modified please take a look
60 60 in there as well.
61 61 """
62 62
63 63 def load_default_context(self):
64 64 c = self._get_local_tmpl_context()
65 65 c.user = c.auth_user.get_instance()
66 66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 67 return c
68 68
69 69 @LoginRequired()
70 70 @NotAnonymous()
71 71 def my_account_profile(self):
72 72 c = self.load_default_context()
73 73 c.active = 'profile'
74 74 c.extern_type = c.user.extern_type
75 75 return self._get_template_context(c)
76 76
77 77 @LoginRequired()
78 78 @NotAnonymous()
79 79 def my_account_edit(self):
80 80 c = self.load_default_context()
81 81 c.active = 'profile_edit'
82 82 c.extern_type = c.user.extern_type
83 83 c.extern_name = c.user.extern_name
84 84
85 85 schema = user_schema.UserProfileSchema().bind(
86 86 username=c.user.username, user_emails=c.user.emails)
87 87 appstruct = {
88 88 'username': c.user.username,
89 89 'email': c.user.email,
90 90 'firstname': c.user.firstname,
91 91 'lastname': c.user.lastname,
92 92 'description': c.user.description,
93 93 }
94 94 c.form = forms.RcForm(
95 95 schema, appstruct=appstruct,
96 96 action=h.route_path('my_account_update'),
97 97 buttons=(forms.buttons.save, forms.buttons.reset))
98 98
99 99 return self._get_template_context(c)
100 100
101 101 @LoginRequired()
102 102 @NotAnonymous()
103 103 @CSRFRequired()
104 104 def my_account_update(self):
105 105 _ = self.request.translate
106 106 c = self.load_default_context()
107 107 c.active = 'profile_edit'
108 108 c.perm_user = c.auth_user
109 109 c.extern_type = c.user.extern_type
110 110 c.extern_name = c.user.extern_name
111 111
112 112 schema = user_schema.UserProfileSchema().bind(
113 113 username=c.user.username, user_emails=c.user.emails)
114 114 form = forms.RcForm(
115 115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116 116
117 117 controls = self.request.POST.items()
118 118 try:
119 119 valid_data = form.validate(controls)
120 120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 121 'new_password', 'password_confirmation']
122 122 if c.extern_type != "rhodecode":
123 123 # forbid updating username for external accounts
124 124 skip_attrs.append('username')
125 125 old_email = c.user.email
126 126 UserModel().update_user(
127 127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 128 **valid_data)
129 129 if old_email != valid_data['email']:
130 130 old = UserEmailMap.query() \
131 131 .filter(UserEmailMap.user == c.user)\
132 132 .filter(UserEmailMap.email == valid_data['email'])\
133 133 .first()
134 134 old.email = old_email
135 135 h.flash(_('Your account was updated successfully'), category='success')
136 136 Session().commit()
137 137 except forms.ValidationFailure as e:
138 138 c.form = e
139 139 return self._get_template_context(c)
140 140 except Exception:
141 141 log.exception("Exception updating user")
142 142 h.flash(_('Error occurred during update of user'),
143 143 category='error')
144 144 raise HTTPFound(h.route_path('my_account_profile'))
145 145
146 146 @LoginRequired()
147 147 @NotAnonymous()
148 148 def my_account_password(self):
149 149 c = self.load_default_context()
150 150 c.active = 'password'
151 151 c.extern_type = c.user.extern_type
152 152
153 153 schema = user_schema.ChangePasswordSchema().bind(
154 154 username=c.user.username)
155 155
156 156 form = forms.Form(
157 157 schema,
158 158 action=h.route_path('my_account_password_update'),
159 159 buttons=(forms.buttons.save, forms.buttons.reset))
160 160
161 161 c.form = form
162 162 return self._get_template_context(c)
163 163
164 164 @LoginRequired()
165 165 @NotAnonymous()
166 166 @CSRFRequired()
167 167 def my_account_password_update(self):
168 168 _ = self.request.translate
169 169 c = self.load_default_context()
170 170 c.active = 'password'
171 171 c.extern_type = c.user.extern_type
172 172
173 173 schema = user_schema.ChangePasswordSchema().bind(
174 174 username=c.user.username)
175 175
176 176 form = forms.Form(
177 177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178 178
179 179 if c.extern_type != 'rhodecode':
180 180 raise HTTPFound(self.request.route_path('my_account_password'))
181 181
182 182 controls = self.request.POST.items()
183 183 try:
184 184 valid_data = form.validate(controls)
185 185 UserModel().update_user(c.user.user_id, **valid_data)
186 186 c.user.update_userdata(force_password_change=False)
187 187 Session().commit()
188 188 except forms.ValidationFailure as e:
189 189 c.form = e
190 190 return self._get_template_context(c)
191 191
192 192 except Exception:
193 193 log.exception("Exception updating password")
194 194 h.flash(_('Error occurred during update of user password'),
195 195 category='error')
196 196 else:
197 197 instance = c.auth_user.get_instance()
198 198 self.session.setdefault('rhodecode_user', {}).update(
199 199 {'password': md5(instance.password)})
200 200 self.session.save()
201 201 h.flash(_("Successfully updated password"), category='success')
202 202
203 203 raise HTTPFound(self.request.route_path('my_account_password'))
204 204
205 205 @LoginRequired()
206 206 @NotAnonymous()
207 207 def my_account_auth_tokens(self):
208 208 _ = self.request.translate
209 209
210 210 c = self.load_default_context()
211 211 c.active = 'auth_tokens'
212 212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 213 c.role_values = [
214 214 (x, AuthTokenModel.cls._get_role_name(x))
215 215 for x in AuthTokenModel.cls.ROLES]
216 216 c.role_options = [(c.role_values, _("Role"))]
217 217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 218 c.user.user_id, show_expired=True)
219 219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 220 return self._get_template_context(c)
221 221
222 222 @LoginRequired()
223 223 @NotAnonymous()
224 224 @CSRFRequired()
225 225 def my_account_auth_tokens_view(self):
226 226 _ = self.request.translate
227 227 c = self.load_default_context()
228 228
229 229 auth_token_id = self.request.POST.get('auth_token_id')
230 230
231 231 if auth_token_id:
232 232 token = UserApiKeys.get_or_404(auth_token_id)
233 233 if token.user.user_id != c.user.user_id:
234 234 raise HTTPNotFound()
235 235
236 236 return {
237 237 'auth_token': token.api_key
238 238 }
239 239
240 240 def maybe_attach_token_scope(self, token):
241 241 # implemented in EE edition
242 242 pass
243 243
244 244 @LoginRequired()
245 245 @NotAnonymous()
246 246 @CSRFRequired()
247 247 def my_account_auth_tokens_add(self):
248 248 _ = self.request.translate
249 249 c = self.load_default_context()
250 250
251 251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 252 description = self.request.POST.get('description')
253 253 role = self.request.POST.get('role')
254 254
255 255 token = UserModel().add_auth_token(
256 256 user=c.user.user_id,
257 257 lifetime_minutes=lifetime, role=role, description=description,
258 258 scope_callback=self.maybe_attach_token_scope)
259 259 token_data = token.get_api_data()
260 260
261 261 audit_logger.store_web(
262 262 'user.edit.token.add', action_data={
263 263 'data': {'token': token_data, 'user': 'self'}},
264 264 user=self._rhodecode_user, )
265 265 Session().commit()
266 266
267 267 h.flash(_("Auth token successfully created"), category='success')
268 268 return HTTPFound(h.route_path('my_account_auth_tokens'))
269 269
270 270 @LoginRequired()
271 271 @NotAnonymous()
272 272 @CSRFRequired()
273 273 def my_account_auth_tokens_delete(self):
274 274 _ = self.request.translate
275 275 c = self.load_default_context()
276 276
277 277 del_auth_token = self.request.POST.get('del_auth_token')
278 278
279 279 if del_auth_token:
280 280 token = UserApiKeys.get_or_404(del_auth_token)
281 281 token_data = token.get_api_data()
282 282
283 283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 284 audit_logger.store_web(
285 285 'user.edit.token.delete', action_data={
286 286 'data': {'token': token_data, 'user': 'self'}},
287 287 user=self._rhodecode_user,)
288 288 Session().commit()
289 289 h.flash(_("Auth token successfully deleted"), category='success')
290 290
291 291 return HTTPFound(h.route_path('my_account_auth_tokens'))
292 292
293 293 @LoginRequired()
294 294 @NotAnonymous()
295 295 def my_account_emails(self):
296 296 _ = self.request.translate
297 297
298 298 c = self.load_default_context()
299 299 c.active = 'emails'
300 300
301 301 c.user_email_map = UserEmailMap.query()\
302 302 .filter(UserEmailMap.user == c.user).all()
303 303
304 304 schema = user_schema.AddEmailSchema().bind(
305 305 username=c.user.username, user_emails=c.user.emails)
306 306
307 307 form = forms.RcForm(schema,
308 308 action=h.route_path('my_account_emails_add'),
309 309 buttons=(forms.buttons.save, forms.buttons.reset))
310 310
311 311 c.form = form
312 312 return self._get_template_context(c)
313 313
314 314 @LoginRequired()
315 315 @NotAnonymous()
316 316 @CSRFRequired()
317 317 def my_account_emails_add(self):
318 318 _ = self.request.translate
319 319 c = self.load_default_context()
320 320 c.active = 'emails'
321 321
322 322 schema = user_schema.AddEmailSchema().bind(
323 323 username=c.user.username, user_emails=c.user.emails)
324 324
325 325 form = forms.RcForm(
326 326 schema, action=h.route_path('my_account_emails_add'),
327 327 buttons=(forms.buttons.save, forms.buttons.reset))
328 328
329 329 controls = self.request.POST.items()
330 330 try:
331 331 valid_data = form.validate(controls)
332 332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 333 audit_logger.store_web(
334 334 'user.edit.email.add', action_data={
335 335 'data': {'email': valid_data['email'], 'user': 'self'}},
336 336 user=self._rhodecode_user,)
337 337 Session().commit()
338 338 except formencode.Invalid as error:
339 339 h.flash(h.escape(error.error_dict['email']), category='error')
340 340 except forms.ValidationFailure as e:
341 341 c.user_email_map = UserEmailMap.query() \
342 342 .filter(UserEmailMap.user == c.user).all()
343 343 c.form = e
344 344 return self._get_template_context(c)
345 345 except Exception:
346 346 log.exception("Exception adding email")
347 347 h.flash(_('Error occurred during adding email'),
348 348 category='error')
349 349 else:
350 350 h.flash(_("Successfully added email"), category='success')
351 351
352 352 raise HTTPFound(self.request.route_path('my_account_emails'))
353 353
354 354 @LoginRequired()
355 355 @NotAnonymous()
356 356 @CSRFRequired()
357 357 def my_account_emails_delete(self):
358 358 _ = self.request.translate
359 359 c = self.load_default_context()
360 360
361 361 del_email_id = self.request.POST.get('del_email_id')
362 362 if del_email_id:
363 363 email = UserEmailMap.get_or_404(del_email_id).email
364 364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 365 audit_logger.store_web(
366 366 'user.edit.email.delete', action_data={
367 367 'data': {'email': email, 'user': 'self'}},
368 368 user=self._rhodecode_user,)
369 369 Session().commit()
370 370 h.flash(_("Email successfully deleted"),
371 371 category='success')
372 372 return HTTPFound(h.route_path('my_account_emails'))
373 373
374 374 @LoginRequired()
375 375 @NotAnonymous()
376 376 @CSRFRequired()
377 377 def my_account_notifications_test_channelstream(self):
378 378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 379 self._rhodecode_user.username, datetime.datetime.now())
380 380 payload = {
381 381 # 'channel': 'broadcast',
382 382 'type': 'message',
383 383 'timestamp': datetime.datetime.utcnow(),
384 384 'user': 'system',
385 385 'pm_users': [self._rhodecode_user.username],
386 386 'message': {
387 387 'message': message,
388 388 'level': 'info',
389 389 'topic': '/notifications'
390 390 }
391 391 }
392 392
393 393 registry = self.request.registry
394 394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 395 channelstream_config = rhodecode_plugins.get('channelstream', {})
396 396
397 397 try:
398 398 channelstream_request(channelstream_config, [payload], '/message')
399 399 except ChannelstreamException as e:
400 400 log.exception('Failed to send channelstream data')
401 401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
402 402 return {"response": 'Channelstream data sent. '
403 403 'You should see a new live message now.'}
404 404
405 405 def _load_my_repos_data(self, watched=False):
406 406
407 407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408 408
409 409 if watched:
410 410 # repos user watch
411 411 repo_list = Session().query(
412 412 Repository
413 413 ) \
414 414 .join(
415 415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 416 ) \
417 417 .filter(
418 418 UserFollowing.user_id == self._rhodecode_user.user_id
419 419 ) \
420 420 .filter(or_(
421 421 # generate multiple IN to fix limitation problems
422 422 *in_filter_generator(Repository.repo_id, allowed_ids))
423 423 ) \
424 424 .order_by(Repository.repo_name) \
425 425 .all()
426 426
427 427 else:
428 428 # repos user is owner of
429 429 repo_list = Session().query(
430 430 Repository
431 431 ) \
432 432 .filter(
433 433 Repository.user_id == self._rhodecode_user.user_id
434 434 ) \
435 435 .filter(or_(
436 436 # generate multiple IN to fix limitation problems
437 437 *in_filter_generator(Repository.repo_id, allowed_ids))
438 438 ) \
439 439 .order_by(Repository.repo_name) \
440 440 .all()
441 441
442 442 _render = self.request.get_partial_renderer(
443 443 'rhodecode:templates/data_table/_dt_elements.mako')
444 444
445 445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 447 short_name=False, admin=False)
448 448
449 449 repos_data = []
450 450 for repo in repo_list:
451 451 row = {
452 452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 453 repo.private, repo.archived, repo.fork),
454 454 "name_raw": repo.repo_name.lower(),
455 455 }
456 456
457 457 repos_data.append(row)
458 458
459 459 # json used to render the grid
460 460 return ext_json.str_json(repos_data)
461 461
462 462 @LoginRequired()
463 463 @NotAnonymous()
464 464 def my_account_repos(self):
465 465 c = self.load_default_context()
466 466 c.active = 'repos'
467 467
468 468 # json used to render the grid
469 469 c.data = self._load_my_repos_data()
470 470 return self._get_template_context(c)
471 471
472 472 @LoginRequired()
473 473 @NotAnonymous()
474 474 def my_account_watched(self):
475 475 c = self.load_default_context()
476 476 c.active = 'watched'
477 477
478 478 # json used to render the grid
479 479 c.data = self._load_my_repos_data(watched=True)
480 480 return self._get_template_context(c)
481 481
482 482 @LoginRequired()
483 483 @NotAnonymous()
484 484 def my_account_bookmarks(self):
485 485 c = self.load_default_context()
486 486 c.active = 'bookmarks'
487 487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 488 self._rhodecode_db_user.user_id, cache=False)
489 489 return self._get_template_context(c)
490 490
491 491 def _process_bookmark_entry(self, entry, user_id):
492 492 position = safe_int(entry.get('position'))
493 493 cur_position = safe_int(entry.get('cur_position'))
494 494 if position is None:
495 495 return
496 496
497 497 # check if this is an existing entry
498 498 is_new = False
499 499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500 500
501 501 if db_entry and str2bool(entry.get('remove')):
502 502 log.debug('Marked bookmark %s for deletion', db_entry)
503 503 Session().delete(db_entry)
504 504 return
505 505
506 506 if not db_entry:
507 507 # new
508 508 db_entry = UserBookmark()
509 509 is_new = True
510 510
511 511 should_save = False
512 512 default_redirect_url = ''
513 513
514 514 # save repo
515 515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 516 repo = Repository.get(entry['bookmark_repo'])
517 517 perm_check = HasRepoPermissionAny(
518 518 'repository.read', 'repository.write', 'repository.admin')
519 519 if repo and perm_check(repo_name=repo.repo_name):
520 520 db_entry.repository = repo
521 521 should_save = True
522 522 default_redirect_url = '${repo_url}'
523 523 # save repo group
524 524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 526 perm_check = HasRepoGroupPermissionAny(
527 527 'group.read', 'group.write', 'group.admin')
528 528
529 529 if repo_group and perm_check(group_name=repo_group.group_name):
530 530 db_entry.repository_group = repo_group
531 531 should_save = True
532 532 default_redirect_url = '${repo_group_url}'
533 533 # save generic info
534 534 elif entry.get('title') and entry.get('redirect_url'):
535 535 should_save = True
536 536
537 537 if should_save:
538 538 # mark user and position
539 539 db_entry.user_id = user_id
540 540 db_entry.position = position
541 541 db_entry.title = entry.get('title')
542 542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544 544
545 545 Session().add(db_entry)
546 546
547 547 @LoginRequired()
548 548 @NotAnonymous()
549 549 @CSRFRequired()
550 550 def my_account_bookmarks_update(self):
551 551 _ = self.request.translate
552 552 c = self.load_default_context()
553 553 c.active = 'bookmarks'
554 554
555 555 controls = peppercorn.parse(self.request.POST.items())
556 556 user_id = c.user.user_id
557 557
558 558 # validate positions
559 559 positions = {}
560 560 for entry in controls.get('bookmarks', []):
561 561 position = safe_int(entry['position'])
562 562 if position is None:
563 563 continue
564 564
565 565 if position in positions:
566 566 h.flash(_("Position {} is defined twice. "
567 567 "Please correct this error.").format(position), category='error')
568 568 return HTTPFound(h.route_path('my_account_bookmarks'))
569 569
570 570 entry['position'] = position
571 571 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 572 positions[position] = entry
573 573
574 574 try:
575 575 for entry in positions.values():
576 576 self._process_bookmark_entry(entry, user_id)
577 577
578 578 Session().commit()
579 579 h.flash(_("Update Bookmarks"), category='success')
580 580 except IntegrityError:
581 581 h.flash(_("Failed to update bookmarks. "
582 582 "Make sure an unique position is used."), category='error')
583 583
584 584 return HTTPFound(h.route_path('my_account_bookmarks'))
585 585
586 586 @LoginRequired()
587 587 @NotAnonymous()
588 588 def my_account_goto_bookmark(self):
589 589
590 590 bookmark_id = self.request.matchdict['bookmark_id']
591 591 user_bookmark = UserBookmark().query()\
592 592 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 593 .filter(UserBookmark.position == bookmark_id).scalar()
594 594
595 595 redirect_url = h.route_path('my_account_bookmarks')
596 596 if not user_bookmark:
597 597 raise HTTPFound(redirect_url)
598 598
599 599 # repository set
600 600 if user_bookmark.repository:
601 601 repo_name = user_bookmark.repository.repo_name
602 602 base_redirect_url = h.route_path(
603 603 'repo_summary', repo_name=repo_name)
604 604 if user_bookmark.redirect_url and \
605 605 '${repo_url}' in user_bookmark.redirect_url:
606 606 redirect_url = string.Template(user_bookmark.redirect_url)\
607 607 .safe_substitute({'repo_url': base_redirect_url})
608 608 else:
609 609 redirect_url = base_redirect_url
610 610 # repository group set
611 611 elif user_bookmark.repository_group:
612 612 repo_group_name = user_bookmark.repository_group.group_name
613 613 base_redirect_url = h.route_path(
614 614 'repo_group_home', repo_group_name=repo_group_name)
615 615 if user_bookmark.redirect_url and \
616 616 '${repo_group_url}' in user_bookmark.redirect_url:
617 617 redirect_url = string.Template(user_bookmark.redirect_url)\
618 618 .safe_substitute({'repo_group_url': base_redirect_url})
619 619 else:
620 620 redirect_url = base_redirect_url
621 621 # custom URL set
622 622 elif user_bookmark.redirect_url:
623 623 server_url = h.route_url('home').rstrip('/')
624 624 redirect_url = string.Template(user_bookmark.redirect_url) \
625 625 .safe_substitute({'server_url': server_url})
626 626
627 627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 628 raise HTTPFound(redirect_url)
629 629
630 630 @LoginRequired()
631 631 @NotAnonymous()
632 632 def my_account_perms(self):
633 633 c = self.load_default_context()
634 634 c.active = 'perms'
635 635
636 636 c.perm_user = c.auth_user
637 637 return self._get_template_context(c)
638 638
639 639 @LoginRequired()
640 640 @NotAnonymous()
641 641 def my_notifications(self):
642 642 c = self.load_default_context()
643 643 c.active = 'notifications'
644 644
645 645 return self._get_template_context(c)
646 646
647 647 @LoginRequired()
648 648 @NotAnonymous()
649 649 @CSRFRequired()
650 650 def my_notifications_toggle_visibility(self):
651 651 user = self._rhodecode_db_user
652 652 new_status = not user.user_data.get('notification_status', True)
653 653 user.update_userdata(notification_status=new_status)
654 654 Session().commit()
655 655 return user.user_data['notification_status']
656 656
657 657 def _get_pull_requests_list(self, statuses, filter_type=None):
658 658 draw, start, limit = self._extract_chunk(self.request)
659 659 search_q, order_by, order_dir = self._extract_ordering(self.request)
660 660
661 661 _render = self.request.get_partial_renderer(
662 662 'rhodecode:templates/data_table/_dt_elements.mako')
663 663
664 664 if filter_type == 'awaiting_my_review':
665 665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 666 user_id=self._rhodecode_user.user_id,
667 667 statuses=statuses, query=search_q,
668 668 offset=start, length=limit, order_by=order_by,
669 669 order_dir=order_dir)
670 670
671 671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 673 else:
674 674 pull_requests = PullRequestModel().get_im_participating_in(
675 675 user_id=self._rhodecode_user.user_id,
676 676 statuses=statuses, query=search_q,
677 677 offset=start, length=limit, order_by=order_by,
678 678 order_dir=order_dir)
679 679
680 680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
681 681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
682 682
683 683 data = []
684 684 comments_model = CommentsModel()
685 685 for pr in pull_requests:
686 686 repo_id = pr.target_repo_id
687 687 comments_count = comments_model.get_all_comments(
688 688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
689 689 owned = pr.user_id == self._rhodecode_user.user_id
690 690
691 691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 693 if review_statuses and review_statuses[4]:
694 694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 695 my_review_status = statuses[0][1].status
696 696
697 697 data.append({
698 698 'target_repo': _render('pullrequest_target_repo',
699 699 pr.target_repo.repo_name),
700 700 'name': _render('pullrequest_name',
701 701 pr.pull_request_id, pr.pull_request_state,
702 702 pr.work_in_progress, pr.target_repo.repo_name,
703 703 short=True),
704 704 'name_raw': pr.pull_request_id,
705 705 'status': _render('pullrequest_status',
706 706 pr.calculated_review_status()),
707 707 'my_status': _render('pullrequest_status',
708 708 my_review_status),
709 709 'title': _render('pullrequest_title', pr.title, pr.description),
710 710 'description': h.escape(pr.description),
711 711 'updated_on': _render('pullrequest_updated_on',
712 712 h.datetime_to_time(pr.updated_on),
713 713 pr.versions_count),
714 714 'updated_on_raw': h.datetime_to_time(pr.updated_on),
715 715 'created_on': _render('pullrequest_updated_on',
716 716 h.datetime_to_time(pr.created_on)),
717 717 'created_on_raw': h.datetime_to_time(pr.created_on),
718 718 'state': pr.pull_request_state,
719 719 'author': _render('pullrequest_author',
720 720 pr.author.full_contact, ),
721 721 'author_raw': pr.author.full_name,
722 722 'comments': _render('pullrequest_comments', comments_count),
723 723 'comments_raw': comments_count,
724 724 'closed': pr.is_closed(),
725 725 'owned': owned
726 726 })
727 727
728 728 # json used to render the grid
729 729 data = ({
730 730 'draw': draw,
731 731 'data': data,
732 732 'recordsTotal': pull_requests_total_count,
733 733 'recordsFiltered': pull_requests_total_count,
734 734 })
735 735 return data
736 736
737 737 @LoginRequired()
738 738 @NotAnonymous()
739 739 def my_account_pullrequests(self):
740 740 c = self.load_default_context()
741 741 c.active = 'pullrequests'
742 742 req_get = self.request.GET
743 743
744 744 c.closed = str2bool(req_get.get('closed'))
745 745 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746 746
747 747 c.selected_filter = 'all'
748 748 if c.closed:
749 749 c.selected_filter = 'all_closed'
750 750 if c.awaiting_my_review:
751 751 c.selected_filter = 'awaiting_my_review'
752 752
753 753 return self._get_template_context(c)
754 754
755 755 @LoginRequired()
756 756 @NotAnonymous()
757 757 def my_account_pullrequests_data(self):
758 758 self.load_default_context()
759 759 req_get = self.request.GET
760 760
761 761 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
762 762 closed = str2bool(req_get.get('closed'))
763 763
764 764 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
765 765 if closed:
766 766 statuses += [PullRequest.STATUS_CLOSED]
767 767
768 768 filter_type = \
769 769 'awaiting_my_review' if awaiting_my_review \
770 770 else None
771 771
772 772 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
773 773 return data
774 774
775 775 @LoginRequired()
776 776 @NotAnonymous()
777 777 def my_account_user_group_membership(self):
778 778 c = self.load_default_context()
779 779 c.active = 'user_group_membership'
780 780 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
781 781 for group in self._rhodecode_db_user.group_member]
782 782 c.user_groups = ext_json.str_json(groups)
783 783 return self._get_template_context(c)
@@ -1,185 +1,184 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import logging
22 21
23 22 from pyramid.httpexceptions import (
24 23 HTTPFound, HTTPNotFound, HTTPInternalServerError)
25 24
26 25 from rhodecode.apps._base import BaseAppView
27 26 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
28 27
29 28 from rhodecode.lib import helpers as h
30 29 from rhodecode.lib.helpers import SqlPage
31 30 from rhodecode.lib.utils2 import safe_int
32 31 from rhodecode.model.db import Notification
33 32 from rhodecode.model.notification import NotificationModel
34 33 from rhodecode.model.meta import Session
35 34
36 35
37 36 log = logging.getLogger(__name__)
38 37
39 38
40 39 class MyAccountNotificationsView(BaseAppView):
41 40
42 41 def load_default_context(self):
43 42 c = self._get_local_tmpl_context()
44 43 c.user = c.auth_user.get_instance()
45 44
46 45 return c
47 46
48 47 def _has_permissions(self, notification):
49 48 def is_owner():
50 49 user_id = self._rhodecode_db_user.user_id
51 50 for user_notification in notification.notifications_to_users:
52 51 if user_notification.user.user_id == user_id:
53 52 return True
54 53 return False
55 54 return h.HasPermissionAny('hg.admin')() or is_owner()
56 55
57 56 @LoginRequired()
58 57 @NotAnonymous()
59 58 def notifications_show_all(self):
60 59 c = self.load_default_context()
61 60
62 61 c.unread_count = NotificationModel().get_unread_cnt_for_user(
63 62 self._rhodecode_db_user.user_id)
64 63
65 64 _current_filter = self.request.GET.getall('type') or ['unread']
66 65
67 66 notifications = NotificationModel().get_for_user(
68 67 self._rhodecode_db_user.user_id,
69 68 filter_=_current_filter)
70 69
71 70 p = safe_int(self.request.GET.get('page', 1), 1)
72 71
73 72 def url_generator(page_num):
74 73 query_params = {
75 74 'page': page_num
76 75 }
77 76 _query = self.request.GET.mixed()
78 77 query_params.update(_query)
79 78 return self.request.current_route_path(_query=query_params)
80 79
81 80 c.notifications = SqlPage(notifications, page=p, items_per_page=10,
82 81 url_maker=url_generator)
83 82
84 83 c.unread_type = 'unread'
85 84 c.all_type = 'all'
86 85 c.pull_request_type = Notification.TYPE_PULL_REQUEST
87 86 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
88 87 Notification.TYPE_PULL_REQUEST_COMMENT]
89 88
90 89 c.current_filter = 'unread' # default filter
91 90
92 91 if _current_filter == [c.pull_request_type]:
93 92 c.current_filter = 'pull_request'
94 93 elif _current_filter == c.comment_type:
95 94 c.current_filter = 'comment'
96 95 elif _current_filter == [c.unread_type]:
97 96 c.current_filter = 'unread'
98 97 elif _current_filter == [c.all_type]:
99 98 c.current_filter = 'all'
100 99 return self._get_template_context(c)
101 100
102 101 @LoginRequired()
103 102 @NotAnonymous()
104 103 def notifications_show(self):
105 104 c = self.load_default_context()
106 105 notification_id = self.request.matchdict['notification_id']
107 106 notification = Notification.get_or_404(notification_id)
108 107
109 108 if not self._has_permissions(notification):
110 109 log.debug('User %s does not have permission to access notification',
111 110 self._rhodecode_user)
112 111 raise HTTPNotFound()
113 112
114 113 u_notification = NotificationModel().get_user_notification(
115 114 self._rhodecode_db_user.user_id, notification)
116 115 if not u_notification:
117 116 log.debug('User %s notification does not exist',
118 117 self._rhodecode_user)
119 118 raise HTTPNotFound()
120 119
121 120 # when opening this notification, mark it as read for this use
122 121 if not u_notification.read:
123 122 u_notification.mark_as_read()
124 123 Session().commit()
125 124
126 125 c.notification = notification
127 126
128 127 return self._get_template_context(c)
129 128
130 129 @LoginRequired()
131 130 @NotAnonymous()
132 131 @CSRFRequired()
133 132 def notifications_mark_all_read(self):
134 133 NotificationModel().mark_all_read_for_user(
135 134 self._rhodecode_db_user.user_id,
136 135 filter_=self.request.GET.getall('type'))
137 136 Session().commit()
138 137 raise HTTPFound(h.route_path('notifications_show_all'))
139 138
140 139 @LoginRequired()
141 140 @NotAnonymous()
142 141 @CSRFRequired()
143 142 def notification_update(self):
144 143 notification_id = self.request.matchdict['notification_id']
145 144 notification = Notification.get_or_404(notification_id)
146 145
147 146 if not self._has_permissions(notification):
148 147 log.debug('User %s does not have permission to access notification',
149 148 self._rhodecode_user)
150 149 raise HTTPNotFound()
151 150
152 151 try:
153 152 # updates notification read flag
154 153 NotificationModel().mark_read(
155 154 self._rhodecode_user.user_id, notification)
156 155 Session().commit()
157 156 return 'ok'
158 157 except Exception:
159 158 Session().rollback()
160 159 log.exception("Exception updating a notification item")
161 160
162 161 raise HTTPInternalServerError()
163 162
164 163 @LoginRequired()
165 164 @NotAnonymous()
166 165 @CSRFRequired()
167 166 def notification_delete(self):
168 167 notification_id = self.request.matchdict['notification_id']
169 168 notification = Notification.get_or_404(notification_id)
170 169 if not self._has_permissions(notification):
171 170 log.debug('User %s does not have permission to access notification',
172 171 self._rhodecode_user)
173 172 raise HTTPNotFound()
174 173
175 174 try:
176 175 # deletes only notification2user
177 176 NotificationModel().delete(
178 177 self._rhodecode_user.user_id, notification)
179 178 Session().commit()
180 179 return 'ok'
181 180 except Exception:
182 181 Session().rollback()
183 182 log.exception("Exception deleting a notification item")
184 183
185 184 raise HTTPInternalServerError()
@@ -1,146 +1,146 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound
24 24
25 25 from rhodecode.apps._base import BaseAppView, DataGridAppView
26 26 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
27 27 from rhodecode.events import trigger
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib import audit_logger
30 30 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
31 31 from rhodecode.model.db import IntegrityError, UserSshKeys
32 32 from rhodecode.model.meta import Session
33 33 from rhodecode.model.ssh_key import SshKeyModel
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class MyAccountSshKeysView(BaseAppView, DataGridAppView):
39 39
40 40 def load_default_context(self):
41 41 c = self._get_local_tmpl_context()
42 42 c.user = c.auth_user.get_instance()
43 43 c.ssh_enabled = self.request.registry.settings.get(
44 44 'ssh.generate_authorized_keyfile')
45 45 return c
46 46
47 47 @LoginRequired()
48 48 @NotAnonymous()
49 49 def my_account_ssh_keys(self):
50 50 _ = self.request.translate
51 51
52 52 c = self.load_default_context()
53 53 c.active = 'ssh_keys'
54 54 c.default_key = self.request.GET.get('default_key')
55 55 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
56 56 return self._get_template_context(c)
57 57
58 58 @LoginRequired()
59 59 @NotAnonymous()
60 60 def ssh_keys_generate_keypair(self):
61 61 _ = self.request.translate
62 62 c = self.load_default_context()
63 63
64 64 c.active = 'ssh_keys_generate'
65 65 if c.ssh_key_generator_enabled:
66 66 private_format = self.request.GET.get('private_format') \
67 67 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
68 68 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
69 69 c.private, c.public = SshKeyModel().generate_keypair(
70 70 comment=comment, private_format=private_format)
71 71 c.target_form_url = h.route_path(
72 72 'my_account_ssh_keys', _query=dict(default_key=c.public))
73 73 return self._get_template_context(c)
74 74
75 75 @LoginRequired()
76 76 @NotAnonymous()
77 77 @CSRFRequired()
78 78 def my_account_ssh_keys_add(self):
79 79 _ = self.request.translate
80 80 c = self.load_default_context()
81 81
82 82 user_data = c.user.get_api_data()
83 83 key_data = self.request.POST.get('key_data')
84 84 description = self.request.POST.get('description')
85 85 fingerprint = 'unknown'
86 86 try:
87 87 if not key_data:
88 88 raise ValueError('Please add a valid public key')
89 89
90 90 key = SshKeyModel().parse_key(key_data.strip())
91 91 fingerprint = key.hash_md5()
92 92
93 93 ssh_key = SshKeyModel().create(
94 94 c.user.user_id, fingerprint, key.keydata, description)
95 95 ssh_key_data = ssh_key.get_api_data()
96 96
97 97 audit_logger.store_web(
98 98 'user.edit.ssh_key.add', action_data={
99 99 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
100 100 user=self._rhodecode_user, )
101 101 Session().commit()
102 102
103 103 # Trigger an event on change of keys.
104 104 trigger(SshKeyFileChangeEvent(), self.request.registry)
105 105
106 106 h.flash(_("Ssh Key successfully created"), category='success')
107 107
108 108 except IntegrityError:
109 109 log.exception("Exception during ssh key saving")
110 110 err = 'Such key with fingerprint `{}` already exists, ' \
111 111 'please use a different one'.format(fingerprint)
112 112 h.flash(_('An error occurred during ssh key saving: {}').format(err),
113 113 category='error')
114 114 except Exception as e:
115 115 log.exception("Exception during ssh key saving")
116 116 h.flash(_('An error occurred during ssh key saving: {}').format(e),
117 117 category='error')
118 118
119 119 return HTTPFound(h.route_path('my_account_ssh_keys'))
120 120
121 121 @LoginRequired()
122 122 @NotAnonymous()
123 123 @CSRFRequired()
124 124 def my_account_ssh_keys_delete(self):
125 125 _ = self.request.translate
126 126 c = self.load_default_context()
127 127
128 128 user_data = c.user.get_api_data()
129 129
130 130 del_ssh_key = self.request.POST.get('del_ssh_key')
131 131
132 132 if del_ssh_key:
133 133 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
134 134 ssh_key_data = ssh_key.get_api_data()
135 135
136 136 SshKeyModel().delete(del_ssh_key, c.user.user_id)
137 137 audit_logger.store_web(
138 138 'user.edit.ssh_key.delete', action_data={
139 139 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
140 140 user=self._rhodecode_user,)
141 141 Session().commit()
142 142 # Trigger an event on change of keys.
143 143 trigger(SshKeyFileChangeEvent(), self.request.registry)
144 144 h.flash(_("Ssh key successfully deleted"), category='success')
145 145
146 146 return HTTPFound(h.route_path('my_account_ssh_keys'))
@@ -1,64 +1,64 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from rhodecode.apps._base import ADMIN_PREFIX
22 22
23 23
24 24 def admin_routes(config):
25 25 from rhodecode.apps.ops.views import OpsView
26 26
27 27 config.add_route(
28 28 name='ops_ping',
29 29 pattern='/ping')
30 30 config.add_view(
31 31 OpsView,
32 32 attr='ops_ping',
33 33 route_name='ops_ping', request_method='GET',
34 34 renderer='json_ext')
35 35
36 36 config.add_route(
37 37 name='ops_error_test',
38 38 pattern='/error')
39 39 config.add_view(
40 40 OpsView,
41 41 attr='ops_error_test',
42 42 route_name='ops_error_test', request_method='GET',
43 43 renderer='json_ext')
44 44
45 45 config.add_route(
46 46 name='ops_redirect_test',
47 47 pattern='/redirect')
48 48 config.add_view(
49 49 OpsView,
50 50 attr='ops_redirect_test',
51 51 route_name='ops_redirect_test', request_method='GET',
52 52 renderer='json_ext')
53 53
54 54 config.add_route(
55 55 name='ops_healthcheck',
56 56 pattern='/status')
57 57 config.add_view(
58 58 OpsView,
59 59 attr='ops_healthcheck',
60 60 route_name='ops_healthcheck', request_method='GET',
61 61 renderer='json_ext')
62 62
63 63 def includeme(config):
64 64 config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops')
@@ -1,96 +1,96 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import LoginRequired
30 30 from collections import OrderedDict
31 31 from rhodecode.model.db import UserApiKeys
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class OpsView(BaseAppView):
37 37
38 38 def load_default_context(self):
39 39 c = self._get_local_tmpl_context()
40 40 c.user = c.auth_user.get_instance()
41 41
42 42 return c
43 43
44 44 def ops_ping(self):
45 45 data = OrderedDict()
46 46 data['instance'] = self.request.registry.settings.get('instance_id')
47 47
48 48 if getattr(self.request, 'user'):
49 49 caller_name = 'anonymous'
50 50 if self.request.user.user_id:
51 51 caller_name = self.request.user.username
52 52
53 53 data['caller_ip'] = self.request.user.ip_addr
54 54 data['caller_name'] = caller_name
55 55
56 56 return {'ok': data}
57 57
58 58 def ops_error_test(self):
59 59 """
60 60 Test exception handling and emails on errors
61 61 """
62 62
63 63 class TestException(Exception):
64 64 pass
65 65 # add timeout so we add some sort of rate limiter
66 66 time.sleep(2)
67 67 msg = ('RhodeCode Enterprise test exception. '
68 68 'Client:{}. Generation time: {}.'.format(self.request.user, time.time()))
69 69 raise TestException(msg)
70 70
71 71 def ops_redirect_test(self):
72 72 """
73 73 Test redirect handling
74 74 """
75 75 redirect_to = self.request.GET.get('to') or h.route_path('home')
76 76 raise HTTPFound(redirect_to)
77 77
78 78 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_HTTP])
79 79 def ops_healthcheck(self):
80 80 from rhodecode.lib.system_info import load_system_info
81 81
82 82 vcsserver_info = load_system_info('vcs_server')
83 83 if vcsserver_info:
84 84 vcsserver_info = vcsserver_info['human_value']
85 85
86 86 db_info = load_system_info('database_info')
87 87 if db_info:
88 88 db_info = db_info['human_value']
89 89
90 90 health_spec = {
91 91 'caller_ip': self.request.user.ip_addr,
92 92 'vcsserver': vcsserver_info,
93 93 'db': db_info,
94 94 }
95 95
96 96 return {'healthcheck': health_spec}
@@ -1,102 +1,102 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import add_route_with_slash
21 21 from rhodecode.apps.repo_group.views.repo_group_settings import RepoGroupSettingsView
22 22 from rhodecode.apps.repo_group.views.repo_group_advanced import RepoGroupAdvancedSettingsView
23 23 from rhodecode.apps.repo_group.views.repo_group_permissions import RepoGroupPermissionsView
24 24 from rhodecode.apps.home.views import HomeView
25 25
26 26
27 27 def includeme(config):
28 28
29 29 # Settings
30 30 config.add_route(
31 31 name='edit_repo_group',
32 32 pattern='/{repo_group_name:.*?[^/]}/_edit',
33 33 repo_group_route=True)
34 34 config.add_view(
35 35 RepoGroupSettingsView,
36 36 attr='edit_settings',
37 37 route_name='edit_repo_group', request_method='GET',
38 38 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
39 39 config.add_view(
40 40 RepoGroupSettingsView,
41 41 attr='edit_settings_update',
42 42 route_name='edit_repo_group', request_method='POST',
43 43 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
44 44
45 45 # Settings advanced
46 46 config.add_route(
47 47 name='edit_repo_group_advanced',
48 48 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced',
49 49 repo_group_route=True)
50 50 config.add_view(
51 51 RepoGroupAdvancedSettingsView,
52 52 attr='edit_repo_group_advanced',
53 53 route_name='edit_repo_group_advanced', request_method='GET',
54 54 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
55 55
56 56 config.add_route(
57 57 name='edit_repo_group_advanced_delete',
58 58 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced/delete',
59 59 repo_group_route=True)
60 60 config.add_view(
61 61 RepoGroupAdvancedSettingsView,
62 62 attr='edit_repo_group_delete',
63 63 route_name='edit_repo_group_advanced_delete', request_method='POST',
64 64 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
65 65
66 66 # settings permissions
67 67 config.add_route(
68 68 name='edit_repo_group_perms',
69 69 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions',
70 70 repo_group_route=True)
71 71 config.add_view(
72 72 RepoGroupPermissionsView,
73 73 attr='edit_repo_group_permissions',
74 74 route_name='edit_repo_group_perms', request_method='GET',
75 75 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
76 76
77 77 config.add_route(
78 78 name='edit_repo_group_perms_update',
79 79 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions/update',
80 80 repo_group_route=True)
81 81 config.add_view(
82 82 RepoGroupPermissionsView,
83 83 attr='edit_repo_groups_permissions_update',
84 84 route_name='edit_repo_group_perms_update', request_method='POST',
85 85 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
86 86
87 87 # Summary, NOTE(marcink): needs to be at the end for catch-all
88 88 add_route_with_slash(
89 89 config,
90 90 name='repo_group_home',
91 91 pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True)
92 92 config.add_view(
93 93 HomeView,
94 94 attr='repo_group_main_page',
95 95 route_name='repo_group_home', request_method='GET',
96 96 renderer='rhodecode:templates/index_repo_group.mako')
97 97 config.add_view(
98 98 HomeView,
99 99 attr='repo_group_main_page',
100 100 route_name='repo_group_home_slash', request_method='GET',
101 101 renderer='rhodecode:templates/index_repo_group.mako')
102 102
@@ -1,19 +1,18 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,89 +1,88 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests import assert_session_flash
24 23
25 24
26 25 def route_path(name, params=None, **kwargs):
27 26 import urllib.request, urllib.parse, urllib.error
28 27
29 28 base_url = {
30 29 'edit_repo_group_advanced':
31 30 '/{repo_group_name}/_settings/advanced',
32 31 'edit_repo_group_advanced_delete':
33 32 '/{repo_group_name}/_settings/advanced/delete',
34 33 }[name].format(**kwargs)
35 34
36 35 if params:
37 36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 37 return base_url
39 38
40 39
41 40 @pytest.mark.usefixtures("app")
42 41 class TestRepoGroupsAdvancedView(object):
43 42
44 43 @pytest.mark.parametrize('repo_group_name', [
45 44 'gro',
46 45 '12345',
47 46 ])
48 47 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
49 48 user_util._test_name = repo_group_name
50 49 gr = user_util.create_repo_group()
51 50 self.app.get(
52 51 route_path('edit_repo_group_advanced',
53 52 repo_group_name=gr.group_name))
54 53
55 54 def test_show_advanced_settings_delete(self, autologin_user, user_util,
56 55 csrf_token):
57 56 gr = user_util.create_repo_group(auto_cleanup=False)
58 57 repo_group_name = gr.group_name
59 58
60 59 params = dict(
61 60 csrf_token=csrf_token
62 61 )
63 62 response = self.app.post(
64 63 route_path('edit_repo_group_advanced_delete',
65 64 repo_group_name=repo_group_name), params=params)
66 65 assert_session_flash(
67 66 response, 'Removed repository group `{}`'.format(repo_group_name))
68 67
69 68 def test_delete_not_possible_with_objects_inside(self, autologin_user,
70 69 repo_groups, csrf_token):
71 70 zombie_group, parent_group, child_group = repo_groups
72 71
73 72 response = self.app.get(
74 73 route_path('edit_repo_group_advanced',
75 74 repo_group_name=parent_group.group_name))
76 75
77 76 response.mustcontain(
78 77 'This repository group includes 1 children repository group')
79 78
80 79 params = dict(
81 80 csrf_token=csrf_token
82 81 )
83 82 response = self.app.post(
84 83 route_path('edit_repo_group_advanced_delete',
85 84 repo_group_name=parent_group.group_name), params=params)
86 85
87 86 assert_session_flash(
88 87 response, 'This repository group contains 1 subgroup '
89 88 'and cannot be deleted')
@@ -1,86 +1,85 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests.utils import permission_update_data_generator
24 23
25 24
26 25 def route_path(name, params=None, **kwargs):
27 26 import urllib.request, urllib.parse, urllib.error
28 27
29 28 base_url = {
30 29 'edit_repo_group_perms':
31 30 '/{repo_group_name:}/_settings/permissions',
32 31 'edit_repo_group_perms_update':
33 32 '/{repo_group_name}/_settings/permissions/update',
34 33 }[name].format(**kwargs)
35 34
36 35 if params:
37 36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 37 return base_url
39 38
40 39
41 40 @pytest.mark.usefixtures("app")
42 41 class TestRepoGroupPermissionsView(object):
43 42
44 43 def test_edit_perms_view(self, user_util, autologin_user):
45 44 repo_group = user_util.create_repo_group()
46 45
47 46 self.app.get(
48 47 route_path('edit_repo_group_perms',
49 48 repo_group_name=repo_group.group_name), status=200)
50 49
51 50 def test_update_permissions(self, csrf_token, user_util):
52 51 repo_group = user_util.create_repo_group()
53 52 repo_group_name = repo_group.group_name
54 53 user = user_util.create_user()
55 54 user_id = user.user_id
56 55 username = user.username
57 56
58 57 # grant new
59 58 form_data = permission_update_data_generator(
60 59 csrf_token,
61 60 default='group.write',
62 61 grant=[(user_id, 'group.write', username, 'user')])
63 62
64 63 # recursive flag required for repo groups
65 64 form_data.extend([('recursive', u'none')])
66 65
67 66 response = self.app.post(
68 67 route_path('edit_repo_group_perms_update',
69 68 repo_group_name=repo_group_name), form_data).follow()
70 69
71 70 assert 'Repository Group permissions updated' in response
72 71
73 72 # revoke given
74 73 form_data = permission_update_data_generator(
75 74 csrf_token,
76 75 default='group.read',
77 76 revoke=[(user_id, 'user')])
78 77
79 78 # recursive flag required for repo groups
80 79 form_data.extend([('recursive', u'none')])
81 80
82 81 response = self.app.post(
83 82 route_path('edit_repo_group_perms_update',
84 83 repo_group_name=repo_group_name), form_data).follow()
85 84
86 85 assert 'Repository Group permissions updated' in response
@@ -1,90 +1,89 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests import assert_session_flash
24 23
25 24
26 25 def route_path(name, params=None, **kwargs):
27 26 import urllib.request, urllib.parse, urllib.error
28 27
29 28 base_url = {
30 29 'edit_repo_group': '/{repo_group_name}/_edit',
31 30 # Update is POST to the above url
32 31 }[name].format(**kwargs)
33 32
34 33 if params:
35 34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 35 return base_url
37 36
38 37
39 38 @pytest.mark.usefixtures("app")
40 39 class TestRepoGroupsSettingsView(object):
41 40
42 41 @pytest.mark.parametrize('repo_group_name', [
43 42 'gro',
44 43 u'12345',
45 44 ])
46 45 def test_edit(self, user_util, autologin_user, repo_group_name):
47 46 user_util._test_name = repo_group_name
48 47 repo_group = user_util.create_repo_group()
49 48
50 49 self.app.get(
51 50 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
52 51 status=200)
53 52
54 53 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
55 54 repo_group = user_util.create_repo_group()
56 55 repo_group_name = repo_group.group_name
57 56
58 57 description = 'description for newly created repo group'
59 58 form_data = rc_fixture._get_group_create_params(
60 59 group_name=repo_group.group_name,
61 60 group_description=description,
62 61 csrf_token=csrf_token,
63 62 repo_group_name=repo_group.group_name,
64 63 repo_group_owner=repo_group.user.username)
65 64
66 65 response = self.app.post(
67 66 route_path('edit_repo_group',
68 67 repo_group_name=repo_group.group_name),
69 68 form_data,
70 69 status=302)
71 70
72 71 assert_session_flash(
73 72 response, 'Repository Group `{}` updated successfully'.format(
74 73 repo_group_name))
75 74
76 75 def test_update_fails_when_parent_pointing_to_self(
77 76 self, csrf_token, user_util, autologin_user, rc_fixture):
78 77 group = user_util.create_repo_group()
79 78 response = self.app.post(
80 79 route_path('edit_repo_group', repo_group_name=group.group_name),
81 80 rc_fixture._get_group_create_params(
82 81 repo_group_name=group.group_name,
83 82 repo_group_owner=group.user.username,
84 83 repo_group=group.group_id,
85 84 csrf_token=csrf_token),
86 85 status=200
87 86 )
88 87 response.mustcontain(
89 88 '<span class="error-message">"{}" is not one of -1'.format(
90 89 group.group_id))
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/ No newline at end of file
@@ -1,105 +1,105 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.apps._base import RepoGroupAppView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib import audit_logger
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, CSRFRequired, HasRepoGroupPermissionAnyDecorator)
31 31 from rhodecode.model.repo_group import RepoGroupModel
32 32 from rhodecode.model.meta import Session
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class RepoGroupAdvancedSettingsView(RepoGroupAppView):
38 38 def load_default_context(self):
39 39 c = self._get_local_tmpl_context()
40 40 return c
41 41
42 42 @LoginRequired()
43 43 @HasRepoGroupPermissionAnyDecorator('group.admin')
44 44 def edit_repo_group_advanced(self):
45 45 _ = self.request.translate
46 46 c = self.load_default_context()
47 47 c.active = 'advanced'
48 48 c.repo_group = self.db_repo_group
49 49
50 50 # update commit cache if GET flag is present
51 51 if self.request.GET.get('update_commit_cache'):
52 52 self.db_repo_group.update_commit_cache()
53 53 h.flash(_('updated commit cache'), category='success')
54 54
55 55 return self._get_template_context(c)
56 56
57 57 @LoginRequired()
58 58 @HasRepoGroupPermissionAnyDecorator('group.admin')
59 59 @CSRFRequired()
60 60 def edit_repo_group_delete(self):
61 61 _ = self.request.translate
62 62 _ungettext = self.request.plularize
63 63 c = self.load_default_context()
64 64 c.repo_group = self.db_repo_group
65 65
66 66 repos = c.repo_group.repositories.all()
67 67 if repos:
68 68 msg = _ungettext(
69 69 'This repository group contains %(num)d repository and cannot be deleted',
70 70 'This repository group contains %(num)d repositories and cannot be'
71 71 ' deleted',
72 72 len(repos)) % {'num': len(repos)}
73 73 h.flash(msg, category='warning')
74 74 raise HTTPFound(
75 75 h.route_path('edit_repo_group_advanced',
76 76 repo_group_name=self.db_repo_group_name))
77 77
78 78 children = c.repo_group.children.all()
79 79 if children:
80 80 msg = _ungettext(
81 81 'This repository group contains %(num)d subgroup and cannot be deleted',
82 82 'This repository group contains %(num)d subgroups and cannot be deleted',
83 83 len(children)) % {'num': len(children)}
84 84 h.flash(msg, category='warning')
85 85 raise HTTPFound(
86 86 h.route_path('edit_repo_group_advanced',
87 87 repo_group_name=self.db_repo_group_name))
88 88
89 89 try:
90 90 old_values = c.repo_group.get_api_data()
91 91 RepoGroupModel().delete(self.db_repo_group_name)
92 92
93 93 audit_logger.store_web(
94 94 'repo_group.delete', action_data={'old_data': old_values},
95 95 user=c.rhodecode_user)
96 96
97 97 Session().commit()
98 98 h.flash(_('Removed repository group `%s`') % self.db_repo_group_name,
99 99 category='success')
100 100 except Exception:
101 101 log.exception("Exception during deletion of repository group")
102 102 h.flash(_('Error occurred during deletion of repository group %s')
103 103 % self.db_repo_group_name, category='error')
104 104
105 105 raise HTTPFound(h.route_path('repo_groups'))
@@ -1,104 +1,104 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.apps._base import RepoGroupAppView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib import audit_logger
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
31 31 from rhodecode.model.db import User
32 32 from rhodecode.model.permission import PermissionModel
33 33 from rhodecode.model.repo_group import RepoGroupModel
34 34 from rhodecode.model.forms import RepoGroupPermsForm
35 35 from rhodecode.model.meta import Session
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class RepoGroupPermissionsView(RepoGroupAppView):
41 41 def load_default_context(self):
42 42 c = self._get_local_tmpl_context()
43 43
44 44 return c
45 45
46 46 @LoginRequired()
47 47 @HasRepoGroupPermissionAnyDecorator('group.admin')
48 48 def edit_repo_group_permissions(self):
49 49 c = self.load_default_context()
50 50 c.active = 'permissions'
51 51 c.repo_group = self.db_repo_group
52 52 return self._get_template_context(c)
53 53
54 54 @LoginRequired()
55 55 @HasRepoGroupPermissionAnyDecorator('group.admin')
56 56 @CSRFRequired()
57 57 def edit_repo_groups_permissions_update(self):
58 58 _ = self.request.translate
59 59 c = self.load_default_context()
60 60 c.active = 'perms'
61 61 c.repo_group = self.db_repo_group
62 62
63 63 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
64 64 form = RepoGroupPermsForm(self.request.translate, valid_recursive_choices)()\
65 65 .to_python(self.request.POST)
66 66
67 67 if not c.rhodecode_user.is_admin:
68 68 if self._revoke_perms_on_yourself(form):
69 69 msg = _('Cannot change permission for yourself as admin')
70 70 h.flash(msg, category='warning')
71 71 raise HTTPFound(
72 72 h.route_path('edit_repo_group_perms',
73 73 repo_group_name=self.db_repo_group_name))
74 74
75 75 # iterate over all members(if in recursive mode) of this groups and
76 76 # set the permissions !
77 77 # this can be potentially heavy operation
78 78 changes = RepoGroupModel().update_permissions(
79 79 c.repo_group,
80 80 form['perm_additions'], form['perm_updates'], form['perm_deletions'],
81 81 form['recursive'])
82 82
83 83 action_data = {
84 84 'added': changes['added'],
85 85 'updated': changes['updated'],
86 86 'deleted': changes['deleted'],
87 87 }
88 88 audit_logger.store_web(
89 89 'repo_group.edit.permissions', action_data=action_data,
90 90 user=c.rhodecode_user)
91 91
92 92 Session().commit()
93 93 h.flash(_('Repository Group permissions updated'), category='success')
94 94
95 95 affected_user_ids = None
96 96 if changes.get('default_user_changed', False):
97 97 # if we change the default user, we need to flush everyone permissions
98 98 affected_user_ids = User.get_all_user_ids()
99 99 PermissionModel().flush_user_permission_caches(
100 100 changes, affected_user_ids=affected_user_ids)
101 101
102 102 raise HTTPFound(
103 103 h.route_path('edit_repo_group_perms',
104 104 repo_group_name=self.db_repo_group_name))
@@ -1,188 +1,188 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import deform
23 23
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26
27 27 from rhodecode import events
28 28 from rhodecode.apps._base import RepoGroupAppView
29 29 from rhodecode.forms import RcForm
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasPermissionAll,
34 34 HasRepoGroupPermissionAny, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
35 35 from rhodecode.model.db import Session, RepoGroup, User
36 36 from rhodecode.model.permission import PermissionModel
37 37 from rhodecode.model.scm import RepoGroupList
38 38 from rhodecode.model.repo_group import RepoGroupModel
39 39 from rhodecode.model.validation_schema.schemas import repo_group_schema
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class RepoGroupSettingsView(RepoGroupAppView):
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47 c.repo_group = self.db_repo_group
48 48 no_parrent = not c.repo_group.parent_group
49 49 can_create_in_root = self._can_create_repo_group()
50 50
51 51 show_root_location = False
52 52 if no_parrent or can_create_in_root:
53 53 # we're global admin, we're ok and we can create TOP level groups
54 54 # or in case this group is already at top-level we also allow
55 55 # creation in root
56 56 show_root_location = True
57 57
58 58 acl_groups = RepoGroupList(
59 59 RepoGroup.query().all(),
60 60 perm_set=['group.admin'])
61 61 c.repo_groups = RepoGroup.groups_choices(
62 62 groups=acl_groups,
63 63 show_empty_group=show_root_location)
64 64 # filter out current repo group
65 65 exclude_group_ids = [c.repo_group.group_id]
66 66 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
67 67 c.repo_groups)
68 68 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
69 69
70 70 parent_group = c.repo_group.parent_group
71 71
72 72 add_parent_group = (parent_group and (
73 73 parent_group.group_id not in c.repo_groups_choices))
74 74 if add_parent_group:
75 75 c.repo_groups_choices.append(parent_group.group_id)
76 76 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
77 77 return c
78 78
79 79 def _can_create_repo_group(self, parent_group_id=None):
80 80 is_admin = HasPermissionAll('hg.admin')('group create controller')
81 81 create_repo_group = HasPermissionAll(
82 82 'hg.repogroup.create.true')('group create controller')
83 83 if is_admin or (create_repo_group and not parent_group_id):
84 84 # we're global admin, or we have global repo group create
85 85 # permission
86 86 # we're ok and we can create TOP level groups
87 87 return True
88 88 elif parent_group_id:
89 89 # we check the permission if we can write to parent group
90 90 group = RepoGroup.get(parent_group_id)
91 91 group_name = group.group_name if group else None
92 92 if HasRepoGroupPermissionAny('group.admin')(
93 93 group_name, 'check if user is an admin of group'):
94 94 # we're an admin of passed in group, we're ok.
95 95 return True
96 96 else:
97 97 return False
98 98 return False
99 99
100 100 def _get_schema(self, c, old_values=None):
101 101 return repo_group_schema.RepoGroupSettingsSchema().bind(
102 102 repo_group_repo_group_options=c.repo_groups_choices,
103 103 repo_group_repo_group_items=c.repo_groups,
104 104
105 105 # user caller
106 106 user=self._rhodecode_user,
107 107 old_values=old_values
108 108 )
109 109
110 110 @LoginRequired()
111 111 @HasRepoGroupPermissionAnyDecorator('group.admin')
112 112 def edit_settings(self):
113 113 c = self.load_default_context()
114 114 c.active = 'settings'
115 115
116 116 defaults = RepoGroupModel()._get_defaults(self.db_repo_group_name)
117 117 defaults['repo_group_owner'] = defaults['user']
118 118
119 119 schema = self._get_schema(c)
120 120 c.form = RcForm(schema, appstruct=defaults)
121 121 return self._get_template_context(c)
122 122
123 123 @LoginRequired()
124 124 @HasRepoGroupPermissionAnyDecorator('group.admin')
125 125 @CSRFRequired()
126 126 def edit_settings_update(self):
127 127 _ = self.request.translate
128 128 c = self.load_default_context()
129 129 c.active = 'settings'
130 130
131 131 old_repo_group_name = self.db_repo_group_name
132 132 new_repo_group_name = old_repo_group_name
133 133
134 134 old_values = RepoGroupModel()._get_defaults(self.db_repo_group_name)
135 135 schema = self._get_schema(c, old_values=old_values)
136 136
137 137 c.form = RcForm(schema)
138 138 pstruct = self.request.POST.items()
139 139
140 140 try:
141 141 schema_data = c.form.validate(pstruct)
142 142 except deform.ValidationFailure as err_form:
143 143 return self._get_template_context(c)
144 144
145 145 # data is now VALID, proceed with updates
146 146 # save validated data back into the updates dict
147 147 validated_updates = dict(
148 148 group_name=schema_data['repo_group']['repo_group_name_without_group'],
149 149 group_parent_id=schema_data['repo_group']['repo_group_id'],
150 150 user=schema_data['repo_group_owner'],
151 151 group_description=schema_data['repo_group_description'],
152 152 enable_locking=schema_data['repo_group_enable_locking'],
153 153 )
154 154
155 155 try:
156 156 RepoGroupModel().update(self.db_repo_group, validated_updates)
157 157
158 158 audit_logger.store_web(
159 159 'repo_group.edit', action_data={'old_data': old_values},
160 160 user=c.rhodecode_user)
161 161
162 162 Session().commit()
163 163
164 164 # use the new full name for redirect once we know we updated
165 165 # the name on filesystem and in DB
166 166 new_repo_group_name = schema_data['repo_group']['repo_group_name_with_group']
167 167
168 168 h.flash(_('Repository Group `{}` updated successfully').format(
169 169 old_repo_group_name), category='success')
170 170
171 171 except Exception:
172 172 log.exception("Exception during update or repository group")
173 173 h.flash(_('Error occurred during update of repository group %s')
174 174 % old_repo_group_name, category='error')
175 175
176 176 name_changed = old_repo_group_name != new_repo_group_name
177 177 if name_changed:
178 178 current_perms = self.db_repo_group.permissions(expand_from_user_groups=True)
179 179 affected_user_ids = [perm['user_id'] for perm in current_perms]
180 180
181 181 # NOTE(marcink): also add owner maybe it has changed
182 182 owner = User.get_by_username(schema_data['repo_group_owner'])
183 183 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
184 184 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
185 185 PermissionModel().trigger_permission_flush(affected_user_ids)
186 186
187 187 raise HTTPFound(
188 188 h.route_path('edit_repo_group', repo_group_name=new_repo_group_name))
@@ -1,1227 +1,1227 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import add_route_with_slash
21 21
22 22
23 23 def includeme(config):
24 24 from rhodecode.apps.repository.views.repo_artifacts import RepoArtifactsView
25 25 from rhodecode.apps.repository.views.repo_audit_logs import AuditLogsView
26 26 from rhodecode.apps.repository.views.repo_automation import RepoAutomationView
27 27 from rhodecode.apps.repository.views.repo_bookmarks import RepoBookmarksView
28 28 from rhodecode.apps.repository.views.repo_branch_permissions import RepoSettingsBranchPermissionsView
29 29 from rhodecode.apps.repository.views.repo_branches import RepoBranchesView
30 30 from rhodecode.apps.repository.views.repo_caches import RepoCachesView
31 31 from rhodecode.apps.repository.views.repo_changelog import RepoChangelogView
32 32 from rhodecode.apps.repository.views.repo_checks import RepoChecksView
33 33 from rhodecode.apps.repository.views.repo_commits import RepoCommitsView
34 34 from rhodecode.apps.repository.views.repo_compare import RepoCompareView
35 35 from rhodecode.apps.repository.views.repo_feed import RepoFeedView
36 36 from rhodecode.apps.repository.views.repo_files import RepoFilesView
37 37 from rhodecode.apps.repository.views.repo_forks import RepoForksView
38 38 from rhodecode.apps.repository.views.repo_maintainance import RepoMaintenanceView
39 39 from rhodecode.apps.repository.views.repo_permissions import RepoSettingsPermissionsView
40 40 from rhodecode.apps.repository.views.repo_pull_requests import RepoPullRequestsView
41 41 from rhodecode.apps.repository.views.repo_review_rules import RepoReviewRulesView
42 42 from rhodecode.apps.repository.views.repo_settings import RepoSettingsView
43 43 from rhodecode.apps.repository.views.repo_settings_advanced import RepoSettingsAdvancedView
44 44 from rhodecode.apps.repository.views.repo_settings_fields import RepoSettingsFieldsView
45 45 from rhodecode.apps.repository.views.repo_settings_issue_trackers import RepoSettingsIssueTrackersView
46 46 from rhodecode.apps.repository.views.repo_settings_remote import RepoSettingsRemoteView
47 47 from rhodecode.apps.repository.views.repo_settings_vcs import RepoSettingsVcsView
48 48 from rhodecode.apps.repository.views.repo_strip import RepoStripView
49 49 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
50 50 from rhodecode.apps.repository.views.repo_tags import RepoTagsView
51 51
52 52 # repo creating checks, special cases that aren't repo routes
53 53 config.add_route(
54 54 name='repo_creating',
55 55 pattern='/{repo_name:.*?[^/]}/repo_creating')
56 56 config.add_view(
57 57 RepoChecksView,
58 58 attr='repo_creating',
59 59 route_name='repo_creating', request_method='GET',
60 60 renderer='rhodecode:templates/admin/repos/repo_creating.mako')
61 61
62 62 config.add_route(
63 63 name='repo_creating_check',
64 64 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
65 65 config.add_view(
66 66 RepoChecksView,
67 67 attr='repo_creating_check',
68 68 route_name='repo_creating_check', request_method='GET',
69 69 renderer='json_ext')
70 70
71 71 # Summary
72 72 # NOTE(marcink): one additional route is defined in very bottom, catch
73 73 # all pattern
74 74 config.add_route(
75 75 name='repo_summary_explicit',
76 76 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
77 77 config.add_view(
78 78 RepoSummaryView,
79 79 attr='summary',
80 80 route_name='repo_summary_explicit', request_method='GET',
81 81 renderer='rhodecode:templates/summary/summary.mako')
82 82
83 83 config.add_route(
84 84 name='repo_summary_commits',
85 85 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
86 86 config.add_view(
87 87 RepoSummaryView,
88 88 attr='summary_commits',
89 89 route_name='repo_summary_commits', request_method='GET',
90 90 renderer='rhodecode:templates/summary/summary_commits.mako')
91 91
92 92 # Commits
93 93 config.add_route(
94 94 name='repo_commit',
95 95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
96 96 config.add_view(
97 97 RepoCommitsView,
98 98 attr='repo_commit_show',
99 99 route_name='repo_commit', request_method='GET',
100 100 renderer=None)
101 101
102 102 config.add_route(
103 103 name='repo_commit_children',
104 104 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
105 105 config.add_view(
106 106 RepoCommitsView,
107 107 attr='repo_commit_children',
108 108 route_name='repo_commit_children', request_method='GET',
109 109 renderer='json_ext', xhr=True)
110 110
111 111 config.add_route(
112 112 name='repo_commit_parents',
113 113 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
114 114 config.add_view(
115 115 RepoCommitsView,
116 116 attr='repo_commit_parents',
117 117 route_name='repo_commit_parents', request_method='GET',
118 118 renderer='json_ext')
119 119
120 120 config.add_route(
121 121 name='repo_commit_raw',
122 122 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
123 123 config.add_view(
124 124 RepoCommitsView,
125 125 attr='repo_commit_raw',
126 126 route_name='repo_commit_raw', request_method='GET',
127 127 renderer=None)
128 128
129 129 config.add_route(
130 130 name='repo_commit_patch',
131 131 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
132 132 config.add_view(
133 133 RepoCommitsView,
134 134 attr='repo_commit_patch',
135 135 route_name='repo_commit_patch', request_method='GET',
136 136 renderer=None)
137 137
138 138 config.add_route(
139 139 name='repo_commit_download',
140 140 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
141 141 config.add_view(
142 142 RepoCommitsView,
143 143 attr='repo_commit_download',
144 144 route_name='repo_commit_download', request_method='GET',
145 145 renderer=None)
146 146
147 147 config.add_route(
148 148 name='repo_commit_data',
149 149 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
150 150 config.add_view(
151 151 RepoCommitsView,
152 152 attr='repo_commit_data',
153 153 route_name='repo_commit_data', request_method='GET',
154 154 renderer='json_ext', xhr=True)
155 155
156 156 config.add_route(
157 157 name='repo_commit_comment_create',
158 158 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
159 159 config.add_view(
160 160 RepoCommitsView,
161 161 attr='repo_commit_comment_create',
162 162 route_name='repo_commit_comment_create', request_method='POST',
163 163 renderer='json_ext')
164 164
165 165 config.add_route(
166 166 name='repo_commit_comment_preview',
167 167 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
168 168 config.add_view(
169 169 RepoCommitsView,
170 170 attr='repo_commit_comment_preview',
171 171 route_name='repo_commit_comment_preview', request_method='POST',
172 172 renderer='string', xhr=True)
173 173
174 174 config.add_route(
175 175 name='repo_commit_comment_history_view',
176 176 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/history_view/{comment_history_id}', repo_route=True)
177 177 config.add_view(
178 178 RepoCommitsView,
179 179 attr='repo_commit_comment_history_view',
180 180 route_name='repo_commit_comment_history_view', request_method='POST',
181 181 renderer='string', xhr=True)
182 182
183 183 config.add_route(
184 184 name='repo_commit_comment_attachment_upload',
185 185 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
186 186 config.add_view(
187 187 RepoCommitsView,
188 188 attr='repo_commit_comment_attachment_upload',
189 189 route_name='repo_commit_comment_attachment_upload', request_method='POST',
190 190 renderer='json_ext', xhr=True)
191 191
192 192 config.add_route(
193 193 name='repo_commit_comment_delete',
194 194 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
195 195 config.add_view(
196 196 RepoCommitsView,
197 197 attr='repo_commit_comment_delete',
198 198 route_name='repo_commit_comment_delete', request_method='POST',
199 199 renderer='json_ext')
200 200
201 201 config.add_route(
202 202 name='repo_commit_comment_edit',
203 203 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
204 204 config.add_view(
205 205 RepoCommitsView,
206 206 attr='repo_commit_comment_edit',
207 207 route_name='repo_commit_comment_edit', request_method='POST',
208 208 renderer='json_ext')
209 209
210 210 # still working url for backward compat.
211 211 config.add_route(
212 212 name='repo_commit_raw_deprecated',
213 213 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
214 214 config.add_view(
215 215 RepoCommitsView,
216 216 attr='repo_commit_raw',
217 217 route_name='repo_commit_raw_deprecated', request_method='GET',
218 218 renderer=None)
219 219
220 220 # Files
221 221 config.add_route(
222 222 name='repo_archivefile',
223 223 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
224 224 config.add_view(
225 225 RepoFilesView,
226 226 attr='repo_archivefile',
227 227 route_name='repo_archivefile', request_method='GET',
228 228 renderer=None)
229 229
230 230 config.add_route(
231 231 name='repo_files_diff',
232 232 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
233 233 config.add_view(
234 234 RepoFilesView,
235 235 attr='repo_files_diff',
236 236 route_name='repo_files_diff', request_method='GET',
237 237 renderer=None)
238 238
239 239 config.add_route( # legacy route to make old links work
240 240 name='repo_files_diff_2way_redirect',
241 241 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
242 242 config.add_view(
243 243 RepoFilesView,
244 244 attr='repo_files_diff_2way_redirect',
245 245 route_name='repo_files_diff_2way_redirect', request_method='GET',
246 246 renderer=None)
247 247
248 248 config.add_route(
249 249 name='repo_files',
250 250 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
251 251 config.add_view(
252 252 RepoFilesView,
253 253 attr='repo_files',
254 254 route_name='repo_files', request_method='GET',
255 255 renderer=None)
256 256
257 257 config.add_route(
258 258 name='repo_files:default_path',
259 259 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
260 260 config.add_view(
261 261 RepoFilesView,
262 262 attr='repo_files',
263 263 route_name='repo_files:default_path', request_method='GET',
264 264 renderer=None)
265 265
266 266 config.add_route(
267 267 name='repo_files:default_commit',
268 268 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
269 269 config.add_view(
270 270 RepoFilesView,
271 271 attr='repo_files',
272 272 route_name='repo_files:default_commit', request_method='GET',
273 273 renderer=None)
274 274
275 275 config.add_route(
276 276 name='repo_files:rendered',
277 277 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
278 278 config.add_view(
279 279 RepoFilesView,
280 280 attr='repo_files',
281 281 route_name='repo_files:rendered', request_method='GET',
282 282 renderer=None)
283 283
284 284 config.add_route(
285 285 name='repo_files:annotated',
286 286 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
287 287 config.add_view(
288 288 RepoFilesView,
289 289 attr='repo_files',
290 290 route_name='repo_files:annotated', request_method='GET',
291 291 renderer=None)
292 292
293 293 config.add_route(
294 294 name='repo_files:annotated_previous',
295 295 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
296 296 config.add_view(
297 297 RepoFilesView,
298 298 attr='repo_files_annotated_previous',
299 299 route_name='repo_files:annotated_previous', request_method='GET',
300 300 renderer=None)
301 301
302 302 config.add_route(
303 303 name='repo_nodetree_full',
304 304 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
305 305 config.add_view(
306 306 RepoFilesView,
307 307 attr='repo_nodetree_full',
308 308 route_name='repo_nodetree_full', request_method='GET',
309 309 renderer=None, xhr=True)
310 310
311 311 config.add_route(
312 312 name='repo_nodetree_full:default_path',
313 313 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
314 314 config.add_view(
315 315 RepoFilesView,
316 316 attr='repo_nodetree_full',
317 317 route_name='repo_nodetree_full:default_path', request_method='GET',
318 318 renderer=None, xhr=True)
319 319
320 320 config.add_route(
321 321 name='repo_files_nodelist',
322 322 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
323 323 config.add_view(
324 324 RepoFilesView,
325 325 attr='repo_nodelist',
326 326 route_name='repo_files_nodelist', request_method='GET',
327 327 renderer='json_ext', xhr=True)
328 328
329 329 config.add_route(
330 330 name='repo_file_raw',
331 331 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
332 332 config.add_view(
333 333 RepoFilesView,
334 334 attr='repo_file_raw',
335 335 route_name='repo_file_raw', request_method='GET',
336 336 renderer=None)
337 337
338 338 config.add_route(
339 339 name='repo_file_download',
340 340 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
341 341 config.add_view(
342 342 RepoFilesView,
343 343 attr='repo_file_download',
344 344 route_name='repo_file_download', request_method='GET',
345 345 renderer=None)
346 346
347 347 config.add_route( # backward compat to keep old links working
348 348 name='repo_file_download:legacy',
349 349 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
350 350 repo_route=True)
351 351 config.add_view(
352 352 RepoFilesView,
353 353 attr='repo_file_download',
354 354 route_name='repo_file_download:legacy', request_method='GET',
355 355 renderer=None)
356 356
357 357 config.add_route(
358 358 name='repo_file_history',
359 359 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
360 360 config.add_view(
361 361 RepoFilesView,
362 362 attr='repo_file_history',
363 363 route_name='repo_file_history', request_method='GET',
364 364 renderer='json_ext')
365 365
366 366 config.add_route(
367 367 name='repo_file_authors',
368 368 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
369 369 config.add_view(
370 370 RepoFilesView,
371 371 attr='repo_file_authors',
372 372 route_name='repo_file_authors', request_method='GET',
373 373 renderer='rhodecode:templates/files/file_authors_box.mako')
374 374
375 375 config.add_route(
376 376 name='repo_files_check_head',
377 377 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
378 378 repo_route=True)
379 379 config.add_view(
380 380 RepoFilesView,
381 381 attr='repo_files_check_head',
382 382 route_name='repo_files_check_head', request_method='POST',
383 383 renderer='json_ext', xhr=True)
384 384
385 385 config.add_route(
386 386 name='repo_files_remove_file',
387 387 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
388 388 repo_route=True)
389 389 config.add_view(
390 390 RepoFilesView,
391 391 attr='repo_files_remove_file',
392 392 route_name='repo_files_remove_file', request_method='GET',
393 393 renderer='rhodecode:templates/files/files_delete.mako')
394 394
395 395 config.add_route(
396 396 name='repo_files_delete_file',
397 397 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
398 398 repo_route=True)
399 399 config.add_view(
400 400 RepoFilesView,
401 401 attr='repo_files_delete_file',
402 402 route_name='repo_files_delete_file', request_method='POST',
403 403 renderer=None)
404 404
405 405 config.add_route(
406 406 name='repo_files_edit_file',
407 407 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
408 408 repo_route=True)
409 409 config.add_view(
410 410 RepoFilesView,
411 411 attr='repo_files_edit_file',
412 412 route_name='repo_files_edit_file', request_method='GET',
413 413 renderer='rhodecode:templates/files/files_edit.mako')
414 414
415 415 config.add_route(
416 416 name='repo_files_update_file',
417 417 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
418 418 repo_route=True)
419 419 config.add_view(
420 420 RepoFilesView,
421 421 attr='repo_files_update_file',
422 422 route_name='repo_files_update_file', request_method='POST',
423 423 renderer=None)
424 424
425 425 config.add_route(
426 426 name='repo_files_add_file',
427 427 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
428 428 repo_route=True)
429 429 config.add_view(
430 430 RepoFilesView,
431 431 attr='repo_files_add_file',
432 432 route_name='repo_files_add_file', request_method='GET',
433 433 renderer='rhodecode:templates/files/files_add.mako')
434 434
435 435 config.add_route(
436 436 name='repo_files_upload_file',
437 437 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
438 438 repo_route=True)
439 439 config.add_view(
440 440 RepoFilesView,
441 441 attr='repo_files_add_file',
442 442 route_name='repo_files_upload_file', request_method='GET',
443 443 renderer='rhodecode:templates/files/files_upload.mako')
444 444 config.add_view( # POST creates
445 445 RepoFilesView,
446 446 attr='repo_files_upload_file',
447 447 route_name='repo_files_upload_file', request_method='POST',
448 448 renderer='json_ext')
449 449
450 450 config.add_route(
451 451 name='repo_files_create_file',
452 452 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
453 453 repo_route=True)
454 454 config.add_view( # POST creates
455 455 RepoFilesView,
456 456 attr='repo_files_create_file',
457 457 route_name='repo_files_create_file', request_method='POST',
458 458 renderer=None)
459 459
460 460 # Refs data
461 461 config.add_route(
462 462 name='repo_refs_data',
463 463 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
464 464 config.add_view(
465 465 RepoSummaryView,
466 466 attr='repo_refs_data',
467 467 route_name='repo_refs_data', request_method='GET',
468 468 renderer='json_ext')
469 469
470 470 config.add_route(
471 471 name='repo_refs_changelog_data',
472 472 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
473 473 config.add_view(
474 474 RepoSummaryView,
475 475 attr='repo_refs_changelog_data',
476 476 route_name='repo_refs_changelog_data', request_method='GET',
477 477 renderer='json_ext')
478 478
479 479 config.add_route(
480 480 name='repo_stats',
481 481 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
482 482 config.add_view(
483 483 RepoSummaryView,
484 484 attr='repo_stats',
485 485 route_name='repo_stats', request_method='GET',
486 486 renderer='json_ext')
487 487
488 488 # Commits
489 489 config.add_route(
490 490 name='repo_commits',
491 491 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
492 492 config.add_view(
493 493 RepoChangelogView,
494 494 attr='repo_changelog',
495 495 route_name='repo_commits', request_method='GET',
496 496 renderer='rhodecode:templates/commits/changelog.mako')
497 497 # old routes for backward compat
498 498 config.add_view(
499 499 RepoChangelogView,
500 500 attr='repo_changelog',
501 501 route_name='repo_changelog', request_method='GET',
502 502 renderer='rhodecode:templates/commits/changelog.mako')
503 503
504 504 config.add_route(
505 505 name='repo_commits_elements',
506 506 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
507 507 config.add_view(
508 508 RepoChangelogView,
509 509 attr='repo_commits_elements',
510 510 route_name='repo_commits_elements', request_method=('GET', 'POST'),
511 511 renderer='rhodecode:templates/commits/changelog_elements.mako',
512 512 xhr=True)
513 513
514 514 config.add_route(
515 515 name='repo_commits_elements_file',
516 516 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
517 517 config.add_view(
518 518 RepoChangelogView,
519 519 attr='repo_commits_elements',
520 520 route_name='repo_commits_elements_file', request_method=('GET', 'POST'),
521 521 renderer='rhodecode:templates/commits/changelog_elements.mako',
522 522 xhr=True)
523 523
524 524 config.add_route(
525 525 name='repo_commits_file',
526 526 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
527 527 config.add_view(
528 528 RepoChangelogView,
529 529 attr='repo_changelog',
530 530 route_name='repo_commits_file', request_method='GET',
531 531 renderer='rhodecode:templates/commits/changelog.mako')
532 532 # old routes for backward compat
533 533 config.add_view(
534 534 RepoChangelogView,
535 535 attr='repo_changelog',
536 536 route_name='repo_changelog_file', request_method='GET',
537 537 renderer='rhodecode:templates/commits/changelog.mako')
538 538
539 539 # Changelog (old deprecated name for commits page)
540 540 config.add_route(
541 541 name='repo_changelog',
542 542 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
543 543 config.add_route(
544 544 name='repo_changelog_file',
545 545 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
546 546
547 547 # Compare
548 548 config.add_route(
549 549 name='repo_compare_select',
550 550 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
551 551 config.add_view(
552 552 RepoCompareView,
553 553 attr='compare_select',
554 554 route_name='repo_compare_select', request_method='GET',
555 555 renderer='rhodecode:templates/compare/compare_diff.mako')
556 556
557 557 config.add_route(
558 558 name='repo_compare',
559 559 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
560 560 config.add_view(
561 561 RepoCompareView,
562 562 attr='compare',
563 563 route_name='repo_compare', request_method='GET',
564 564 renderer=None)
565 565
566 566 # Tags
567 567 config.add_route(
568 568 name='tags_home',
569 569 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
570 570 config.add_view(
571 571 RepoTagsView,
572 572 attr='tags',
573 573 route_name='tags_home', request_method='GET',
574 574 renderer='rhodecode:templates/tags/tags.mako')
575 575
576 576 # Branches
577 577 config.add_route(
578 578 name='branches_home',
579 579 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
580 580 config.add_view(
581 581 RepoBranchesView,
582 582 attr='branches',
583 583 route_name='branches_home', request_method='GET',
584 584 renderer='rhodecode:templates/branches/branches.mako')
585 585
586 586 # Bookmarks
587 587 config.add_route(
588 588 name='bookmarks_home',
589 589 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
590 590 config.add_view(
591 591 RepoBookmarksView,
592 592 attr='bookmarks',
593 593 route_name='bookmarks_home', request_method='GET',
594 594 renderer='rhodecode:templates/bookmarks/bookmarks.mako')
595 595
596 596 # Forks
597 597 config.add_route(
598 598 name='repo_fork_new',
599 599 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
600 600 repo_forbid_when_archived=True,
601 601 repo_accepted_types=['hg', 'git'])
602 602 config.add_view(
603 603 RepoForksView,
604 604 attr='repo_fork_new',
605 605 route_name='repo_fork_new', request_method='GET',
606 606 renderer='rhodecode:templates/forks/forks.mako')
607 607
608 608 config.add_route(
609 609 name='repo_fork_create',
610 610 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
611 611 repo_forbid_when_archived=True,
612 612 repo_accepted_types=['hg', 'git'])
613 613 config.add_view(
614 614 RepoForksView,
615 615 attr='repo_fork_create',
616 616 route_name='repo_fork_create', request_method='POST',
617 617 renderer='rhodecode:templates/forks/fork.mako')
618 618
619 619 config.add_route(
620 620 name='repo_forks_show_all',
621 621 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
622 622 repo_accepted_types=['hg', 'git'])
623 623 config.add_view(
624 624 RepoForksView,
625 625 attr='repo_forks_show_all',
626 626 route_name='repo_forks_show_all', request_method='GET',
627 627 renderer='rhodecode:templates/forks/forks.mako')
628 628
629 629 config.add_route(
630 630 name='repo_forks_data',
631 631 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
632 632 repo_accepted_types=['hg', 'git'])
633 633 config.add_view(
634 634 RepoForksView,
635 635 attr='repo_forks_data',
636 636 route_name='repo_forks_data', request_method='GET',
637 637 renderer='json_ext', xhr=True)
638 638
639 639 # Pull Requests
640 640 config.add_route(
641 641 name='pullrequest_show',
642 642 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
643 643 repo_route=True)
644 644 config.add_view(
645 645 RepoPullRequestsView,
646 646 attr='pull_request_show',
647 647 route_name='pullrequest_show', request_method='GET',
648 648 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
649 649
650 650 config.add_route(
651 651 name='pullrequest_show_all',
652 652 pattern='/{repo_name:.*?[^/]}/pull-request',
653 653 repo_route=True, repo_accepted_types=['hg', 'git'])
654 654 config.add_view(
655 655 RepoPullRequestsView,
656 656 attr='pull_request_list',
657 657 route_name='pullrequest_show_all', request_method='GET',
658 658 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
659 659
660 660 config.add_route(
661 661 name='pullrequest_show_all_data',
662 662 pattern='/{repo_name:.*?[^/]}/pull-request-data',
663 663 repo_route=True, repo_accepted_types=['hg', 'git'])
664 664 config.add_view(
665 665 RepoPullRequestsView,
666 666 attr='pull_request_list_data',
667 667 route_name='pullrequest_show_all_data', request_method='GET',
668 668 renderer='json_ext', xhr=True)
669 669
670 670 config.add_route(
671 671 name='pullrequest_repo_refs',
672 672 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
673 673 repo_route=True)
674 674 config.add_view(
675 675 RepoPullRequestsView,
676 676 attr='pull_request_repo_refs',
677 677 route_name='pullrequest_repo_refs', request_method='GET',
678 678 renderer='json_ext', xhr=True)
679 679
680 680 config.add_route(
681 681 name='pullrequest_repo_targets',
682 682 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
683 683 repo_route=True)
684 684 config.add_view(
685 685 RepoPullRequestsView,
686 686 attr='pullrequest_repo_targets',
687 687 route_name='pullrequest_repo_targets', request_method='GET',
688 688 renderer='json_ext', xhr=True)
689 689
690 690 config.add_route(
691 691 name='pullrequest_new',
692 692 pattern='/{repo_name:.*?[^/]}/pull-request/new',
693 693 repo_route=True, repo_accepted_types=['hg', 'git'],
694 694 repo_forbid_when_archived=True)
695 695 config.add_view(
696 696 RepoPullRequestsView,
697 697 attr='pull_request_new',
698 698 route_name='pullrequest_new', request_method='GET',
699 699 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
700 700
701 701 config.add_route(
702 702 name='pullrequest_create',
703 703 pattern='/{repo_name:.*?[^/]}/pull-request/create',
704 704 repo_route=True, repo_accepted_types=['hg', 'git'],
705 705 repo_forbid_when_archived=True)
706 706 config.add_view(
707 707 RepoPullRequestsView,
708 708 attr='pull_request_create',
709 709 route_name='pullrequest_create', request_method='POST',
710 710 renderer=None)
711 711
712 712 config.add_route(
713 713 name='pullrequest_update',
714 714 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
715 715 repo_route=True, repo_forbid_when_archived=True)
716 716 config.add_view(
717 717 RepoPullRequestsView,
718 718 attr='pull_request_update',
719 719 route_name='pullrequest_update', request_method='POST',
720 720 renderer='json_ext')
721 721
722 722 config.add_route(
723 723 name='pullrequest_merge',
724 724 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
725 725 repo_route=True, repo_forbid_when_archived=True)
726 726 config.add_view(
727 727 RepoPullRequestsView,
728 728 attr='pull_request_merge',
729 729 route_name='pullrequest_merge', request_method='POST',
730 730 renderer='json_ext')
731 731
732 732 config.add_route(
733 733 name='pullrequest_delete',
734 734 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
735 735 repo_route=True, repo_forbid_when_archived=True)
736 736 config.add_view(
737 737 RepoPullRequestsView,
738 738 attr='pull_request_delete',
739 739 route_name='pullrequest_delete', request_method='POST',
740 740 renderer='json_ext')
741 741
742 742 config.add_route(
743 743 name='pullrequest_comment_create',
744 744 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
745 745 repo_route=True)
746 746 config.add_view(
747 747 RepoPullRequestsView,
748 748 attr='pull_request_comment_create',
749 749 route_name='pullrequest_comment_create', request_method='POST',
750 750 renderer='json_ext')
751 751
752 752 config.add_route(
753 753 name='pullrequest_comment_edit',
754 754 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
755 755 repo_route=True, repo_accepted_types=['hg', 'git'])
756 756 config.add_view(
757 757 RepoPullRequestsView,
758 758 attr='pull_request_comment_edit',
759 759 route_name='pullrequest_comment_edit', request_method='POST',
760 760 renderer='json_ext')
761 761
762 762 config.add_route(
763 763 name='pullrequest_comment_delete',
764 764 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
765 765 repo_route=True, repo_accepted_types=['hg', 'git'])
766 766 config.add_view(
767 767 RepoPullRequestsView,
768 768 attr='pull_request_comment_delete',
769 769 route_name='pullrequest_comment_delete', request_method='POST',
770 770 renderer='json_ext')
771 771
772 772 config.add_route(
773 773 name='pullrequest_comments',
774 774 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
775 775 repo_route=True)
776 776 config.add_view(
777 777 RepoPullRequestsView,
778 778 attr='pullrequest_comments',
779 779 route_name='pullrequest_comments', request_method='POST',
780 780 renderer='string_html', xhr=True)
781 781
782 782 config.add_route(
783 783 name='pullrequest_todos',
784 784 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
785 785 repo_route=True)
786 786 config.add_view(
787 787 RepoPullRequestsView,
788 788 attr='pullrequest_todos',
789 789 route_name='pullrequest_todos', request_method='POST',
790 790 renderer='string_html', xhr=True)
791 791
792 792 config.add_route(
793 793 name='pullrequest_drafts',
794 794 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
795 795 repo_route=True)
796 796 config.add_view(
797 797 RepoPullRequestsView,
798 798 attr='pullrequest_drafts',
799 799 route_name='pullrequest_drafts', request_method='POST',
800 800 renderer='string_html', xhr=True)
801 801
802 802 # Artifacts, (EE feature)
803 803 config.add_route(
804 804 name='repo_artifacts_list',
805 805 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
806 806 config.add_view(
807 807 RepoArtifactsView,
808 808 attr='repo_artifacts',
809 809 route_name='repo_artifacts_list', request_method='GET',
810 810 renderer='rhodecode:templates/artifacts/artifact_list.mako')
811 811
812 812 # Settings
813 813 config.add_route(
814 814 name='edit_repo',
815 815 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
816 816 config.add_view(
817 817 RepoSettingsView,
818 818 attr='edit_settings',
819 819 route_name='edit_repo', request_method='GET',
820 820 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
821 821 # update is POST on edit_repo
822 822 config.add_view(
823 823 RepoSettingsView,
824 824 attr='edit_settings_update',
825 825 route_name='edit_repo', request_method='POST',
826 826 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
827 827
828 828 # Settings advanced
829 829 config.add_route(
830 830 name='edit_repo_advanced',
831 831 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
832 832 config.add_view(
833 833 RepoSettingsAdvancedView,
834 834 attr='edit_advanced',
835 835 route_name='edit_repo_advanced', request_method='GET',
836 836 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
837 837
838 838 config.add_route(
839 839 name='edit_repo_advanced_archive',
840 840 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
841 841 config.add_view(
842 842 RepoSettingsAdvancedView,
843 843 attr='edit_advanced_archive',
844 844 route_name='edit_repo_advanced_archive', request_method='POST',
845 845 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
846 846
847 847 config.add_route(
848 848 name='edit_repo_advanced_delete',
849 849 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
850 850 config.add_view(
851 851 RepoSettingsAdvancedView,
852 852 attr='edit_advanced_delete',
853 853 route_name='edit_repo_advanced_delete', request_method='POST',
854 854 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
855 855
856 856 config.add_route(
857 857 name='edit_repo_advanced_locking',
858 858 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
859 859 config.add_view(
860 860 RepoSettingsAdvancedView,
861 861 attr='edit_advanced_toggle_locking',
862 862 route_name='edit_repo_advanced_locking', request_method='POST',
863 863 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
864 864
865 865 config.add_route(
866 866 name='edit_repo_advanced_journal',
867 867 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
868 868 config.add_view(
869 869 RepoSettingsAdvancedView,
870 870 attr='edit_advanced_journal',
871 871 route_name='edit_repo_advanced_journal', request_method='POST',
872 872 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
873 873
874 874 config.add_route(
875 875 name='edit_repo_advanced_fork',
876 876 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
877 877 config.add_view(
878 878 RepoSettingsAdvancedView,
879 879 attr='edit_advanced_fork',
880 880 route_name='edit_repo_advanced_fork', request_method='POST',
881 881 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
882 882
883 883 config.add_route(
884 884 name='edit_repo_advanced_hooks',
885 885 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
886 886 config.add_view(
887 887 RepoSettingsAdvancedView,
888 888 attr='edit_advanced_install_hooks',
889 889 route_name='edit_repo_advanced_hooks', request_method='GET',
890 890 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
891 891
892 892 # Caches
893 893 config.add_route(
894 894 name='edit_repo_caches',
895 895 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
896 896 config.add_view(
897 897 RepoCachesView,
898 898 attr='repo_caches',
899 899 route_name='edit_repo_caches', request_method='GET',
900 900 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
901 901 config.add_view(
902 902 RepoCachesView,
903 903 attr='repo_caches_purge',
904 904 route_name='edit_repo_caches', request_method='POST')
905 905
906 906 # Permissions
907 907 config.add_route(
908 908 name='edit_repo_perms',
909 909 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
910 910 config.add_view(
911 911 RepoSettingsPermissionsView,
912 912 attr='edit_permissions',
913 913 route_name='edit_repo_perms', request_method='GET',
914 914 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
915 915 config.add_view(
916 916 RepoSettingsPermissionsView,
917 917 attr='edit_permissions_update',
918 918 route_name='edit_repo_perms', request_method='POST',
919 919 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
920 920
921 921 config.add_route(
922 922 name='edit_repo_perms_set_private',
923 923 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
924 924 config.add_view(
925 925 RepoSettingsPermissionsView,
926 926 attr='edit_permissions_set_private_repo',
927 927 route_name='edit_repo_perms_set_private', request_method='POST',
928 928 renderer='json_ext')
929 929
930 930 # Permissions Branch (EE feature)
931 931 config.add_route(
932 932 name='edit_repo_perms_branch',
933 933 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
934 934 config.add_view(
935 935 RepoSettingsBranchPermissionsView,
936 936 attr='branch_permissions',
937 937 route_name='edit_repo_perms_branch', request_method='GET',
938 938 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
939 939
940 940 config.add_route(
941 941 name='edit_repo_perms_branch_delete',
942 942 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
943 943 repo_route=True)
944 944 ## Only implemented in EE
945 945
946 946 # Maintenance
947 947 config.add_route(
948 948 name='edit_repo_maintenance',
949 949 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
950 950 config.add_view(
951 951 RepoMaintenanceView,
952 952 attr='repo_maintenance',
953 953 route_name='edit_repo_maintenance', request_method='GET',
954 954 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
955 955
956 956 config.add_route(
957 957 name='edit_repo_maintenance_execute',
958 958 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
959 959 config.add_view(
960 960 RepoMaintenanceView,
961 961 attr='repo_maintenance_execute',
962 962 route_name='edit_repo_maintenance_execute', request_method='GET',
963 963 renderer='json', xhr=True)
964 964
965 965 # Fields
966 966 config.add_route(
967 967 name='edit_repo_fields',
968 968 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
969 969 config.add_view(
970 970 RepoSettingsFieldsView,
971 971 attr='repo_field_edit',
972 972 route_name='edit_repo_fields', request_method='GET',
973 973 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
974 974
975 975 config.add_route(
976 976 name='edit_repo_fields_create',
977 977 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
978 978 config.add_view(
979 979 RepoSettingsFieldsView,
980 980 attr='repo_field_create',
981 981 route_name='edit_repo_fields_create', request_method='POST',
982 982 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
983 983
984 984 config.add_route(
985 985 name='edit_repo_fields_delete',
986 986 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
987 987 config.add_view(
988 988 RepoSettingsFieldsView,
989 989 attr='repo_field_delete',
990 990 route_name='edit_repo_fields_delete', request_method='POST',
991 991 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
992 992
993 993 # Locking
994 994 config.add_route(
995 995 name='repo_edit_toggle_locking',
996 996 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
997 997 config.add_view(
998 998 RepoSettingsView,
999 999 attr='edit_advanced_toggle_locking',
1000 1000 route_name='repo_edit_toggle_locking', request_method='GET',
1001 1001 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1002 1002
1003 1003 # Remote
1004 1004 config.add_route(
1005 1005 name='edit_repo_remote',
1006 1006 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
1007 1007 config.add_view(
1008 1008 RepoSettingsRemoteView,
1009 1009 attr='repo_remote_edit_form',
1010 1010 route_name='edit_repo_remote', request_method='GET',
1011 1011 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1012 1012
1013 1013 config.add_route(
1014 1014 name='edit_repo_remote_pull',
1015 1015 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
1016 1016 config.add_view(
1017 1017 RepoSettingsRemoteView,
1018 1018 attr='repo_remote_pull_changes',
1019 1019 route_name='edit_repo_remote_pull', request_method='POST',
1020 1020 renderer=None)
1021 1021
1022 1022 config.add_route(
1023 1023 name='edit_repo_remote_push',
1024 1024 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
1025 1025
1026 1026 # Statistics
1027 1027 config.add_route(
1028 1028 name='edit_repo_statistics',
1029 1029 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
1030 1030 config.add_view(
1031 1031 RepoSettingsView,
1032 1032 attr='edit_statistics_form',
1033 1033 route_name='edit_repo_statistics', request_method='GET',
1034 1034 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1035 1035
1036 1036 config.add_route(
1037 1037 name='edit_repo_statistics_reset',
1038 1038 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
1039 1039 config.add_view(
1040 1040 RepoSettingsView,
1041 1041 attr='repo_statistics_reset',
1042 1042 route_name='edit_repo_statistics_reset', request_method='POST',
1043 1043 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1044 1044
1045 1045 # Issue trackers
1046 1046 config.add_route(
1047 1047 name='edit_repo_issuetracker',
1048 1048 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
1049 1049 config.add_view(
1050 1050 RepoSettingsIssueTrackersView,
1051 1051 attr='repo_issuetracker',
1052 1052 route_name='edit_repo_issuetracker', request_method='GET',
1053 1053 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1054 1054
1055 1055 config.add_route(
1056 1056 name='edit_repo_issuetracker_test',
1057 1057 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
1058 1058 config.add_view(
1059 1059 RepoSettingsIssueTrackersView,
1060 1060 attr='repo_issuetracker_test',
1061 1061 route_name='edit_repo_issuetracker_test', request_method='POST',
1062 1062 renderer='string', xhr=True)
1063 1063
1064 1064 config.add_route(
1065 1065 name='edit_repo_issuetracker_delete',
1066 1066 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
1067 1067 config.add_view(
1068 1068 RepoSettingsIssueTrackersView,
1069 1069 attr='repo_issuetracker_delete',
1070 1070 route_name='edit_repo_issuetracker_delete', request_method='POST',
1071 1071 renderer='json_ext', xhr=True)
1072 1072
1073 1073 config.add_route(
1074 1074 name='edit_repo_issuetracker_update',
1075 1075 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
1076 1076 config.add_view(
1077 1077 RepoSettingsIssueTrackersView,
1078 1078 attr='repo_issuetracker_update',
1079 1079 route_name='edit_repo_issuetracker_update', request_method='POST',
1080 1080 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1081 1081
1082 1082 # VCS Settings
1083 1083 config.add_route(
1084 1084 name='edit_repo_vcs',
1085 1085 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
1086 1086 config.add_view(
1087 1087 RepoSettingsVcsView,
1088 1088 attr='repo_vcs_settings',
1089 1089 route_name='edit_repo_vcs', request_method='GET',
1090 1090 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1091 1091
1092 1092 config.add_route(
1093 1093 name='edit_repo_vcs_update',
1094 1094 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
1095 1095 config.add_view(
1096 1096 RepoSettingsVcsView,
1097 1097 attr='repo_settings_vcs_update',
1098 1098 route_name='edit_repo_vcs_update', request_method='POST',
1099 1099 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1100 1100
1101 1101 # svn pattern
1102 1102 config.add_route(
1103 1103 name='edit_repo_vcs_svn_pattern_delete',
1104 1104 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
1105 1105 config.add_view(
1106 1106 RepoSettingsVcsView,
1107 1107 attr='repo_settings_delete_svn_pattern',
1108 1108 route_name='edit_repo_vcs_svn_pattern_delete', request_method='POST',
1109 1109 renderer='json_ext', xhr=True)
1110 1110
1111 1111 # Repo Review Rules (EE feature)
1112 1112 config.add_route(
1113 1113 name='repo_reviewers',
1114 1114 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
1115 1115 config.add_view(
1116 1116 RepoReviewRulesView,
1117 1117 attr='repo_review_rules',
1118 1118 route_name='repo_reviewers', request_method='GET',
1119 1119 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1120 1120
1121 1121 config.add_route(
1122 1122 name='repo_default_reviewers_data',
1123 1123 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
1124 1124 config.add_view(
1125 1125 RepoReviewRulesView,
1126 1126 attr='repo_default_reviewers_data',
1127 1127 route_name='repo_default_reviewers_data', request_method='GET',
1128 1128 renderer='json_ext')
1129 1129
1130 1130 # Repo Automation (EE feature)
1131 1131 config.add_route(
1132 1132 name='repo_automation',
1133 1133 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
1134 1134 config.add_view(
1135 1135 RepoAutomationView,
1136 1136 attr='repo_automation',
1137 1137 route_name='repo_automation', request_method='GET',
1138 1138 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1139 1139
1140 1140 # Strip
1141 1141 config.add_route(
1142 1142 name='edit_repo_strip',
1143 1143 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
1144 1144 config.add_view(
1145 1145 RepoStripView,
1146 1146 attr='strip',
1147 1147 route_name='edit_repo_strip', request_method='GET',
1148 1148 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1149 1149
1150 1150 config.add_route(
1151 1151 name='strip_check',
1152 1152 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
1153 1153 config.add_view(
1154 1154 RepoStripView,
1155 1155 attr='strip_check',
1156 1156 route_name='strip_check', request_method='POST',
1157 1157 renderer='json', xhr=True)
1158 1158
1159 1159 config.add_route(
1160 1160 name='strip_execute',
1161 1161 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
1162 1162 config.add_view(
1163 1163 RepoStripView,
1164 1164 attr='strip_execute',
1165 1165 route_name='strip_execute', request_method='POST',
1166 1166 renderer='json', xhr=True)
1167 1167
1168 1168 # Audit logs
1169 1169 config.add_route(
1170 1170 name='edit_repo_audit_logs',
1171 1171 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
1172 1172 config.add_view(
1173 1173 AuditLogsView,
1174 1174 attr='repo_audit_logs',
1175 1175 route_name='edit_repo_audit_logs', request_method='GET',
1176 1176 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1177 1177
1178 1178 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
1179 1179 config.add_route(
1180 1180 name='rss_feed_home',
1181 1181 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
1182 1182 config.add_view(
1183 1183 RepoFeedView,
1184 1184 attr='rss',
1185 1185 route_name='rss_feed_home', request_method='GET', renderer=None)
1186 1186
1187 1187 config.add_route(
1188 1188 name='rss_feed_home_old',
1189 1189 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
1190 1190 config.add_view(
1191 1191 RepoFeedView,
1192 1192 attr='rss',
1193 1193 route_name='rss_feed_home_old', request_method='GET', renderer=None)
1194 1194
1195 1195 config.add_route(
1196 1196 name='atom_feed_home',
1197 1197 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
1198 1198 config.add_view(
1199 1199 RepoFeedView,
1200 1200 attr='atom',
1201 1201 route_name='atom_feed_home', request_method='GET', renderer=None)
1202 1202
1203 1203 config.add_route(
1204 1204 name='atom_feed_home_old',
1205 1205 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
1206 1206 config.add_view(
1207 1207 RepoFeedView,
1208 1208 attr='atom',
1209 1209 route_name='atom_feed_home_old', request_method='GET', renderer=None)
1210 1210
1211 1211 # NOTE(marcink): needs to be at the end for catch-all
1212 1212 add_route_with_slash(
1213 1213 config,
1214 1214 name='repo_summary',
1215 1215 pattern='/{repo_name:.*?[^/]}', repo_route=True)
1216 1216 config.add_view(
1217 1217 RepoSummaryView,
1218 1218 attr='summary',
1219 1219 route_name='repo_summary', request_method='GET',
1220 1220 renderer='rhodecode:templates/summary/summary.mako')
1221 1221
1222 1222 # TODO(marcink): there's no such route??
1223 1223 config.add_view(
1224 1224 RepoSummaryView,
1225 1225 attr='summary',
1226 1226 route_name='repo_summary_slash', request_method='GET',
1227 1227 renderer='rhodecode:templates/summary/summary.mako') No newline at end of file
@@ -1,19 +1,18 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,84 +1,83 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21 from rhodecode.model.db import Repository
23 22
24 23
25 24 def route_path(name, params=None, **kwargs):
26 25 import urllib.request, urllib.parse, urllib.error
27 26
28 27 base_url = {
29 28 'pullrequest_show_all': '/{repo_name}/pull-request',
30 29 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 30 }[name].format(**kwargs)
32 31
33 32 if params:
34 33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 34 return base_url
36 35
37 36
38 37 @pytest.mark.backends("git", "hg")
39 38 @pytest.mark.usefixtures('autologin_user', 'app')
40 39 class TestPullRequestList(object):
41 40
42 41 @pytest.mark.parametrize('params, expected_title', [
43 42 ({'source': 0, 'closed': 1}, 'Closed'),
44 43 ({'source': 0, 'my': 1}, 'Created by me'),
45 44 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 45 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 46 ({'source': 1}, 'From this repo'),
48 47 ])
49 48 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 49 pull_request = pr_util.create_pull_request()
51 50
52 51 response = self.app.get(
53 52 route_path('pullrequest_show_all',
54 53 repo_name=pull_request.target_repo.repo_name,
55 54 params=params))
56 55
57 56 assert_response = response.assert_response()
58 57
59 58 element = assert_response.get_element('.title .active')
60 59 element_text = element.text_content()
61 60 assert expected_title == element_text
62 61
63 62 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 63 pull_request = pr_util.create_pull_request()
65 64 response = self.app.get(
66 65 route_path('pullrequest_show_all_data',
67 66 repo_name=pull_request.target_repo.repo_name),
68 67 extra_environ=xhr_header)
69 68
70 69 assert response.json['recordsTotal'] == 1
71 70 assert response.json['data'][0]['description'] == 'Description'
72 71
73 72 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 73 xss_description = "<script>alert('Hi!')</script>"
75 74 pull_request = pr_util.create_pull_request(description=xss_description)
76 75
77 76 response = self.app.get(
78 77 route_path('pullrequest_show_all_data',
79 78 repo_name=pull_request.target_repo.repo_name),
80 79 extra_environ=xhr_header)
81 80
82 81 assert response.json['recordsTotal'] == 1
83 82 assert response.json['data'][0]['description'] == \
84 83 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,52 +1,51 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21 from rhodecode.model.db import Repository
23 22
24 23
25 24 def route_path(name, params=None, **kwargs):
26 25 import urllib.request, urllib.parse, urllib.error
27 26
28 27 base_url = {
29 28 'bookmarks_home': '/{repo_name}/bookmarks',
30 29 }[name].format(**kwargs)
31 30
32 31 if params:
33 32 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 33 return base_url
35 34
36 35
37 36 @pytest.mark.usefixtures('autologin_user', 'app')
38 37 class TestBookmarks(object):
39 38
40 39 def test_index(self, backend):
41 40 if backend.alias == 'hg':
42 41 response = self.app.get(
43 42 route_path('bookmarks_home', repo_name=backend.repo_name))
44 43
45 44 repo = Repository.get_by_repo_name(backend.repo_name)
46 45 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
47 46 assert commit_id in response
48 47 assert obj_name in response
49 48 else:
50 49 self.app.get(
51 50 route_path('bookmarks_home', repo_name=backend.repo_name),
52 51 status=404)
@@ -1,48 +1,47 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21 from rhodecode.model.db import Repository
23 22
24 23
25 24 def route_path(name, params=None, **kwargs):
26 25 import urllib.request, urllib.parse, urllib.error
27 26
28 27 base_url = {
29 28 'branches_home': '/{repo_name}/branches',
30 29 }[name].format(**kwargs)
31 30
32 31 if params:
33 32 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 33 return base_url
35 34
36 35
37 36 @pytest.mark.usefixtures('autologin_user', 'app')
38 37 class TestBranchesController(object):
39 38
40 39 def test_index(self, backend):
41 40 response = self.app.get(
42 41 route_path('branches_home', repo_name=backend.repo_name))
43 42
44 43 repo = Repository.get_by_repo_name(backend.repo_name)
45 44
46 45 for commit_id, obj_name in repo.scm_instance().branches.items():
47 46 assert commit_id in response
48 47 assert obj_name in response
@@ -1,220 +1,219 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import re
22 21
23 22 import pytest
24 23
25 24 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
26 25 from rhodecode.tests import TestController
27 26
28 27 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
29 28
30 29
31 30 def route_path(name, params=None, **kwargs):
32 31 import urllib.request, urllib.parse, urllib.error
33 32
34 33 base_url = {
35 34 'repo_changelog': '/{repo_name}/changelog',
36 35 'repo_commits': '/{repo_name}/commits',
37 36 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
38 37 'repo_commits_elements': '/{repo_name}/commits_elements',
39 38 }[name].format(**kwargs)
40 39
41 40 if params:
42 41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 42 return base_url
44 43
45 44
46 45 def assert_commits_on_page(response, indexes):
47 46 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.body)]
48 47 assert found_indexes == indexes
49 48
50 49
51 50 class TestChangelogController(TestController):
52 51
53 52 def test_commits_page(self, backend):
54 53 self.log_user()
55 54 response = self.app.get(
56 55 route_path('repo_commits', repo_name=backend.repo_name))
57 56
58 57 first_idx = -1
59 58 last_idx = -DEFAULT_CHANGELOG_SIZE
60 59 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
61 60
62 61 def test_changelog(self, backend):
63 62 self.log_user()
64 63 response = self.app.get(
65 64 route_path('repo_changelog', repo_name=backend.repo_name))
66 65
67 66 first_idx = -1
68 67 last_idx = -DEFAULT_CHANGELOG_SIZE
69 68 self.assert_commit_range_on_page(
70 69 response, first_idx, last_idx, backend)
71 70
72 71 @pytest.mark.backends("hg", "git")
73 72 def test_changelog_filtered_by_branch(self, backend):
74 73 self.log_user()
75 74 self.app.get(
76 75 route_path('repo_changelog', repo_name=backend.repo_name,
77 76 params=dict(branch=backend.default_branch_name)),
78 77 status=200)
79 78
80 79 @pytest.mark.backends("hg", "git")
81 80 def test_commits_filtered_by_branch(self, backend):
82 81 self.log_user()
83 82 self.app.get(
84 83 route_path('repo_commits', repo_name=backend.repo_name,
85 84 params=dict(branch=backend.default_branch_name)),
86 85 status=200)
87 86
88 87 @pytest.mark.backends("svn")
89 88 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
90 89 repo = backend['svn-simple-layout']
91 90 response = self.app.get(
92 91 route_path('repo_changelog', repo_name=repo.repo_name,
93 92 params=dict(branch='trunk')),
94 93 status=200)
95 94
96 95 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
97 96
98 97 def test_commits_filtered_by_wrong_branch(self, backend):
99 98 self.log_user()
100 99 branch = 'wrong-branch-name'
101 100 response = self.app.get(
102 101 route_path('repo_commits', repo_name=backend.repo_name,
103 102 params=dict(branch=branch)),
104 103 status=302)
105 104 expected_url = '/{repo}/commits/{branch}'.format(
106 105 repo=backend.repo_name, branch=branch)
107 106 assert expected_url in response.location
108 107 response = response.follow()
109 108 expected_warning = 'Branch {} is not found.'.format(branch)
110 109 assert expected_warning in response.text
111 110
112 111 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
113 112 def test_changelog_filtered_by_branch_with_merges(
114 113 self, autologin_user, backend):
115 114
116 115 # Note: The changelog of branch "b" does not contain the commit "a1"
117 116 # although this is a parent of commit "b1". And branch "b" has commits
118 117 # which have a smaller index than commit "a1".
119 118 commits = [
120 119 {'message': 'a'},
121 120 {'message': 'b', 'branch': 'b'},
122 121 {'message': 'a1', 'parents': ['a']},
123 122 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
124 123 ]
125 124 backend.create_repo(commits)
126 125
127 126 self.app.get(
128 127 route_path('repo_changelog', repo_name=backend.repo_name,
129 128 params=dict(branch='b')),
130 129 status=200)
131 130
132 131 @pytest.mark.backends("hg")
133 132 def test_commits_closed_branches(self, autologin_user, backend):
134 133 repo = backend['closed_branch']
135 134 response = self.app.get(
136 135 route_path('repo_commits', repo_name=repo.repo_name,
137 136 params=dict(branch='experimental')),
138 137 status=200)
139 138
140 139 assert_commits_on_page(response, indexes=[3, 1])
141 140
142 141 def test_changelog_pagination(self, backend):
143 142 self.log_user()
144 143 # pagination, walk up to page 6
145 144 changelog_url = route_path(
146 145 'repo_commits', repo_name=backend.repo_name)
147 146
148 147 for page in range(1, 7):
149 148 response = self.app.get(changelog_url, {'page': page})
150 149
151 150 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
152 151 last_idx = -DEFAULT_CHANGELOG_SIZE * page
153 152 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
154 153
155 154 def assert_commit_range_on_page(
156 155 self, response, first_idx, last_idx, backend):
157 156 input_template = (
158 157 """<input class="commit-range" """
159 158 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
160 159 """data-short-id="%(short_id)s" id="%(raw_id)s" """
161 160 """name="%(raw_id)s" type="checkbox" value="1" />"""
162 161 )
163 162
164 163 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
165 164 repo = backend.repo
166 165
167 166 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
168 167 response.mustcontain(
169 168 input_template % {'raw_id': first_commit_on_page.raw_id,
170 169 'idx': first_commit_on_page.idx,
171 170 'short_id': first_commit_on_page.short_id})
172 171
173 172 response.mustcontain(commit_span_template % (
174 173 first_commit_on_page.idx, first_commit_on_page.short_id)
175 174 )
176 175
177 176 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
178 177 response.mustcontain(
179 178 input_template % {'raw_id': last_commit_on_page.raw_id,
180 179 'idx': last_commit_on_page.idx,
181 180 'short_id': last_commit_on_page.short_id})
182 181 response.mustcontain(commit_span_template % (
183 182 last_commit_on_page.idx, last_commit_on_page.short_id)
184 183 )
185 184
186 185 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
187 186 first_span_of_next_page = commit_span_template % (
188 187 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
189 188 assert first_span_of_next_page not in response
190 189
191 190 @pytest.mark.parametrize('test_path', [
192 191 'vcs/exceptions.py',
193 192 '/vcs/exceptions.py',
194 193 '//vcs/exceptions.py'
195 194 ])
196 195 def test_commits_with_filenode(self, backend, test_path):
197 196 self.log_user()
198 197 response = self.app.get(
199 198 route_path('repo_commits_file', repo_name=backend.repo_name,
200 199 commit_id='tip', f_path=test_path),
201 200 )
202 201
203 202 # history commits messages
204 203 response.mustcontain('Added exceptions module, this time for real')
205 204 response.mustcontain('Added not implemented hg backend test case')
206 205 response.mustcontain('Added BaseChangeset class')
207 206
208 207 def test_commits_with_filenode_that_is_dirnode(self, backend):
209 208 self.log_user()
210 209 self.app.get(
211 210 route_path('repo_commits_file', repo_name=backend.repo_name,
212 211 commit_id='tip', f_path='/tests'),
213 212 status=302)
214 213
215 214 def test_commits_with_filenode_not_existing(self, backend):
216 215 self.log_user()
217 216 self.app.get(
218 217 route_path('repo_commits_file', repo_name=backend.repo_name,
219 218 commit_id='tip', f_path='wrong_path'),
220 219 status=302)
@@ -1,494 +1,493 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests import TestController
24 23
25 24 from rhodecode.model.db import ChangesetComment, Notification
26 25 from rhodecode.model.meta import Session
27 26 from rhodecode.lib import helpers as h
28 27
29 28
30 29 def route_path(name, params=None, **kwargs):
31 30 import urllib.request, urllib.parse, urllib.error
32 31
33 32 base_url = {
34 33 'repo_commit': '/{repo_name}/changeset/{commit_id}',
35 34 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 35 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 36 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 37 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
39 38 }[name].format(**kwargs)
40 39
41 40 if params:
42 41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 42 return base_url
44 43
45 44
46 45 @pytest.mark.backends("git", "hg", "svn")
47 46 class TestRepoCommitCommentsView(TestController):
48 47
49 48 @pytest.fixture(autouse=True)
50 49 def prepare(self, request, baseapp):
51 50 for x in ChangesetComment.query().all():
52 51 Session().delete(x)
53 52 Session().commit()
54 53
55 54 for x in Notification.query().all():
56 55 Session().delete(x)
57 56 Session().commit()
58 57
59 58 request.addfinalizer(self.cleanup)
60 59
61 60 def cleanup(self):
62 61 for x in ChangesetComment.query().all():
63 62 Session().delete(x)
64 63 Session().commit()
65 64
66 65 for x in Notification.query().all():
67 66 Session().delete(x)
68 67 Session().commit()
69 68
70 69 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 70 def test_create(self, comment_type, backend):
72 71 self.log_user()
73 72 commit = backend.repo.get_commit('300')
74 73 commit_id = commit.raw_id
75 74 text = u'CommentOnCommit'
76 75
77 76 params = {'text': text, 'csrf_token': self.csrf_token,
78 77 'comment_type': comment_type}
79 78 self.app.post(
80 79 route_path('repo_commit_comment_create',
81 80 repo_name=backend.repo_name, commit_id=commit_id),
82 81 params=params)
83 82
84 83 response = self.app.get(
85 84 route_path('repo_commit',
86 85 repo_name=backend.repo_name, commit_id=commit_id))
87 86
88 87 # test DB
89 88 assert ChangesetComment.query().count() == 1
90 89 assert_comment_links(response, ChangesetComment.query().count(), 0)
91 90
92 91 assert Notification.query().count() == 1
93 92 assert ChangesetComment.query().count() == 1
94 93
95 94 notification = Notification.query().all()[0]
96 95
97 96 comment_id = ChangesetComment.query().first().comment_id
98 97 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99 98
100 99 author = notification.created_by_user.username_and_name
101 100 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 101 author, comment_type, h.show_id(commit), backend.repo_name)
103 102 assert sbj == notification.subject
104 103
105 104 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 105 backend.repo_name, commit_id, comment_id))
107 106 assert lnk in notification.body
108 107
109 108 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 109 def test_create_inline(self, comment_type, backend):
111 110 self.log_user()
112 111 commit = backend.repo.get_commit('300')
113 112 commit_id = commit.raw_id
114 113 text = u'CommentOnCommit'
115 114 f_path = 'vcs/web/simplevcs/views/repository.py'
116 115 line = 'n1'
117 116
118 117 params = {'text': text, 'f_path': f_path, 'line': line,
119 118 'comment_type': comment_type,
120 119 'csrf_token': self.csrf_token}
121 120
122 121 self.app.post(
123 122 route_path('repo_commit_comment_create',
124 123 repo_name=backend.repo_name, commit_id=commit_id),
125 124 params=params)
126 125
127 126 response = self.app.get(
128 127 route_path('repo_commit',
129 128 repo_name=backend.repo_name, commit_id=commit_id))
130 129
131 130 # test DB
132 131 assert ChangesetComment.query().count() == 1
133 132 assert_comment_links(response, 0, ChangesetComment.query().count())
134 133
135 134 if backend.alias == 'svn':
136 135 response.mustcontain(
137 136 '''data-f-path="vcs/commands/summary.py" '''
138 137 '''data-anchor-id="c-300-ad05457a43f8"'''
139 138 )
140 139 if backend.alias == 'git':
141 140 response.mustcontain(
142 141 '''data-f-path="vcs/backends/hg.py" '''
143 142 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 143 )
145 144
146 145 if backend.alias == 'hg':
147 146 response.mustcontain(
148 147 '''data-f-path="vcs/backends/hg.py" '''
149 148 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 149 )
151 150
152 151 assert Notification.query().count() == 1
153 152 assert ChangesetComment.query().count() == 1
154 153
155 154 notification = Notification.query().all()[0]
156 155 comment = ChangesetComment.query().first()
157 156 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158 157
159 158 assert comment.revision == commit_id
160 159
161 160 author = notification.created_by_user.username_and_name
162 161 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 162 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164 163
165 164 assert sbj == notification.subject
166 165
167 166 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 167 backend.repo_name, commit_id, comment.comment_id))
169 168 assert lnk in notification.body
170 169 assert 'on line n1' in notification.body
171 170
172 171 def test_create_with_mention(self, backend):
173 172 self.log_user()
174 173
175 174 commit_id = backend.repo.get_commit('300').raw_id
176 175 text = u'@test_regular check CommentOnCommit'
177 176
178 177 params = {'text': text, 'csrf_token': self.csrf_token}
179 178 self.app.post(
180 179 route_path('repo_commit_comment_create',
181 180 repo_name=backend.repo_name, commit_id=commit_id),
182 181 params=params)
183 182
184 183 response = self.app.get(
185 184 route_path('repo_commit',
186 185 repo_name=backend.repo_name, commit_id=commit_id))
187 186 # test DB
188 187 assert ChangesetComment.query().count() == 1
189 188 assert_comment_links(response, ChangesetComment.query().count(), 0)
190 189
191 190 notification = Notification.query().one()
192 191
193 192 assert len(notification.recipients) == 2
194 193 users = [x.username for x in notification.recipients]
195 194
196 195 # test_regular gets notification by @mention
197 196 assert sorted(users) == [u'test_admin', u'test_regular']
198 197
199 198 def test_create_with_status_change(self, backend):
200 199 self.log_user()
201 200 commit = backend.repo.get_commit('300')
202 201 commit_id = commit.raw_id
203 202 text = u'CommentOnCommit'
204 203 f_path = 'vcs/web/simplevcs/views/repository.py'
205 204 line = 'n1'
206 205
207 206 params = {'text': text, 'changeset_status': 'approved',
208 207 'csrf_token': self.csrf_token}
209 208
210 209 self.app.post(
211 210 route_path(
212 211 'repo_commit_comment_create',
213 212 repo_name=backend.repo_name, commit_id=commit_id),
214 213 params=params)
215 214
216 215 response = self.app.get(
217 216 route_path('repo_commit',
218 217 repo_name=backend.repo_name, commit_id=commit_id))
219 218
220 219 # test DB
221 220 assert ChangesetComment.query().count() == 1
222 221 assert_comment_links(response, ChangesetComment.query().count(), 0)
223 222
224 223 assert Notification.query().count() == 1
225 224 assert ChangesetComment.query().count() == 1
226 225
227 226 notification = Notification.query().all()[0]
228 227
229 228 comment_id = ChangesetComment.query().first().comment_id
230 229 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231 230
232 231 author = notification.created_by_user.username_and_name
233 232 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 233 author, h.show_id(commit), backend.repo_name)
235 234 assert sbj == notification.subject
236 235
237 236 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 237 backend.repo_name, commit_id, comment_id))
239 238 assert lnk in notification.body
240 239
241 240 def test_delete(self, backend):
242 241 self.log_user()
243 242 commit_id = backend.repo.get_commit('300').raw_id
244 243 text = u'CommentOnCommit'
245 244
246 245 params = {'text': text, 'csrf_token': self.csrf_token}
247 246 self.app.post(
248 247 route_path(
249 248 'repo_commit_comment_create',
250 249 repo_name=backend.repo_name, commit_id=commit_id),
251 250 params=params)
252 251
253 252 comments = ChangesetComment.query().all()
254 253 assert len(comments) == 1
255 254 comment_id = comments[0].comment_id
256 255
257 256 self.app.post(
258 257 route_path('repo_commit_comment_delete',
259 258 repo_name=backend.repo_name,
260 259 commit_id=commit_id,
261 260 comment_id=comment_id),
262 261 params={'csrf_token': self.csrf_token})
263 262
264 263 comments = ChangesetComment.query().all()
265 264 assert len(comments) == 0
266 265
267 266 response = self.app.get(
268 267 route_path('repo_commit',
269 268 repo_name=backend.repo_name, commit_id=commit_id))
270 269 assert_comment_links(response, 0, 0)
271 270
272 271 def test_edit(self, backend):
273 272 self.log_user()
274 273 commit_id = backend.repo.get_commit('300').raw_id
275 274 text = u'CommentOnCommit'
276 275
277 276 params = {'text': text, 'csrf_token': self.csrf_token}
278 277 self.app.post(
279 278 route_path(
280 279 'repo_commit_comment_create',
281 280 repo_name=backend.repo_name, commit_id=commit_id),
282 281 params=params)
283 282
284 283 comments = ChangesetComment.query().all()
285 284 assert len(comments) == 1
286 285 comment_id = comments[0].comment_id
287 286 test_text = 'test_text'
288 287 self.app.post(
289 288 route_path(
290 289 'repo_commit_comment_edit',
291 290 repo_name=backend.repo_name,
292 291 commit_id=commit_id,
293 292 comment_id=comment_id,
294 293 ),
295 294 params={
296 295 'csrf_token': self.csrf_token,
297 296 'text': test_text,
298 297 'version': '0',
299 298 })
300 299
301 300 text_form_db = ChangesetComment.query().filter(
302 301 ChangesetComment.comment_id == comment_id).first().text
303 302 assert test_text == text_form_db
304 303
305 304 def test_edit_without_change(self, backend):
306 305 self.log_user()
307 306 commit_id = backend.repo.get_commit('300').raw_id
308 307 text = u'CommentOnCommit'
309 308
310 309 params = {'text': text, 'csrf_token': self.csrf_token}
311 310 self.app.post(
312 311 route_path(
313 312 'repo_commit_comment_create',
314 313 repo_name=backend.repo_name, commit_id=commit_id),
315 314 params=params)
316 315
317 316 comments = ChangesetComment.query().all()
318 317 assert len(comments) == 1
319 318 comment_id = comments[0].comment_id
320 319
321 320 response = self.app.post(
322 321 route_path(
323 322 'repo_commit_comment_edit',
324 323 repo_name=backend.repo_name,
325 324 commit_id=commit_id,
326 325 comment_id=comment_id,
327 326 ),
328 327 params={
329 328 'csrf_token': self.csrf_token,
330 329 'text': text,
331 330 'version': '0',
332 331 },
333 332 status=404,
334 333 )
335 334 assert response.status_int == 404
336 335
337 336 def test_edit_try_edit_already_edited(self, backend):
338 337 self.log_user()
339 338 commit_id = backend.repo.get_commit('300').raw_id
340 339 text = u'CommentOnCommit'
341 340
342 341 params = {'text': text, 'csrf_token': self.csrf_token}
343 342 self.app.post(
344 343 route_path(
345 344 'repo_commit_comment_create',
346 345 repo_name=backend.repo_name, commit_id=commit_id
347 346 ),
348 347 params=params,
349 348 )
350 349
351 350 comments = ChangesetComment.query().all()
352 351 assert len(comments) == 1
353 352 comment_id = comments[0].comment_id
354 353 test_text = 'test_text'
355 354 self.app.post(
356 355 route_path(
357 356 'repo_commit_comment_edit',
358 357 repo_name=backend.repo_name,
359 358 commit_id=commit_id,
360 359 comment_id=comment_id,
361 360 ),
362 361 params={
363 362 'csrf_token': self.csrf_token,
364 363 'text': test_text,
365 364 'version': '0',
366 365 }
367 366 )
368 367 test_text_v2 = 'test_v2'
369 368 response = self.app.post(
370 369 route_path(
371 370 'repo_commit_comment_edit',
372 371 repo_name=backend.repo_name,
373 372 commit_id=commit_id,
374 373 comment_id=comment_id,
375 374 ),
376 375 params={
377 376 'csrf_token': self.csrf_token,
378 377 'text': test_text_v2,
379 378 'version': '0',
380 379 },
381 380 status=409,
382 381 )
383 382 assert response.status_int == 409
384 383
385 384 text_form_db = ChangesetComment.query().filter(
386 385 ChangesetComment.comment_id == comment_id).first().text
387 386
388 387 assert test_text == text_form_db
389 388 assert test_text_v2 != text_form_db
390 389
391 390 def test_edit_forbidden_for_immutable_comments(self, backend):
392 391 self.log_user()
393 392 commit_id = backend.repo.get_commit('300').raw_id
394 393 text = u'CommentOnCommit'
395 394
396 395 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 396 self.app.post(
398 397 route_path(
399 398 'repo_commit_comment_create',
400 399 repo_name=backend.repo_name,
401 400 commit_id=commit_id,
402 401 ),
403 402 params=params
404 403 )
405 404
406 405 comments = ChangesetComment.query().all()
407 406 assert len(comments) == 1
408 407 comment_id = comments[0].comment_id
409 408
410 409 comment = ChangesetComment.get(comment_id)
411 410 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 411 Session().add(comment)
413 412 Session().commit()
414 413
415 414 response = self.app.post(
416 415 route_path(
417 416 'repo_commit_comment_edit',
418 417 repo_name=backend.repo_name,
419 418 commit_id=commit_id,
420 419 comment_id=comment_id,
421 420 ),
422 421 params={
423 422 'csrf_token': self.csrf_token,
424 423 'text': 'test_text',
425 424 },
426 425 status=403,
427 426 )
428 427 assert response.status_int == 403
429 428
430 429 def test_delete_forbidden_for_immutable_comments(self, backend):
431 430 self.log_user()
432 431 commit_id = backend.repo.get_commit('300').raw_id
433 432 text = u'CommentOnCommit'
434 433
435 434 params = {'text': text, 'csrf_token': self.csrf_token}
436 435 self.app.post(
437 436 route_path(
438 437 'repo_commit_comment_create',
439 438 repo_name=backend.repo_name, commit_id=commit_id),
440 439 params=params)
441 440
442 441 comments = ChangesetComment.query().all()
443 442 assert len(comments) == 1
444 443 comment_id = comments[0].comment_id
445 444
446 445 comment = ChangesetComment.get(comment_id)
447 446 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
448 447 Session().add(comment)
449 448 Session().commit()
450 449
451 450 self.app.post(
452 451 route_path('repo_commit_comment_delete',
453 452 repo_name=backend.repo_name,
454 453 commit_id=commit_id,
455 454 comment_id=comment_id),
456 455 params={'csrf_token': self.csrf_token},
457 456 status=403)
458 457
459 458 @pytest.mark.parametrize('renderer, text_input, output', [
460 459 ('rst', 'plain text', '<p>plain text</p>'),
461 460 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
462 461 ('rst', '*italics*', '<em>italics</em>'),
463 462 ('rst', '**bold**', '<strong>bold</strong>'),
464 463 ('markdown', 'plain text', '<p>plain text</p>'),
465 464 ('markdown', '# header', '<h1>header</h1>'),
466 465 ('markdown', '*italics*', '<em>italics</em>'),
467 466 ('markdown', '**bold**', '<strong>bold</strong>'),
468 467 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
469 468 'md-header', 'md-italics', 'md-bold', ])
470 469 def test_preview(self, renderer, text_input, output, backend, xhr_header):
471 470 self.log_user()
472 471 params = {
473 472 'renderer': renderer,
474 473 'text': text_input,
475 474 'csrf_token': self.csrf_token
476 475 }
477 476 commit_id = '0' * 16 # fake this for tests
478 477 response = self.app.post(
479 478 route_path('repo_commit_comment_preview',
480 479 repo_name=backend.repo_name, commit_id=commit_id,),
481 480 params=params,
482 481 extra_environ=xhr_header)
483 482
484 483 response.mustcontain(output)
485 484
486 485
487 486 def assert_comment_links(response, comments, inline_comments):
488 487 response.mustcontain(
489 488 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 489 response.mustcontain(
491 490 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
492 491
493 492
494 493
@@ -1,327 +1,326 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
24 23 from rhodecode.lib.helpers import _shorten_commit_id
25 24
26 25
27 26 def route_path(name, params=None, **kwargs):
28 27 import urllib.request, urllib.parse, urllib.error
29 28
30 29 base_url = {
31 30 'repo_commit': '/{repo_name}/changeset/{commit_id}',
32 31 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
33 32 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
34 33 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
35 34 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
36 35 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
37 36 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
38 37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
39 38 }[name].format(**kwargs)
40 39
41 40 if params:
42 41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 42 return base_url
44 43
45 44
46 45 @pytest.mark.usefixtures("app")
47 46 class TestRepoCommitView(object):
48 47
49 48 def test_show_commit(self, backend):
50 49 commit_id = self.commit_id[backend.alias]
51 50 response = self.app.get(route_path(
52 51 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
53 52 response.mustcontain('Added a symlink')
54 53 response.mustcontain(commit_id)
55 54 response.mustcontain('No newline at end of file')
56 55
57 56 def test_show_raw(self, backend):
58 57 commit_id = self.commit_id[backend.alias]
59 58 response = self.app.get(route_path(
60 59 'repo_commit_raw',
61 60 repo_name=backend.repo_name, commit_id=commit_id))
62 61 assert response.text == self.diffs[backend.alias]
63 62
64 63 def test_show_raw_patch(self, backend):
65 64 response = self.app.get(route_path(
66 65 'repo_commit_patch', repo_name=backend.repo_name,
67 66 commit_id=self.commit_id[backend.alias]))
68 67 assert response.text == self.patches[backend.alias]
69 68
70 69 def test_commit_download(self, backend):
71 70 response = self.app.get(route_path(
72 71 'repo_commit_download',
73 72 repo_name=backend.repo_name,
74 73 commit_id=self.commit_id[backend.alias]))
75 74 assert response.text == self.diffs[backend.alias]
76 75
77 76 def test_single_commit_page_different_ops(self, backend):
78 77 commit_id = {
79 78 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
80 79 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
81 80 'svn': '337',
82 81 }
83 82 diff_stat = {
84 83 'hg': (21, 943, 288),
85 84 'git': (20, 941, 286),
86 85 'svn': (21, 943, 288),
87 86 }
88 87
89 88 commit_id = commit_id[backend.alias]
90 89 response = self.app.get(route_path(
91 90 'repo_commit',
92 91 repo_name=backend.repo_name, commit_id=commit_id))
93 92
94 93 response.mustcontain(_shorten_commit_id(commit_id))
95 94
96 95 compare_page = ComparePage(response)
97 96 file_changes = diff_stat[backend.alias]
98 97 compare_page.contains_change_summary(*file_changes)
99 98
100 99 # files op files
101 100 response.mustcontain('File not present at commit: %s' %
102 101 _shorten_commit_id(commit_id))
103 102
104 103 # svn uses a different filename
105 104 if backend.alias == 'svn':
106 105 response.mustcontain('new file 10644')
107 106 else:
108 107 response.mustcontain('new file 100644')
109 108 response.mustcontain('Changed theme to ADC theme') # commit msg
110 109
111 110 self._check_new_diff_menus(response, right_menu=True)
112 111
113 112 def test_commit_range_page_different_ops(self, backend):
114 113 commit_id_range = {
115 114 'hg': (
116 115 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
117 116 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
118 117 'git': (
119 118 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
120 119 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
121 120 'svn': (
122 121 '335',
123 122 '337'),
124 123 }
125 124 commit_ids = commit_id_range[backend.alias]
126 125 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
127 126 response = self.app.get(route_path(
128 127 'repo_commit',
129 128 repo_name=backend.repo_name, commit_id=commit_id))
130 129
131 130 response.mustcontain(_shorten_commit_id(commit_ids[0]))
132 131 response.mustcontain(_shorten_commit_id(commit_ids[1]))
133 132
134 133 compare_page = ComparePage(response)
135 134
136 135 # svn is special
137 136 if backend.alias == 'svn':
138 137 response.mustcontain('new file 10644')
139 138 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
140 139 compare_page.contains_change_summary(*file_changes)
141 140 elif backend.alias == 'git':
142 141 response.mustcontain('new file 100644')
143 142 for file_changes in [(12, 222, 20), (20, 941, 286)]:
144 143 compare_page.contains_change_summary(*file_changes)
145 144 else:
146 145 response.mustcontain('new file 100644')
147 146 for file_changes in [(12, 222, 20), (21, 943, 288)]:
148 147 compare_page.contains_change_summary(*file_changes)
149 148
150 149 # files op files
151 150 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
152 151 response.mustcontain('Added docstrings to vcs.cli') # commit msg
153 152 response.mustcontain('Changed theme to ADC theme') # commit msg
154 153
155 154 self._check_new_diff_menus(response)
156 155
157 156 def test_combined_compare_commit_page_different_ops(self, backend):
158 157 commit_id_range = {
159 158 'hg': (
160 159 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
161 160 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
162 161 'git': (
163 162 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
164 163 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
165 164 'svn': (
166 165 '335',
167 166 '337'),
168 167 }
169 168 commit_ids = commit_id_range[backend.alias]
170 169 response = self.app.get(route_path(
171 170 'repo_compare',
172 171 repo_name=backend.repo_name,
173 172 source_ref_type='rev', source_ref=commit_ids[0],
174 173 target_ref_type='rev', target_ref=commit_ids[1], ))
175 174
176 175 response.mustcontain(_shorten_commit_id(commit_ids[0]))
177 176 response.mustcontain(_shorten_commit_id(commit_ids[1]))
178 177
179 178 # files op files
180 179 response.mustcontain('File not present at commit: %s' %
181 180 _shorten_commit_id(commit_ids[1]))
182 181
183 182 compare_page = ComparePage(response)
184 183
185 184 # svn is special
186 185 if backend.alias == 'svn':
187 186 response.mustcontain('new file 10644')
188 187 file_changes = (32, 1179, 310)
189 188 compare_page.contains_change_summary(*file_changes)
190 189 elif backend.alias == 'git':
191 190 response.mustcontain('new file 100644')
192 191 file_changes = (31, 1163, 306)
193 192 compare_page.contains_change_summary(*file_changes)
194 193 else:
195 194 response.mustcontain('new file 100644')
196 195 file_changes = (32, 1165, 308)
197 196 compare_page.contains_change_summary(*file_changes)
198 197
199 198 response.mustcontain('Added docstrings to vcs.cli') # commit msg
200 199 response.mustcontain('Changed theme to ADC theme') # commit msg
201 200
202 201 self._check_new_diff_menus(response)
203 202
204 203 def test_changeset_range(self, backend):
205 204 self._check_changeset_range(
206 205 backend, self.commit_id_range, self.commit_id_range_result)
207 206
208 207 def test_changeset_range_with_initial_commit(self, backend):
209 208 commit_id_range = {
210 209 'hg': (
211 210 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
212 211 '...6cba7170863a2411822803fa77a0a264f1310b35'),
213 212 'git': (
214 213 'c1214f7e79e02fc37156ff215cd71275450cffc3'
215 214 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
216 215 'svn': '1...3',
217 216 }
218 217 commit_id_range_result = {
219 218 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
220 219 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
221 220 'svn': ['1', '2', '3'],
222 221 }
223 222 self._check_changeset_range(
224 223 backend, commit_id_range, commit_id_range_result)
225 224
226 225 def _check_changeset_range(
227 226 self, backend, commit_id_ranges, commit_id_range_result):
228 227 response = self.app.get(
229 228 route_path('repo_commit',
230 229 repo_name=backend.repo_name,
231 230 commit_id=commit_id_ranges[backend.alias]))
232 231
233 232 expected_result = commit_id_range_result[backend.alias]
234 233 response.mustcontain('{} commits'.format(len(expected_result)))
235 234 for commit_id in expected_result:
236 235 response.mustcontain(commit_id)
237 236
238 237 commit_id = {
239 238 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
240 239 'svn': '393',
241 240 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
242 241 }
243 242
244 243 commit_id_range = {
245 244 'hg': (
246 245 'a53d9201d4bc278910d416d94941b7ea007ecd52'
247 246 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
248 247 'git': (
249 248 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
250 249 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
251 250 'svn': '391...393',
252 251 }
253 252
254 253 commit_id_range_result = {
255 254 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
256 255 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
257 256 'svn': ['391', '392', '393'],
258 257 }
259 258
260 259 diffs = {
261 260 'hg': r"""diff --git a/README b/README
262 261 new file mode 120000
263 262 --- /dev/null
264 263 +++ b/README
265 264 @@ -0,0 +1,1 @@
266 265 +README.rst
267 266 \ No newline at end of file
268 267 """,
269 268 'git': r"""diff --git a/README b/README
270 269 new file mode 120000
271 270 index 0000000..92cacd2
272 271 --- /dev/null
273 272 +++ b/README
274 273 @@ -0,0 +1 @@
275 274 +README.rst
276 275 \ No newline at end of file
277 276 """,
278 277 'svn': """Index: README
279 278 ===================================================================
280 279 diff --git a/README b/README
281 280 new file mode 10644
282 281 --- /dev/null\t(revision 0)
283 282 +++ b/README\t(revision 393)
284 283 @@ -0,0 +1 @@
285 284 +link README.rst
286 285 \\ No newline at end of file
287 286 """,
288 287 }
289 288
290 289 patches = {
291 290 'hg': r"""# HG changeset patch
292 291 # User Marcin Kuzminski <marcin@python-works.com>
293 292 # Date 2014-01-07 12:21:40
294 293 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
295 294 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
296 295
297 296 Added a symlink
298 297
299 298 """ + diffs['hg'],
300 299 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
301 300 From: Marcin Kuzminski <marcin@python-works.com>
302 301 Date: 2014-01-07 12:22:20
303 302 Subject: [PATCH] Added a symlink
304 303
305 304 ---
306 305
307 306 """ + diffs['git'],
308 307 'svn': r"""# SVN changeset patch
309 308 # User marcin
310 309 # Date 2014-09-02 12:25:22.071142
311 310 # Revision 393
312 311
313 312 Added a symlink
314 313
315 314 """ + diffs['svn'],
316 315 }
317 316
318 317 def _check_new_diff_menus(self, response, right_menu=False,):
319 318 # individual file diff menus
320 319 for elem in ['Show file before', 'Show file after']:
321 320 response.mustcontain(elem)
322 321
323 322 # right pane diff menus
324 323 if right_menu:
325 324 for elem in ['Hide whitespace changes', 'Toggle wide diff',
326 325 'Show full context diff']:
327 326 response.mustcontain(elem)
@@ -1,672 +1,671 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import mock
22 21 import pytest
23 22 import lxml.html
24 23
25 24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 25 from rhodecode.tests import assert_session_flash
27 26 from rhodecode.tests.utils import AssertResponse, commit_change
28 27
29 28
30 29 def route_path(name, params=None, **kwargs):
31 30 import urllib.request, urllib.parse, urllib.error
32 31
33 32 base_url = {
34 33 'repo_compare_select': '/{repo_name}/compare',
35 34 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 35 }[name].format(**kwargs)
37 36
38 37 if params:
39 38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 39 return base_url
41 40
42 41
43 42 @pytest.mark.usefixtures("autologin_user", "app")
44 43 class TestCompareView(object):
45 44
46 45 def test_compare_index_is_reached_at_least_once(self, backend):
47 46 repo = backend.repo
48 47 self.app.get(
49 48 route_path('repo_compare_select', repo_name=repo.repo_name))
50 49
51 50 @pytest.mark.xfail_backends("svn", reason="Requires pull")
52 51 def test_compare_remote_with_different_commit_indexes(self, backend):
53 52 # Preparing the following repository structure:
54 53 #
55 54 # Origin repository has two commits:
56 55 #
57 56 # 0 1
58 57 # A -- D
59 58 #
60 59 # The fork of it has a few more commits and "D" has a commit index
61 60 # which does not exist in origin.
62 61 #
63 62 # 0 1 2 3 4
64 63 # A -- -- -- D -- E
65 64 # \- B -- C
66 65 #
67 66
68 67 fork = backend.create_repo()
69 68
70 69 # prepare fork
71 70 commit0 = commit_change(
72 71 fork.repo_name, filename='file1', content='A',
73 72 message='A', vcs_type=backend.alias, parent=None, newfile=True)
74 73
75 74 commit1 = commit_change(
76 75 fork.repo_name, filename='file1', content='B',
77 76 message='B, child of A', vcs_type=backend.alias, parent=commit0)
78 77
79 78 commit_change( # commit 2
80 79 fork.repo_name, filename='file1', content='C',
81 80 message='C, child of B', vcs_type=backend.alias, parent=commit1)
82 81
83 82 commit3 = commit_change(
84 83 fork.repo_name, filename='file1', content='D',
85 84 message='D, child of A', vcs_type=backend.alias, parent=commit0)
86 85
87 86 commit4 = commit_change(
88 87 fork.repo_name, filename='file1', content='E',
89 88 message='E, child of D', vcs_type=backend.alias, parent=commit3)
90 89
91 90 # prepare origin repository, taking just the history up to D
92 91 origin = backend.create_repo()
93 92
94 93 origin_repo = origin.scm_instance(cache=False)
95 94 origin_repo.config.clear_section('hooks')
96 95 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
97 96 origin_repo = origin.scm_instance(cache=False) # cache rebuild
98 97
99 98 # Verify test fixture setup
100 99 # This does not work for git
101 100 if backend.alias != 'git':
102 101 assert 5 == len(fork.scm_instance().commit_ids)
103 102 assert 2 == len(origin_repo.commit_ids)
104 103
105 104 # Comparing the revisions
106 105 response = self.app.get(
107 106 route_path('repo_compare',
108 107 repo_name=origin.repo_name,
109 108 source_ref_type="rev", source_ref=commit3.raw_id,
110 109 target_ref_type="rev", target_ref=commit4.raw_id,
111 110 params=dict(merge='1', target_repo=fork.repo_name)
112 111 ))
113 112
114 113 compare_page = ComparePage(response)
115 114 compare_page.contains_commits([commit4])
116 115
117 116 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
118 117 def test_compare_forks_on_branch_extra_commits(self, backend):
119 118 repo1 = backend.create_repo()
120 119
121 120 # commit something !
122 121 commit0 = commit_change(
123 122 repo1.repo_name, filename='file1', content='line1\n',
124 123 message='commit1', vcs_type=backend.alias, parent=None,
125 124 newfile=True)
126 125
127 126 # fork this repo
128 127 repo2 = backend.create_fork()
129 128
130 129 # add two extra commit into fork
131 130 commit1 = commit_change(
132 131 repo2.repo_name, filename='file1', content='line1\nline2\n',
133 132 message='commit2', vcs_type=backend.alias, parent=commit0)
134 133
135 134 commit2 = commit_change(
136 135 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
137 136 message='commit3', vcs_type=backend.alias, parent=commit1)
138 137
139 138 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
140 139 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
141 140
142 141 response = self.app.get(
143 142 route_path('repo_compare',
144 143 repo_name=repo1.repo_name,
145 144 source_ref_type="branch", source_ref=commit_id2,
146 145 target_ref_type="branch", target_ref=commit_id1,
147 146 params=dict(merge='1', target_repo=repo2.repo_name)
148 147 ))
149 148
150 149 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
151 150 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
152 151
153 152 compare_page = ComparePage(response)
154 153 compare_page.contains_change_summary(1, 2, 0)
155 154 compare_page.contains_commits([commit1, commit2])
156 155
157 156 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
158 157 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
159 158
160 159 # Swap is removed when comparing branches since it's a PR feature and
161 160 # it is then a preview mode
162 161 compare_page.swap_is_hidden()
163 162 compare_page.target_source_are_disabled()
164 163
165 164 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
166 165 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
167 166 repo1 = backend.create_repo()
168 167
169 168 # commit something !
170 169 commit0 = commit_change(
171 170 repo1.repo_name, filename='file1', content='line1\n',
172 171 message='commit1', vcs_type=backend.alias, parent=None,
173 172 newfile=True)
174 173
175 174 # fork this repo
176 175 repo2 = backend.create_fork()
177 176
178 177 # now commit something to origin repo
179 178 commit_change(
180 179 repo1.repo_name, filename='file2', content='line1file2\n',
181 180 message='commit2', vcs_type=backend.alias, parent=commit0,
182 181 newfile=True)
183 182
184 183 # add two extra commit into fork
185 184 commit1 = commit_change(
186 185 repo2.repo_name, filename='file1', content='line1\nline2\n',
187 186 message='commit2', vcs_type=backend.alias, parent=commit0)
188 187
189 188 commit2 = commit_change(
190 189 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
191 190 message='commit3', vcs_type=backend.alias, parent=commit1)
192 191
193 192 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
194 193 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
195 194
196 195 response = self.app.get(
197 196 route_path('repo_compare',
198 197 repo_name=repo1.repo_name,
199 198 source_ref_type="branch", source_ref=commit_id2,
200 199 target_ref_type="branch", target_ref=commit_id1,
201 200 params=dict(merge='1', target_repo=repo2.repo_name),
202 201 ))
203 202
204 203 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
205 204 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
206 205
207 206 compare_page = ComparePage(response)
208 207 compare_page.contains_change_summary(1, 2, 0)
209 208 compare_page.contains_commits([commit1, commit2])
210 209 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
211 210 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
212 211
213 212 # Swap is removed when comparing branches since it's a PR feature and
214 213 # it is then a preview mode
215 214 compare_page.swap_is_hidden()
216 215 compare_page.target_source_are_disabled()
217 216
218 217 @pytest.mark.xfail_backends("svn")
219 218 # TODO(marcink): no svn support for compare two seperate repos
220 219 def test_compare_of_unrelated_forks(self, backend):
221 220 orig = backend.create_repo(number_of_commits=1)
222 221 fork = backend.create_repo(number_of_commits=1)
223 222
224 223 response = self.app.get(
225 224 route_path('repo_compare',
226 225 repo_name=orig.repo_name,
227 226 source_ref_type="rev", source_ref="tip",
228 227 target_ref_type="rev", target_ref="tip",
229 228 params=dict(merge='1', target_repo=fork.repo_name),
230 229 ),
231 230 status=302)
232 231 response = response.follow()
233 232 response.mustcontain("Repositories unrelated.")
234 233
235 234 @pytest.mark.xfail_backends("svn")
236 235 def test_compare_cherry_pick_commits_from_bottom(self, backend):
237 236
238 237 # repo1:
239 238 # commit0:
240 239 # commit1:
241 240 # repo1-fork- in which we will cherry pick bottom commits
242 241 # commit0:
243 242 # commit1:
244 243 # commit2: x
245 244 # commit3: x
246 245 # commit4: x
247 246 # commit5:
248 247 # make repo1, and commit1+commit2
249 248
250 249 repo1 = backend.create_repo()
251 250
252 251 # commit something !
253 252 commit0 = commit_change(
254 253 repo1.repo_name, filename='file1', content='line1\n',
255 254 message='commit1', vcs_type=backend.alias, parent=None,
256 255 newfile=True)
257 256 commit1 = commit_change(
258 257 repo1.repo_name, filename='file1', content='line1\nline2\n',
259 258 message='commit2', vcs_type=backend.alias, parent=commit0)
260 259
261 260 # fork this repo
262 261 repo2 = backend.create_fork()
263 262
264 263 # now make commit3-6
265 264 commit2 = commit_change(
266 265 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
267 266 message='commit3', vcs_type=backend.alias, parent=commit1)
268 267 commit3 = commit_change(
269 268 repo1.repo_name, filename='file1',
270 269 content='line1\nline2\nline3\nline4\n', message='commit4',
271 270 vcs_type=backend.alias, parent=commit2)
272 271 commit4 = commit_change(
273 272 repo1.repo_name, filename='file1',
274 273 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
275 274 vcs_type=backend.alias, parent=commit3)
276 275 commit_change( # commit 5
277 276 repo1.repo_name, filename='file1',
278 277 content='line1\nline2\nline3\nline4\nline5\nline6\n',
279 278 message='commit6', vcs_type=backend.alias, parent=commit4)
280 279
281 280 response = self.app.get(
282 281 route_path('repo_compare',
283 282 repo_name=repo2.repo_name,
284 283 # parent of commit2, in target repo2
285 284 source_ref_type="rev", source_ref=commit1.raw_id,
286 285 target_ref_type="rev", target_ref=commit4.raw_id,
287 286 params=dict(merge='1', target_repo=repo1.repo_name),
288 287 ))
289 288 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
290 289 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
291 290
292 291 # files
293 292 compare_page = ComparePage(response)
294 293 compare_page.contains_change_summary(1, 3, 0)
295 294 compare_page.contains_commits([commit2, commit3, commit4])
296 295 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
297 296 compare_page.contains_file_links_and_anchors([('file1', anchor),])
298 297
299 298 @pytest.mark.xfail_backends("svn")
300 299 def test_compare_cherry_pick_commits_from_top(self, backend):
301 300 # repo1:
302 301 # commit0:
303 302 # commit1:
304 303 # repo1-fork- in which we will cherry pick bottom commits
305 304 # commit0:
306 305 # commit1:
307 306 # commit2:
308 307 # commit3: x
309 308 # commit4: x
310 309 # commit5: x
311 310
312 311 # make repo1, and commit1+commit2
313 312 repo1 = backend.create_repo()
314 313
315 314 # commit something !
316 315 commit0 = commit_change(
317 316 repo1.repo_name, filename='file1', content='line1\n',
318 317 message='commit1', vcs_type=backend.alias, parent=None,
319 318 newfile=True)
320 319 commit1 = commit_change(
321 320 repo1.repo_name, filename='file1', content='line1\nline2\n',
322 321 message='commit2', vcs_type=backend.alias, parent=commit0)
323 322
324 323 # fork this repo
325 324 backend.create_fork()
326 325
327 326 # now make commit3-6
328 327 commit2 = commit_change(
329 328 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
330 329 message='commit3', vcs_type=backend.alias, parent=commit1)
331 330 commit3 = commit_change(
332 331 repo1.repo_name, filename='file1',
333 332 content='line1\nline2\nline3\nline4\n', message='commit4',
334 333 vcs_type=backend.alias, parent=commit2)
335 334 commit4 = commit_change(
336 335 repo1.repo_name, filename='file1',
337 336 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
338 337 vcs_type=backend.alias, parent=commit3)
339 338 commit5 = commit_change(
340 339 repo1.repo_name, filename='file1',
341 340 content='line1\nline2\nline3\nline4\nline5\nline6\n',
342 341 message='commit6', vcs_type=backend.alias, parent=commit4)
343 342
344 343 response = self.app.get(
345 344 route_path('repo_compare',
346 345 repo_name=repo1.repo_name,
347 346 # parent of commit3, not in source repo2
348 347 source_ref_type="rev", source_ref=commit2.raw_id,
349 348 target_ref_type="rev", target_ref=commit5.raw_id,
350 349 params=dict(merge='1'),))
351 350
352 351 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
353 352 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
354 353
355 354 compare_page = ComparePage(response)
356 355 compare_page.contains_change_summary(1, 3, 0)
357 356 compare_page.contains_commits([commit3, commit4, commit5])
358 357
359 358 # files
360 359 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
361 360 compare_page.contains_file_links_and_anchors([('file1', anchor),])
362 361
363 362 @pytest.mark.xfail_backends("svn")
364 363 def test_compare_remote_branches(self, backend):
365 364 repo1 = backend.repo
366 365 repo2 = backend.create_fork()
367 366
368 367 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
369 368 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
370 369 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
371 370 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
372 371
373 372 response = self.app.get(
374 373 route_path('repo_compare',
375 374 repo_name=repo1.repo_name,
376 375 source_ref_type="rev", source_ref=commit_id1,
377 376 target_ref_type="rev", target_ref=commit_id2,
378 377 params=dict(merge='1', target_repo=repo2.repo_name),
379 378 ))
380 379
381 380 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
382 381 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
383 382
384 383 compare_page = ComparePage(response)
385 384
386 385 # outgoing commits between those commits
387 386 compare_page.contains_commits(
388 387 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
389 388
390 389 # files
391 390 compare_page.contains_file_links_and_anchors([
392 391 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
393 392 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
394 393 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
395 394 ])
396 395
397 396 @pytest.mark.xfail_backends("svn")
398 397 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
399 398 repo1 = backend.create_repo()
400 399 r1_name = repo1.repo_name
401 400
402 401 commit0 = commit_change(
403 402 repo=r1_name, filename='file1',
404 403 content='line1', message='commit1', vcs_type=backend.alias,
405 404 newfile=True)
406 405 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
407 406
408 407 # fork the repo1
409 408 repo2 = backend.create_fork()
410 409 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
411 410
412 411 self.r2_id = repo2.repo_id
413 412 r2_name = repo2.repo_name
414 413
415 414 commit1 = commit_change(
416 415 repo=r2_name, filename='file1-fork',
417 416 content='file1-line1-from-fork', message='commit1-fork',
418 417 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
419 418 newfile=True)
420 419
421 420 commit2 = commit_change(
422 421 repo=r2_name, filename='file2-fork',
423 422 content='file2-line1-from-fork', message='commit2-fork',
424 423 vcs_type=backend.alias, parent=commit1,
425 424 newfile=True)
426 425
427 426 commit_change( # commit 3
428 427 repo=r2_name, filename='file3-fork',
429 428 content='file3-line1-from-fork', message='commit3-fork',
430 429 vcs_type=backend.alias, parent=commit2, newfile=True)
431 430
432 431 # compare !
433 432 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
434 433 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
435 434
436 435 response = self.app.get(
437 436 route_path('repo_compare',
438 437 repo_name=r2_name,
439 438 source_ref_type="branch", source_ref=commit_id1,
440 439 target_ref_type="branch", target_ref=commit_id2,
441 440 params=dict(merge='1', target_repo=r1_name),
442 441 ))
443 442
444 443 response.mustcontain('%s@%s' % (r2_name, commit_id1))
445 444 response.mustcontain('%s@%s' % (r1_name, commit_id2))
446 445 response.mustcontain('No files')
447 446 response.mustcontain('No commits in this compare')
448 447
449 448 commit0 = commit_change(
450 449 repo=r1_name, filename='file2',
451 450 content='line1-added-after-fork', message='commit2-parent',
452 451 vcs_type=backend.alias, parent=None, newfile=True)
453 452
454 453 # compare !
455 454 response = self.app.get(
456 455 route_path('repo_compare',
457 456 repo_name=r2_name,
458 457 source_ref_type="branch", source_ref=commit_id1,
459 458 target_ref_type="branch", target_ref=commit_id2,
460 459 params=dict(merge='1', target_repo=r1_name),
461 460 ))
462 461
463 462 response.mustcontain('%s@%s' % (r2_name, commit_id1))
464 463 response.mustcontain('%s@%s' % (r1_name, commit_id2))
465 464
466 465 response.mustcontain("""commit2-parent""")
467 466 response.mustcontain("""line1-added-after-fork""")
468 467 compare_page = ComparePage(response)
469 468 compare_page.contains_change_summary(1, 1, 0)
470 469
471 470 @pytest.mark.xfail_backends("svn")
472 471 def test_compare_commits(self, backend, xhr_header):
473 472 commit0 = backend.repo.get_commit(commit_idx=0)
474 473 commit1 = backend.repo.get_commit(commit_idx=1)
475 474
476 475 response = self.app.get(
477 476 route_path('repo_compare',
478 477 repo_name=backend.repo_name,
479 478 source_ref_type="rev", source_ref=commit0.raw_id,
480 479 target_ref_type="rev", target_ref=commit1.raw_id,
481 480 params=dict(merge='1')
482 481 ),
483 482 extra_environ=xhr_header, )
484 483
485 484 # outgoing commits between those commits
486 485 compare_page = ComparePage(response)
487 486 compare_page.contains_commits(commits=[commit1])
488 487
489 488 def test_errors_when_comparing_unknown_source_repo(self, backend):
490 489 repo = backend.repo
491 490 badrepo = 'badrepo'
492 491
493 492 response = self.app.get(
494 493 route_path('repo_compare',
495 494 repo_name=badrepo,
496 495 source_ref_type="rev", source_ref='tip',
497 496 target_ref_type="rev", target_ref='tip',
498 497 params=dict(merge='1', target_repo=repo.repo_name)
499 498 ),
500 499 status=404)
501 500
502 501 def test_errors_when_comparing_unknown_target_repo(self, backend):
503 502 repo = backend.repo
504 503 badrepo = 'badrepo'
505 504
506 505 response = self.app.get(
507 506 route_path('repo_compare',
508 507 repo_name=repo.repo_name,
509 508 source_ref_type="rev", source_ref='tip',
510 509 target_ref_type="rev", target_ref='tip',
511 510 params=dict(merge='1', target_repo=badrepo),
512 511 ),
513 512 status=302)
514 513 redirected = response.follow()
515 514 redirected.mustcontain(
516 515 'Could not find the target repo: `{}`'.format(badrepo))
517 516
518 517 def test_compare_not_in_preview_mode(self, backend_stub):
519 518 commit0 = backend_stub.repo.get_commit(commit_idx=0)
520 519 commit1 = backend_stub.repo.get_commit(commit_idx=1)
521 520
522 521 response = self.app.get(
523 522 route_path('repo_compare',
524 523 repo_name=backend_stub.repo_name,
525 524 source_ref_type="rev", source_ref=commit0.raw_id,
526 525 target_ref_type="rev", target_ref=commit1.raw_id,
527 526 ))
528 527
529 528 # outgoing commits between those commits
530 529 compare_page = ComparePage(response)
531 530 compare_page.swap_is_visible()
532 531 compare_page.target_source_are_enabled()
533 532
534 533 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
535 534 orig = backend_hg.create_repo(number_of_commits=1)
536 535 fork = backend_hg.create_fork()
537 536
538 537 settings_util.create_repo_rhodecode_ui(
539 538 orig, 'extensions', value='', key='largefiles', active=False)
540 539 settings_util.create_repo_rhodecode_ui(
541 540 fork, 'extensions', value='', key='largefiles', active=True)
542 541
543 542 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
544 543 'MercurialRepository.compare')
545 544 with mock.patch(compare_module) as compare_mock:
546 545 compare_mock.side_effect = RepositoryRequirementError()
547 546
548 547 response = self.app.get(
549 548 route_path('repo_compare',
550 549 repo_name=orig.repo_name,
551 550 source_ref_type="rev", source_ref="tip",
552 551 target_ref_type="rev", target_ref="tip",
553 552 params=dict(merge='1', target_repo=fork.repo_name),
554 553 ),
555 554 status=302)
556 555
557 556 assert_session_flash(
558 557 response,
559 558 'Could not compare repos with different large file settings')
560 559
561 560
562 561 @pytest.mark.usefixtures("autologin_user")
563 562 class TestCompareControllerSvn(object):
564 563
565 564 def test_supports_references_with_path(self, app, backend_svn):
566 565 repo = backend_svn['svn-simple-layout']
567 566 commit_id = repo.get_commit(commit_idx=-1).raw_id
568 567 response = app.get(
569 568 route_path('repo_compare',
570 569 repo_name=repo.repo_name,
571 570 source_ref_type="tag",
572 571 source_ref="%s@%s" % ('tags/v0.1', commit_id),
573 572 target_ref_type="tag",
574 573 target_ref="%s@%s" % ('tags/v0.2', commit_id),
575 574 params=dict(merge='1'),
576 575 ),
577 576 status=200)
578 577
579 578 # Expecting no commits, since both paths are at the same revision
580 579 response.mustcontain('No commits in this compare')
581 580
582 581 # Should find only one file changed when comparing those two tags
583 582 response.mustcontain('example.py')
584 583 compare_page = ComparePage(response)
585 584 compare_page.contains_change_summary(1, 5, 1)
586 585
587 586 def test_shows_commits_if_different_ids(self, app, backend_svn):
588 587 repo = backend_svn['svn-simple-layout']
589 588 source_id = repo.get_commit(commit_idx=-6).raw_id
590 589 target_id = repo.get_commit(commit_idx=-1).raw_id
591 590 response = app.get(
592 591 route_path('repo_compare',
593 592 repo_name=repo.repo_name,
594 593 source_ref_type="tag",
595 594 source_ref="%s@%s" % ('tags/v0.1', source_id),
596 595 target_ref_type="tag",
597 596 target_ref="%s@%s" % ('tags/v0.2', target_id),
598 597 params=dict(merge='1')
599 598 ),
600 599 status=200)
601 600
602 601 # It should show commits
603 602 assert 'No commits in this compare' not in response.text
604 603
605 604 # Should find only one file changed when comparing those two tags
606 605 response.mustcontain('example.py')
607 606 compare_page = ComparePage(response)
608 607 compare_page.contains_change_summary(1, 5, 1)
609 608
610 609
611 610 class ComparePage(AssertResponse):
612 611 """
613 612 Abstracts the page template from the tests
614 613 """
615 614
616 615 def contains_file_links_and_anchors(self, files):
617 616 doc = lxml.html.fromstring(self.response.body)
618 617 for filename, file_id in files:
619 618 self.contains_one_anchor(file_id)
620 619 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
621 620 assert len(diffblock) == 2
622 621 for lnk in diffblock[0].cssselect('a'):
623 622 if 'permalink' in lnk.text:
624 623 assert '#{}'.format(file_id) in lnk.attrib['href']
625 624 break
626 625 else:
627 626 pytest.fail('Unable to find permalink')
628 627
629 628 def contains_change_summary(self, files_changed, inserted, deleted):
630 629 template = (
631 630 '{files_changed} file{plural} changed: '
632 631 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
633 632 self.response.mustcontain(template.format(
634 633 files_changed=files_changed,
635 634 plural="s" if files_changed > 1 else "",
636 635 inserted=inserted,
637 636 deleted=deleted))
638 637
639 638 def contains_commits(self, commits, ancestors=None):
640 639 response = self.response
641 640
642 641 for commit in commits:
643 642 # Expecting to see the commit message in an element which
644 643 # has the ID "c-{commit.raw_id}"
645 644 self.element_contains('#c-' + commit.raw_id, commit.message)
646 645 self.contains_one_link(
647 646 'r%s:%s' % (commit.idx, commit.short_id),
648 647 self._commit_url(commit))
649 648
650 649 if ancestors:
651 650 response.mustcontain('Ancestor')
652 651 for ancestor in ancestors:
653 652 self.contains_one_link(
654 653 ancestor.short_id, self._commit_url(ancestor))
655 654
656 655 def _commit_url(self, commit):
657 656 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
658 657
659 658 def swap_is_hidden(self):
660 659 assert '<a id="btn-swap"' not in self.response.text
661 660
662 661 def swap_is_visible(self):
663 662 assert '<a id="btn-swap"' in self.response.text
664 663
665 664 def target_source_are_disabled(self):
666 665 response = self.response
667 666 response.mustcontain("var enable_fields = false;")
668 667 response.mustcontain('.select2("enable", enable_fields)')
669 668
670 669 def target_source_are_enabled(self):
671 670 response = self.response
672 671 response.mustcontain("var enable_fields = true;")
@@ -1,167 +1,166 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from .test_repo_compare import ComparePage
24 23
25 24
26 25 def route_path(name, params=None, **kwargs):
27 26 import urllib.request, urllib.parse, urllib.error
28 27
29 28 base_url = {
30 29 'repo_compare_select': '/{repo_name}/compare',
31 30 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
32 31 }[name].format(**kwargs)
33 32
34 33 if params:
35 34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 35 return base_url
37 36
38 37
39 38 @pytest.mark.usefixtures("autologin_user", "app")
40 39 class TestCompareView(object):
41 40
42 41 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
43 42 def test_compare_tag(self, backend):
44 43 tag1 = 'v0.1.2'
45 44 tag2 = 'v0.1.3'
46 45 response = self.app.get(
47 46 route_path(
48 47 'repo_compare',
49 48 repo_name=backend.repo_name,
50 49 source_ref_type="tag", source_ref=tag1,
51 50 target_ref_type="tag", target_ref=tag2),
52 51 status=200)
53 52
54 53 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
55 54 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
56 55
57 56 # outgoing commits between tags
58 57 commit_indexes = {
59 58 'git': [113] + range(115, 121),
60 59 'hg': [112] + range(115, 121),
61 60 }
62 61 repo = backend.repo
63 62 commits = (repo.get_commit(commit_idx=idx)
64 63 for idx in commit_indexes[backend.alias])
65 64 compare_page = ComparePage(response)
66 65 compare_page.contains_change_summary(11, 94, 64)
67 66 compare_page.contains_commits(commits)
68 67
69 68 # files diff
70 69 short_id = short_id_new = ''
71 70 if backend.alias == 'git':
72 71 short_id = '5a3a8fb00555'
73 72 short_id_new = '0ba5f8a46600'
74 73 if backend.alias == 'hg':
75 74 short_id = '17544fbfcd33'
76 75 short_id_new = 'a7e60bff65d5'
77 76
78 77 compare_page.contains_file_links_and_anchors([
79 78 # modified
80 79 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
81 80 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
82 81 # added
83 82 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
84 83 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
85 84 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
86 85 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
87 86 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
88 87 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
89 88 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
90 89 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
91 90 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
92 91 ])
93 92
94 93 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
95 94 def test_compare_tag_branch(self, backend):
96 95 revisions = {
97 96 'hg': {
98 97 'tag': 'v0.2.0',
99 98 'branch': 'default',
100 99 'response': (147, 5701, 10177)
101 100 },
102 101 'git': {
103 102 'tag': 'v0.2.2',
104 103 'branch': 'master',
105 104 'response': (70, 1855, 3002)
106 105 },
107 106 }
108 107
109 108 # Backend specific data, depends on the test repository for
110 109 # functional tests.
111 110 data = revisions[backend.alias]
112 111
113 112 response = self.app.get(
114 113 route_path(
115 114 'repo_compare',
116 115 repo_name=backend.repo_name,
117 116 source_ref_type='branch', source_ref=data['branch'],
118 117 target_ref_type="tag", target_ref=data['tag'],
119 118 ))
120 119
121 120 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
122 121 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
123 122 compare_page = ComparePage(response)
124 123 compare_page.contains_change_summary(*data['response'])
125 124
126 125 def test_index_branch(self, backend):
127 126 head_id = backend.default_head_id
128 127 response = self.app.get(
129 128 route_path(
130 129 'repo_compare',
131 130 repo_name=backend.repo_name,
132 131 source_ref_type="branch", source_ref=head_id,
133 132 target_ref_type="branch", target_ref=head_id,
134 133 ))
135 134
136 135 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
137 136
138 137 # branches are equal
139 138 response.mustcontain('No files')
140 139 response.mustcontain('No commits in this compare')
141 140
142 141 def test_compare_commits(self, backend):
143 142 repo = backend.repo
144 143 commit1 = repo.get_commit(commit_idx=0)
145 144 commit1_short_id = commit1.short_id
146 145 commit2 = repo.get_commit(commit_idx=1)
147 146 commit2_short_id = commit2.short_id
148 147
149 148 response = self.app.get(
150 149 route_path(
151 150 'repo_compare',
152 151 repo_name=backend.repo_name,
153 152 source_ref_type="rev", source_ref=commit1.raw_id,
154 153 target_ref_type="rev", target_ref=commit2.raw_id,
155 154 ))
156 155 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
157 156 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
158 157 compare_page = ComparePage(response)
159 158
160 159 # files
161 160 compare_page.contains_change_summary(1, 7, 0)
162 161
163 162 # outgoing commits between those commits
164 163 compare_page.contains_commits([commit2])
165 164 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
166 165 response.mustcontain(anchor)
167 166 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
@@ -1,291 +1,290 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
24 23 from rhodecode.lib.vcs import nodes
25 24 from rhodecode.lib.vcs.backends.base import EmptyCommit
26 25 from rhodecode.tests.fixture import Fixture
27 26 from rhodecode.tests.utils import commit_change
28 27
29 28 fixture = Fixture()
30 29
31 30
32 31 def route_path(name, params=None, **kwargs):
33 32 import urllib.request, urllib.parse, urllib.error
34 33
35 34 base_url = {
36 35 'repo_compare_select': '/{repo_name}/compare',
37 36 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 37 }[name].format(**kwargs)
39 38
40 39 if params:
41 40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 41 return base_url
43 42
44 43
45 44 @pytest.mark.usefixtures("autologin_user", "app")
46 45 class TestSideBySideDiff(object):
47 46
48 47 def test_diff_sidebyside_single_commit(self, app, backend):
49 48 commit_id_range = {
50 49 'hg': {
51 50 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
52 51 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
53 52 'changes': (21, 943, 288),
54 53 },
55 54 'git': {
56 55 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
57 56 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
58 57 'changes': (20, 941, 286),
59 58 },
60 59
61 60 'svn': {
62 61 'commits': ['336',
63 62 '337'],
64 63 'changes': (21, 943, 288),
65 64 },
66 65 }
67 66
68 67 commit_info = commit_id_range[backend.alias]
69 68 commit2, commit1 = commit_info['commits']
70 69 file_changes = commit_info['changes']
71 70
72 71 response = self.app.get(route_path(
73 72 'repo_compare',
74 73 repo_name=backend.repo_name,
75 74 source_ref_type='rev',
76 75 source_ref=commit2,
77 76 target_repo=backend.repo_name,
78 77 target_ref_type='rev',
79 78 target_ref=commit1,
80 79 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
81 80 ))
82 81
83 82 compare_page = ComparePage(response)
84 83 compare_page.contains_change_summary(*file_changes)
85 84 response.mustcontain('Collapse 1 commit')
86 85
87 86 def test_diff_sidebyside_two_commits(self, app, backend):
88 87 commit_id_range = {
89 88 'hg': {
90 89 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
91 90 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
92 91 'changes': (32, 1165, 308),
93 92 },
94 93 'git': {
95 94 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
96 95 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
97 96 'changes': (31, 1163, 306),
98 97 },
99 98
100 99 'svn': {
101 100 'commits': ['335',
102 101 '337'],
103 102 'changes': (32, 1179, 310),
104 103 },
105 104 }
106 105
107 106 commit_info = commit_id_range[backend.alias]
108 107 commit2, commit1 = commit_info['commits']
109 108 file_changes = commit_info['changes']
110 109
111 110 response = self.app.get(route_path(
112 111 'repo_compare',
113 112 repo_name=backend.repo_name,
114 113 source_ref_type='rev',
115 114 source_ref=commit2,
116 115 target_repo=backend.repo_name,
117 116 target_ref_type='rev',
118 117 target_ref=commit1,
119 118 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
120 119 ))
121 120
122 121 compare_page = ComparePage(response)
123 122 compare_page.contains_change_summary(*file_changes)
124 123
125 124 response.mustcontain('Collapse 2 commits')
126 125
127 126 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
128 127 commit_id_range = {
129 128
130 129 'svn': {
131 130 'commits': ['330',
132 131 '337'],
133 132
134 133 },
135 134 }
136 135
137 136 commit_info = commit_id_range['svn']
138 137 commit2, commit1 = commit_info['commits']
139 138
140 139 response = self.app.get(route_path(
141 140 'repo_compare',
142 141 repo_name=backend_svn.repo_name,
143 142 source_ref_type='rev',
144 143 source_ref=commit2,
145 144 target_repo=backend_svn.repo_name,
146 145 target_ref_type='rev',
147 146 target_ref=commit1,
148 147 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
149 148 ))
150 149
151 150 response.mustcontain('Expand 7 commits')
152 151
153 152 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
154 153 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
155 154 f_path = 'test_sidebyside_file.py'
156 155 commit1_content = 'content-25d7e49c18b159446c\n'
157 156 commit2_content = 'content-603d6c72c46d953420\n'
158 157 repo = backend.create_repo()
159 158
160 159 commit1 = commit_change(
161 160 repo.repo_name, filename=f_path, content=commit1_content,
162 161 message='A', vcs_type=backend.alias, parent=None, newfile=True)
163 162
164 163 commit2 = commit_change(
165 164 repo.repo_name, filename=f_path, content=commit2_content,
166 165 message='B, child of A', vcs_type=backend.alias, parent=commit1)
167 166
168 167 response = self.app.get(route_path(
169 168 'repo_compare',
170 169 repo_name=repo.repo_name,
171 170 source_ref_type='rev',
172 171 source_ref=EmptyCommit().raw_id,
173 172 target_ref_type='rev',
174 173 target_ref=commit2.raw_id,
175 174 params=dict(diffmode='sidebyside')
176 175 ))
177 176
178 177 response.mustcontain('Collapse 2 commits')
179 178 response.mustcontain('123 file changed')
180 179
181 180 response.mustcontain(
182 181 'r%s:%s...r%s:%s' % (
183 182 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
184 183
185 184 response.mustcontain(f_path)
186 185
187 186 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
188 187 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
189 188 f_path = 'test_sidebyside_file.py'
190 189 commit1_content = 'content-25d7e49c18b159446c\n'
191 190 commit2_content = 'content-603d6c72c46d953420\n'
192 191 repo = backend.create_repo()
193 192
194 193 commit1 = commit_change(
195 194 repo.repo_name, filename=f_path, content=commit1_content,
196 195 message='A', vcs_type=backend.alias, parent=None, newfile=True)
197 196
198 197 commit2 = commit_change(
199 198 repo.repo_name, filename=f_path, content=commit2_content,
200 199 message='B, child of A', vcs_type=backend.alias, parent=commit1)
201 200
202 201 response = self.app.get(route_path(
203 202 'repo_compare',
204 203 repo_name=repo.repo_name,
205 204 source_ref_type='rev',
206 205 source_ref=EmptyCommit().raw_id,
207 206 target_ref_type='rev',
208 207 target_ref=commit2.raw_id,
209 208 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
210 209 ))
211 210
212 211 response.mustcontain('Collapse 2 commits')
213 212 response.mustcontain('1 file changed')
214 213
215 214 response.mustcontain(
216 215 'r%s:%s...r%s:%s' % (
217 216 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
218 217
219 218 response.mustcontain(f_path)
220 219
221 220 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
222 221 commits = [
223 222 {'message': 'First commit'},
224 223 {'message': 'Second commit'},
225 224 {'message': 'Commit with binary',
226 225 'added': [nodes.FileNode('file.empty', content='')]},
227 226 ]
228 227 f_path = 'file.empty'
229 228 repo = backend.create_repo(commits=commits)
230 229 commit1 = repo.get_commit(commit_idx=0)
231 230 commit2 = repo.get_commit(commit_idx=1)
232 231 commit3 = repo.get_commit(commit_idx=2)
233 232
234 233 response = self.app.get(route_path(
235 234 'repo_compare',
236 235 repo_name=repo.repo_name,
237 236 source_ref_type='rev',
238 237 source_ref=commit1.raw_id,
239 238 target_ref_type='rev',
240 239 target_ref=commit3.raw_id,
241 240 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
242 241 ))
243 242
244 243 response.mustcontain('Collapse 2 commits')
245 244 response.mustcontain('1 file changed')
246 245
247 246 response.mustcontain(
248 247 'r%s:%s...r%s:%s' % (
249 248 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
250 249
251 250 response.mustcontain(f_path)
252 251
253 252 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
254 253 commit_id_range = {
255 254 'hg': {
256 255 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
257 256 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
258 257 'changes': (1, 3, 3)
259 258 },
260 259 'git': {
261 260 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
262 261 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
263 262 'changes': (1, 3, 3)
264 263 },
265 264
266 265 'svn': {
267 266 'commits': ['335',
268 267 '337'],
269 268 'changes': (1, 3, 3)
270 269 },
271 270 }
272 271 f_path = 'docs/conf.py'
273 272
274 273 commit_info = commit_id_range[backend.alias]
275 274 commit2, commit1 = commit_info['commits']
276 275 file_changes = commit_info['changes']
277 276
278 277 response = self.app.get(route_path(
279 278 'repo_compare',
280 279 repo_name=backend.repo_name,
281 280 source_ref_type='rev',
282 281 source_ref=commit2,
283 282 target_ref_type='rev',
284 283 target_ref=commit1,
285 284 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
286 285 ))
287 286
288 287 response.mustcontain('Collapse 2 commits')
289 288
290 289 compare_page = ComparePage(response)
291 290 compare_page.contains_change_summary(*file_changes)
@@ -1,1092 +1,1091 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import os
22 21
23 22 import mock
24 23 import pytest
25 24
26 25 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
28 27 from rhodecode.lib import helpers as h
29 28 from collections import OrderedDict
30 29 from rhodecode.lib.ext_json import json
31 30 from rhodecode.lib.vcs import nodes
32 31
33 32 from rhodecode.lib.vcs.conf import settings
34 33 from rhodecode.tests import assert_session_flash
35 34 from rhodecode.tests.fixture import Fixture
36 35 from rhodecode.model.db import Session
37 36
38 37 fixture = Fixture()
39 38
40 39
41 40 def get_node_history(backend_type):
42 41 return {
43 42 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 43 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 44 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 45 }[backend_type]
47 46
48 47
49 48 def route_path(name, params=None, **kwargs):
50 49 import urllib.request, urllib.parse, urllib.error
51 50
52 51 base_url = {
53 52 'repo_summary': '/{repo_name}',
54 53 'repo_archivefile': '/{repo_name}/archive/{fname}',
55 54 'repo_files_diff': '/{repo_name}/diff/{f_path}',
56 55 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
57 56 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
58 57 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
59 58 'repo_files:default_commit': '/{repo_name}/files',
60 59 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
61 60 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
62 61 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
63 62 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
64 63 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
65 64 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
66 65 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
67 66 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
68 67 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
69 68 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
70 69 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
71 70 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
72 71 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
73 72 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
74 73 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
75 74 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
76 75 }[name].format(**kwargs)
77 76
78 77 if params:
79 78 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
80 79 return base_url
81 80
82 81
83 82 def assert_files_in_response(response, files, params):
84 83 template = (
85 84 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
86 85 _assert_items_in_response(response, files, template, params)
87 86
88 87
89 88 def assert_dirs_in_response(response, dirs, params):
90 89 template = (
91 90 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
92 91 _assert_items_in_response(response, dirs, template, params)
93 92
94 93
95 94 def _assert_items_in_response(response, items, template, params):
96 95 for item in items:
97 96 item_params = {'name': item}
98 97 item_params.update(params)
99 98 response.mustcontain(template % item_params)
100 99
101 100
102 101 def assert_timeago_in_response(response, items, params):
103 102 for item in items:
104 103 response.mustcontain(h.age_component(params['date']))
105 104
106 105
107 106 @pytest.mark.usefixtures("app")
108 107 class TestFilesViews(object):
109 108
110 109 def test_show_files(self, backend):
111 110 response = self.app.get(
112 111 route_path('repo_files',
113 112 repo_name=backend.repo_name,
114 113 commit_id='tip', f_path='/'))
115 114 commit = backend.repo.get_commit()
116 115
117 116 params = {
118 117 'repo_name': backend.repo_name,
119 118 'commit_id': commit.raw_id,
120 119 'date': commit.date
121 120 }
122 121 assert_dirs_in_response(response, ['docs', 'vcs'], params)
123 122 files = [
124 123 '.gitignore',
125 124 '.hgignore',
126 125 '.hgtags',
127 126 # TODO: missing in Git
128 127 # '.travis.yml',
129 128 'MANIFEST.in',
130 129 'README.rst',
131 130 # TODO: File is missing in svn repository
132 131 # 'run_test_and_report.sh',
133 132 'setup.cfg',
134 133 'setup.py',
135 134 'test_and_report.sh',
136 135 'tox.ini',
137 136 ]
138 137 assert_files_in_response(response, files, params)
139 138 assert_timeago_in_response(response, files, params)
140 139
141 140 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
142 141 repo = backend_hg['subrepos']
143 142 response = self.app.get(
144 143 route_path('repo_files',
145 144 repo_name=repo.repo_name,
146 145 commit_id='tip', f_path='/'))
147 146 assert_response = response.assert_response()
148 147 assert_response.contains_one_link(
149 148 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
150 149
151 150 def test_show_files_links_submodules_with_absolute_url_subpaths(
152 151 self, backend_hg):
153 152 repo = backend_hg['subrepos']
154 153 response = self.app.get(
155 154 route_path('repo_files',
156 155 repo_name=repo.repo_name,
157 156 commit_id='tip', f_path='/'))
158 157 assert_response = response.assert_response()
159 158 assert_response.contains_one_link(
160 159 'subpaths-path @ 000000000000',
161 160 'http://sub-base.example.com/subpaths-path')
162 161
163 162 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
164 163 def test_files_menu(self, backend):
165 164 new_branch = "temp_branch_name"
166 165 commits = [
167 166 {'message': 'a'},
168 167 {'message': 'b', 'branch': new_branch}
169 168 ]
170 169 backend.create_repo(commits)
171 170 backend.repo.landing_rev = "branch:%s" % new_branch
172 171 Session().commit()
173 172
174 173 # get response based on tip and not new commit
175 174 response = self.app.get(
176 175 route_path('repo_files',
177 176 repo_name=backend.repo_name,
178 177 commit_id='tip', f_path='/'))
179 178
180 179 # make sure Files menu url is not tip but new commit
181 180 landing_rev = backend.repo.landing_ref_name
182 181 files_url = route_path('repo_files:default_path',
183 182 repo_name=backend.repo_name,
184 183 commit_id=landing_rev, params={'at': landing_rev})
185 184
186 185 assert landing_rev != 'tip'
187 186 response.mustcontain(
188 187 '<li class="active"><a class="menulink" href="%s">' % files_url)
189 188
190 189 def test_show_files_commit(self, backend):
191 190 commit = backend.repo.get_commit(commit_idx=32)
192 191
193 192 response = self.app.get(
194 193 route_path('repo_files',
195 194 repo_name=backend.repo_name,
196 195 commit_id=commit.raw_id, f_path='/'))
197 196
198 197 dirs = ['docs', 'tests']
199 198 files = ['README.rst']
200 199 params = {
201 200 'repo_name': backend.repo_name,
202 201 'commit_id': commit.raw_id,
203 202 }
204 203 assert_dirs_in_response(response, dirs, params)
205 204 assert_files_in_response(response, files, params)
206 205
207 206 def test_show_files_different_branch(self, backend):
208 207 branches = dict(
209 208 hg=(150, ['git']),
210 209 # TODO: Git test repository does not contain other branches
211 210 git=(633, ['master']),
212 211 # TODO: Branch support in Subversion
213 212 svn=(150, [])
214 213 )
215 214 idx, branches = branches[backend.alias]
216 215 commit = backend.repo.get_commit(commit_idx=idx)
217 216 response = self.app.get(
218 217 route_path('repo_files',
219 218 repo_name=backend.repo_name,
220 219 commit_id=commit.raw_id, f_path='/'))
221 220
222 221 assert_response = response.assert_response()
223 222 for branch in branches:
224 223 assert_response.element_contains('.tags .branchtag', branch)
225 224
226 225 def test_show_files_paging(self, backend):
227 226 repo = backend.repo
228 227 indexes = [73, 92, 109, 1, 0]
229 228 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
230 229 for rev in indexes]
231 230
232 231 for idx in idx_map:
233 232 response = self.app.get(
234 233 route_path('repo_files',
235 234 repo_name=backend.repo_name,
236 235 commit_id=idx[1], f_path='/'))
237 236
238 237 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
239 238
240 239 def test_file_source(self, backend):
241 240 commit = backend.repo.get_commit(commit_idx=167)
242 241 response = self.app.get(
243 242 route_path('repo_files',
244 243 repo_name=backend.repo_name,
245 244 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
246 245
247 246 msgbox = """<div class="commit">%s</div>"""
248 247 response.mustcontain(msgbox % (commit.message, ))
249 248
250 249 assert_response = response.assert_response()
251 250 if commit.branch:
252 251 assert_response.element_contains(
253 252 '.tags.tags-main .branchtag', commit.branch)
254 253 if commit.tags:
255 254 for tag in commit.tags:
256 255 assert_response.element_contains('.tags.tags-main .tagtag', tag)
257 256
258 257 def test_file_source_annotated(self, backend):
259 258 response = self.app.get(
260 259 route_path('repo_files:annotated',
261 260 repo_name=backend.repo_name,
262 261 commit_id='tip', f_path='vcs/nodes.py'))
263 262 expected_commits = {
264 263 'hg': 'r356',
265 264 'git': 'r345',
266 265 'svn': 'r208',
267 266 }
268 267 response.mustcontain(expected_commits[backend.alias])
269 268
270 269 def test_file_source_authors(self, backend):
271 270 response = self.app.get(
272 271 route_path('repo_file_authors',
273 272 repo_name=backend.repo_name,
274 273 commit_id='tip', f_path='vcs/nodes.py'))
275 274 expected_authors = {
276 275 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 276 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
278 277 'svn': ('marcin', 'lukasz'),
279 278 }
280 279
281 280 for author in expected_authors[backend.alias]:
282 281 response.mustcontain(author)
283 282
284 283 def test_file_source_authors_with_annotation(self, backend):
285 284 response = self.app.get(
286 285 route_path('repo_file_authors',
287 286 repo_name=backend.repo_name,
288 287 commit_id='tip', f_path='vcs/nodes.py',
289 288 params=dict(annotate=1)))
290 289 expected_authors = {
291 290 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 291 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
293 292 'svn': ('marcin', 'lukasz'),
294 293 }
295 294
296 295 for author in expected_authors[backend.alias]:
297 296 response.mustcontain(author)
298 297
299 298 def test_file_source_history(self, backend, xhr_header):
300 299 response = self.app.get(
301 300 route_path('repo_file_history',
302 301 repo_name=backend.repo_name,
303 302 commit_id='tip', f_path='vcs/nodes.py'),
304 303 extra_environ=xhr_header)
305 304 assert get_node_history(backend.alias) == json.loads(response.body)
306 305
307 306 def test_file_source_history_svn(self, backend_svn, xhr_header):
308 307 simple_repo = backend_svn['svn-simple-layout']
309 308 response = self.app.get(
310 309 route_path('repo_file_history',
311 310 repo_name=simple_repo.repo_name,
312 311 commit_id='tip', f_path='trunk/example.py'),
313 312 extra_environ=xhr_header)
314 313
315 314 expected_data = json.loads(
316 315 fixture.load_resource('svn_node_history_branches.json'))
317 316
318 317 assert expected_data == response.json
319 318
320 319 def test_file_source_history_with_annotation(self, backend, xhr_header):
321 320 response = self.app.get(
322 321 route_path('repo_file_history',
323 322 repo_name=backend.repo_name,
324 323 commit_id='tip', f_path='vcs/nodes.py',
325 324 params=dict(annotate=1)),
326 325
327 326 extra_environ=xhr_header)
328 327 assert get_node_history(backend.alias) == json.loads(response.body)
329 328
330 329 def test_tree_search_top_level(self, backend, xhr_header):
331 330 commit = backend.repo.get_commit(commit_idx=173)
332 331 response = self.app.get(
333 332 route_path('repo_files_nodelist',
334 333 repo_name=backend.repo_name,
335 334 commit_id=commit.raw_id, f_path='/'),
336 335 extra_environ=xhr_header)
337 336 assert 'nodes' in response.json
338 337 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
339 338
340 339 def test_tree_search_missing_xhr(self, backend):
341 340 self.app.get(
342 341 route_path('repo_files_nodelist',
343 342 repo_name=backend.repo_name,
344 343 commit_id='tip', f_path='/'),
345 344 status=404)
346 345
347 346 def test_tree_search_at_path(self, backend, xhr_header):
348 347 commit = backend.repo.get_commit(commit_idx=173)
349 348 response = self.app.get(
350 349 route_path('repo_files_nodelist',
351 350 repo_name=backend.repo_name,
352 351 commit_id=commit.raw_id, f_path='/docs'),
353 352 extra_environ=xhr_header)
354 353 assert 'nodes' in response.json
355 354 nodes = response.json['nodes']
356 355 assert {'name': 'docs/api', 'type': 'dir'} in nodes
357 356 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
358 357
359 358 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
360 359 commit = backend.repo.get_commit(commit_idx=173)
361 360 response = self.app.get(
362 361 route_path('repo_files_nodelist',
363 362 repo_name=backend.repo_name,
364 363 commit_id=commit.raw_id, f_path='/docs/api'),
365 364 extra_environ=xhr_header)
366 365 assert 'nodes' in response.json
367 366 nodes = response.json['nodes']
368 367 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
369 368
370 369 def test_tree_search_at_path_missing_xhr(self, backend):
371 370 self.app.get(
372 371 route_path('repo_files_nodelist',
373 372 repo_name=backend.repo_name,
374 373 commit_id='tip', f_path='/docs'),
375 374 status=404)
376 375
377 376 def test_nodetree(self, backend, xhr_header):
378 377 commit = backend.repo.get_commit(commit_idx=173)
379 378 response = self.app.get(
380 379 route_path('repo_nodetree_full',
381 380 repo_name=backend.repo_name,
382 381 commit_id=commit.raw_id, f_path='/'),
383 382 extra_environ=xhr_header)
384 383
385 384 assert_response = response.assert_response()
386 385
387 386 for attr in ['data-commit-id', 'data-date', 'data-author']:
388 387 elements = assert_response.get_elements('[{}]'.format(attr))
389 388 assert len(elements) > 1
390 389
391 390 for element in elements:
392 391 assert element.get(attr)
393 392
394 393 def test_nodetree_if_file(self, backend, xhr_header):
395 394 commit = backend.repo.get_commit(commit_idx=173)
396 395 response = self.app.get(
397 396 route_path('repo_nodetree_full',
398 397 repo_name=backend.repo_name,
399 398 commit_id=commit.raw_id, f_path='README.rst'),
400 399 extra_environ=xhr_header)
401 400 assert response.text == ''
402 401
403 402 def test_nodetree_wrong_path(self, backend, xhr_header):
404 403 commit = backend.repo.get_commit(commit_idx=173)
405 404 response = self.app.get(
406 405 route_path('repo_nodetree_full',
407 406 repo_name=backend.repo_name,
408 407 commit_id=commit.raw_id, f_path='/dont-exist'),
409 408 extra_environ=xhr_header)
410 409
411 410 err = 'error: There is no file nor ' \
412 411 'directory at the given path'
413 412 assert err in response.text
414 413
415 414 def test_nodetree_missing_xhr(self, backend):
416 415 self.app.get(
417 416 route_path('repo_nodetree_full',
418 417 repo_name=backend.repo_name,
419 418 commit_id='tip', f_path='/'),
420 419 status=404)
421 420
422 421
423 422 @pytest.mark.usefixtures("app", "autologin_user")
424 423 class TestRawFileHandling(object):
425 424
426 425 def test_download_file(self, backend):
427 426 commit = backend.repo.get_commit(commit_idx=173)
428 427 response = self.app.get(
429 428 route_path('repo_file_download',
430 429 repo_name=backend.repo_name,
431 430 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
432 431
433 432 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
434 433 assert response.content_type == "text/x-python"
435 434
436 435 def test_download_file_wrong_cs(self, backend):
437 436 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
438 437
439 438 response = self.app.get(
440 439 route_path('repo_file_download',
441 440 repo_name=backend.repo_name,
442 441 commit_id=raw_id, f_path='vcs/nodes.svg'),
443 442 status=404)
444 443
445 444 msg = """No such commit exists for this repository"""
446 445 response.mustcontain(msg)
447 446
448 447 def test_download_file_wrong_f_path(self, backend):
449 448 commit = backend.repo.get_commit(commit_idx=173)
450 449 f_path = 'vcs/ERRORnodes.py'
451 450
452 451 response = self.app.get(
453 452 route_path('repo_file_download',
454 453 repo_name=backend.repo_name,
455 454 commit_id=commit.raw_id, f_path=f_path),
456 455 status=404)
457 456
458 457 msg = (
459 458 "There is no file nor directory at the given path: "
460 459 "`%s` at commit %s" % (f_path, commit.short_id))
461 460 response.mustcontain(msg)
462 461
463 462 def test_file_raw(self, backend):
464 463 commit = backend.repo.get_commit(commit_idx=173)
465 464 response = self.app.get(
466 465 route_path('repo_file_raw',
467 466 repo_name=backend.repo_name,
468 467 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
469 468
470 469 assert response.content_type == "text/plain"
471 470
472 471 def test_file_raw_binary(self, backend):
473 472 commit = backend.repo.get_commit()
474 473 response = self.app.get(
475 474 route_path('repo_file_raw',
476 475 repo_name=backend.repo_name,
477 476 commit_id=commit.raw_id,
478 477 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
479 478
480 479 assert response.content_disposition == 'inline'
481 480
482 481 def test_raw_file_wrong_cs(self, backend):
483 482 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
484 483
485 484 response = self.app.get(
486 485 route_path('repo_file_raw',
487 486 repo_name=backend.repo_name,
488 487 commit_id=raw_id, f_path='vcs/nodes.svg'),
489 488 status=404)
490 489
491 490 msg = """No such commit exists for this repository"""
492 491 response.mustcontain(msg)
493 492
494 493 def test_raw_wrong_f_path(self, backend):
495 494 commit = backend.repo.get_commit(commit_idx=173)
496 495 f_path = 'vcs/ERRORnodes.py'
497 496 response = self.app.get(
498 497 route_path('repo_file_raw',
499 498 repo_name=backend.repo_name,
500 499 commit_id=commit.raw_id, f_path=f_path),
501 500 status=404)
502 501
503 502 msg = (
504 503 "There is no file nor directory at the given path: "
505 504 "`%s` at commit %s" % (f_path, commit.short_id))
506 505 response.mustcontain(msg)
507 506
508 507 def test_raw_svg_should_not_be_rendered(self, backend):
509 508 backend.create_repo()
510 509 backend.ensure_file("xss.svg")
511 510 response = self.app.get(
512 511 route_path('repo_file_raw',
513 512 repo_name=backend.repo_name,
514 513 commit_id='tip', f_path='xss.svg'),)
515 514 # If the content type is image/svg+xml then it allows to render HTML
516 515 # and malicious SVG.
517 516 assert response.content_type == "text/plain"
518 517
519 518
520 519 @pytest.mark.usefixtures("app")
521 520 class TestRepositoryArchival(object):
522 521
523 522 def test_archival(self, backend):
524 523 backend.enable_downloads()
525 524 commit = backend.repo.get_commit(commit_idx=173)
526 525 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
527 526
528 527 short = commit.short_id + extension
529 528 fname = commit.raw_id + extension
530 529 filename = '%s-%s' % (backend.repo_name, short)
531 530 response = self.app.get(
532 531 route_path('repo_archivefile',
533 532 repo_name=backend.repo_name,
534 533 fname=fname))
535 534
536 535 assert response.status == '200 OK'
537 536 headers = [
538 537 ('Content-Disposition', 'attachment; filename=%s' % filename),
539 538 ('Content-Type', '%s' % content_type),
540 539 ]
541 540
542 541 for header in headers:
543 542 assert header in response.headers.items()
544 543
545 544 def test_archival_no_hash(self, backend):
546 545 backend.enable_downloads()
547 546 commit = backend.repo.get_commit(commit_idx=173)
548 547 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
549 548
550 549 short = 'plain' + extension
551 550 fname = commit.raw_id + extension
552 551 filename = '%s-%s' % (backend.repo_name, short)
553 552 response = self.app.get(
554 553 route_path('repo_archivefile',
555 554 repo_name=backend.repo_name,
556 555 fname=fname, params={'with_hash': 0}))
557 556
558 557 assert response.status == '200 OK'
559 558 headers = [
560 559 ('Content-Disposition', 'attachment; filename=%s' % filename),
561 560 ('Content-Type', '%s' % content_type),
562 561 ]
563 562
564 563 for header in headers:
565 564 assert header in response.headers.items()
566 565
567 566 @pytest.mark.parametrize('arch_ext',[
568 567 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
569 568 def test_archival_wrong_ext(self, backend, arch_ext):
570 569 backend.enable_downloads()
571 570 commit = backend.repo.get_commit(commit_idx=173)
572 571
573 572 fname = commit.raw_id + '.' + arch_ext
574 573
575 574 response = self.app.get(
576 575 route_path('repo_archivefile',
577 576 repo_name=backend.repo_name,
578 577 fname=fname))
579 578 response.mustcontain(
580 579 'Unknown archive type for: `{}`'.format(fname))
581 580
582 581 @pytest.mark.parametrize('commit_id', [
583 582 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
584 583 def test_archival_wrong_commit_id(self, backend, commit_id):
585 584 backend.enable_downloads()
586 585 fname = '%s.zip' % commit_id
587 586
588 587 response = self.app.get(
589 588 route_path('repo_archivefile',
590 589 repo_name=backend.repo_name,
591 590 fname=fname))
592 591 response.mustcontain('Unknown commit_id')
593 592
594 593
595 594 @pytest.mark.usefixtures("app")
596 595 class TestFilesDiff(object):
597 596
598 597 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
599 598 def test_file_full_diff(self, backend, diff):
600 599 commit1 = backend.repo.get_commit(commit_idx=-1)
601 600 commit2 = backend.repo.get_commit(commit_idx=-2)
602 601
603 602 response = self.app.get(
604 603 route_path('repo_files_diff',
605 604 repo_name=backend.repo_name,
606 605 f_path='README'),
607 606 params={
608 607 'diff1': commit2.raw_id,
609 608 'diff2': commit1.raw_id,
610 609 'fulldiff': '1',
611 610 'diff': diff,
612 611 })
613 612
614 613 if diff == 'diff':
615 614 # use redirect since this is OLD view redirecting to compare page
616 615 response = response.follow()
617 616
618 617 # It's a symlink to README.rst
619 618 response.mustcontain('README.rst')
620 619 response.mustcontain('No newline at end of file')
621 620
622 621 def test_file_binary_diff(self, backend):
623 622 commits = [
624 623 {'message': 'First commit'},
625 624 {'message': 'Commit with binary',
626 625 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
627 626 ]
628 627 repo = backend.create_repo(commits=commits)
629 628
630 629 response = self.app.get(
631 630 route_path('repo_files_diff',
632 631 repo_name=backend.repo_name,
633 632 f_path='file.bin'),
634 633 params={
635 634 'diff1': repo.get_commit(commit_idx=0).raw_id,
636 635 'diff2': repo.get_commit(commit_idx=1).raw_id,
637 636 'fulldiff': '1',
638 637 'diff': 'diff',
639 638 })
640 639 # use redirect since this is OLD view redirecting to compare page
641 640 response = response.follow()
642 641 response.mustcontain('Collapse 1 commit')
643 642 file_changes = (1, 0, 0)
644 643
645 644 compare_page = ComparePage(response)
646 645 compare_page.contains_change_summary(*file_changes)
647 646
648 647 if backend.alias == 'svn':
649 648 response.mustcontain('new file 10644')
650 649 # TODO(marcink): SVN doesn't yet detect binary changes
651 650 else:
652 651 response.mustcontain('new file 100644')
653 652 response.mustcontain('binary diff hidden')
654 653
655 654 def test_diff_2way(self, backend):
656 655 commit1 = backend.repo.get_commit(commit_idx=-1)
657 656 commit2 = backend.repo.get_commit(commit_idx=-2)
658 657 response = self.app.get(
659 658 route_path('repo_files_diff_2way_redirect',
660 659 repo_name=backend.repo_name,
661 660 f_path='README'),
662 661 params={
663 662 'diff1': commit2.raw_id,
664 663 'diff2': commit1.raw_id,
665 664 })
666 665 # use redirect since this is OLD view redirecting to compare page
667 666 response = response.follow()
668 667
669 668 # It's a symlink to README.rst
670 669 response.mustcontain('README.rst')
671 670 response.mustcontain('No newline at end of file')
672 671
673 672 def test_requires_one_commit_id(self, backend, autologin_user):
674 673 response = self.app.get(
675 674 route_path('repo_files_diff',
676 675 repo_name=backend.repo_name,
677 676 f_path='README.rst'),
678 677 status=400)
679 678 response.mustcontain(
680 679 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
681 680
682 681 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
683 682 repo = vcsbackend.repo
684 683 response = self.app.get(
685 684 route_path('repo_files_diff',
686 685 repo_name=repo.name,
687 686 f_path='does-not-exist-in-any-commit'),
688 687 params={
689 688 'diff1': repo[0].raw_id,
690 689 'diff2': repo[1].raw_id
691 690 })
692 691
693 692 response = response.follow()
694 693 response.mustcontain('No files')
695 694
696 695 def test_returns_redirect_if_file_not_changed(self, backend):
697 696 commit = backend.repo.get_commit(commit_idx=-1)
698 697 response = self.app.get(
699 698 route_path('repo_files_diff_2way_redirect',
700 699 repo_name=backend.repo_name,
701 700 f_path='README'),
702 701 params={
703 702 'diff1': commit.raw_id,
704 703 'diff2': commit.raw_id,
705 704 })
706 705
707 706 response = response.follow()
708 707 response.mustcontain('No files')
709 708 response.mustcontain('No commits in this compare')
710 709
711 710 def test_supports_diff_to_different_path_svn(self, backend_svn):
712 711 #TODO: check this case
713 712 return
714 713
715 714 repo = backend_svn['svn-simple-layout'].scm_instance()
716 715 commit_id_1 = '24'
717 716 commit_id_2 = '26'
718 717
719 718 response = self.app.get(
720 719 route_path('repo_files_diff',
721 720 repo_name=backend_svn.repo_name,
722 721 f_path='trunk/example.py'),
723 722 params={
724 723 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
725 724 'diff2': commit_id_2,
726 725 })
727 726
728 727 response = response.follow()
729 728 response.mustcontain(
730 729 # diff contains this
731 730 "Will print out a useful message on invocation.")
732 731
733 732 # Note: Expecting that we indicate the user what's being compared
734 733 response.mustcontain("trunk/example.py")
735 734 response.mustcontain("tags/v0.2/example.py")
736 735
737 736 def test_show_rev_redirects_to_svn_path(self, backend_svn):
738 737 #TODO: check this case
739 738 return
740 739
741 740 repo = backend_svn['svn-simple-layout'].scm_instance()
742 741 commit_id = repo[-1].raw_id
743 742
744 743 response = self.app.get(
745 744 route_path('repo_files_diff',
746 745 repo_name=backend_svn.repo_name,
747 746 f_path='trunk/example.py'),
748 747 params={
749 748 'diff1': 'branches/argparse/example.py@' + commit_id,
750 749 'diff2': commit_id,
751 750 },
752 751 status=302)
753 752 response = response.follow()
754 753 assert response.headers['Location'].endswith(
755 754 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
756 755
757 756 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
758 757 #TODO: check this case
759 758 return
760 759
761 760 repo = backend_svn['svn-simple-layout'].scm_instance()
762 761 commit_id = repo[-1].raw_id
763 762 response = self.app.get(
764 763 route_path('repo_files_diff',
765 764 repo_name=backend_svn.repo_name,
766 765 f_path='trunk/example.py'),
767 766 params={
768 767 'diff1': 'branches/argparse/example.py@' + commit_id,
769 768 'diff2': commit_id,
770 769 'show_rev': 'Show at Revision',
771 770 'annotate': 'true',
772 771 },
773 772 status=302)
774 773 response = response.follow()
775 774 assert response.headers['Location'].endswith(
776 775 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
777 776
778 777
779 778 @pytest.mark.usefixtures("app", "autologin_user")
780 779 class TestModifyFilesWithWebInterface(object):
781 780
782 781 def test_add_file_view(self, backend):
783 782 self.app.get(
784 783 route_path('repo_files_add_file',
785 784 repo_name=backend.repo_name,
786 785 commit_id='tip', f_path='/')
787 786 )
788 787
789 788 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
790 789 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
791 790 backend.create_repo()
792 791 filename = 'init.py'
793 792 response = self.app.post(
794 793 route_path('repo_files_create_file',
795 794 repo_name=backend.repo_name,
796 795 commit_id='tip', f_path='/'),
797 796 params={
798 797 'content': "",
799 798 'filename': filename,
800 799 'csrf_token': csrf_token,
801 800 },
802 801 status=302)
803 802 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
804 803 assert_session_flash(response, expected_msg)
805 804
806 805 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
807 806 commit_id = backend.repo.get_commit().raw_id
808 807 response = self.app.post(
809 808 route_path('repo_files_create_file',
810 809 repo_name=backend.repo_name,
811 810 commit_id=commit_id, f_path='/'),
812 811 params={
813 812 'content': "foo",
814 813 'csrf_token': csrf_token,
815 814 },
816 815 status=302)
817 816
818 817 assert_session_flash(response, 'No filename specified')
819 818
820 819 def test_add_file_into_repo_errors_and_no_commits(
821 820 self, backend, csrf_token):
822 821 repo = backend.create_repo()
823 822 # Create a file with no filename, it will display an error but
824 823 # the repo has no commits yet
825 824 response = self.app.post(
826 825 route_path('repo_files_create_file',
827 826 repo_name=repo.repo_name,
828 827 commit_id='tip', f_path='/'),
829 828 params={
830 829 'content': "foo",
831 830 'csrf_token': csrf_token,
832 831 },
833 832 status=302)
834 833
835 834 assert_session_flash(response, 'No filename specified')
836 835
837 836 # Not allowed, redirect to the summary
838 837 redirected = response.follow()
839 838 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
840 839
841 840 # As there are no commits, displays the summary page with the error of
842 841 # creating a file with no filename
843 842
844 843 assert redirected.request.path == summary_url
845 844
846 845 @pytest.mark.parametrize("filename, clean_filename", [
847 846 ('/abs/foo', 'abs/foo'),
848 847 ('../rel/foo', 'rel/foo'),
849 848 ('file/../foo/foo', 'file/foo/foo'),
850 849 ])
851 850 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
852 851 repo = backend.create_repo()
853 852 commit_id = repo.get_commit().raw_id
854 853
855 854 response = self.app.post(
856 855 route_path('repo_files_create_file',
857 856 repo_name=repo.repo_name,
858 857 commit_id=commit_id, f_path='/'),
859 858 params={
860 859 'content': "foo",
861 860 'filename': filename,
862 861 'csrf_token': csrf_token,
863 862 },
864 863 status=302)
865 864
866 865 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
867 866 assert_session_flash(response, expected_msg)
868 867
869 868 @pytest.mark.parametrize("cnt, filename, content", [
870 869 (1, 'foo.txt', "Content"),
871 870 (2, 'dir/foo.rst', "Content"),
872 871 (3, 'dir/foo-second.rst', "Content"),
873 872 (4, 'rel/dir/foo.bar', "Content"),
874 873 ])
875 874 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
876 875 repo = backend.create_repo()
877 876 commit_id = repo.get_commit().raw_id
878 877 response = self.app.post(
879 878 route_path('repo_files_create_file',
880 879 repo_name=repo.repo_name,
881 880 commit_id=commit_id, f_path='/'),
882 881 params={
883 882 'content': content,
884 883 'filename': filename,
885 884 'csrf_token': csrf_token,
886 885 },
887 886 status=302)
888 887
889 888 expected_msg = 'Successfully committed new file `{}`'.format(filename)
890 889 assert_session_flash(response, expected_msg)
891 890
892 891 def test_edit_file_view(self, backend):
893 892 response = self.app.get(
894 893 route_path('repo_files_edit_file',
895 894 repo_name=backend.repo_name,
896 895 commit_id=backend.default_head_id,
897 896 f_path='vcs/nodes.py'),
898 897 status=200)
899 898 response.mustcontain("Module holding everything related to vcs nodes.")
900 899
901 900 def test_edit_file_view_not_on_branch(self, backend):
902 901 repo = backend.create_repo()
903 902 backend.ensure_file("vcs/nodes.py")
904 903
905 904 response = self.app.get(
906 905 route_path('repo_files_edit_file',
907 906 repo_name=repo.repo_name,
908 907 commit_id='tip',
909 908 f_path='vcs/nodes.py'),
910 909 status=302)
911 910 assert_session_flash(
912 911 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
913 912
914 913 def test_edit_file_view_commit_changes(self, backend, csrf_token):
915 914 repo = backend.create_repo()
916 915 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
917 916
918 917 response = self.app.post(
919 918 route_path('repo_files_update_file',
920 919 repo_name=repo.repo_name,
921 920 commit_id=backend.default_head_id,
922 921 f_path='vcs/nodes.py'),
923 922 params={
924 923 'content': "print 'hello world'",
925 924 'message': 'I committed',
926 925 'filename': "vcs/nodes.py",
927 926 'csrf_token': csrf_token,
928 927 },
929 928 status=302)
930 929 assert_session_flash(
931 930 response, 'Successfully committed changes to file `vcs/nodes.py`')
932 931 tip = repo.get_commit(commit_idx=-1)
933 932 assert tip.message == 'I committed'
934 933
935 934 def test_edit_file_view_commit_changes_default_message(self, backend,
936 935 csrf_token):
937 936 repo = backend.create_repo()
938 937 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
939 938
940 939 commit_id = (
941 940 backend.default_branch_name or
942 941 backend.repo.scm_instance().commit_ids[-1])
943 942
944 943 response = self.app.post(
945 944 route_path('repo_files_update_file',
946 945 repo_name=repo.repo_name,
947 946 commit_id=commit_id,
948 947 f_path='vcs/nodes.py'),
949 948 params={
950 949 'content': "print 'hello world'",
951 950 'message': '',
952 951 'filename': "vcs/nodes.py",
953 952 'csrf_token': csrf_token,
954 953 },
955 954 status=302)
956 955 assert_session_flash(
957 956 response, 'Successfully committed changes to file `vcs/nodes.py`')
958 957 tip = repo.get_commit(commit_idx=-1)
959 958 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
960 959
961 960 def test_delete_file_view(self, backend):
962 961 self.app.get(
963 962 route_path('repo_files_remove_file',
964 963 repo_name=backend.repo_name,
965 964 commit_id=backend.default_head_id,
966 965 f_path='vcs/nodes.py'),
967 966 status=200)
968 967
969 968 def test_delete_file_view_not_on_branch(self, backend):
970 969 repo = backend.create_repo()
971 970 backend.ensure_file('vcs/nodes.py')
972 971
973 972 response = self.app.get(
974 973 route_path('repo_files_remove_file',
975 974 repo_name=repo.repo_name,
976 975 commit_id='tip',
977 976 f_path='vcs/nodes.py'),
978 977 status=302)
979 978 assert_session_flash(
980 979 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
981 980
982 981 def test_delete_file_view_commit_changes(self, backend, csrf_token):
983 982 repo = backend.create_repo()
984 983 backend.ensure_file("vcs/nodes.py")
985 984
986 985 response = self.app.post(
987 986 route_path('repo_files_delete_file',
988 987 repo_name=repo.repo_name,
989 988 commit_id=backend.default_head_id,
990 989 f_path='vcs/nodes.py'),
991 990 params={
992 991 'message': 'i committed',
993 992 'csrf_token': csrf_token,
994 993 },
995 994 status=302)
996 995 assert_session_flash(
997 996 response, 'Successfully deleted file `vcs/nodes.py`')
998 997
999 998
1000 999 @pytest.mark.usefixtures("app")
1001 1000 class TestFilesViewOtherCases(object):
1002 1001
1003 1002 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1004 1003 self, backend_stub, autologin_regular_user, user_regular,
1005 1004 user_util):
1006 1005
1007 1006 repo = backend_stub.create_repo()
1008 1007 user_util.grant_user_permission_to_repo(
1009 1008 repo, user_regular, 'repository.write')
1010 1009 response = self.app.get(
1011 1010 route_path('repo_files',
1012 1011 repo_name=repo.repo_name,
1013 1012 commit_id='tip', f_path='/'))
1014 1013
1015 1014 repo_file_add_url = route_path(
1016 1015 'repo_files_add_file',
1017 1016 repo_name=repo.repo_name,
1018 1017 commit_id=0, f_path='')
1019 1018
1020 1019 assert_session_flash(
1021 1020 response,
1022 1021 'There are no files yet. <a class="alert-link" '
1023 1022 'href="{}">Click here to add a new file.</a>'
1024 1023 .format(repo_file_add_url))
1025 1024
1026 1025 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1027 1026 self, backend_stub, autologin_regular_user):
1028 1027 repo = backend_stub.create_repo()
1029 1028 # init session for anon user
1030 1029 route_path('repo_summary', repo_name=repo.repo_name)
1031 1030
1032 1031 repo_file_add_url = route_path(
1033 1032 'repo_files_add_file',
1034 1033 repo_name=repo.repo_name,
1035 1034 commit_id=0, f_path='')
1036 1035
1037 1036 response = self.app.get(
1038 1037 route_path('repo_files',
1039 1038 repo_name=repo.repo_name,
1040 1039 commit_id='tip', f_path='/'))
1041 1040
1042 1041 assert_session_flash(response, no_=repo_file_add_url)
1043 1042
1044 1043 @pytest.mark.parametrize('file_node', [
1045 1044 'archive/file.zip',
1046 1045 'diff/my-file.txt',
1047 1046 'render.py',
1048 1047 'render',
1049 1048 'remove_file',
1050 1049 'remove_file/to-delete.txt',
1051 1050 ])
1052 1051 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1053 1052 backend.create_repo()
1054 1053 backend.ensure_file(file_node)
1055 1054
1056 1055 self.app.get(
1057 1056 route_path('repo_files',
1058 1057 repo_name=backend.repo_name,
1059 1058 commit_id='tip', f_path=file_node),
1060 1059 status=200)
1061 1060
1062 1061
1063 1062 class TestAdjustFilePathForSvn(object):
1064 1063 """
1065 1064 SVN specific adjustments of node history in RepoFilesView.
1066 1065 """
1067 1066
1068 1067 def test_returns_path_relative_to_matched_reference(self):
1069 1068 repo = self._repo(branches=['trunk'])
1070 1069 self.assert_file_adjustment('trunk/file', 'file', repo)
1071 1070
1072 1071 def test_does_not_modify_file_if_no_reference_matches(self):
1073 1072 repo = self._repo(branches=['trunk'])
1074 1073 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1075 1074
1076 1075 def test_does_not_adjust_partial_directory_names(self):
1077 1076 repo = self._repo(branches=['trun'])
1078 1077 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1079 1078
1080 1079 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1081 1080 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1082 1081 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1083 1082
1084 1083 def assert_file_adjustment(self, f_path, expected, repo):
1085 1084 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1086 1085 assert result == expected
1087 1086
1088 1087 def _repo(self, branches=None):
1089 1088 repo = mock.Mock()
1090 1089 repo.branches = OrderedDict((name, '0') for name in branches or [])
1091 1090 repo.tags = {}
1092 1091 return repo
@@ -1,333 +1,332 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
24 23
25 24 from rhodecode.tests.fixture import Fixture
26 25 from rhodecode.lib import helpers as h
27 26
28 27 from rhodecode.model.db import Repository
29 28 from rhodecode.model.repo import RepoModel
30 29 from rhodecode.model.user import UserModel
31 30 from rhodecode.model.meta import Session
32 31
33 32 fixture = Fixture()
34 33
35 34
36 35 def route_path(name, params=None, **kwargs):
37 36 import urllib.request, urllib.parse, urllib.error
38 37
39 38 base_url = {
40 39 'repo_summary': '/{repo_name}',
41 40 'repo_creating_check': '/{repo_name}/repo_creating_check',
42 41 'repo_fork_new': '/{repo_name}/fork',
43 42 'repo_fork_create': '/{repo_name}/fork/create',
44 43 'repo_forks_show_all': '/{repo_name}/forks',
45 44 'repo_forks_data': '/{repo_name}/forks/data',
46 45 }[name].format(**kwargs)
47 46
48 47 if params:
49 48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
50 49 return base_url
51 50
52 51
53 52 FORK_NAME = {
54 53 'hg': HG_FORK,
55 54 'git': GIT_FORK
56 55 }
57 56
58 57
59 58 @pytest.mark.skip_backends('svn')
60 59 class TestRepoForkViewTests(TestController):
61 60
62 61 def test_show_forks(self, backend, xhr_header):
63 62 self.log_user()
64 63 response = self.app.get(
65 64 route_path('repo_forks_data', repo_name=backend.repo_name),
66 65 extra_environ=xhr_header)
67 66
68 67 assert response.json == {u'data': [], u'draw': None,
69 68 u'recordsFiltered': 0, u'recordsTotal': 0}
70 69
71 70 def test_no_permissions_to_fork_page(self, backend, user_util):
72 71 user = user_util.create_user(password='qweqwe')
73 72 user_id = user.user_id
74 73 self.log_user(user.username, 'qweqwe')
75 74
76 75 user_model = UserModel()
77 76 user_model.revoke_perm(user_id, 'hg.fork.repository')
78 77 user_model.grant_perm(user_id, 'hg.fork.none')
79 78 u = UserModel().get(user_id)
80 79 u.inherit_default_permissions = False
81 80 Session().commit()
82 81 # try create a fork
83 82 self.app.get(
84 83 route_path('repo_fork_new', repo_name=backend.repo_name),
85 84 status=404)
86 85
87 86 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
88 87 user = user_util.create_user(password='qweqwe')
89 88 user_id = user.user_id
90 89 self.log_user(user.username, 'qweqwe')
91 90
92 91 user_model = UserModel()
93 92 user_model.revoke_perm(user_id, 'hg.fork.repository')
94 93 user_model.grant_perm(user_id, 'hg.fork.none')
95 94 u = UserModel().get(user_id)
96 95 u.inherit_default_permissions = False
97 96 Session().commit()
98 97 # try create a fork
99 98 self.app.post(
100 99 route_path('repo_fork_create', repo_name=backend.repo_name),
101 100 {'csrf_token': csrf_token},
102 101 status=404)
103 102
104 103 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
105 104 # try create a fork
106 105 response = self.app.post(
107 106 route_path('repo_fork_create', repo_name=backend.repo_name),
108 107 {'csrf_token': csrf_token},
109 108 status=200)
110 109 # test if html fill works fine
111 110 response.mustcontain('Missing value')
112 111
113 112 def test_create_fork_page(self, autologin_user, backend):
114 113 self.app.get(
115 114 route_path('repo_fork_new', repo_name=backend.repo_name),
116 115 status=200)
117 116
118 117 def test_create_and_show_fork(
119 118 self, autologin_user, backend, csrf_token, xhr_header):
120 119
121 120 # create a fork
122 121 fork_name = FORK_NAME[backend.alias]
123 122 description = 'fork of vcs test'
124 123 repo_name = backend.repo_name
125 124 source_repo = Repository.get_by_repo_name(repo_name)
126 125 creation_args = {
127 126 'repo_name': fork_name,
128 127 'repo_group': '',
129 128 'fork_parent_id': source_repo.repo_id,
130 129 'repo_type': backend.alias,
131 130 'description': description,
132 131 'private': 'False',
133 132 'csrf_token': csrf_token,
134 133 }
135 134
136 135 self.app.post(
137 136 route_path('repo_fork_create', repo_name=repo_name), creation_args)
138 137
139 138 response = self.app.get(
140 139 route_path('repo_forks_data', repo_name=repo_name),
141 140 extra_environ=xhr_header)
142 141
143 142 assert response.json['data'][0]['fork_name'] == \
144 143 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
145 144
146 145 # remove this fork
147 146 fixture.destroy_repo(fork_name)
148 147
149 148 def test_fork_create(self, autologin_user, backend, csrf_token):
150 149 fork_name = FORK_NAME[backend.alias]
151 150 description = 'fork of vcs test'
152 151 repo_name = backend.repo_name
153 152 source_repo = Repository.get_by_repo_name(repo_name)
154 153 creation_args = {
155 154 'repo_name': fork_name,
156 155 'repo_group': '',
157 156 'fork_parent_id': source_repo.repo_id,
158 157 'repo_type': backend.alias,
159 158 'description': description,
160 159 'private': 'False',
161 160 'csrf_token': csrf_token,
162 161 }
163 162 self.app.post(
164 163 route_path('repo_fork_create', repo_name=repo_name), creation_args)
165 164 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
166 165 assert repo.fork.repo_name == backend.repo_name
167 166
168 167 # run the check page that triggers the flash message
169 168 response = self.app.get(
170 169 route_path('repo_creating_check', repo_name=fork_name))
171 170 # test if we have a message that fork is ok
172 171 assert_session_flash(response,
173 172 'Forked repository %s as <a href="/%s">%s</a>' % (
174 173 repo_name, fork_name, fork_name))
175 174
176 175 # test if the fork was created in the database
177 176 fork_repo = Session().query(Repository)\
178 177 .filter(Repository.repo_name == fork_name).one()
179 178
180 179 assert fork_repo.repo_name == fork_name
181 180 assert fork_repo.fork.repo_name == repo_name
182 181
183 182 # test if the repository is visible in the list ?
184 183 response = self.app.get(
185 184 h.route_path('repo_summary', repo_name=fork_name))
186 185 response.mustcontain(fork_name)
187 186 response.mustcontain(backend.alias)
188 187 response.mustcontain('Fork of')
189 188 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
190 189
191 190 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
192 191 group = fixture.create_repo_group('vc')
193 192 group_id = group.group_id
194 193 fork_name = FORK_NAME[backend.alias]
195 194 fork_name_full = 'vc/%s' % fork_name
196 195 description = 'fork of vcs test'
197 196 repo_name = backend.repo_name
198 197 source_repo = Repository.get_by_repo_name(repo_name)
199 198 creation_args = {
200 199 'repo_name': fork_name,
201 200 'repo_group': group_id,
202 201 'fork_parent_id': source_repo.repo_id,
203 202 'repo_type': backend.alias,
204 203 'description': description,
205 204 'private': 'False',
206 205 'csrf_token': csrf_token,
207 206 }
208 207 self.app.post(
209 208 route_path('repo_fork_create', repo_name=repo_name), creation_args)
210 209 repo = Repository.get_by_repo_name(fork_name_full)
211 210 assert repo.fork.repo_name == backend.repo_name
212 211
213 212 # run the check page that triggers the flash message
214 213 response = self.app.get(
215 214 route_path('repo_creating_check', repo_name=fork_name_full))
216 215 # test if we have a message that fork is ok
217 216 assert_session_flash(response,
218 217 'Forked repository %s as <a href="/%s">%s</a>' % (
219 218 repo_name, fork_name_full, fork_name_full))
220 219
221 220 # test if the fork was created in the database
222 221 fork_repo = Session().query(Repository)\
223 222 .filter(Repository.repo_name == fork_name_full).one()
224 223
225 224 assert fork_repo.repo_name == fork_name_full
226 225 assert fork_repo.fork.repo_name == repo_name
227 226
228 227 # test if the repository is visible in the list ?
229 228 response = self.app.get(
230 229 h.route_path('repo_summary', repo_name=fork_name_full))
231 230 response.mustcontain(fork_name_full)
232 231 response.mustcontain(backend.alias)
233 232
234 233 response.mustcontain('Fork of')
235 234 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
236 235
237 236 fixture.destroy_repo(fork_name_full)
238 237 fixture.destroy_repo_group(group_id)
239 238
240 239 def test_fork_read_permission(self, backend, xhr_header, user_util):
241 240 user = user_util.create_user(password='qweqwe')
242 241 user_id = user.user_id
243 242 self.log_user(user.username, 'qweqwe')
244 243
245 244 # create a fake fork
246 245 fork = user_util.create_repo(repo_type=backend.alias)
247 246 source = user_util.create_repo(repo_type=backend.alias)
248 247 repo_name = source.repo_name
249 248
250 249 fork.fork_id = source.repo_id
251 250 fork_name = fork.repo_name
252 251 Session().commit()
253 252
254 253 forks = Repository.query()\
255 254 .filter(Repository.repo_type == backend.alias)\
256 255 .filter(Repository.fork_id == source.repo_id).all()
257 256 assert 1 == len(forks)
258 257
259 258 # set read permissions for this
260 259 RepoModel().grant_user_permission(
261 260 repo=forks[0], user=user_id, perm='repository.read')
262 261 Session().commit()
263 262
264 263 response = self.app.get(
265 264 route_path('repo_forks_data', repo_name=repo_name),
266 265 extra_environ=xhr_header)
267 266
268 267 assert response.json['data'][0]['fork_name'] == \
269 268 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
270 269
271 270 def test_fork_none_permission(self, backend, xhr_header, user_util):
272 271 user = user_util.create_user(password='qweqwe')
273 272 user_id = user.user_id
274 273 self.log_user(user.username, 'qweqwe')
275 274
276 275 # create a fake fork
277 276 fork = user_util.create_repo(repo_type=backend.alias)
278 277 source = user_util.create_repo(repo_type=backend.alias)
279 278 repo_name = source.repo_name
280 279
281 280 fork.fork_id = source.repo_id
282 281
283 282 Session().commit()
284 283
285 284 forks = Repository.query()\
286 285 .filter(Repository.repo_type == backend.alias)\
287 286 .filter(Repository.fork_id == source.repo_id).all()
288 287 assert 1 == len(forks)
289 288
290 289 # set none
291 290 RepoModel().grant_user_permission(
292 291 repo=forks[0], user=user_id, perm='repository.none')
293 292 Session().commit()
294 293
295 294 # fork shouldn't be there
296 295 response = self.app.get(
297 296 route_path('repo_forks_data', repo_name=repo_name),
298 297 extra_environ=xhr_header)
299 298
300 299 assert response.json == {u'data': [], u'draw': None,
301 300 u'recordsFiltered': 0, u'recordsTotal': 0}
302 301
303 302 @pytest.mark.parametrize('url_type', [
304 303 'repo_fork_new',
305 304 'repo_fork_create'
306 305 ])
307 306 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
308 307 user = user_util.create_user(password='qweqwe')
309 308 self.log_user(user.username, 'qweqwe')
310 309
311 310 # create a temporary repo
312 311 source = user_util.create_repo(repo_type=backend.alias)
313 312 repo_name = source.repo_name
314 313 repo = Repository.get_by_repo_name(repo_name)
315 314 repo.archived = True
316 315 Session().commit()
317 316
318 317 response = self.app.get(
319 318 route_path(url_type, repo_name=repo_name), status=302)
320 319
321 320 msg = 'Action not supported for archived repository.'
322 321 assert_session_flash(response, msg)
323 322
324 323
325 324 class TestSVNFork(TestController):
326 325 @pytest.mark.parametrize('route_name', [
327 326 'repo_fork_create', 'repo_fork_new'
328 327 ])
329 328 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
330 329
331 330 self.app.get(route_path(
332 331 route_name, repo_name=backend_svn.repo_name),
333 332 status=404)
@@ -1,149 +1,148 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.lib.utils2 import md5
24 23 from rhodecode.model.db import Repository
25 24 from rhodecode.model.meta import Session
26 25 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
27 26
28 27
29 28 def route_path(name, params=None, **kwargs):
30 29 import urllib.request, urllib.parse, urllib.error
31 30
32 31 base_url = {
33 32 'repo_summary': '/{repo_name}',
34 33 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
35 34 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
36 35 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
37 36 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
38 37 }[name].format(**kwargs)
39 38
40 39 if params:
41 40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 41 return base_url
43 42
44 43
45 44 @pytest.mark.usefixtures("app")
46 45 class TestRepoIssueTracker(object):
47 46 def test_issuetracker_index(self, autologin_user, backend):
48 47 repo = backend.create_repo()
49 48 response = self.app.get(route_path('edit_repo_issuetracker',
50 49 repo_name=repo.repo_name))
51 50 assert response.status_code == 200
52 51
53 52 def test_add_and_test_issuetracker_patterns(
54 53 self, autologin_user, backend, csrf_token, request, xhr_header):
55 54 pattern = 'issuetracker_pat'
56 55 another_pattern = pattern+'1'
57 56 post_url = route_path(
58 57 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
59 58 post_data = {
60 59 'new_pattern_pattern_0': pattern,
61 60 'new_pattern_url_0': 'http://url',
62 61 'new_pattern_prefix_0': 'prefix',
63 62 'new_pattern_description_0': 'description',
64 63 'new_pattern_pattern_1': another_pattern,
65 64 'new_pattern_url_1': '/url1',
66 65 'new_pattern_prefix_1': 'prefix1',
67 66 'new_pattern_description_1': 'description1',
68 67 'csrf_token': csrf_token
69 68 }
70 69 self.app.post(post_url, post_data, status=302)
71 70 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
72 71 settings = self.settings_model.get_repo_settings()
73 72 self.uid = md5(pattern)
74 73 assert settings[self.uid]['pat'] == pattern
75 74 self.another_uid = md5(another_pattern)
76 75 assert settings[self.another_uid]['pat'] == another_pattern
77 76
78 77 # test pattern
79 78 data = {'test_text': 'example of issuetracker_pat replacement',
80 79 'csrf_token': csrf_token}
81 80 response = self.app.post(
82 81 route_path('edit_repo_issuetracker_test',
83 82 repo_name=backend.repo.repo_name),
84 83 extra_environ=xhr_header, params=data)
85 84
86 85 assert response.text == \
87 86 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
88 87
89 88 @request.addfinalizer
90 89 def cleanup():
91 90 self.settings_model.delete_entries(self.uid)
92 91 self.settings_model.delete_entries(self.another_uid)
93 92
94 93 def test_edit_issuetracker_pattern(
95 94 self, autologin_user, backend, csrf_token, request):
96 95 entry_key = 'issuetracker_pat_'
97 96 pattern = 'issuetracker_pat2'
98 97 old_pattern = 'issuetracker_pat'
99 98 old_uid = md5(old_pattern)
100 99
101 100 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
102 101 entry_key+old_uid, old_pattern, 'unicode')
103 102 Session().add(sett)
104 103 Session().commit()
105 104 post_url = route_path(
106 105 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
107 106 post_data = {
108 107 'new_pattern_pattern_0': pattern,
109 108 'new_pattern_url_0': '/url',
110 109 'new_pattern_prefix_0': 'prefix',
111 110 'new_pattern_description_0': 'description',
112 111 'uid': old_uid,
113 112 'csrf_token': csrf_token
114 113 }
115 114 self.app.post(post_url, post_data, status=302)
116 115 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
117 116 settings = self.settings_model.get_repo_settings()
118 117 self.uid = md5(pattern)
119 118 assert settings[self.uid]['pat'] == pattern
120 119 with pytest.raises(KeyError):
121 120 key = settings[old_uid]
122 121
123 122 @request.addfinalizer
124 123 def cleanup():
125 124 self.settings_model.delete_entries(self.uid)
126 125
127 126 def test_delete_issuetracker_pattern(
128 127 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
129 128 repo = backend.create_repo()
130 129 repo_name = repo.repo_name
131 130 entry_key = 'issuetracker_pat_'
132 131 pattern = 'issuetracker_pat3'
133 132 uid = md5(pattern)
134 133 settings_util.create_repo_rhodecode_setting(
135 134 repo=backend.repo, name=entry_key+uid,
136 135 value=entry_key, type_='unicode', cleanup=False)
137 136
138 137 self.app.post(
139 138 route_path(
140 139 'edit_repo_issuetracker_delete',
141 140 repo_name=backend.repo.repo_name),
142 141 {
143 142 'uid': uid,
144 143 'csrf_token': csrf_token,
145 144 '': ''
146 145 }, extra_environ=xhr_header, status=200)
147 146 settings = IssueTrackerSettingsModel(
148 147 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
149 148 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,74 +1,73 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import mock
22 21 import pytest
23 22
24 23 from rhodecode.lib.utils2 import str2bool
25 24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 25 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
27 26 from rhodecode.model.meta import Session
28 27 from rhodecode.tests import (
29 28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
30 29 from rhodecode.tests.fixture import Fixture
31 30
32 31 fixture = Fixture()
33 32
34 33
35 34 def route_path(name, params=None, **kwargs):
36 35 import urllib.request, urllib.parse, urllib.error
37 36
38 37 base_url = {
39 38 'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
40 39 'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
41 40
42 41 }[name].format(**kwargs)
43 42
44 43 if params:
45 44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 45 return base_url
47 46
48 47
49 48 def _get_permission_for_user(user, repo):
50 49 perm = UserRepoToPerm.query()\
51 50 .filter(UserRepoToPerm.repository ==
52 51 Repository.get_by_repo_name(repo))\
53 52 .filter(UserRepoToPerm.user == User.get_by_username(user))\
54 53 .all()
55 54 return perm
56 55
57 56
58 57 @pytest.mark.usefixtures('autologin_user', 'app')
59 58 class TestAdminRepoMaintenance(object):
60 59 @pytest.mark.parametrize('urlname', [
61 60 'edit_repo_maintenance',
62 61 ])
63 62 def test_show_page(self, urlname, app, backend):
64 63 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
65 64
66 65 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
67 66 repo_name = backend_hg.repo_name
68 67
69 68 response = app.get(
70 69 route_path('edit_repo_maintenance_execute',
71 70 repo_name=repo_name,),
72 71 extra_environ=xhr_header)
73 72
74 73 assert "HG Verify repo" in ''.join(response.json)
@@ -1,77 +1,76 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests.utils import permission_update_data_generator
24 23
25 24
26 25 def route_path(name, params=None, **kwargs):
27 26 import urllib.request, urllib.parse, urllib.error
28 27
29 28 base_url = {
30 29 'edit_repo_perms': '/{repo_name}/settings/permissions'
31 30 # update is the same url
32 31 }[name].format(**kwargs)
33 32
34 33 if params:
35 34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 35 return base_url
37 36
38 37
39 38 @pytest.mark.usefixtures("app")
40 39 class TestRepoPermissionsView(object):
41 40
42 41 def test_edit_perms_view(self, user_util, autologin_user):
43 42 repo = user_util.create_repo()
44 43 self.app.get(
45 44 route_path('edit_repo_perms',
46 45 repo_name=repo.repo_name), status=200)
47 46
48 47 def test_update_permissions(self, csrf_token, user_util):
49 48 repo = user_util.create_repo()
50 49 repo_name = repo.repo_name
51 50 user = user_util.create_user()
52 51 user_id = user.user_id
53 52 username = user.username
54 53
55 54 # grant new
56 55 form_data = permission_update_data_generator(
57 56 csrf_token,
58 57 default='repository.write',
59 58 grant=[(user_id, 'repository.write', username, 'user')])
60 59
61 60 response = self.app.post(
62 61 route_path('edit_repo_perms',
63 62 repo_name=repo_name), form_data).follow()
64 63
65 64 assert 'Repository access permissions updated' in response
66 65
67 66 # revoke given
68 67 form_data = permission_update_data_generator(
69 68 csrf_token,
70 69 default='repository.read',
71 70 revoke=[(user_id, 'user')])
72 71
73 72 response = self.app.post(
74 73 route_path('edit_repo_perms',
75 74 repo_name=repo_name), form_data).follow()
76 75
77 76 assert 'Repository access permissions updated' in response
@@ -1,1680 +1,1679 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19 import mock
21 20 import pytest
22 21
23 22 import rhodecode
24 23 from rhodecode.lib import helpers as h
25 24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
26 25 from rhodecode.lib.vcs.nodes import FileNode
27 26 from rhodecode.lib.ext_json import json
28 27 from rhodecode.model.changeset_status import ChangesetStatusModel
29 28 from rhodecode.model.db import (
30 29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
31 30 from rhodecode.model.meta import Session
32 31 from rhodecode.model.pull_request import PullRequestModel
33 32 from rhodecode.model.user import UserModel
34 33 from rhodecode.model.comment import CommentsModel
35 34 from rhodecode.tests import (
36 35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
37 36
38 37
39 38 def route_path(name, params=None, **kwargs):
40 39 import urllib.request, urllib.parse, urllib.error
41 40
42 41 base_url = {
43 42 'repo_changelog': '/{repo_name}/changelog',
44 43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
45 44 'repo_commits': '/{repo_name}/commits',
46 45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
47 46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
48 47 'pullrequest_show_all': '/{repo_name}/pull-request',
49 48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
50 49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
51 50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
52 51 'pullrequest_new': '/{repo_name}/pull-request/new',
53 52 'pullrequest_create': '/{repo_name}/pull-request/create',
54 53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
55 54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
56 55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
57 56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
58 57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
59 58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
60 59 }[name].format(**kwargs)
61 60
62 61 if params:
63 62 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
64 63 return base_url
65 64
66 65
67 66 @pytest.mark.usefixtures('app', 'autologin_user')
68 67 @pytest.mark.backends("git", "hg")
69 68 class TestPullrequestsView(object):
70 69
71 70 def test_index(self, backend):
72 71 self.app.get(route_path(
73 72 'pullrequest_new',
74 73 repo_name=backend.repo_name))
75 74
76 75 def test_option_menu_create_pull_request_exists(self, backend):
77 76 repo_name = backend.repo_name
78 77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
79 78
80 79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
81 80 'pullrequest_new', repo_name=repo_name)
82 81 response.mustcontain(create_pr_link)
83 82
84 83 def test_create_pr_form_with_raw_commit_id(self, backend):
85 84 repo = backend.repo
86 85
87 86 self.app.get(
88 87 route_path('pullrequest_new', repo_name=repo.repo_name,
89 88 commit=repo.get_commit().raw_id),
90 89 status=200)
91 90
92 91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
93 92 @pytest.mark.parametrize('range_diff', ["0", "1"])
94 93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
95 94 pull_request = pr_util.create_pull_request(
96 95 mergeable=pr_merge_enabled, enable_notifications=False)
97 96
98 97 response = self.app.get(route_path(
99 98 'pullrequest_show',
100 99 repo_name=pull_request.target_repo.scm_instance().name,
101 100 pull_request_id=pull_request.pull_request_id,
102 101 params={'range-diff': range_diff}))
103 102
104 103 for commit_id in pull_request.revisions:
105 104 response.mustcontain(commit_id)
106 105
107 106 response.mustcontain(pull_request.target_ref_parts.type)
108 107 response.mustcontain(pull_request.target_ref_parts.name)
109 108
110 109 response.mustcontain('class="pull-request-merge"')
111 110
112 111 if pr_merge_enabled:
113 112 response.mustcontain('Pull request reviewer approval is pending')
114 113 else:
115 114 response.mustcontain('Server-side pull request merging is disabled.')
116 115
117 116 if range_diff == "1":
118 117 response.mustcontain('Turn off: Show the diff as commit range')
119 118
120 119 def test_show_versions_of_pr(self, backend, csrf_token):
121 120 commits = [
122 121 {'message': 'initial-commit',
123 122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
124 123
125 124 {'message': 'commit-1',
126 125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
127 126 # Above is the initial version of PR that changes a single line
128 127
129 128 # from now on we'll add 3x commit adding a nother line on each step
130 129 {'message': 'commit-2',
131 130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
132 131
133 132 {'message': 'commit-3',
134 133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
135 134
136 135 {'message': 'commit-4',
137 136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
138 137 ]
139 138
140 139 commit_ids = backend.create_master_repo(commits)
141 140 target = backend.create_repo(heads=['initial-commit'])
142 141 source = backend.create_repo(heads=['commit-1'])
143 142 source_repo_name = source.repo_name
144 143 target_repo_name = target.repo_name
145 144
146 145 target_ref = 'branch:{branch}:{commit_id}'.format(
147 146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
148 147 source_ref = 'branch:{branch}:{commit_id}'.format(
149 148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
150 149
151 150 response = self.app.post(
152 151 route_path('pullrequest_create', repo_name=source.repo_name),
153 152 [
154 153 ('source_repo', source_repo_name),
155 154 ('source_ref', source_ref),
156 155 ('target_repo', target_repo_name),
157 156 ('target_ref', target_ref),
158 157 ('common_ancestor', commit_ids['initial-commit']),
159 158 ('pullrequest_title', 'Title'),
160 159 ('pullrequest_desc', 'Description'),
161 160 ('description_renderer', 'markdown'),
162 161 ('__start__', 'review_members:sequence'),
163 162 ('__start__', 'reviewer:mapping'),
164 163 ('user_id', '1'),
165 164 ('__start__', 'reasons:sequence'),
166 165 ('reason', 'Some reason'),
167 166 ('__end__', 'reasons:sequence'),
168 167 ('__start__', 'rules:sequence'),
169 168 ('__end__', 'rules:sequence'),
170 169 ('mandatory', 'False'),
171 170 ('__end__', 'reviewer:mapping'),
172 171 ('__end__', 'review_members:sequence'),
173 172 ('__start__', 'revisions:sequence'),
174 173 ('revisions', commit_ids['commit-1']),
175 174 ('__end__', 'revisions:sequence'),
176 175 ('user', ''),
177 176 ('csrf_token', csrf_token),
178 177 ],
179 178 status=302)
180 179
181 180 location = response.headers['Location']
182 181
183 182 pull_request_id = location.rsplit('/', 1)[1]
184 183 assert pull_request_id != 'new'
185 184 pull_request = PullRequest.get(int(pull_request_id))
186 185
187 186 pull_request_id = pull_request.pull_request_id
188 187
189 188 # Show initial version of PR
190 189 response = self.app.get(
191 190 route_path('pullrequest_show',
192 191 repo_name=target_repo_name,
193 192 pull_request_id=pull_request_id))
194 193
195 194 response.mustcontain('commit-1')
196 195 response.mustcontain(no=['commit-2'])
197 196 response.mustcontain(no=['commit-3'])
198 197 response.mustcontain(no=['commit-4'])
199 198
200 199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
201 200 response.mustcontain(no=['LINE3'])
202 201 response.mustcontain(no=['LINE4'])
203 202 response.mustcontain(no=['LINE5'])
204 203
205 204 # update PR #1
206 205 source_repo = Repository.get_by_repo_name(source_repo_name)
207 206 backend.pull_heads(source_repo, heads=['commit-2'])
208 207 response = self.app.post(
209 208 route_path('pullrequest_update',
210 209 repo_name=target_repo_name, pull_request_id=pull_request_id),
211 210 params={'update_commits': 'true', 'csrf_token': csrf_token})
212 211
213 212 # update PR #2
214 213 source_repo = Repository.get_by_repo_name(source_repo_name)
215 214 backend.pull_heads(source_repo, heads=['commit-3'])
216 215 response = self.app.post(
217 216 route_path('pullrequest_update',
218 217 repo_name=target_repo_name, pull_request_id=pull_request_id),
219 218 params={'update_commits': 'true', 'csrf_token': csrf_token})
220 219
221 220 # update PR #3
222 221 source_repo = Repository.get_by_repo_name(source_repo_name)
223 222 backend.pull_heads(source_repo, heads=['commit-4'])
224 223 response = self.app.post(
225 224 route_path('pullrequest_update',
226 225 repo_name=target_repo_name, pull_request_id=pull_request_id),
227 226 params={'update_commits': 'true', 'csrf_token': csrf_token})
228 227
229 228 # Show final version !
230 229 response = self.app.get(
231 230 route_path('pullrequest_show',
232 231 repo_name=target_repo_name,
233 232 pull_request_id=pull_request_id))
234 233
235 234 # 3 updates, and the latest == 4
236 235 response.mustcontain('4 versions available for this pull request')
237 236 response.mustcontain(no=['rhodecode diff rendering error'])
238 237
239 238 # initial show must have 3 commits, and 3 adds
240 239 response.mustcontain('commit-1')
241 240 response.mustcontain('commit-2')
242 241 response.mustcontain('commit-3')
243 242 response.mustcontain('commit-4')
244 243
245 244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
246 245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
247 246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
248 247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
249 248
250 249 # fetch versions
251 250 pr = PullRequest.get(pull_request_id)
252 251 versions = [x.pull_request_version_id for x in pr.versions.all()]
253 252 assert len(versions) == 3
254 253
255 254 # show v1,v2,v3,v4
256 255 def cb_line(text):
257 256 return 'cb-addition"></span><span>{}</span>'.format(text)
258 257
259 258 def cb_context(text):
260 259 return '<span class="cb-code"><span class="cb-action cb-context">' \
261 260 '</span><span>{}</span></span>'.format(text)
262 261
263 262 commit_tests = {
264 263 # in response, not in response
265 264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
266 265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
267 266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
268 267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
269 268 }
270 269 diff_tests = {
271 270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
272 271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
273 272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
274 273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
275 274 }
276 275 for idx, ver in enumerate(versions, 1):
277 276
278 277 response = self.app.get(
279 278 route_path('pullrequest_show',
280 279 repo_name=target_repo_name,
281 280 pull_request_id=pull_request_id,
282 281 params={'version': ver}))
283 282
284 283 response.mustcontain(no=['rhodecode diff rendering error'])
285 284 response.mustcontain('Showing changes at v{}'.format(idx))
286 285
287 286 yes, no = commit_tests[idx]
288 287 for y in yes:
289 288 response.mustcontain(y)
290 289 for n in no:
291 290 response.mustcontain(no=n)
292 291
293 292 yes, no = diff_tests[idx]
294 293 for y in yes:
295 294 response.mustcontain(cb_line(y))
296 295 for n in no:
297 296 response.mustcontain(no=n)
298 297
299 298 # show diff between versions
300 299 diff_compare_tests = {
301 300 1: (['LINE3'], ['LINE1', 'LINE2']),
302 301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
303 302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
304 303 }
305 304 for idx, ver in enumerate(versions, 1):
306 305 adds, context = diff_compare_tests[idx]
307 306
308 307 to_ver = ver+1
309 308 if idx == 3:
310 309 to_ver = 'latest'
311 310
312 311 response = self.app.get(
313 312 route_path('pullrequest_show',
314 313 repo_name=target_repo_name,
315 314 pull_request_id=pull_request_id,
316 315 params={'from_version': versions[0], 'version': to_ver}))
317 316
318 317 response.mustcontain(no=['rhodecode diff rendering error'])
319 318
320 319 for a in adds:
321 320 response.mustcontain(cb_line(a))
322 321 for c in context:
323 322 response.mustcontain(cb_context(c))
324 323
325 324 # test version v2 -> v3
326 325 response = self.app.get(
327 326 route_path('pullrequest_show',
328 327 repo_name=target_repo_name,
329 328 pull_request_id=pull_request_id,
330 329 params={'from_version': versions[1], 'version': versions[2]}))
331 330
332 331 response.mustcontain(cb_context('LINE1'))
333 332 response.mustcontain(cb_context('LINE2'))
334 333 response.mustcontain(cb_context('LINE3'))
335 334 response.mustcontain(cb_line('LINE4'))
336 335
337 336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
338 337 # Logout
339 338 response = self.app.post(
340 339 h.route_path('logout'),
341 340 params={'csrf_token': csrf_token})
342 341 # Login as regular user
343 342 response = self.app.post(h.route_path('login'),
344 343 {'username': TEST_USER_REGULAR_LOGIN,
345 344 'password': 'test12'})
346 345
347 346 pull_request = pr_util.create_pull_request(
348 347 author=TEST_USER_REGULAR_LOGIN)
349 348
350 349 response = self.app.get(route_path(
351 350 'pullrequest_show',
352 351 repo_name=pull_request.target_repo.scm_instance().name,
353 352 pull_request_id=pull_request.pull_request_id))
354 353
355 354 response.mustcontain('Server-side pull request merging is disabled.')
356 355
357 356 assert_response = response.assert_response()
358 357 # for regular user without a merge permissions, we don't see it
359 358 assert_response.no_element_exists('#close-pull-request-action')
360 359
361 360 user_util.grant_user_permission_to_repo(
362 361 pull_request.target_repo,
363 362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
364 363 'repository.write')
365 364 response = self.app.get(route_path(
366 365 'pullrequest_show',
367 366 repo_name=pull_request.target_repo.scm_instance().name,
368 367 pull_request_id=pull_request.pull_request_id))
369 368
370 369 response.mustcontain('Server-side pull request merging is disabled.')
371 370
372 371 assert_response = response.assert_response()
373 372 # now regular user has a merge permissions, we have CLOSE button
374 373 assert_response.one_element_exists('#close-pull-request-action')
375 374
376 375 def test_show_invalid_commit_id(self, pr_util):
377 376 # Simulating invalid revisions which will cause a lookup error
378 377 pull_request = pr_util.create_pull_request()
379 378 pull_request.revisions = ['invalid']
380 379 Session().add(pull_request)
381 380 Session().commit()
382 381
383 382 response = self.app.get(route_path(
384 383 'pullrequest_show',
385 384 repo_name=pull_request.target_repo.scm_instance().name,
386 385 pull_request_id=pull_request.pull_request_id))
387 386
388 387 for commit_id in pull_request.revisions:
389 388 response.mustcontain(commit_id)
390 389
391 390 def test_show_invalid_source_reference(self, pr_util):
392 391 pull_request = pr_util.create_pull_request()
393 392 pull_request.source_ref = 'branch:b:invalid'
394 393 Session().add(pull_request)
395 394 Session().commit()
396 395
397 396 self.app.get(route_path(
398 397 'pullrequest_show',
399 398 repo_name=pull_request.target_repo.scm_instance().name,
400 399 pull_request_id=pull_request.pull_request_id))
401 400
402 401 def test_edit_title_description(self, pr_util, csrf_token):
403 402 pull_request = pr_util.create_pull_request()
404 403 pull_request_id = pull_request.pull_request_id
405 404
406 405 response = self.app.post(
407 406 route_path('pullrequest_update',
408 407 repo_name=pull_request.target_repo.repo_name,
409 408 pull_request_id=pull_request_id),
410 409 params={
411 410 'edit_pull_request': 'true',
412 411 'title': 'New title',
413 412 'description': 'New description',
414 413 'csrf_token': csrf_token})
415 414
416 415 assert_session_flash(
417 416 response, u'Pull request title & description updated.',
418 417 category='success')
419 418
420 419 pull_request = PullRequest.get(pull_request_id)
421 420 assert pull_request.title == 'New title'
422 421 assert pull_request.description == 'New description'
423 422
424 423 def test_edit_title_description(self, pr_util, csrf_token):
425 424 pull_request = pr_util.create_pull_request()
426 425 pull_request_id = pull_request.pull_request_id
427 426
428 427 response = self.app.post(
429 428 route_path('pullrequest_update',
430 429 repo_name=pull_request.target_repo.repo_name,
431 430 pull_request_id=pull_request_id),
432 431 params={
433 432 'edit_pull_request': 'true',
434 433 'title': 'New title {} {2} {foo}',
435 434 'description': 'New description',
436 435 'csrf_token': csrf_token})
437 436
438 437 assert_session_flash(
439 438 response, u'Pull request title & description updated.',
440 439 category='success')
441 440
442 441 pull_request = PullRequest.get(pull_request_id)
443 442 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
444 443
445 444 def test_edit_title_description_closed(self, pr_util, csrf_token):
446 445 pull_request = pr_util.create_pull_request()
447 446 pull_request_id = pull_request.pull_request_id
448 447 repo_name = pull_request.target_repo.repo_name
449 448 pr_util.close()
450 449
451 450 response = self.app.post(
452 451 route_path('pullrequest_update',
453 452 repo_name=repo_name, pull_request_id=pull_request_id),
454 453 params={
455 454 'edit_pull_request': 'true',
456 455 'title': 'New title',
457 456 'description': 'New description',
458 457 'csrf_token': csrf_token}, status=200)
459 458 assert_session_flash(
460 459 response, u'Cannot update closed pull requests.',
461 460 category='error')
462 461
463 462 def test_update_invalid_source_reference(self, pr_util, csrf_token):
464 463 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
465 464
466 465 pull_request = pr_util.create_pull_request()
467 466 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
468 467 Session().add(pull_request)
469 468 Session().commit()
470 469
471 470 pull_request_id = pull_request.pull_request_id
472 471
473 472 response = self.app.post(
474 473 route_path('pullrequest_update',
475 474 repo_name=pull_request.target_repo.repo_name,
476 475 pull_request_id=pull_request_id),
477 476 params={'update_commits': 'true', 'csrf_token': csrf_token})
478 477
479 478 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
480 479 UpdateFailureReason.MISSING_SOURCE_REF])
481 480 assert_session_flash(response, expected_msg, category='error')
482 481
483 482 def test_missing_target_reference(self, pr_util, csrf_token):
484 483 from rhodecode.lib.vcs.backends.base import MergeFailureReason
485 484 pull_request = pr_util.create_pull_request(
486 485 approved=True, mergeable=True)
487 486 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
488 487 pull_request.target_ref = unicode_reference
489 488 Session().add(pull_request)
490 489 Session().commit()
491 490
492 491 pull_request_id = pull_request.pull_request_id
493 492 pull_request_url = route_path(
494 493 'pullrequest_show',
495 494 repo_name=pull_request.target_repo.repo_name,
496 495 pull_request_id=pull_request_id)
497 496
498 497 response = self.app.get(pull_request_url)
499 498 target_ref_id = 'invalid-branch'
500 499 merge_resp = MergeResponse(
501 500 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
502 501 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
503 502 response.assert_response().element_contains(
504 503 'div[data-role="merge-message"]', merge_resp.merge_status_message)
505 504
506 505 def test_comment_and_close_pull_request_custom_message_approved(
507 506 self, pr_util, csrf_token, xhr_header):
508 507
509 508 pull_request = pr_util.create_pull_request(approved=True)
510 509 pull_request_id = pull_request.pull_request_id
511 510 author = pull_request.user_id
512 511 repo = pull_request.target_repo.repo_id
513 512
514 513 self.app.post(
515 514 route_path('pullrequest_comment_create',
516 515 repo_name=pull_request.target_repo.scm_instance().name,
517 516 pull_request_id=pull_request_id),
518 517 params={
519 518 'close_pull_request': '1',
520 519 'text': 'Closing a PR',
521 520 'csrf_token': csrf_token},
522 521 extra_environ=xhr_header,)
523 522
524 523 journal = UserLog.query()\
525 524 .filter(UserLog.user_id == author)\
526 525 .filter(UserLog.repository_id == repo) \
527 526 .order_by(UserLog.user_log_id.asc()) \
528 527 .all()
529 528 assert journal[-1].action == 'repo.pull_request.close'
530 529
531 530 pull_request = PullRequest.get(pull_request_id)
532 531 assert pull_request.is_closed()
533 532
534 533 status = ChangesetStatusModel().get_status(
535 534 pull_request.source_repo, pull_request=pull_request)
536 535 assert status == ChangesetStatus.STATUS_APPROVED
537 536 comments = ChangesetComment().query() \
538 537 .filter(ChangesetComment.pull_request == pull_request) \
539 538 .order_by(ChangesetComment.comment_id.asc())\
540 539 .all()
541 540 assert comments[-1].text == 'Closing a PR'
542 541
543 542 def test_comment_force_close_pull_request_rejected(
544 543 self, pr_util, csrf_token, xhr_header):
545 544 pull_request = pr_util.create_pull_request()
546 545 pull_request_id = pull_request.pull_request_id
547 546 PullRequestModel().update_reviewers(
548 547 pull_request_id, [
549 548 (1, ['reason'], False, 'reviewer', []),
550 549 (2, ['reason2'], False, 'reviewer', [])],
551 550 pull_request.author)
552 551 author = pull_request.user_id
553 552 repo = pull_request.target_repo.repo_id
554 553
555 554 self.app.post(
556 555 route_path('pullrequest_comment_create',
557 556 repo_name=pull_request.target_repo.scm_instance().name,
558 557 pull_request_id=pull_request_id),
559 558 params={
560 559 'close_pull_request': '1',
561 560 'csrf_token': csrf_token},
562 561 extra_environ=xhr_header)
563 562
564 563 pull_request = PullRequest.get(pull_request_id)
565 564
566 565 journal = UserLog.query()\
567 566 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
568 567 .order_by(UserLog.user_log_id.asc()) \
569 568 .all()
570 569 assert journal[-1].action == 'repo.pull_request.close'
571 570
572 571 # check only the latest status, not the review status
573 572 status = ChangesetStatusModel().get_status(
574 573 pull_request.source_repo, pull_request=pull_request)
575 574 assert status == ChangesetStatus.STATUS_REJECTED
576 575
577 576 def test_comment_and_close_pull_request(
578 577 self, pr_util, csrf_token, xhr_header):
579 578 pull_request = pr_util.create_pull_request()
580 579 pull_request_id = pull_request.pull_request_id
581 580
582 581 response = self.app.post(
583 582 route_path('pullrequest_comment_create',
584 583 repo_name=pull_request.target_repo.scm_instance().name,
585 584 pull_request_id=pull_request.pull_request_id),
586 585 params={
587 586 'close_pull_request': 'true',
588 587 'csrf_token': csrf_token},
589 588 extra_environ=xhr_header)
590 589
591 590 assert response.json
592 591
593 592 pull_request = PullRequest.get(pull_request_id)
594 593 assert pull_request.is_closed()
595 594
596 595 # check only the latest status, not the review status
597 596 status = ChangesetStatusModel().get_status(
598 597 pull_request.source_repo, pull_request=pull_request)
599 598 assert status == ChangesetStatus.STATUS_REJECTED
600 599
601 600 def test_comment_and_close_pull_request_try_edit_comment(
602 601 self, pr_util, csrf_token, xhr_header
603 602 ):
604 603 pull_request = pr_util.create_pull_request()
605 604 pull_request_id = pull_request.pull_request_id
606 605 target_scm = pull_request.target_repo.scm_instance()
607 606 target_scm_name = target_scm.name
608 607
609 608 response = self.app.post(
610 609 route_path(
611 610 'pullrequest_comment_create',
612 611 repo_name=target_scm_name,
613 612 pull_request_id=pull_request_id,
614 613 ),
615 614 params={
616 615 'close_pull_request': 'true',
617 616 'csrf_token': csrf_token,
618 617 },
619 618 extra_environ=xhr_header)
620 619
621 620 assert response.json
622 621
623 622 pull_request = PullRequest.get(pull_request_id)
624 623 target_scm = pull_request.target_repo.scm_instance()
625 624 target_scm_name = target_scm.name
626 625 assert pull_request.is_closed()
627 626
628 627 # check only the latest status, not the review status
629 628 status = ChangesetStatusModel().get_status(
630 629 pull_request.source_repo, pull_request=pull_request)
631 630 assert status == ChangesetStatus.STATUS_REJECTED
632 631
633 632 for comment_id in response.json.keys():
634 633 test_text = 'test'
635 634 response = self.app.post(
636 635 route_path(
637 636 'pullrequest_comment_edit',
638 637 repo_name=target_scm_name,
639 638 pull_request_id=pull_request_id,
640 639 comment_id=comment_id,
641 640 ),
642 641 extra_environ=xhr_header,
643 642 params={
644 643 'csrf_token': csrf_token,
645 644 'text': test_text,
646 645 },
647 646 status=403,
648 647 )
649 648 assert response.status_int == 403
650 649
651 650 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
652 651 pull_request = pr_util.create_pull_request()
653 652 target_scm = pull_request.target_repo.scm_instance()
654 653 target_scm_name = target_scm.name
655 654
656 655 response = self.app.post(
657 656 route_path(
658 657 'pullrequest_comment_create',
659 658 repo_name=target_scm_name,
660 659 pull_request_id=pull_request.pull_request_id),
661 660 params={
662 661 'csrf_token': csrf_token,
663 662 'text': 'init',
664 663 },
665 664 extra_environ=xhr_header,
666 665 )
667 666 assert response.json
668 667
669 668 for comment_id in response.json.keys():
670 669 assert comment_id
671 670 test_text = 'test'
672 671 self.app.post(
673 672 route_path(
674 673 'pullrequest_comment_edit',
675 674 repo_name=target_scm_name,
676 675 pull_request_id=pull_request.pull_request_id,
677 676 comment_id=comment_id,
678 677 ),
679 678 extra_environ=xhr_header,
680 679 params={
681 680 'csrf_token': csrf_token,
682 681 'text': test_text,
683 682 'version': '0',
684 683 },
685 684
686 685 )
687 686 text_form_db = ChangesetComment.query().filter(
688 687 ChangesetComment.comment_id == comment_id).first().text
689 688 assert test_text == text_form_db
690 689
691 690 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
692 691 pull_request = pr_util.create_pull_request()
693 692 target_scm = pull_request.target_repo.scm_instance()
694 693 target_scm_name = target_scm.name
695 694
696 695 response = self.app.post(
697 696 route_path(
698 697 'pullrequest_comment_create',
699 698 repo_name=target_scm_name,
700 699 pull_request_id=pull_request.pull_request_id),
701 700 params={
702 701 'csrf_token': csrf_token,
703 702 'text': 'init',
704 703 },
705 704 extra_environ=xhr_header,
706 705 )
707 706 assert response.json
708 707
709 708 for comment_id in response.json.keys():
710 709 test_text = 'init'
711 710 response = self.app.post(
712 711 route_path(
713 712 'pullrequest_comment_edit',
714 713 repo_name=target_scm_name,
715 714 pull_request_id=pull_request.pull_request_id,
716 715 comment_id=comment_id,
717 716 ),
718 717 extra_environ=xhr_header,
719 718 params={
720 719 'csrf_token': csrf_token,
721 720 'text': test_text,
722 721 'version': '0',
723 722 },
724 723 status=404,
725 724
726 725 )
727 726 assert response.status_int == 404
728 727
729 728 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
730 729 pull_request = pr_util.create_pull_request()
731 730 target_scm = pull_request.target_repo.scm_instance()
732 731 target_scm_name = target_scm.name
733 732
734 733 response = self.app.post(
735 734 route_path(
736 735 'pullrequest_comment_create',
737 736 repo_name=target_scm_name,
738 737 pull_request_id=pull_request.pull_request_id),
739 738 params={
740 739 'csrf_token': csrf_token,
741 740 'text': 'init',
742 741 },
743 742 extra_environ=xhr_header,
744 743 )
745 744 assert response.json
746 745 for comment_id in response.json.keys():
747 746 test_text = 'test'
748 747 self.app.post(
749 748 route_path(
750 749 'pullrequest_comment_edit',
751 750 repo_name=target_scm_name,
752 751 pull_request_id=pull_request.pull_request_id,
753 752 comment_id=comment_id,
754 753 ),
755 754 extra_environ=xhr_header,
756 755 params={
757 756 'csrf_token': csrf_token,
758 757 'text': test_text,
759 758 'version': '0',
760 759 },
761 760
762 761 )
763 762 test_text_v2 = 'test_v2'
764 763 response = self.app.post(
765 764 route_path(
766 765 'pullrequest_comment_edit',
767 766 repo_name=target_scm_name,
768 767 pull_request_id=pull_request.pull_request_id,
769 768 comment_id=comment_id,
770 769 ),
771 770 extra_environ=xhr_header,
772 771 params={
773 772 'csrf_token': csrf_token,
774 773 'text': test_text_v2,
775 774 'version': '0',
776 775 },
777 776 status=409,
778 777 )
779 778 assert response.status_int == 409
780 779
781 780 text_form_db = ChangesetComment.query().filter(
782 781 ChangesetComment.comment_id == comment_id).first().text
783 782
784 783 assert test_text == text_form_db
785 784 assert test_text_v2 != text_form_db
786 785
787 786 def test_comment_and_comment_edit_permissions_forbidden(
788 787 self, autologin_regular_user, user_regular, user_admin, pr_util,
789 788 csrf_token, xhr_header):
790 789 pull_request = pr_util.create_pull_request(
791 790 author=user_admin.username, enable_notifications=False)
792 791 comment = CommentsModel().create(
793 792 text='test',
794 793 repo=pull_request.target_repo.scm_instance().name,
795 794 user=user_admin,
796 795 pull_request=pull_request,
797 796 )
798 797 response = self.app.post(
799 798 route_path(
800 799 'pullrequest_comment_edit',
801 800 repo_name=pull_request.target_repo.scm_instance().name,
802 801 pull_request_id=pull_request.pull_request_id,
803 802 comment_id=comment.comment_id,
804 803 ),
805 804 extra_environ=xhr_header,
806 805 params={
807 806 'csrf_token': csrf_token,
808 807 'text': 'test_text',
809 808 },
810 809 status=403,
811 810 )
812 811 assert response.status_int == 403
813 812
814 813 def test_create_pull_request(self, backend, csrf_token):
815 814 commits = [
816 815 {'message': 'ancestor'},
817 816 {'message': 'change'},
818 817 {'message': 'change2'},
819 818 ]
820 819 commit_ids = backend.create_master_repo(commits)
821 820 target = backend.create_repo(heads=['ancestor'])
822 821 source = backend.create_repo(heads=['change2'])
823 822
824 823 response = self.app.post(
825 824 route_path('pullrequest_create', repo_name=source.repo_name),
826 825 [
827 826 ('source_repo', source.repo_name),
828 827 ('source_ref', 'branch:default:' + commit_ids['change2']),
829 828 ('target_repo', target.repo_name),
830 829 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
831 830 ('common_ancestor', commit_ids['ancestor']),
832 831 ('pullrequest_title', 'Title'),
833 832 ('pullrequest_desc', 'Description'),
834 833 ('description_renderer', 'markdown'),
835 834 ('__start__', 'review_members:sequence'),
836 835 ('__start__', 'reviewer:mapping'),
837 836 ('user_id', '1'),
838 837 ('__start__', 'reasons:sequence'),
839 838 ('reason', 'Some reason'),
840 839 ('__end__', 'reasons:sequence'),
841 840 ('__start__', 'rules:sequence'),
842 841 ('__end__', 'rules:sequence'),
843 842 ('mandatory', 'False'),
844 843 ('__end__', 'reviewer:mapping'),
845 844 ('__end__', 'review_members:sequence'),
846 845 ('__start__', 'revisions:sequence'),
847 846 ('revisions', commit_ids['change']),
848 847 ('revisions', commit_ids['change2']),
849 848 ('__end__', 'revisions:sequence'),
850 849 ('user', ''),
851 850 ('csrf_token', csrf_token),
852 851 ],
853 852 status=302)
854 853
855 854 location = response.headers['Location']
856 855 pull_request_id = location.rsplit('/', 1)[1]
857 856 assert pull_request_id != 'new'
858 857 pull_request = PullRequest.get(int(pull_request_id))
859 858
860 859 # check that we have now both revisions
861 860 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
862 861 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
863 862 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
864 863 assert pull_request.target_ref == expected_target_ref
865 864
866 865 def test_reviewer_notifications(self, backend, csrf_token):
867 866 # We have to use the app.post for this test so it will create the
868 867 # notifications properly with the new PR
869 868 commits = [
870 869 {'message': 'ancestor',
871 870 'added': [FileNode('file_A', content='content_of_ancestor')]},
872 871 {'message': 'change',
873 872 'added': [FileNode('file_a', content='content_of_change')]},
874 873 {'message': 'change-child'},
875 874 {'message': 'ancestor-child', 'parents': ['ancestor'],
876 875 'added': [
877 876 FileNode('file_B', content='content_of_ancestor_child')]},
878 877 {'message': 'ancestor-child-2'},
879 878 ]
880 879 commit_ids = backend.create_master_repo(commits)
881 880 target = backend.create_repo(heads=['ancestor-child'])
882 881 source = backend.create_repo(heads=['change'])
883 882
884 883 response = self.app.post(
885 884 route_path('pullrequest_create', repo_name=source.repo_name),
886 885 [
887 886 ('source_repo', source.repo_name),
888 887 ('source_ref', 'branch:default:' + commit_ids['change']),
889 888 ('target_repo', target.repo_name),
890 889 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
891 890 ('common_ancestor', commit_ids['ancestor']),
892 891 ('pullrequest_title', 'Title'),
893 892 ('pullrequest_desc', 'Description'),
894 893 ('description_renderer', 'markdown'),
895 894 ('__start__', 'review_members:sequence'),
896 895 ('__start__', 'reviewer:mapping'),
897 896 ('user_id', '2'),
898 897 ('__start__', 'reasons:sequence'),
899 898 ('reason', 'Some reason'),
900 899 ('__end__', 'reasons:sequence'),
901 900 ('__start__', 'rules:sequence'),
902 901 ('__end__', 'rules:sequence'),
903 902 ('mandatory', 'False'),
904 903 ('__end__', 'reviewer:mapping'),
905 904 ('__end__', 'review_members:sequence'),
906 905 ('__start__', 'revisions:sequence'),
907 906 ('revisions', commit_ids['change']),
908 907 ('__end__', 'revisions:sequence'),
909 908 ('user', ''),
910 909 ('csrf_token', csrf_token),
911 910 ],
912 911 status=302)
913 912
914 913 location = response.headers['Location']
915 914
916 915 pull_request_id = location.rsplit('/', 1)[1]
917 916 assert pull_request_id != 'new'
918 917 pull_request = PullRequest.get(int(pull_request_id))
919 918
920 919 # Check that a notification was made
921 920 notifications = Notification.query()\
922 921 .filter(Notification.created_by == pull_request.author.user_id,
923 922 Notification.type_ == Notification.TYPE_PULL_REQUEST,
924 923 Notification.subject.contains(
925 924 "requested a pull request review. !%s" % pull_request_id))
926 925 assert len(notifications.all()) == 1
927 926
928 927 # Change reviewers and check that a notification was made
929 928 PullRequestModel().update_reviewers(
930 929 pull_request.pull_request_id, [
931 930 (1, [], False, 'reviewer', [])
932 931 ],
933 932 pull_request.author)
934 933 assert len(notifications.all()) == 2
935 934
936 935 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
937 936 commits = [
938 937 {'message': 'ancestor',
939 938 'added': [FileNode('file_A', content='content_of_ancestor')]},
940 939 {'message': 'change',
941 940 'added': [FileNode('file_a', content='content_of_change')]},
942 941 {'message': 'change-child'},
943 942 {'message': 'ancestor-child', 'parents': ['ancestor'],
944 943 'added': [
945 944 FileNode('file_B', content='content_of_ancestor_child')]},
946 945 {'message': 'ancestor-child-2'},
947 946 ]
948 947 commit_ids = backend.create_master_repo(commits)
949 948 target = backend.create_repo(heads=['ancestor-child'])
950 949 source = backend.create_repo(heads=['change'])
951 950
952 951 response = self.app.post(
953 952 route_path('pullrequest_create', repo_name=source.repo_name),
954 953 [
955 954 ('source_repo', source.repo_name),
956 955 ('source_ref', 'branch:default:' + commit_ids['change']),
957 956 ('target_repo', target.repo_name),
958 957 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
959 958 ('common_ancestor', commit_ids['ancestor']),
960 959 ('pullrequest_title', 'Title'),
961 960 ('pullrequest_desc', 'Description'),
962 961 ('description_renderer', 'markdown'),
963 962 ('__start__', 'review_members:sequence'),
964 963 ('__start__', 'reviewer:mapping'),
965 964 ('user_id', '1'),
966 965 ('__start__', 'reasons:sequence'),
967 966 ('reason', 'Some reason'),
968 967 ('__end__', 'reasons:sequence'),
969 968 ('__start__', 'rules:sequence'),
970 969 ('__end__', 'rules:sequence'),
971 970 ('mandatory', 'False'),
972 971 ('__end__', 'reviewer:mapping'),
973 972 ('__end__', 'review_members:sequence'),
974 973 ('__start__', 'revisions:sequence'),
975 974 ('revisions', commit_ids['change']),
976 975 ('__end__', 'revisions:sequence'),
977 976 ('user', ''),
978 977 ('csrf_token', csrf_token),
979 978 ],
980 979 status=302)
981 980
982 981 location = response.headers['Location']
983 982
984 983 pull_request_id = location.rsplit('/', 1)[1]
985 984 assert pull_request_id != 'new'
986 985 pull_request = PullRequest.get(int(pull_request_id))
987 986
988 987 # target_ref has to point to the ancestor's commit_id in order to
989 988 # show the correct diff
990 989 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
991 990 assert pull_request.target_ref == expected_target_ref
992 991
993 992 # Check generated diff contents
994 993 response = response.follow()
995 994 response.mustcontain(no=['content_of_ancestor'])
996 995 response.mustcontain(no=['content_of_ancestor-child'])
997 996 response.mustcontain('content_of_change')
998 997
999 998 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
1000 999 # Clear any previous calls to rcextensions
1001 1000 rhodecode.EXTENSIONS.calls.clear()
1002 1001
1003 1002 pull_request = pr_util.create_pull_request(
1004 1003 approved=True, mergeable=True)
1005 1004 pull_request_id = pull_request.pull_request_id
1006 1005 repo_name = pull_request.target_repo.scm_instance().name,
1007 1006
1008 1007 url = route_path('pullrequest_merge',
1009 1008 repo_name=str(repo_name[0]),
1010 1009 pull_request_id=pull_request_id)
1011 1010 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1012 1011
1013 1012 pull_request = PullRequest.get(pull_request_id)
1014 1013
1015 1014 assert response.status_int == 200
1016 1015 assert pull_request.is_closed()
1017 1016 assert_pull_request_status(
1018 1017 pull_request, ChangesetStatus.STATUS_APPROVED)
1019 1018
1020 1019 # Check the relevant log entries were added
1021 1020 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1022 1021 actions = [log.action for log in user_logs]
1023 1022 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1024 1023 expected_actions = [
1025 1024 u'repo.pull_request.close',
1026 1025 u'repo.pull_request.merge',
1027 1026 u'repo.pull_request.comment.create'
1028 1027 ]
1029 1028 assert actions == expected_actions
1030 1029
1031 1030 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1032 1031 actions = [log for log in user_logs]
1033 1032 assert actions[-1].action == 'user.push'
1034 1033 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1035 1034
1036 1035 # Check post_push rcextension was really executed
1037 1036 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1038 1037 assert len(push_calls) == 1
1039 1038 unused_last_call_args, last_call_kwargs = push_calls[0]
1040 1039 assert last_call_kwargs['action'] == 'push'
1041 1040 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1042 1041
1043 1042 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1044 1043 pull_request = pr_util.create_pull_request(mergeable=False)
1045 1044 pull_request_id = pull_request.pull_request_id
1046 1045 pull_request = PullRequest.get(pull_request_id)
1047 1046
1048 1047 response = self.app.post(
1049 1048 route_path('pullrequest_merge',
1050 1049 repo_name=pull_request.target_repo.scm_instance().name,
1051 1050 pull_request_id=pull_request.pull_request_id),
1052 1051 params={'csrf_token': csrf_token}).follow()
1053 1052
1054 1053 assert response.status_int == 200
1055 1054 response.mustcontain(
1056 1055 'Merge is not currently possible because of below failed checks.')
1057 1056 response.mustcontain('Server-side pull request merging is disabled.')
1058 1057
1059 1058 @pytest.mark.skip_backends('svn')
1060 1059 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1061 1060 pull_request = pr_util.create_pull_request(mergeable=True)
1062 1061 pull_request_id = pull_request.pull_request_id
1063 1062 repo_name = pull_request.target_repo.scm_instance().name
1064 1063
1065 1064 response = self.app.post(
1066 1065 route_path('pullrequest_merge',
1067 1066 repo_name=repo_name, pull_request_id=pull_request_id),
1068 1067 params={'csrf_token': csrf_token}).follow()
1069 1068
1070 1069 assert response.status_int == 200
1071 1070
1072 1071 response.mustcontain(
1073 1072 'Merge is not currently possible because of below failed checks.')
1074 1073 response.mustcontain('Pull request reviewer approval is pending.')
1075 1074
1076 1075 def test_merge_pull_request_renders_failure_reason(
1077 1076 self, user_regular, csrf_token, pr_util):
1078 1077 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1079 1078 pull_request_id = pull_request.pull_request_id
1080 1079 repo_name = pull_request.target_repo.scm_instance().name
1081 1080
1082 1081 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1083 1082 MergeFailureReason.PUSH_FAILED,
1084 1083 metadata={'target': 'shadow repo',
1085 1084 'merge_commit': 'xxx'})
1086 1085 model_patcher = mock.patch.multiple(
1087 1086 PullRequestModel,
1088 1087 merge_repo=mock.Mock(return_value=merge_resp),
1089 1088 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1090 1089
1091 1090 with model_patcher:
1092 1091 response = self.app.post(
1093 1092 route_path('pullrequest_merge',
1094 1093 repo_name=repo_name,
1095 1094 pull_request_id=pull_request_id),
1096 1095 params={'csrf_token': csrf_token}, status=302)
1097 1096
1098 1097 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1099 1098 metadata={'target': 'shadow repo',
1100 1099 'merge_commit': 'xxx'})
1101 1100 assert_session_flash(response, merge_resp.merge_status_message)
1102 1101
1103 1102 def test_update_source_revision(self, backend, csrf_token):
1104 1103 commits = [
1105 1104 {'message': 'ancestor'},
1106 1105 {'message': 'change'},
1107 1106 {'message': 'change-2'},
1108 1107 ]
1109 1108 commit_ids = backend.create_master_repo(commits)
1110 1109 target = backend.create_repo(heads=['ancestor'])
1111 1110 source = backend.create_repo(heads=['change'])
1112 1111
1113 1112 # create pr from a in source to A in target
1114 1113 pull_request = PullRequest()
1115 1114
1116 1115 pull_request.source_repo = source
1117 1116 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1118 1117 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1119 1118
1120 1119 pull_request.target_repo = target
1121 1120 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1122 1121 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1123 1122
1124 1123 pull_request.revisions = [commit_ids['change']]
1125 1124 pull_request.title = u"Test"
1126 1125 pull_request.description = u"Description"
1127 1126 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1128 1127 pull_request.pull_request_state = PullRequest.STATE_CREATED
1129 1128 Session().add(pull_request)
1130 1129 Session().commit()
1131 1130 pull_request_id = pull_request.pull_request_id
1132 1131
1133 1132 # source has ancestor - change - change-2
1134 1133 backend.pull_heads(source, heads=['change-2'])
1135 1134 target_repo_name = target.repo_name
1136 1135
1137 1136 # update PR
1138 1137 self.app.post(
1139 1138 route_path('pullrequest_update',
1140 1139 repo_name=target_repo_name, pull_request_id=pull_request_id),
1141 1140 params={'update_commits': 'true', 'csrf_token': csrf_token})
1142 1141
1143 1142 response = self.app.get(
1144 1143 route_path('pullrequest_show',
1145 1144 repo_name=target_repo_name,
1146 1145 pull_request_id=pull_request.pull_request_id))
1147 1146
1148 1147 assert response.status_int == 200
1149 1148 response.mustcontain('Pull request updated to')
1150 1149 response.mustcontain('with 1 added, 0 removed commits.')
1151 1150
1152 1151 # check that we have now both revisions
1153 1152 pull_request = PullRequest.get(pull_request_id)
1154 1153 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1155 1154
1156 1155 def test_update_target_revision(self, backend, csrf_token):
1157 1156 commits = [
1158 1157 {'message': 'ancestor'},
1159 1158 {'message': 'change'},
1160 1159 {'message': 'ancestor-new', 'parents': ['ancestor']},
1161 1160 {'message': 'change-rebased'},
1162 1161 ]
1163 1162 commit_ids = backend.create_master_repo(commits)
1164 1163 target = backend.create_repo(heads=['ancestor'])
1165 1164 source = backend.create_repo(heads=['change'])
1166 1165
1167 1166 # create pr from a in source to A in target
1168 1167 pull_request = PullRequest()
1169 1168
1170 1169 pull_request.source_repo = source
1171 1170 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1172 1171 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1173 1172
1174 1173 pull_request.target_repo = target
1175 1174 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1176 1175 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1177 1176
1178 1177 pull_request.revisions = [commit_ids['change']]
1179 1178 pull_request.title = u"Test"
1180 1179 pull_request.description = u"Description"
1181 1180 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1182 1181 pull_request.pull_request_state = PullRequest.STATE_CREATED
1183 1182
1184 1183 Session().add(pull_request)
1185 1184 Session().commit()
1186 1185 pull_request_id = pull_request.pull_request_id
1187 1186
1188 1187 # target has ancestor - ancestor-new
1189 1188 # source has ancestor - ancestor-new - change-rebased
1190 1189 backend.pull_heads(target, heads=['ancestor-new'])
1191 1190 backend.pull_heads(source, heads=['change-rebased'])
1192 1191 target_repo_name = target.repo_name
1193 1192
1194 1193 # update PR
1195 1194 url = route_path('pullrequest_update',
1196 1195 repo_name=target_repo_name,
1197 1196 pull_request_id=pull_request_id)
1198 1197 self.app.post(url,
1199 1198 params={'update_commits': 'true', 'csrf_token': csrf_token},
1200 1199 status=200)
1201 1200
1202 1201 # check that we have now both revisions
1203 1202 pull_request = PullRequest.get(pull_request_id)
1204 1203 assert pull_request.revisions == [commit_ids['change-rebased']]
1205 1204 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1206 1205 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1207 1206
1208 1207 response = self.app.get(
1209 1208 route_path('pullrequest_show',
1210 1209 repo_name=target_repo_name,
1211 1210 pull_request_id=pull_request.pull_request_id))
1212 1211 assert response.status_int == 200
1213 1212 response.mustcontain('Pull request updated to')
1214 1213 response.mustcontain('with 1 added, 1 removed commits.')
1215 1214
1216 1215 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1217 1216 backend = backend_git
1218 1217 commits = [
1219 1218 {'message': 'master-commit-1'},
1220 1219 {'message': 'master-commit-2-change-1'},
1221 1220 {'message': 'master-commit-3-change-2'},
1222 1221
1223 1222 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1224 1223 {'message': 'feat-commit-2'},
1225 1224 ]
1226 1225 commit_ids = backend.create_master_repo(commits)
1227 1226 target = backend.create_repo(heads=['master-commit-3-change-2'])
1228 1227 source = backend.create_repo(heads=['feat-commit-2'])
1229 1228
1230 1229 # create pr from a in source to A in target
1231 1230 pull_request = PullRequest()
1232 1231 pull_request.source_repo = source
1233 1232
1234 1233 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1235 1234 branch=backend.default_branch_name,
1236 1235 commit_id=commit_ids['master-commit-3-change-2'])
1237 1236
1238 1237 pull_request.target_repo = target
1239 1238 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1240 1239 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1241 1240
1242 1241 pull_request.revisions = [
1243 1242 commit_ids['feat-commit-1'],
1244 1243 commit_ids['feat-commit-2']
1245 1244 ]
1246 1245 pull_request.title = u"Test"
1247 1246 pull_request.description = u"Description"
1248 1247 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1249 1248 pull_request.pull_request_state = PullRequest.STATE_CREATED
1250 1249 Session().add(pull_request)
1251 1250 Session().commit()
1252 1251 pull_request_id = pull_request.pull_request_id
1253 1252
1254 1253 # PR is created, now we simulate a force-push into target,
1255 1254 # that drops a 2 last commits
1256 1255 vcsrepo = target.scm_instance()
1257 1256 vcsrepo.config.clear_section('hooks')
1258 1257 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1259 1258 target_repo_name = target.repo_name
1260 1259
1261 1260 # update PR
1262 1261 url = route_path('pullrequest_update',
1263 1262 repo_name=target_repo_name,
1264 1263 pull_request_id=pull_request_id)
1265 1264 self.app.post(url,
1266 1265 params={'update_commits': 'true', 'csrf_token': csrf_token},
1267 1266 status=200)
1268 1267
1269 1268 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1270 1269 assert response.status_int == 200
1271 1270 response.mustcontain('Pull request updated to')
1272 1271 response.mustcontain('with 0 added, 0 removed commits.')
1273 1272
1274 1273 def test_update_of_ancestor_reference(self, backend, csrf_token):
1275 1274 commits = [
1276 1275 {'message': 'ancestor'},
1277 1276 {'message': 'change'},
1278 1277 {'message': 'change-2'},
1279 1278 {'message': 'ancestor-new', 'parents': ['ancestor']},
1280 1279 {'message': 'change-rebased'},
1281 1280 ]
1282 1281 commit_ids = backend.create_master_repo(commits)
1283 1282 target = backend.create_repo(heads=['ancestor'])
1284 1283 source = backend.create_repo(heads=['change'])
1285 1284
1286 1285 # create pr from a in source to A in target
1287 1286 pull_request = PullRequest()
1288 1287 pull_request.source_repo = source
1289 1288
1290 1289 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1291 1290 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1292 1291 pull_request.target_repo = target
1293 1292 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1294 1293 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1295 1294 pull_request.revisions = [commit_ids['change']]
1296 1295 pull_request.title = u"Test"
1297 1296 pull_request.description = u"Description"
1298 1297 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1299 1298 pull_request.pull_request_state = PullRequest.STATE_CREATED
1300 1299 Session().add(pull_request)
1301 1300 Session().commit()
1302 1301 pull_request_id = pull_request.pull_request_id
1303 1302
1304 1303 # target has ancestor - ancestor-new
1305 1304 # source has ancestor - ancestor-new - change-rebased
1306 1305 backend.pull_heads(target, heads=['ancestor-new'])
1307 1306 backend.pull_heads(source, heads=['change-rebased'])
1308 1307 target_repo_name = target.repo_name
1309 1308
1310 1309 # update PR
1311 1310 self.app.post(
1312 1311 route_path('pullrequest_update',
1313 1312 repo_name=target_repo_name, pull_request_id=pull_request_id),
1314 1313 params={'update_commits': 'true', 'csrf_token': csrf_token},
1315 1314 status=200)
1316 1315
1317 1316 # Expect the target reference to be updated correctly
1318 1317 pull_request = PullRequest.get(pull_request_id)
1319 1318 assert pull_request.revisions == [commit_ids['change-rebased']]
1320 1319 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1321 1320 branch=backend.default_branch_name,
1322 1321 commit_id=commit_ids['ancestor-new'])
1323 1322 assert pull_request.target_ref == expected_target_ref
1324 1323
1325 1324 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1326 1325 branch_name = 'development'
1327 1326 commits = [
1328 1327 {'message': 'initial-commit'},
1329 1328 {'message': 'old-feature'},
1330 1329 {'message': 'new-feature', 'branch': branch_name},
1331 1330 ]
1332 1331 repo = backend_git.create_repo(commits)
1333 1332 repo_name = repo.repo_name
1334 1333 commit_ids = backend_git.commit_ids
1335 1334
1336 1335 pull_request = PullRequest()
1337 1336 pull_request.source_repo = repo
1338 1337 pull_request.target_repo = repo
1339 1338 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1340 1339 branch=branch_name, commit_id=commit_ids['new-feature'])
1341 1340 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1342 1341 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1343 1342 pull_request.revisions = [commit_ids['new-feature']]
1344 1343 pull_request.title = u"Test"
1345 1344 pull_request.description = u"Description"
1346 1345 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1347 1346 pull_request.pull_request_state = PullRequest.STATE_CREATED
1348 1347 Session().add(pull_request)
1349 1348 Session().commit()
1350 1349
1351 1350 pull_request_id = pull_request.pull_request_id
1352 1351
1353 1352 vcs = repo.scm_instance()
1354 1353 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1355 1354 # NOTE(marcink): run GC to ensure the commits are gone
1356 1355 vcs.run_gc()
1357 1356
1358 1357 response = self.app.get(route_path(
1359 1358 'pullrequest_show',
1360 1359 repo_name=repo_name,
1361 1360 pull_request_id=pull_request_id))
1362 1361
1363 1362 assert response.status_int == 200
1364 1363
1365 1364 response.assert_response().element_contains(
1366 1365 '#changeset_compare_view_content .alert strong',
1367 1366 'Missing commits')
1368 1367 response.assert_response().element_contains(
1369 1368 '#changeset_compare_view_content .alert',
1370 1369 'This pull request cannot be displayed, because one or more'
1371 1370 ' commits no longer exist in the source repository.')
1372 1371
1373 1372 def test_strip_commits_from_pull_request(
1374 1373 self, backend, pr_util, csrf_token):
1375 1374 commits = [
1376 1375 {'message': 'initial-commit'},
1377 1376 {'message': 'old-feature'},
1378 1377 {'message': 'new-feature', 'parents': ['initial-commit']},
1379 1378 ]
1380 1379 pull_request = pr_util.create_pull_request(
1381 1380 commits, target_head='initial-commit', source_head='new-feature',
1382 1381 revisions=['new-feature'])
1383 1382
1384 1383 vcs = pr_util.source_repository.scm_instance()
1385 1384 if backend.alias == 'git':
1386 1385 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1387 1386 else:
1388 1387 vcs.strip(pr_util.commit_ids['new-feature'])
1389 1388
1390 1389 response = self.app.get(route_path(
1391 1390 'pullrequest_show',
1392 1391 repo_name=pr_util.target_repository.repo_name,
1393 1392 pull_request_id=pull_request.pull_request_id))
1394 1393
1395 1394 assert response.status_int == 200
1396 1395
1397 1396 response.assert_response().element_contains(
1398 1397 '#changeset_compare_view_content .alert strong',
1399 1398 'Missing commits')
1400 1399 response.assert_response().element_contains(
1401 1400 '#changeset_compare_view_content .alert',
1402 1401 'This pull request cannot be displayed, because one or more'
1403 1402 ' commits no longer exist in the source repository.')
1404 1403 response.assert_response().element_contains(
1405 1404 '#update_commits',
1406 1405 'Update commits')
1407 1406
1408 1407 def test_strip_commits_and_update(
1409 1408 self, backend, pr_util, csrf_token):
1410 1409 commits = [
1411 1410 {'message': 'initial-commit'},
1412 1411 {'message': 'old-feature'},
1413 1412 {'message': 'new-feature', 'parents': ['old-feature']},
1414 1413 ]
1415 1414 pull_request = pr_util.create_pull_request(
1416 1415 commits, target_head='old-feature', source_head='new-feature',
1417 1416 revisions=['new-feature'], mergeable=True)
1418 1417 pr_id = pull_request.pull_request_id
1419 1418 target_repo_name = pull_request.target_repo.repo_name
1420 1419
1421 1420 vcs = pr_util.source_repository.scm_instance()
1422 1421 if backend.alias == 'git':
1423 1422 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1424 1423 else:
1425 1424 vcs.strip(pr_util.commit_ids['new-feature'])
1426 1425
1427 1426 url = route_path('pullrequest_update',
1428 1427 repo_name=target_repo_name,
1429 1428 pull_request_id=pr_id)
1430 1429 response = self.app.post(url,
1431 1430 params={'update_commits': 'true',
1432 1431 'csrf_token': csrf_token})
1433 1432
1434 1433 assert response.status_int == 200
1435 1434 assert json.loads(response.body) == json.loads('{"response": true, "redirect_url": null}')
1436 1435
1437 1436 # Make sure that after update, it won't raise 500 errors
1438 1437 response = self.app.get(route_path(
1439 1438 'pullrequest_show',
1440 1439 repo_name=target_repo_name,
1441 1440 pull_request_id=pr_id))
1442 1441
1443 1442 assert response.status_int == 200
1444 1443 response.assert_response().element_contains(
1445 1444 '#changeset_compare_view_content .alert strong',
1446 1445 'Missing commits')
1447 1446
1448 1447 def test_branch_is_a_link(self, pr_util):
1449 1448 pull_request = pr_util.create_pull_request()
1450 1449 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1451 1450 pull_request.target_ref = 'branch:target:abcdef1234567890'
1452 1451 Session().add(pull_request)
1453 1452 Session().commit()
1454 1453
1455 1454 response = self.app.get(route_path(
1456 1455 'pullrequest_show',
1457 1456 repo_name=pull_request.target_repo.scm_instance().name,
1458 1457 pull_request_id=pull_request.pull_request_id))
1459 1458 assert response.status_int == 200
1460 1459
1461 1460 source = response.assert_response().get_element('.pr-source-info')
1462 1461 source_parent = source.getparent()
1463 1462 assert len(source_parent) == 1
1464 1463
1465 1464 target = response.assert_response().get_element('.pr-target-info')
1466 1465 target_parent = target.getparent()
1467 1466 assert len(target_parent) == 1
1468 1467
1469 1468 expected_origin_link = route_path(
1470 1469 'repo_commits',
1471 1470 repo_name=pull_request.source_repo.scm_instance().name,
1472 1471 params=dict(branch='origin'))
1473 1472 expected_target_link = route_path(
1474 1473 'repo_commits',
1475 1474 repo_name=pull_request.target_repo.scm_instance().name,
1476 1475 params=dict(branch='target'))
1477 1476 assert source_parent.attrib['href'] == expected_origin_link
1478 1477 assert target_parent.attrib['href'] == expected_target_link
1479 1478
1480 1479 def test_bookmark_is_not_a_link(self, pr_util):
1481 1480 pull_request = pr_util.create_pull_request()
1482 1481 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1483 1482 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1484 1483 Session().add(pull_request)
1485 1484 Session().commit()
1486 1485
1487 1486 response = self.app.get(route_path(
1488 1487 'pullrequest_show',
1489 1488 repo_name=pull_request.target_repo.scm_instance().name,
1490 1489 pull_request_id=pull_request.pull_request_id))
1491 1490 assert response.status_int == 200
1492 1491
1493 1492 source = response.assert_response().get_element('.pr-source-info')
1494 1493 assert source.text.strip() == 'bookmark:origin'
1495 1494 assert source.getparent().attrib.get('href') is None
1496 1495
1497 1496 target = response.assert_response().get_element('.pr-target-info')
1498 1497 assert target.text.strip() == 'bookmark:target'
1499 1498 assert target.getparent().attrib.get('href') is None
1500 1499
1501 1500 def test_tag_is_not_a_link(self, pr_util):
1502 1501 pull_request = pr_util.create_pull_request()
1503 1502 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1504 1503 pull_request.target_ref = 'tag:target:abcdef1234567890'
1505 1504 Session().add(pull_request)
1506 1505 Session().commit()
1507 1506
1508 1507 response = self.app.get(route_path(
1509 1508 'pullrequest_show',
1510 1509 repo_name=pull_request.target_repo.scm_instance().name,
1511 1510 pull_request_id=pull_request.pull_request_id))
1512 1511 assert response.status_int == 200
1513 1512
1514 1513 source = response.assert_response().get_element('.pr-source-info')
1515 1514 assert source.text.strip() == 'tag:origin'
1516 1515 assert source.getparent().attrib.get('href') is None
1517 1516
1518 1517 target = response.assert_response().get_element('.pr-target-info')
1519 1518 assert target.text.strip() == 'tag:target'
1520 1519 assert target.getparent().attrib.get('href') is None
1521 1520
1522 1521 @pytest.mark.parametrize('mergeable', [True, False])
1523 1522 def test_shadow_repository_link(
1524 1523 self, mergeable, pr_util, http_host_only_stub):
1525 1524 """
1526 1525 Check that the pull request summary page displays a link to the shadow
1527 1526 repository if the pull request is mergeable. If it is not mergeable
1528 1527 the link should not be displayed.
1529 1528 """
1530 1529 pull_request = pr_util.create_pull_request(
1531 1530 mergeable=mergeable, enable_notifications=False)
1532 1531 target_repo = pull_request.target_repo.scm_instance()
1533 1532 pr_id = pull_request.pull_request_id
1534 1533 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1535 1534 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1536 1535
1537 1536 response = self.app.get(route_path(
1538 1537 'pullrequest_show',
1539 1538 repo_name=target_repo.name,
1540 1539 pull_request_id=pr_id))
1541 1540
1542 1541 if mergeable:
1543 1542 response.assert_response().element_value_contains(
1544 1543 'input.pr-mergeinfo', shadow_url)
1545 1544 response.assert_response().element_value_contains(
1546 1545 'input.pr-mergeinfo ', 'pr-merge')
1547 1546 else:
1548 1547 response.assert_response().no_element_exists('.pr-mergeinfo')
1549 1548
1550 1549
1551 1550 @pytest.mark.usefixtures('app')
1552 1551 @pytest.mark.backends("git", "hg")
1553 1552 class TestPullrequestsControllerDelete(object):
1554 1553 def test_pull_request_delete_button_permissions_admin(
1555 1554 self, autologin_user, user_admin, pr_util):
1556 1555 pull_request = pr_util.create_pull_request(
1557 1556 author=user_admin.username, enable_notifications=False)
1558 1557
1559 1558 response = self.app.get(route_path(
1560 1559 'pullrequest_show',
1561 1560 repo_name=pull_request.target_repo.scm_instance().name,
1562 1561 pull_request_id=pull_request.pull_request_id))
1563 1562
1564 1563 response.mustcontain('id="delete_pullrequest"')
1565 1564 response.mustcontain('Confirm to delete this pull request')
1566 1565
1567 1566 def test_pull_request_delete_button_permissions_owner(
1568 1567 self, autologin_regular_user, user_regular, pr_util):
1569 1568 pull_request = pr_util.create_pull_request(
1570 1569 author=user_regular.username, enable_notifications=False)
1571 1570
1572 1571 response = self.app.get(route_path(
1573 1572 'pullrequest_show',
1574 1573 repo_name=pull_request.target_repo.scm_instance().name,
1575 1574 pull_request_id=pull_request.pull_request_id))
1576 1575
1577 1576 response.mustcontain('id="delete_pullrequest"')
1578 1577 response.mustcontain('Confirm to delete this pull request')
1579 1578
1580 1579 def test_pull_request_delete_button_permissions_forbidden(
1581 1580 self, autologin_regular_user, user_regular, user_admin, pr_util):
1582 1581 pull_request = pr_util.create_pull_request(
1583 1582 author=user_admin.username, enable_notifications=False)
1584 1583
1585 1584 response = self.app.get(route_path(
1586 1585 'pullrequest_show',
1587 1586 repo_name=pull_request.target_repo.scm_instance().name,
1588 1587 pull_request_id=pull_request.pull_request_id))
1589 1588 response.mustcontain(no=['id="delete_pullrequest"'])
1590 1589 response.mustcontain(no=['Confirm to delete this pull request'])
1591 1590
1592 1591 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1593 1592 self, autologin_regular_user, user_regular, user_admin, pr_util,
1594 1593 user_util):
1595 1594
1596 1595 pull_request = pr_util.create_pull_request(
1597 1596 author=user_admin.username, enable_notifications=False)
1598 1597
1599 1598 user_util.grant_user_permission_to_repo(
1600 1599 pull_request.target_repo, user_regular,
1601 1600 'repository.write')
1602 1601
1603 1602 response = self.app.get(route_path(
1604 1603 'pullrequest_show',
1605 1604 repo_name=pull_request.target_repo.scm_instance().name,
1606 1605 pull_request_id=pull_request.pull_request_id))
1607 1606
1608 1607 response.mustcontain('id="open_edit_pullrequest"')
1609 1608 response.mustcontain('id="delete_pullrequest"')
1610 1609 response.mustcontain(no=['Confirm to delete this pull request'])
1611 1610
1612 1611 def test_delete_comment_returns_404_if_comment_does_not_exist(
1613 1612 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1614 1613
1615 1614 pull_request = pr_util.create_pull_request(
1616 1615 author=user_admin.username, enable_notifications=False)
1617 1616
1618 1617 self.app.post(
1619 1618 route_path(
1620 1619 'pullrequest_comment_delete',
1621 1620 repo_name=pull_request.target_repo.scm_instance().name,
1622 1621 pull_request_id=pull_request.pull_request_id,
1623 1622 comment_id=1024404),
1624 1623 extra_environ=xhr_header,
1625 1624 params={'csrf_token': csrf_token},
1626 1625 status=404
1627 1626 )
1628 1627
1629 1628 def test_delete_comment(
1630 1629 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1631 1630
1632 1631 pull_request = pr_util.create_pull_request(
1633 1632 author=user_admin.username, enable_notifications=False)
1634 1633 comment = pr_util.create_comment()
1635 1634 comment_id = comment.comment_id
1636 1635
1637 1636 response = self.app.post(
1638 1637 route_path(
1639 1638 'pullrequest_comment_delete',
1640 1639 repo_name=pull_request.target_repo.scm_instance().name,
1641 1640 pull_request_id=pull_request.pull_request_id,
1642 1641 comment_id=comment_id),
1643 1642 extra_environ=xhr_header,
1644 1643 params={'csrf_token': csrf_token},
1645 1644 status=200
1646 1645 )
1647 1646 assert response.text == 'true'
1648 1647
1649 1648 @pytest.mark.parametrize('url_type', [
1650 1649 'pullrequest_new',
1651 1650 'pullrequest_create',
1652 1651 'pullrequest_update',
1653 1652 'pullrequest_merge',
1654 1653 ])
1655 1654 def test_pull_request_is_forbidden_on_archived_repo(
1656 1655 self, autologin_user, backend, xhr_header, user_util, url_type):
1657 1656
1658 1657 # create a temporary repo
1659 1658 source = user_util.create_repo(repo_type=backend.alias)
1660 1659 repo_name = source.repo_name
1661 1660 repo = Repository.get_by_repo_name(repo_name)
1662 1661 repo.archived = True
1663 1662 Session().commit()
1664 1663
1665 1664 response = self.app.get(
1666 1665 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1667 1666
1668 1667 msg = 'Action not supported for archived repository.'
1669 1668 assert_session_flash(response, msg)
1670 1669
1671 1670
1672 1671 def assert_pull_request_status(pull_request, expected_status):
1673 1672 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1674 1673 assert status == expected_status
1675 1674
1676 1675
1677 1676 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1678 1677 @pytest.mark.usefixtures("autologin_user")
1679 1678 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1680 1679 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,232 +1,231 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import mock
22 21 import pytest
23 22
24 23 from rhodecode.lib.utils2 import str2bool
25 24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 25 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
27 26 from rhodecode.model.meta import Session
28 27 from rhodecode.tests import (
29 28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
30 29 from rhodecode.tests.fixture import Fixture
31 30
32 31 fixture = Fixture()
33 32
34 33
35 34 def route_path(name, params=None, **kwargs):
36 35 import urllib.request, urllib.parse, urllib.error
37 36
38 37 base_url = {
39 38 'edit_repo': '/{repo_name}/settings',
40 39 'edit_repo_advanced': '/{repo_name}/settings/advanced',
41 40 'edit_repo_caches': '/{repo_name}/settings/caches',
42 41 'edit_repo_perms': '/{repo_name}/settings/permissions',
43 42 'edit_repo_vcs': '/{repo_name}/settings/vcs',
44 43 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
45 44 'edit_repo_fields': '/{repo_name}/settings/fields',
46 45 'edit_repo_remote': '/{repo_name}/settings/remote',
47 46 'edit_repo_statistics': '/{repo_name}/settings/statistics',
48 47 }[name].format(**kwargs)
49 48
50 49 if params:
51 50 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 51 return base_url
53 52
54 53
55 54 def _get_permission_for_user(user, repo):
56 55 perm = UserRepoToPerm.query()\
57 56 .filter(UserRepoToPerm.repository ==
58 57 Repository.get_by_repo_name(repo))\
59 58 .filter(UserRepoToPerm.user == User.get_by_username(user))\
60 59 .all()
61 60 return perm
62 61
63 62
64 63 @pytest.mark.usefixtures('autologin_user', 'app')
65 64 class TestAdminRepoSettings(object):
66 65 @pytest.mark.parametrize('urlname', [
67 66 'edit_repo',
68 67 'edit_repo_caches',
69 68 'edit_repo_perms',
70 69 'edit_repo_advanced',
71 70 'edit_repo_vcs',
72 71 'edit_repo_issuetracker',
73 72 'edit_repo_fields',
74 73 'edit_repo_remote',
75 74 'edit_repo_statistics',
76 75 ])
77 76 def test_show_page(self, urlname, app, backend):
78 77 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
79 78
80 79 def test_edit_accessible_when_missing_requirements(
81 80 self, backend_hg, autologin_user):
82 81 scm_patcher = mock.patch.object(
83 82 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
84 83 with scm_patcher:
85 84 self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name))
86 85
87 86 @pytest.mark.parametrize('update_settings', [
88 87 {'repo_description': 'alter-desc'},
89 88 {'repo_owner': TEST_USER_REGULAR_LOGIN},
90 89 {'repo_private': 'true'},
91 90 {'repo_enable_locking': 'true'},
92 91 {'repo_enable_downloads': 'true'},
93 92 ])
94 93 def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util):
95 94 repo = user_util.create_repo(repo_type=backend.alias)
96 95 repo_name = repo.repo_name
97 96
98 97 params = fixture._get_repo_create_params(
99 98 csrf_token=csrf_token,
100 99 repo_name=repo_name,
101 100 repo_type=backend.alias,
102 101 repo_owner=TEST_USER_ADMIN_LOGIN,
103 102 repo_description='DESC',
104 103
105 104 repo_private='false',
106 105 repo_enable_locking='false',
107 106 repo_enable_downloads='false')
108 107 params.update(update_settings)
109 108 self.app.post(
110 109 route_path('edit_repo', repo_name=repo_name),
111 110 params=params, status=302)
112 111
113 112 repo = Repository.get_by_repo_name(repo_name)
114 113 assert repo.user.username == \
115 114 update_settings.get('repo_owner', repo.user.username)
116 115
117 116 assert repo.description == \
118 117 update_settings.get('repo_description', repo.description)
119 118
120 119 assert repo.private == \
121 120 str2bool(update_settings.get(
122 121 'repo_private', repo.private))
123 122
124 123 assert repo.enable_locking == \
125 124 str2bool(update_settings.get(
126 125 'repo_enable_locking', repo.enable_locking))
127 126
128 127 assert repo.enable_downloads == \
129 128 str2bool(update_settings.get(
130 129 'repo_enable_downloads', repo.enable_downloads))
131 130
132 131 def test_update_repo_name_via_settings(self, csrf_token, user_util, backend):
133 132 repo = user_util.create_repo(repo_type=backend.alias)
134 133 repo_name = repo.repo_name
135 134
136 135 repo_group = user_util.create_repo_group()
137 136 repo_group_name = repo_group.group_name
138 137 new_name = repo_group_name + '_' + repo_name
139 138
140 139 params = fixture._get_repo_create_params(
141 140 csrf_token=csrf_token,
142 141 repo_name=new_name,
143 142 repo_type=backend.alias,
144 143 repo_owner=TEST_USER_ADMIN_LOGIN,
145 144 repo_description='DESC',
146 145 repo_private='false',
147 146 repo_enable_locking='false',
148 147 repo_enable_downloads='false')
149 148 self.app.post(
150 149 route_path('edit_repo', repo_name=repo_name),
151 150 params=params, status=302)
152 151 repo = Repository.get_by_repo_name(new_name)
153 152 assert repo.repo_name == new_name
154 153
155 154 def test_update_repo_group_via_settings(self, csrf_token, user_util, backend):
156 155 repo = user_util.create_repo(repo_type=backend.alias)
157 156 repo_name = repo.repo_name
158 157
159 158 repo_group = user_util.create_repo_group()
160 159 repo_group_name = repo_group.group_name
161 160 repo_group_id = repo_group.group_id
162 161
163 162 new_name = repo_group_name + '/' + repo_name
164 163 params = fixture._get_repo_create_params(
165 164 csrf_token=csrf_token,
166 165 repo_name=repo_name,
167 166 repo_type=backend.alias,
168 167 repo_owner=TEST_USER_ADMIN_LOGIN,
169 168 repo_description='DESC',
170 169 repo_group=repo_group_id,
171 170 repo_private='false',
172 171 repo_enable_locking='false',
173 172 repo_enable_downloads='false')
174 173 self.app.post(
175 174 route_path('edit_repo', repo_name=repo_name),
176 175 params=params, status=302)
177 176 repo = Repository.get_by_repo_name(new_name)
178 177 assert repo.repo_name == new_name
179 178
180 179 def test_set_private_flag_sets_default_user_permissions_to_none(
181 180 self, autologin_user, backend, csrf_token):
182 181
183 182 # initially repository perm should be read
184 183 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
185 184 assert len(perm) == 1
186 185 assert perm[0].permission.permission_name == 'repository.read'
187 186 assert not backend.repo.private
188 187
189 188 response = self.app.post(
190 189 route_path('edit_repo', repo_name=backend.repo_name),
191 190 params=fixture._get_repo_create_params(
192 191 repo_private='true',
193 192 repo_name=backend.repo_name,
194 193 repo_type=backend.alias,
195 194 repo_owner=TEST_USER_ADMIN_LOGIN,
196 195 csrf_token=csrf_token), status=302)
197 196
198 197 assert_session_flash(
199 198 response,
200 199 msg='Repository `%s` updated successfully' % (backend.repo_name))
201 200
202 201 repo = Repository.get_by_repo_name(backend.repo_name)
203 202 assert repo.private is True
204 203
205 204 # now the repo default permission should be None
206 205 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
207 206 assert len(perm) == 1
208 207 assert perm[0].permission.permission_name == 'repository.none'
209 208
210 209 response = self.app.post(
211 210 route_path('edit_repo', repo_name=backend.repo_name),
212 211 params=fixture._get_repo_create_params(
213 212 repo_private='false',
214 213 repo_name=backend.repo_name,
215 214 repo_type=backend.alias,
216 215 repo_owner=TEST_USER_ADMIN_LOGIN,
217 216 csrf_token=csrf_token), status=302)
218 217
219 218 assert_session_flash(
220 219 response,
221 220 msg='Repository `%s` updated successfully' % (backend.repo_name))
222 221 assert backend.repo.private is False
223 222
224 223 # we turn off private now the repo default permission should stay None
225 224 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
226 225 assert len(perm) == 1
227 226 assert perm[0].permission.permission_name == 'repository.none'
228 227
229 228 # update this permission back
230 229 perm[0].permission = Permission.get_by_key('repository.read')
231 230 Session().add(perm[0])
232 231 Session().commit()
@@ -1,173 +1,172 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.lib.utils2 import safe_unicode, safe_str
24 23 from rhodecode.model.db import Repository
25 24 from rhodecode.model.repo import RepoModel
26 25 from rhodecode.tests import (
27 26 HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator)
28 27 from rhodecode.tests.fixture import Fixture
29 28 from rhodecode.tests.utils import repo_on_filesystem
30 29
31 30 fixture = Fixture()
32 31
33 32
34 33 def route_path(name, params=None, **kwargs):
35 34 import urllib.request, urllib.parse, urllib.error
36 35
37 36 base_url = {
38 37 'repo_summary_explicit': '/{repo_name}/summary',
39 38 'repo_summary': '/{repo_name}',
40 39 'edit_repo_advanced': '/{repo_name}/settings/advanced',
41 40 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
42 41 'edit_repo_advanced_archive': '/{repo_name}/settings/advanced/archive',
43 42 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
44 43 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
45 44 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
46 45
47 46 }[name].format(**kwargs)
48 47
49 48 if params:
50 49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
51 50 return base_url
52 51
53 52
54 53 @pytest.mark.usefixtures('autologin_user', 'app')
55 54 class TestAdminRepoSettingsAdvanced(object):
56 55
57 56 def test_set_repo_fork_has_no_self_id(self, autologin_user, backend):
58 57 repo = backend.repo
59 58 response = self.app.get(
60 59 route_path('edit_repo_advanced', repo_name=backend.repo_name))
61 60 opt = """<option value="%s">vcs_test_git</option>""" % repo.repo_id
62 61 response.mustcontain(no=[opt])
63 62
64 63 def test_set_fork_of_target_repo(
65 64 self, autologin_user, backend, csrf_token):
66 65 target_repo = 'target_%s' % backend.alias
67 66 fixture.create_repo(target_repo, repo_type=backend.alias)
68 67 repo2 = Repository.get_by_repo_name(target_repo)
69 68 response = self.app.post(
70 69 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
71 70 params={'id_fork_of': repo2.repo_id,
72 71 'csrf_token': csrf_token})
73 72 repo = Repository.get_by_repo_name(backend.repo_name)
74 73 repo2 = Repository.get_by_repo_name(target_repo)
75 74 assert_session_flash(
76 75 response,
77 76 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name))
78 77
79 78 assert repo.fork == repo2
80 79 response = response.follow()
81 80 # check if given repo is selected
82 81
83 82 opt = 'This repository is a fork of <a href="%s">%s</a>' % (
84 83 route_path('repo_summary', repo_name=repo2.repo_name),
85 84 repo2.repo_name)
86 85
87 86 response.mustcontain(opt)
88 87
89 88 fixture.destroy_repo(target_repo, forks='detach')
90 89
91 90 @pytest.mark.backends("hg", "git")
92 91 def test_set_fork_of_other_type_repo(
93 92 self, autologin_user, backend, csrf_token):
94 93 TARGET_REPO_MAP = {
95 94 'git': {
96 95 'type': 'hg',
97 96 'repo_name': HG_REPO},
98 97 'hg': {
99 98 'type': 'git',
100 99 'repo_name': GIT_REPO},
101 100 }
102 101 target_repo = TARGET_REPO_MAP[backend.alias]
103 102
104 103 repo2 = Repository.get_by_repo_name(target_repo['repo_name'])
105 104 response = self.app.post(
106 105 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
107 106 params={'id_fork_of': repo2.repo_id,
108 107 'csrf_token': csrf_token})
109 108 assert_session_flash(
110 109 response,
111 110 'Cannot set repository as fork of repository with other type')
112 111
113 112 def test_set_fork_of_none(self, autologin_user, backend, csrf_token):
114 113 # mark it as None
115 114 response = self.app.post(
116 115 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
117 116 params={'id_fork_of': None,
118 117 'csrf_token': csrf_token})
119 118 assert_session_flash(
120 119 response,
121 120 'Marked repo %s as fork of %s'
122 121 % (backend.repo_name, "Nothing"))
123 122 assert backend.repo.fork is None
124 123
125 124 def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token):
126 125 repo = Repository.get_by_repo_name(backend.repo_name)
127 126 response = self.app.post(
128 127 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
129 128 params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token})
130 129 assert_session_flash(
131 130 response, 'An error occurred during this operation')
132 131
133 132 @pytest.mark.parametrize(
134 133 "suffix",
135 134 ['', u'ąęł' , '123'],
136 135 ids=no_newline_id_generator)
137 136 def test_advanced_repo_delete(self, autologin_user, backend, suffix, csrf_token):
138 137 repo = backend.create_repo(name_suffix=suffix)
139 138 repo_name = repo.repo_name
140 139 repo_name_str = safe_str(repo.repo_name)
141 140
142 141 response = self.app.post(
143 142 route_path('edit_repo_advanced_delete', repo_name=repo_name_str),
144 143 params={'csrf_token': csrf_token})
145 144 assert_session_flash(response,
146 145 u'Deleted repository `{}`'.format(repo_name))
147 146 response.follow()
148 147
149 148 # check if repo was deleted from db
150 149 assert RepoModel().get_by_repo_name(repo_name) is None
151 150 assert not repo_on_filesystem(repo_name_str)
152 151
153 152 @pytest.mark.parametrize(
154 153 "suffix",
155 154 ['', u'ąęł' , '123'],
156 155 ids=no_newline_id_generator)
157 156 def test_advanced_repo_archive(self, autologin_user, backend, suffix, csrf_token):
158 157 repo = backend.create_repo(name_suffix=suffix)
159 158 repo_name = repo.repo_name
160 159 repo_name_str = safe_str(repo.repo_name)
161 160
162 161 response = self.app.post(
163 162 route_path('edit_repo_advanced_archive', repo_name=repo_name_str),
164 163 params={'csrf_token': csrf_token})
165 164
166 165 assert_session_flash(response,
167 166 u'Archived repository `{}`'.format(repo_name))
168 167
169 168 response = self.app.get(route_path('repo_summary', repo_name=repo_name_str))
170 169 response.mustcontain('This repository has been archived. It is now read-only.')
171 170
172 171 # check if repo was deleted from db
173 172 assert RepoModel().get_by_repo_name(repo_name).archived is True
@@ -1,524 +1,523 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import re
22 21
23 22 import mock
24 23 import pytest
25 24
26 25 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
27 26 from rhodecode.lib import helpers as h
28 27 from collections import OrderedDict
29 28 from rhodecode.lib.utils2 import AttributeDict, safe_str
30 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
31 30 from rhodecode.model.db import Repository
32 31 from rhodecode.model.meta import Session
33 32 from rhodecode.model.repo import RepoModel
34 33 from rhodecode.model.scm import ScmModel
35 34 from rhodecode.tests import assert_session_flash
36 35 from rhodecode.tests.fixture import Fixture
37 36 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
38 37
39 38
40 39 fixture = Fixture()
41 40
42 41
43 42 def route_path(name, params=None, **kwargs):
44 43 import urllib.request, urllib.parse, urllib.error
45 44
46 45 base_url = {
47 46 'repo_summary': '/{repo_name}',
48 47 'repo_stats': '/{repo_name}/repo_stats/{commit_id}',
49 48 'repo_refs_data': '/{repo_name}/refs-data',
50 49 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog',
51 50 'repo_creating_check': '/{repo_name}/repo_creating_check',
52 51 }[name].format(**kwargs)
53 52
54 53 if params:
55 54 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
56 55 return base_url
57 56
58 57
59 58 def assert_clone_url(response, server, repo, disabled=False):
60 59
61 60 response.mustcontain(
62 61 '<input type="text" class="input-monospace clone_url_input" '
63 62 '{disabled}readonly="readonly" '
64 63 'value="http://test_admin@{server}/{repo}"/>'.format(
65 64 server=server, repo=repo, disabled='disabled ' if disabled else ' ')
66 65 )
67 66
68 67
69 68 @pytest.mark.usefixtures('app')
70 69 class TestSummaryView(object):
71 70 def test_index(self, autologin_user, backend, http_host_only_stub):
72 71 repo_id = backend.repo.repo_id
73 72 repo_name = backend.repo_name
74 73 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
75 74 return_value=False):
76 75 response = self.app.get(
77 76 route_path('repo_summary', repo_name=repo_name))
78 77
79 78 # repo type
80 79 response.mustcontain(
81 80 '<i class="icon-%s">' % (backend.alias, )
82 81 )
83 82 # public/private
84 83 response.mustcontain(
85 84 """<i class="icon-unlock-alt">"""
86 85 )
87 86
88 87 # clone url...
89 88 assert_clone_url(response, http_host_only_stub, repo_name)
90 89 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id))
91 90
92 91 def test_index_svn_without_proxy(
93 92 self, autologin_user, backend_svn, http_host_only_stub):
94 93 repo_id = backend_svn.repo.repo_id
95 94 repo_name = backend_svn.repo_name
96 95 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
97 96 # clone url...
98 97
99 98 assert_clone_url(response, http_host_only_stub, repo_name, disabled=True)
100 99 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id), disabled=True)
101 100
102 101 def test_index_with_trailing_slash(
103 102 self, autologin_user, backend, http_host_only_stub):
104 103
105 104 repo_id = backend.repo.repo_id
106 105 repo_name = backend.repo_name
107 106 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
108 107 return_value=False):
109 108 response = self.app.get(
110 109 route_path('repo_summary', repo_name=repo_name) + '/',
111 110 status=200)
112 111
113 112 # clone url...
114 113 assert_clone_url(response, http_host_only_stub, repo_name)
115 114 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id))
116 115
117 116 def test_index_by_id(self, autologin_user, backend):
118 117 repo_id = backend.repo.repo_id
119 118 response = self.app.get(
120 119 route_path('repo_summary', repo_name='_%s' % (repo_id,)))
121 120
122 121 # repo type
123 122 response.mustcontain(
124 123 '<i class="icon-%s">' % (backend.alias, )
125 124 )
126 125 # public/private
127 126 response.mustcontain(
128 127 """<i class="icon-unlock-alt">"""
129 128 )
130 129
131 130 def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user):
132 131 fixture.create_repo(name='repo_1')
133 132 response = self.app.get(route_path('repo_summary', repo_name='repo_1'))
134 133
135 134 try:
136 135 response.mustcontain("repo_1")
137 136 finally:
138 137 RepoModel().delete(Repository.get_by_repo_name('repo_1'))
139 138 Session().commit()
140 139
141 140 def test_index_with_anonymous_access_disabled(
142 141 self, backend, disable_anonymous_user):
143 142 response = self.app.get(
144 143 route_path('repo_summary', repo_name=backend.repo_name), status=302)
145 144 assert 'login' in response.location
146 145
147 146 def _enable_stats(self, repo):
148 147 r = Repository.get_by_repo_name(repo)
149 148 r.enable_statistics = True
150 149 Session().add(r)
151 150 Session().commit()
152 151
153 152 expected_trending = {
154 153 'hg': {
155 154 "py": {"count": 68, "desc": ["Python"]},
156 155 "rst": {"count": 16, "desc": ["Rst"]},
157 156 "css": {"count": 2, "desc": ["Css"]},
158 157 "sh": {"count": 2, "desc": ["Bash"]},
159 158 "bat": {"count": 1, "desc": ["Batch"]},
160 159 "cfg": {"count": 1, "desc": ["Ini"]},
161 160 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
162 161 "ini": {"count": 1, "desc": ["Ini"]},
163 162 "js": {"count": 1, "desc": ["Javascript"]},
164 163 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
165 164 },
166 165 'git': {
167 166 "py": {"count": 68, "desc": ["Python"]},
168 167 "rst": {"count": 16, "desc": ["Rst"]},
169 168 "css": {"count": 2, "desc": ["Css"]},
170 169 "sh": {"count": 2, "desc": ["Bash"]},
171 170 "bat": {"count": 1, "desc": ["Batch"]},
172 171 "cfg": {"count": 1, "desc": ["Ini"]},
173 172 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
174 173 "ini": {"count": 1, "desc": ["Ini"]},
175 174 "js": {"count": 1, "desc": ["Javascript"]},
176 175 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
177 176 },
178 177 'svn': {
179 178 "py": {"count": 75, "desc": ["Python"]},
180 179 "rst": {"count": 16, "desc": ["Rst"]},
181 180 "html": {"count": 11, "desc": ["EvoqueHtml", "Html"]},
182 181 "css": {"count": 2, "desc": ["Css"]},
183 182 "bat": {"count": 1, "desc": ["Batch"]},
184 183 "cfg": {"count": 1, "desc": ["Ini"]},
185 184 "ini": {"count": 1, "desc": ["Ini"]},
186 185 "js": {"count": 1, "desc": ["Javascript"]},
187 186 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]},
188 187 "sh": {"count": 1, "desc": ["Bash"]}
189 188 },
190 189 }
191 190
192 191 def test_repo_stats(self, autologin_user, backend, xhr_header):
193 192 response = self.app.get(
194 193 route_path(
195 194 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
196 195 extra_environ=xhr_header,
197 196 status=200)
198 197 assert re.match(r'6[\d\.]+ KiB', response.json['size'])
199 198
200 199 def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header):
201 200 repo_name = backend.repo_name
202 201
203 202 # codes stats
204 203 self._enable_stats(repo_name)
205 204 ScmModel().mark_for_invalidation(repo_name)
206 205
207 206 response = self.app.get(
208 207 route_path(
209 208 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
210 209 extra_environ=xhr_header,
211 210 status=200)
212 211
213 212 expected_data = self.expected_trending[backend.alias]
214 213 returned_stats = response.json['code_stats']
215 214 for k, v in expected_data.items():
216 215 assert v == returned_stats[k]
217 216
218 217 def test_repo_refs_data(self, backend):
219 218 response = self.app.get(
220 219 route_path('repo_refs_data', repo_name=backend.repo_name),
221 220 status=200)
222 221
223 222 # Ensure that there is the correct amount of items in the result
224 223 repo = backend.repo.scm_instance()
225 224 data = response.json['results']
226 225 items = sum(len(section['children']) for section in data)
227 226 repo_refs = len(repo.branches) + len(repo.tags) + len(repo.bookmarks)
228 227 assert items == repo_refs
229 228
230 229 def test_index_shows_missing_requirements_message(
231 230 self, backend, autologin_user):
232 231 repo_name = backend.repo_name
233 232 scm_patcher = mock.patch.object(
234 233 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
235 234
236 235 with scm_patcher:
237 236 response = self.app.get(
238 237 route_path('repo_summary', repo_name=repo_name))
239 238 assert_response = response.assert_response()
240 239 assert_response.element_contains(
241 240 '.main .alert-warning strong', 'Missing requirements')
242 241 assert_response.element_contains(
243 242 '.main .alert-warning',
244 243 'Commits cannot be displayed, because this repository '
245 244 'uses one or more extensions, which was not enabled.')
246 245
247 246 def test_missing_requirements_page_does_not_contains_switch_to(
248 247 self, autologin_user, backend):
249 248 repo_name = backend.repo_name
250 249 scm_patcher = mock.patch.object(
251 250 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
252 251
253 252 with scm_patcher:
254 253 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
255 254 response.mustcontain(no='Switch To')
256 255
257 256
258 257 @pytest.mark.usefixtures('app')
259 258 class TestRepoLocation(object):
260 259
261 260 @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
262 261 def test_missing_filesystem_repo(
263 262 self, autologin_user, backend, suffix, csrf_token):
264 263 repo = backend.create_repo(name_suffix=suffix)
265 264 repo_name = repo.repo_name
266 265
267 266 # delete from file system
268 267 RepoModel()._delete_filesystem_repo(repo)
269 268
270 269 # test if the repo is still in the database
271 270 new_repo = RepoModel().get_by_repo_name(repo_name)
272 271 assert new_repo.repo_name == repo_name
273 272
274 273 # check if repo is not in the filesystem
275 274 assert not repo_on_filesystem(repo_name)
276 275
277 276 response = self.app.get(
278 277 route_path('repo_summary', repo_name=safe_str(repo_name)), status=302)
279 278
280 279 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
281 280 'Please check if it exist, or is not damaged.' % repo_name
282 281 assert_session_flash(response, msg)
283 282
284 283 @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
285 284 def test_missing_filesystem_repo_on_repo_check(
286 285 self, autologin_user, backend, suffix, csrf_token):
287 286 repo = backend.create_repo(name_suffix=suffix)
288 287 repo_name = repo.repo_name
289 288
290 289 # delete from file system
291 290 RepoModel()._delete_filesystem_repo(repo)
292 291
293 292 # test if the repo is still in the database
294 293 new_repo = RepoModel().get_by_repo_name(repo_name)
295 294 assert new_repo.repo_name == repo_name
296 295
297 296 # check if repo is not in the filesystem
298 297 assert not repo_on_filesystem(repo_name)
299 298
300 299 # flush the session
301 300 self.app.get(
302 301 route_path('repo_summary', repo_name=safe_str(repo_name)),
303 302 status=302)
304 303
305 304 response = self.app.get(
306 305 route_path('repo_creating_check', repo_name=safe_str(repo_name)),
307 306 status=200)
308 307 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
309 308 'Please check if it exist, or is not damaged.' % repo_name
310 309 assert_session_flash(response, msg )
311 310
312 311
313 312 @pytest.fixture()
314 313 def summary_view(context_stub, request_stub, user_util):
315 314 """
316 315 Bootstrap view to test the view functions
317 316 """
318 317 request_stub.matched_route = AttributeDict(name='test_view')
319 318
320 319 request_stub.user = user_util.create_user().AuthUser()
321 320 request_stub.db_repo = user_util.create_repo()
322 321
323 322 view = RepoSummaryView(context=context_stub, request=request_stub)
324 323 return view
325 324
326 325
327 326 @pytest.mark.usefixtures('app')
328 327 class TestCreateReferenceData(object):
329 328
330 329 @pytest.fixture()
331 330 def example_refs(self):
332 331 section_1_refs = OrderedDict((('a', 'a_id'), ('b', 'b_id')))
333 332 example_refs = [
334 333 ('section_1', section_1_refs, 't1'),
335 334 ('section_2', {'c': 'c_id'}, 't2'),
336 335 ]
337 336 return example_refs
338 337
339 338 def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view):
340 339 repo = mock.Mock()
341 340 repo.name = 'test-repo'
342 341 repo.alias = 'git'
343 342 full_repo_name = 'pytest-repo-group/' + repo.name
344 343
345 344 result = summary_view._create_reference_data(
346 345 repo, full_repo_name, example_refs)
347 346
348 347 expected_files_url = '/{}/files/'.format(full_repo_name)
349 348 expected_result = [
350 349 {
351 350 'children': [
352 351 {
353 352 'id': 'a', 'idx': 0, 'raw_id': 'a_id', 'text': 'a', 'type': 't1',
354 353 'files_url': expected_files_url + 'a/?at=a',
355 354 },
356 355 {
357 356 'id': 'b', 'idx': 0, 'raw_id': 'b_id', 'text': 'b', 'type': 't1',
358 357 'files_url': expected_files_url + 'b/?at=b',
359 358 }
360 359 ],
361 360 'text': 'section_1'
362 361 },
363 362 {
364 363 'children': [
365 364 {
366 365 'id': 'c', 'idx': 0, 'raw_id': 'c_id', 'text': 'c', 'type': 't2',
367 366 'files_url': expected_files_url + 'c/?at=c',
368 367 }
369 368 ],
370 369 'text': 'section_2'
371 370 }]
372 371 assert result == expected_result
373 372
374 373 def test_generates_refs_with_path_for_svn(self, example_refs, summary_view):
375 374 repo = mock.Mock()
376 375 repo.name = 'test-repo'
377 376 repo.alias = 'svn'
378 377 full_repo_name = 'pytest-repo-group/' + repo.name
379 378
380 379 result = summary_view._create_reference_data(
381 380 repo, full_repo_name, example_refs)
382 381
383 382 expected_files_url = '/{}/files/'.format(full_repo_name)
384 383 expected_result = [
385 384 {
386 385 'children': [
387 386 {
388 387 'id': 'a@a_id', 'idx': 0, 'raw_id': 'a_id',
389 388 'text': 'a', 'type': 't1',
390 389 'files_url': expected_files_url + 'a_id/a?at=a',
391 390 },
392 391 {
393 392 'id': 'b@b_id', 'idx': 0, 'raw_id': 'b_id',
394 393 'text': 'b', 'type': 't1',
395 394 'files_url': expected_files_url + 'b_id/b?at=b',
396 395 }
397 396 ],
398 397 'text': 'section_1'
399 398 },
400 399 {
401 400 'children': [
402 401 {
403 402 'id': 'c@c_id', 'idx': 0, 'raw_id': 'c_id',
404 403 'text': 'c', 'type': 't2',
405 404 'files_url': expected_files_url + 'c_id/c?at=c',
406 405 }
407 406 ],
408 407 'text': 'section_2'
409 408 }
410 409 ]
411 410 assert result == expected_result
412 411
413 412
414 413 class TestCreateFilesUrl(object):
415 414
416 415 def test_creates_non_svn_url(self, app, summary_view):
417 416 repo = mock.Mock()
418 417 repo.name = 'abcde'
419 418 full_repo_name = 'test-repo-group/' + repo.name
420 419 ref_name = 'branch1'
421 420 raw_id = 'deadbeef0123456789'
422 421 is_svn = False
423 422
424 423 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
425 424 result = summary_view._create_files_url(
426 425 repo, full_repo_name, ref_name, raw_id, is_svn)
427 426 url_mock.assert_called_once_with(
428 427 'repo_files', repo_name=full_repo_name, commit_id=ref_name,
429 428 f_path='', _query=dict(at=ref_name))
430 429 assert result == url_mock.return_value
431 430
432 431 def test_creates_svn_url(self, app, summary_view):
433 432 repo = mock.Mock()
434 433 repo.name = 'abcde'
435 434 full_repo_name = 'test-repo-group/' + repo.name
436 435 ref_name = 'branch1'
437 436 raw_id = 'deadbeef0123456789'
438 437 is_svn = True
439 438
440 439 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
441 440 result = summary_view._create_files_url(
442 441 repo, full_repo_name, ref_name, raw_id, is_svn)
443 442 url_mock.assert_called_once_with(
444 443 'repo_files', repo_name=full_repo_name, f_path=ref_name,
445 444 commit_id=raw_id, _query=dict(at=ref_name))
446 445 assert result == url_mock.return_value
447 446
448 447 def test_name_has_slashes(self, app, summary_view):
449 448 repo = mock.Mock()
450 449 repo.name = 'abcde'
451 450 full_repo_name = 'test-repo-group/' + repo.name
452 451 ref_name = 'branch1/branch2'
453 452 raw_id = 'deadbeef0123456789'
454 453 is_svn = False
455 454
456 455 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
457 456 result = summary_view._create_files_url(
458 457 repo, full_repo_name, ref_name, raw_id, is_svn)
459 458 url_mock.assert_called_once_with(
460 459 'repo_files', repo_name=full_repo_name, commit_id=raw_id,
461 460 f_path='', _query=dict(at=ref_name))
462 461 assert result == url_mock.return_value
463 462
464 463
465 464 class TestReferenceItems(object):
466 465 repo = mock.Mock()
467 466 repo.name = 'pytest-repo'
468 467 repo_full_name = 'pytest-repo-group/' + repo.name
469 468 ref_type = 'branch'
470 469 fake_url = '/abcde/'
471 470
472 471 @staticmethod
473 472 def _format_function(name, id_):
474 473 return 'format_function_{}_{}'.format(name, id_)
475 474
476 475 def test_creates_required_amount_of_items(self, summary_view):
477 476 amount = 100
478 477 refs = {
479 478 'ref{}'.format(i): '{0:040d}'.format(i)
480 479 for i in range(amount)
481 480 }
482 481
483 482 url_patcher = mock.patch.object(summary_view, '_create_files_url')
484 483 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
485 484 return_value=False)
486 485
487 486 with url_patcher as url_mock, svn_patcher:
488 487 result = summary_view._create_reference_items(
489 488 self.repo, self.repo_full_name, refs, self.ref_type,
490 489 self._format_function)
491 490 assert len(result) == amount
492 491 assert url_mock.call_count == amount
493 492
494 493 def test_single_item_details(self, summary_view):
495 494 ref_name = 'ref1'
496 495 ref_id = 'deadbeef'
497 496 refs = {
498 497 ref_name: ref_id
499 498 }
500 499
501 500 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
502 501 return_value=False)
503 502
504 503 url_patcher = mock.patch.object(
505 504 summary_view, '_create_files_url', return_value=self.fake_url)
506 505
507 506 with url_patcher as url_mock, svn_patcher:
508 507 result = summary_view._create_reference_items(
509 508 self.repo, self.repo_full_name, refs, self.ref_type,
510 509 self._format_function)
511 510
512 511 url_mock.assert_called_once_with(
513 512 self.repo, self.repo_full_name, ref_name, ref_id, False)
514 513 expected_result = [
515 514 {
516 515 'text': ref_name,
517 516 'id': self._format_function(ref_name, ref_id),
518 517 'raw_id': ref_id,
519 518 'idx': 0,
520 519 'type': self.ref_type,
521 520 'files_url': self.fake_url
522 521 }
523 522 ]
524 523 assert result == expected_result
@@ -1,685 +1,684 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import mock
22 21 import pytest
23 22
24 23 from rhodecode.lib import auth
25 24 from rhodecode.lib.utils2 import str2bool
26 25 from rhodecode.model.db import (
27 26 Repository, UserRepoToPerm, User)
28 27 from rhodecode.model.meta import Session
29 28 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
30 29 from rhodecode.model.user import UserModel
31 30 from rhodecode.tests import (
32 31 login_user_session, logout_user_session,
33 32 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
34 33 from rhodecode.tests.fixture import Fixture
35 34 from rhodecode.tests.utils import AssertResponse
36 35
37 36 fixture = Fixture()
38 37
39 38
40 39 def route_path(name, params=None, **kwargs):
41 40 import urllib.request, urllib.parse, urllib.error
42 41
43 42 base_url = {
44 43 'repo_summary': '/{repo_name}',
45 44 'repo_creating_check': '/{repo_name}/repo_creating_check',
46 45 'edit_repo': '/{repo_name}/settings',
47 46 'edit_repo_vcs': '/{repo_name}/settings/vcs',
48 47 'edit_repo_vcs_update': '/{repo_name}/settings/vcs/update',
49 48 'edit_repo_vcs_svn_pattern_delete': '/{repo_name}/settings/vcs/svn_pattern/delete'
50 49 }[name].format(**kwargs)
51 50
52 51 if params:
53 52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 53 return base_url
55 54
56 55
57 56 @pytest.mark.usefixtures("app")
58 57 class TestVcsSettings(object):
59 58 FORM_DATA = {
60 59 'inherit_global_settings': False,
61 60 'hooks_changegroup_repo_size': False,
62 61 'hooks_changegroup_push_logger': False,
63 62 'hooks_outgoing_pull_logger': False,
64 63 'extensions_largefiles': False,
65 64 'extensions_evolve': False,
66 65 'phases_publish': 'False',
67 66 'rhodecode_pr_merge_enabled': False,
68 67 'rhodecode_use_outdated_comments': False,
69 68 'new_svn_branch': '',
70 69 'new_svn_tag': ''
71 70 }
72 71
73 72 @pytest.mark.skip_backends('svn')
74 73 def test_global_settings_initial_values(self, autologin_user, backend):
75 74 repo_name = backend.repo_name
76 75 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
77 76
78 77 expected_settings = (
79 78 'rhodecode_use_outdated_comments', 'rhodecode_pr_merge_enabled',
80 79 'hooks_changegroup_repo_size', 'hooks_changegroup_push_logger',
81 80 'hooks_outgoing_pull_logger'
82 81 )
83 82 for setting in expected_settings:
84 83 self.assert_repo_value_equals_global_value(response, setting)
85 84
86 85 def test_show_settings_requires_repo_admin_permission(
87 86 self, backend, user_util, settings_util):
88 87 repo = backend.create_repo()
89 88 repo_name = repo.repo_name
90 89 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
91 90 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
92 91 login_user_session(
93 92 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
94 93 self.app.get(route_path('edit_repo_vcs', repo_name=repo_name), status=200)
95 94
96 95 def test_inherit_global_settings_flag_is_true_by_default(
97 96 self, autologin_user, backend):
98 97 repo_name = backend.repo_name
99 98 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
100 99
101 100 assert_response = response.assert_response()
102 101 element = assert_response.get_element('#inherit_global_settings')
103 102 assert element.checked
104 103
105 104 @pytest.mark.parametrize('checked_value', [True, False])
106 105 def test_inherit_global_settings_value(
107 106 self, autologin_user, backend, checked_value, settings_util):
108 107 repo = backend.create_repo()
109 108 repo_name = repo.repo_name
110 109 settings_util.create_repo_rhodecode_setting(
111 110 repo, 'inherit_vcs_settings', checked_value, 'bool')
112 111 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
113 112
114 113 assert_response = response.assert_response()
115 114 element = assert_response.get_element('#inherit_global_settings')
116 115 assert element.checked == checked_value
117 116
118 117 @pytest.mark.skip_backends('svn')
119 118 def test_hooks_settings_are_created(
120 119 self, autologin_user, backend, csrf_token):
121 120 repo_name = backend.repo_name
122 121 data = self.FORM_DATA.copy()
123 122 data['csrf_token'] = csrf_token
124 123 self.app.post(
125 124 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
126 125 settings = SettingsModel(repo=repo_name)
127 126 try:
128 127 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
129 128 ui = settings.get_ui_by_section_and_key(section, key)
130 129 assert ui.ui_active is False
131 130 finally:
132 131 self._cleanup_repo_settings(settings)
133 132
134 133 def test_hooks_settings_are_not_created_for_svn(
135 134 self, autologin_user, backend_svn, csrf_token):
136 135 repo_name = backend_svn.repo_name
137 136 data = self.FORM_DATA.copy()
138 137 data['csrf_token'] = csrf_token
139 138 self.app.post(
140 139 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
141 140 settings = SettingsModel(repo=repo_name)
142 141 try:
143 142 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
144 143 ui = settings.get_ui_by_section_and_key(section, key)
145 144 assert ui is None
146 145 finally:
147 146 self._cleanup_repo_settings(settings)
148 147
149 148 @pytest.mark.skip_backends('svn')
150 149 def test_hooks_settings_are_updated(
151 150 self, autologin_user, backend, csrf_token):
152 151 repo_name = backend.repo_name
153 152 settings = SettingsModel(repo=repo_name)
154 153 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
155 154 settings.create_ui_section_value(section, '', key=key, active=True)
156 155
157 156 data = self.FORM_DATA.copy()
158 157 data['csrf_token'] = csrf_token
159 158 self.app.post(
160 159 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
161 160 try:
162 161 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
163 162 ui = settings.get_ui_by_section_and_key(section, key)
164 163 assert ui.ui_active is False
165 164 finally:
166 165 self._cleanup_repo_settings(settings)
167 166
168 167 def test_hooks_settings_are_not_updated_for_svn(
169 168 self, autologin_user, backend_svn, csrf_token):
170 169 repo_name = backend_svn.repo_name
171 170 settings = SettingsModel(repo=repo_name)
172 171 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
173 172 settings.create_ui_section_value(section, '', key=key, active=True)
174 173
175 174 data = self.FORM_DATA.copy()
176 175 data['csrf_token'] = csrf_token
177 176 self.app.post(
178 177 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
179 178 try:
180 179 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
181 180 ui = settings.get_ui_by_section_and_key(section, key)
182 181 assert ui.ui_active is True
183 182 finally:
184 183 self._cleanup_repo_settings(settings)
185 184
186 185 @pytest.mark.skip_backends('svn')
187 186 def test_pr_settings_are_created(
188 187 self, autologin_user, backend, csrf_token):
189 188 repo_name = backend.repo_name
190 189 data = self.FORM_DATA.copy()
191 190 data['csrf_token'] = csrf_token
192 191 self.app.post(
193 192 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
194 193 settings = SettingsModel(repo=repo_name)
195 194 try:
196 195 for name in VcsSettingsModel.GENERAL_SETTINGS:
197 196 setting = settings.get_setting_by_name(name)
198 197 assert setting.app_settings_value is False
199 198 finally:
200 199 self._cleanup_repo_settings(settings)
201 200
202 201 def test_pr_settings_are_not_created_for_svn(
203 202 self, autologin_user, backend_svn, csrf_token):
204 203 repo_name = backend_svn.repo_name
205 204 data = self.FORM_DATA.copy()
206 205 data['csrf_token'] = csrf_token
207 206 self.app.post(
208 207 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
209 208 settings = SettingsModel(repo=repo_name)
210 209 try:
211 210 for name in VcsSettingsModel.GENERAL_SETTINGS:
212 211 setting = settings.get_setting_by_name(name)
213 212 assert setting is None
214 213 finally:
215 214 self._cleanup_repo_settings(settings)
216 215
217 216 def test_pr_settings_creation_requires_repo_admin_permission(
218 217 self, backend, user_util, settings_util, csrf_token):
219 218 repo = backend.create_repo()
220 219 repo_name = repo.repo_name
221 220
222 221 logout_user_session(self.app, csrf_token)
223 222 session = login_user_session(
224 223 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
225 224 new_csrf_token = auth.get_csrf_token(session)
226 225
227 226 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
228 227 repo = Repository.get_by_repo_name(repo_name)
229 228 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
230 229 data = self.FORM_DATA.copy()
231 230 data['csrf_token'] = new_csrf_token
232 231 settings = SettingsModel(repo=repo_name)
233 232
234 233 try:
235 234 self.app.post(
236 235 route_path('edit_repo_vcs_update', repo_name=repo_name), data,
237 236 status=302)
238 237 finally:
239 238 self._cleanup_repo_settings(settings)
240 239
241 240 @pytest.mark.skip_backends('svn')
242 241 def test_pr_settings_are_updated(
243 242 self, autologin_user, backend, csrf_token):
244 243 repo_name = backend.repo_name
245 244 settings = SettingsModel(repo=repo_name)
246 245 for name in VcsSettingsModel.GENERAL_SETTINGS:
247 246 settings.create_or_update_setting(name, True, 'bool')
248 247
249 248 data = self.FORM_DATA.copy()
250 249 data['csrf_token'] = csrf_token
251 250 self.app.post(
252 251 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
253 252 try:
254 253 for name in VcsSettingsModel.GENERAL_SETTINGS:
255 254 setting = settings.get_setting_by_name(name)
256 255 assert setting.app_settings_value is False
257 256 finally:
258 257 self._cleanup_repo_settings(settings)
259 258
260 259 def test_pr_settings_are_not_updated_for_svn(
261 260 self, autologin_user, backend_svn, csrf_token):
262 261 repo_name = backend_svn.repo_name
263 262 settings = SettingsModel(repo=repo_name)
264 263 for name in VcsSettingsModel.GENERAL_SETTINGS:
265 264 settings.create_or_update_setting(name, True, 'bool')
266 265
267 266 data = self.FORM_DATA.copy()
268 267 data['csrf_token'] = csrf_token
269 268 self.app.post(
270 269 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
271 270 try:
272 271 for name in VcsSettingsModel.GENERAL_SETTINGS:
273 272 setting = settings.get_setting_by_name(name)
274 273 assert setting.app_settings_value is True
275 274 finally:
276 275 self._cleanup_repo_settings(settings)
277 276
278 277 def test_svn_settings_are_created(
279 278 self, autologin_user, backend_svn, csrf_token, settings_util):
280 279 repo_name = backend_svn.repo_name
281 280 data = self.FORM_DATA.copy()
282 281 data['new_svn_tag'] = 'svn-tag'
283 282 data['new_svn_branch'] = 'svn-branch'
284 283 data['csrf_token'] = csrf_token
285 284
286 285 # Create few global settings to make sure that uniqueness validators
287 286 # are not triggered
288 287 settings_util.create_rhodecode_ui(
289 288 VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
290 289 settings_util.create_rhodecode_ui(
291 290 VcsSettingsModel.SVN_TAG_SECTION, 'svn-tag')
292 291
293 292 self.app.post(
294 293 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
295 294 settings = SettingsModel(repo=repo_name)
296 295 try:
297 296 svn_branches = settings.get_ui_by_section(
298 297 VcsSettingsModel.SVN_BRANCH_SECTION)
299 298 svn_branch_names = [b.ui_value for b in svn_branches]
300 299 svn_tags = settings.get_ui_by_section(
301 300 VcsSettingsModel.SVN_TAG_SECTION)
302 301 svn_tag_names = [b.ui_value for b in svn_tags]
303 302 assert 'svn-branch' in svn_branch_names
304 303 assert 'svn-tag' in svn_tag_names
305 304 finally:
306 305 self._cleanup_repo_settings(settings)
307 306
308 307 def test_svn_settings_are_unique(
309 308 self, autologin_user, backend_svn, csrf_token, settings_util):
310 309 repo = backend_svn.repo
311 310 repo_name = repo.repo_name
312 311 data = self.FORM_DATA.copy()
313 312 data['new_svn_tag'] = 'test_tag'
314 313 data['new_svn_branch'] = 'test_branch'
315 314 data['csrf_token'] = csrf_token
316 315 settings_util.create_repo_rhodecode_ui(
317 316 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch')
318 317 settings_util.create_repo_rhodecode_ui(
319 318 repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag')
320 319
321 320 response = self.app.post(
322 321 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=200)
323 322 response.mustcontain('Pattern already exists')
324 323
325 324 def test_svn_settings_with_empty_values_are_not_created(
326 325 self, autologin_user, backend_svn, csrf_token):
327 326 repo_name = backend_svn.repo_name
328 327 data = self.FORM_DATA.copy()
329 328 data['csrf_token'] = csrf_token
330 329 self.app.post(
331 330 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
332 331 settings = SettingsModel(repo=repo_name)
333 332 try:
334 333 svn_branches = settings.get_ui_by_section(
335 334 VcsSettingsModel.SVN_BRANCH_SECTION)
336 335 svn_tags = settings.get_ui_by_section(
337 336 VcsSettingsModel.SVN_TAG_SECTION)
338 337 assert len(svn_branches) == 0
339 338 assert len(svn_tags) == 0
340 339 finally:
341 340 self._cleanup_repo_settings(settings)
342 341
343 342 def test_svn_settings_are_shown_for_svn_repository(
344 343 self, autologin_user, backend_svn, csrf_token):
345 344 repo_name = backend_svn.repo_name
346 345 response = self.app.get(
347 346 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
348 347 response.mustcontain('Subversion Settings')
349 348
350 349 @pytest.mark.skip_backends('svn')
351 350 def test_svn_settings_are_not_created_for_not_svn_repository(
352 351 self, autologin_user, backend, csrf_token):
353 352 repo_name = backend.repo_name
354 353 data = self.FORM_DATA.copy()
355 354 data['csrf_token'] = csrf_token
356 355 self.app.post(
357 356 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
358 357 settings = SettingsModel(repo=repo_name)
359 358 try:
360 359 svn_branches = settings.get_ui_by_section(
361 360 VcsSettingsModel.SVN_BRANCH_SECTION)
362 361 svn_tags = settings.get_ui_by_section(
363 362 VcsSettingsModel.SVN_TAG_SECTION)
364 363 assert len(svn_branches) == 0
365 364 assert len(svn_tags) == 0
366 365 finally:
367 366 self._cleanup_repo_settings(settings)
368 367
369 368 @pytest.mark.skip_backends('svn')
370 369 def test_svn_settings_are_shown_only_for_svn_repository(
371 370 self, autologin_user, backend, csrf_token):
372 371 repo_name = backend.repo_name
373 372 response = self.app.get(
374 373 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
375 374 response.mustcontain(no='Subversion Settings')
376 375
377 376 def test_hg_settings_are_created(
378 377 self, autologin_user, backend_hg, csrf_token):
379 378 repo_name = backend_hg.repo_name
380 379 data = self.FORM_DATA.copy()
381 380 data['new_svn_tag'] = 'svn-tag'
382 381 data['new_svn_branch'] = 'svn-branch'
383 382 data['csrf_token'] = csrf_token
384 383 self.app.post(
385 384 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
386 385 settings = SettingsModel(repo=repo_name)
387 386 try:
388 387 largefiles_ui = settings.get_ui_by_section_and_key(
389 388 'extensions', 'largefiles')
390 389 assert largefiles_ui.ui_active is False
391 390 phases_ui = settings.get_ui_by_section_and_key(
392 391 'phases', 'publish')
393 392 assert str2bool(phases_ui.ui_value) is False
394 393 finally:
395 394 self._cleanup_repo_settings(settings)
396 395
397 396 def test_hg_settings_are_updated(
398 397 self, autologin_user, backend_hg, csrf_token):
399 398 repo_name = backend_hg.repo_name
400 399 settings = SettingsModel(repo=repo_name)
401 400 settings.create_ui_section_value(
402 401 'extensions', '', key='largefiles', active=True)
403 402 settings.create_ui_section_value(
404 403 'phases', '1', key='publish', active=True)
405 404
406 405 data = self.FORM_DATA.copy()
407 406 data['csrf_token'] = csrf_token
408 407 self.app.post(
409 408 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
410 409 try:
411 410 largefiles_ui = settings.get_ui_by_section_and_key(
412 411 'extensions', 'largefiles')
413 412 assert largefiles_ui.ui_active is False
414 413 phases_ui = settings.get_ui_by_section_and_key(
415 414 'phases', 'publish')
416 415 assert str2bool(phases_ui.ui_value) is False
417 416 finally:
418 417 self._cleanup_repo_settings(settings)
419 418
420 419 def test_hg_settings_are_shown_for_hg_repository(
421 420 self, autologin_user, backend_hg, csrf_token):
422 421 repo_name = backend_hg.repo_name
423 422 response = self.app.get(
424 423 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
425 424 response.mustcontain('Mercurial Settings')
426 425
427 426 @pytest.mark.skip_backends('hg')
428 427 def test_hg_settings_are_created_only_for_hg_repository(
429 428 self, autologin_user, backend, csrf_token):
430 429 repo_name = backend.repo_name
431 430 data = self.FORM_DATA.copy()
432 431 data['csrf_token'] = csrf_token
433 432 self.app.post(
434 433 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
435 434 settings = SettingsModel(repo=repo_name)
436 435 try:
437 436 largefiles_ui = settings.get_ui_by_section_and_key(
438 437 'extensions', 'largefiles')
439 438 assert largefiles_ui is None
440 439 phases_ui = settings.get_ui_by_section_and_key(
441 440 'phases', 'publish')
442 441 assert phases_ui is None
443 442 finally:
444 443 self._cleanup_repo_settings(settings)
445 444
446 445 @pytest.mark.skip_backends('hg')
447 446 def test_hg_settings_are_shown_only_for_hg_repository(
448 447 self, autologin_user, backend, csrf_token):
449 448 repo_name = backend.repo_name
450 449 response = self.app.get(
451 450 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
452 451 response.mustcontain(no='Mercurial Settings')
453 452
454 453 @pytest.mark.skip_backends('hg')
455 454 def test_hg_settings_are_updated_only_for_hg_repository(
456 455 self, autologin_user, backend, csrf_token):
457 456 repo_name = backend.repo_name
458 457 settings = SettingsModel(repo=repo_name)
459 458 settings.create_ui_section_value(
460 459 'extensions', '', key='largefiles', active=True)
461 460 settings.create_ui_section_value(
462 461 'phases', '1', key='publish', active=True)
463 462
464 463 data = self.FORM_DATA.copy()
465 464 data['csrf_token'] = csrf_token
466 465 self.app.post(
467 466 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
468 467 try:
469 468 largefiles_ui = settings.get_ui_by_section_and_key(
470 469 'extensions', 'largefiles')
471 470 assert largefiles_ui.ui_active is True
472 471 phases_ui = settings.get_ui_by_section_and_key(
473 472 'phases', 'publish')
474 473 assert phases_ui.ui_value == '1'
475 474 finally:
476 475 self._cleanup_repo_settings(settings)
477 476
478 477 def test_per_repo_svn_settings_are_displayed(
479 478 self, autologin_user, backend_svn, settings_util):
480 479 repo = backend_svn.create_repo()
481 480 repo_name = repo.repo_name
482 481 branches = [
483 482 settings_util.create_repo_rhodecode_ui(
484 483 repo, VcsSettingsModel.SVN_BRANCH_SECTION,
485 484 'branch_{}'.format(i))
486 485 for i in range(10)]
487 486 tags = [
488 487 settings_util.create_repo_rhodecode_ui(
489 488 repo, VcsSettingsModel.SVN_TAG_SECTION, 'tag_{}'.format(i))
490 489 for i in range(10)]
491 490
492 491 response = self.app.get(
493 492 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
494 493 assert_response = response.assert_response()
495 494 for branch in branches:
496 495 css_selector = '[name=branch_value_{}]'.format(branch.ui_id)
497 496 element = assert_response.get_element(css_selector)
498 497 assert element.value == branch.ui_value
499 498 for tag in tags:
500 499 css_selector = '[name=tag_ui_value_new_{}]'.format(tag.ui_id)
501 500 element = assert_response.get_element(css_selector)
502 501 assert element.value == tag.ui_value
503 502
504 503 def test_per_repo_hg_and_pr_settings_are_not_displayed_for_svn(
505 504 self, autologin_user, backend_svn, settings_util):
506 505 repo = backend_svn.create_repo()
507 506 repo_name = repo.repo_name
508 507 response = self.app.get(
509 508 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
510 509 response.mustcontain(no='<label>Hooks:</label>')
511 510 response.mustcontain(no='<label>Pull Request Settings:</label>')
512 511
513 512 def test_inherit_global_settings_value_is_saved(
514 513 self, autologin_user, backend, csrf_token):
515 514 repo_name = backend.repo_name
516 515 data = self.FORM_DATA.copy()
517 516 data['csrf_token'] = csrf_token
518 517 data['inherit_global_settings'] = True
519 518 self.app.post(
520 519 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
521 520
522 521 settings = SettingsModel(repo=repo_name)
523 522 vcs_settings = VcsSettingsModel(repo=repo_name)
524 523 try:
525 524 assert vcs_settings.inherit_global_settings is True
526 525 finally:
527 526 self._cleanup_repo_settings(settings)
528 527
529 528 def test_repo_cache_is_invalidated_when_settings_are_updated(
530 529 self, autologin_user, backend, csrf_token):
531 530 repo_name = backend.repo_name
532 531 data = self.FORM_DATA.copy()
533 532 data['csrf_token'] = csrf_token
534 533 data['inherit_global_settings'] = True
535 534 settings = SettingsModel(repo=repo_name)
536 535
537 536 invalidation_patcher = mock.patch(
538 537 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
539 538 with invalidation_patcher as invalidation_mock:
540 539 self.app.post(
541 540 route_path('edit_repo_vcs_update', repo_name=repo_name), data,
542 541 status=302)
543 542 try:
544 543 invalidation_mock.assert_called_once_with(repo_name, delete=True)
545 544 finally:
546 545 self._cleanup_repo_settings(settings)
547 546
548 547 def test_other_settings_not_saved_inherit_global_settings_is_true(
549 548 self, autologin_user, backend, csrf_token):
550 549 repo_name = backend.repo_name
551 550 data = self.FORM_DATA.copy()
552 551 data['csrf_token'] = csrf_token
553 552 data['inherit_global_settings'] = True
554 553 self.app.post(
555 554 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
556 555
557 556 settings = SettingsModel(repo=repo_name)
558 557 ui_settings = (
559 558 VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
560 559
561 560 vcs_settings = []
562 561 try:
563 562 for section, key in ui_settings:
564 563 ui = settings.get_ui_by_section_and_key(section, key)
565 564 if ui:
566 565 vcs_settings.append(ui)
567 566 vcs_settings.extend(settings.get_ui_by_section(
568 567 VcsSettingsModel.SVN_BRANCH_SECTION))
569 568 vcs_settings.extend(settings.get_ui_by_section(
570 569 VcsSettingsModel.SVN_TAG_SECTION))
571 570 for name in VcsSettingsModel.GENERAL_SETTINGS:
572 571 setting = settings.get_setting_by_name(name)
573 572 if setting:
574 573 vcs_settings.append(setting)
575 574 assert vcs_settings == []
576 575 finally:
577 576 self._cleanup_repo_settings(settings)
578 577
579 578 def test_delete_svn_branch_and_tag_patterns(
580 579 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
581 580 repo = backend_svn.create_repo()
582 581 repo_name = repo.repo_name
583 582 branch = settings_util.create_repo_rhodecode_ui(
584 583 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
585 584 cleanup=False)
586 585 tag = settings_util.create_repo_rhodecode_ui(
587 586 repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag', cleanup=False)
588 587 data = {
589 588 'csrf_token': csrf_token
590 589 }
591 590 for id_ in (branch.ui_id, tag.ui_id):
592 591 data['delete_svn_pattern'] = id_,
593 592 self.app.post(
594 593 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
595 594 data, extra_environ=xhr_header, status=200)
596 595 settings = VcsSettingsModel(repo=repo_name)
597 596 assert settings.get_repo_svn_branch_patterns() == []
598 597
599 598 def test_delete_svn_branch_requires_repo_admin_permission(
600 599 self, backend_svn, user_util, settings_util, csrf_token, xhr_header):
601 600 repo = backend_svn.create_repo()
602 601 repo_name = repo.repo_name
603 602
604 603 logout_user_session(self.app, csrf_token)
605 604 session = login_user_session(
606 605 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
607 606 csrf_token = auth.get_csrf_token(session)
608 607
609 608 repo = Repository.get_by_repo_name(repo_name)
610 609 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
611 610 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
612 611 branch = settings_util.create_repo_rhodecode_ui(
613 612 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
614 613 cleanup=False)
615 614 data = {
616 615 'csrf_token': csrf_token,
617 616 'delete_svn_pattern': branch.ui_id
618 617 }
619 618 self.app.post(
620 619 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
621 620 data, extra_environ=xhr_header, status=200)
622 621
623 622 def test_delete_svn_branch_raises_400_when_not_found(
624 623 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
625 624 repo_name = backend_svn.repo_name
626 625 data = {
627 626 'delete_svn_pattern': 123,
628 627 'csrf_token': csrf_token
629 628 }
630 629 self.app.post(
631 630 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
632 631 data, extra_environ=xhr_header, status=400)
633 632
634 633 def test_delete_svn_branch_raises_400_when_no_id_specified(
635 634 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
636 635 repo_name = backend_svn.repo_name
637 636 data = {
638 637 'csrf_token': csrf_token
639 638 }
640 639 self.app.post(
641 640 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
642 641 data, extra_environ=xhr_header, status=400)
643 642
644 643 def _cleanup_repo_settings(self, settings_model):
645 644 cleanup = []
646 645 ui_settings = (
647 646 VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
648 647
649 648 for section, key in ui_settings:
650 649 ui = settings_model.get_ui_by_section_and_key(section, key)
651 650 if ui:
652 651 cleanup.append(ui)
653 652
654 653 cleanup.extend(settings_model.get_ui_by_section(
655 654 VcsSettingsModel.INHERIT_SETTINGS))
656 655 cleanup.extend(settings_model.get_ui_by_section(
657 656 VcsSettingsModel.SVN_BRANCH_SECTION))
658 657 cleanup.extend(settings_model.get_ui_by_section(
659 658 VcsSettingsModel.SVN_TAG_SECTION))
660 659
661 660 for name in VcsSettingsModel.GENERAL_SETTINGS:
662 661 setting = settings_model.get_setting_by_name(name)
663 662 if setting:
664 663 cleanup.append(setting)
665 664
666 665 for object_ in cleanup:
667 666 Session().delete(object_)
668 667 Session().commit()
669 668
670 669 def assert_repo_value_equals_global_value(self, response, setting):
671 670 assert_response = response.assert_response()
672 671 global_css_selector = '[name={}_inherited]'.format(setting)
673 672 repo_css_selector = '[name={}]'.format(setting)
674 673 repo_element = assert_response.get_element(repo_css_selector)
675 674 global_element = assert_response.get_element(global_css_selector)
676 675 assert repo_element.value == global_element.value
677 676
678 677
679 678 def _get_permission_for_user(user, repo):
680 679 perm = UserRepoToPerm.query()\
681 680 .filter(UserRepoToPerm.repository ==
682 681 Repository.get_by_repo_name(repo))\
683 682 .filter(UserRepoToPerm.user == User.get_by_username(user))\
684 683 .all()
685 684 return perm
@@ -1,104 +1,103 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import mock
22 21 import pytest
23 22
24 23 import rhodecode
25 24 from rhodecode.model.db import Repository
26 25 from rhodecode.model.settings import SettingsModel
27 26 from rhodecode.tests.utils import AssertResponse
28 27
29 28
30 29 def route_path(name, params=None, **kwargs):
31 30 import urllib.request, urllib.parse, urllib.error
32 31
33 32 base_url = {
34 33 'edit_repo': '/{repo_name}/settings',
35 34 'edit_repo_vcs': '/{repo_name}/settings/vcs',
36 35 'edit_repo_vcs_update': '/{repo_name}/settings/vcs/update',
37 36 }[name].format(**kwargs)
38 37
39 38 if params:
40 39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 40 return base_url
42 41
43 42
44 43 @pytest.mark.usefixtures('autologin_user', 'app')
45 44 class TestAdminRepoVcsSettings(object):
46 45
47 46 @pytest.mark.parametrize('setting_name, setting_backends', [
48 47 ('hg_use_rebase_for_merging', ['hg']),
49 48 ])
50 49 def test_labs_settings_visible_if_enabled(
51 50 self, setting_name, setting_backends, backend):
52 51 if backend.alias not in setting_backends:
53 52 pytest.skip('Setting not available for backend {}'.format(backend))
54 53
55 54 vcs_settings_url = route_path(
56 55 'edit_repo_vcs', repo_name=backend.repo.repo_name)
57 56
58 57 with mock.patch.dict(
59 58 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
60 59 response = self.app.get(vcs_settings_url)
61 60
62 61 assertr = response.assert_response()
63 62 assertr.one_element_exists('#rhodecode_{}'.format(setting_name))
64 63
65 64 @pytest.mark.parametrize('setting_name, setting_backends', [
66 65 ('hg_use_rebase_for_merging', ['hg']),
67 66 ])
68 67 def test_update_boolean_settings(
69 68 self, csrf_token, setting_name, setting_backends, backend):
70 69 if backend.alias not in setting_backends:
71 70 pytest.skip('Setting not available for backend {}'.format(backend))
72 71
73 72 repo = backend.create_repo()
74 73 repo_name = repo.repo_name
75 74
76 75 settings_model = SettingsModel(repo=repo)
77 76 vcs_settings_url = route_path(
78 77 'edit_repo_vcs_update', repo_name=repo_name)
79 78
80 79 self.app.post(
81 80 vcs_settings_url,
82 81 params={
83 82 'inherit_global_settings': False,
84 83 'new_svn_branch': 'dummy-value-for-testing',
85 84 'new_svn_tag': 'dummy-value-for-testing',
86 85 'rhodecode_{}'.format(setting_name): 'true',
87 86 'csrf_token': csrf_token,
88 87 })
89 88 settings_model = SettingsModel(repo=Repository.get_by_repo_name(repo_name))
90 89 setting = settings_model.get_setting_by_name(setting_name)
91 90 assert setting.app_settings_value
92 91
93 92 self.app.post(
94 93 vcs_settings_url,
95 94 params={
96 95 'inherit_global_settings': False,
97 96 'new_svn_branch': 'dummy-value-for-testing',
98 97 'new_svn_tag': 'dummy-value-for-testing',
99 98 'rhodecode_{}'.format(setting_name): 'false',
100 99 'csrf_token': csrf_token,
101 100 })
102 101 settings_model = SettingsModel(repo=Repository.get_by_repo_name(repo_name))
103 102 setting = settings_model.get_setting_by_name(setting_name)
104 103 assert not setting.app_settings_value
@@ -1,113 +1,113 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from rhodecode.lib import helpers as h, rc_cache
22 22 from rhodecode.lib.utils2 import safe_int
23 23 from rhodecode.model.pull_request import get_diff_info
24 24 from rhodecode.model.db import PullRequestReviewers
25 25 # V3 - Reviewers, with default rules data
26 26 # v4 - Added observers metadata
27 27 # v5 - pr_author/commit_author include/exclude logic
28 28 REVIEWER_API_VERSION = 'V5'
29 29
30 30
31 31 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
32 32 """
33 33 Returns json struct of a reviewer for frontend
34 34
35 35 :param user: the reviewer
36 36 :param reasons: list of strings of why they are reviewers
37 37 :param mandatory: bool, to set user as mandatory
38 38 """
39 39 role = role or PullRequestReviewers.ROLE_REVIEWER
40 40 if role not in PullRequestReviewers.ROLES:
41 41 raise ValueError('role is not one of %s', PullRequestReviewers.ROLES)
42 42
43 43 return {
44 44 'user_id': user.user_id,
45 45 'reasons': reasons or [],
46 46 'rules': rules or [],
47 47 'role': role,
48 48 'mandatory': mandatory,
49 49 'user_group': user_group,
50 50 'username': user.username,
51 51 'first_name': user.first_name,
52 52 'last_name': user.last_name,
53 53 'user_link': h.link_to_user(user),
54 54 'gravatar_link': h.gravatar_url(user.email, 14),
55 55 }
56 56
57 57
58 58 def to_reviewers(e):
59 59 if isinstance(e, (tuple, list)):
60 60 return map(reviewer_as_json, e)
61 61 else:
62 62 return reviewer_as_json(e)
63 63
64 64
65 65 def get_default_reviewers_data(current_user, source_repo, source_ref, target_repo, target_ref,
66 66 include_diff_info=True):
67 67 """
68 68 Return json for default reviewers of a repository
69 69 """
70 70
71 71 diff_info = {}
72 72 if include_diff_info:
73 73 diff_info = get_diff_info(
74 74 source_repo, source_ref.commit_id, target_repo, target_ref.commit_id)
75 75
76 76 reasons = ['Default reviewer', 'Repository owner']
77 77 json_reviewers = [reviewer_as_json(
78 78 user=target_repo.user, reasons=reasons, mandatory=False, rules=None, role=None)]
79 79
80 80 compute_key = rc_cache.utils.compute_key_from_params(
81 81 current_user.user_id, source_repo.repo_id, source_ref.type, source_ref.name,
82 82 source_ref.commit_id, target_repo.repo_id, target_ref.type, target_ref.name,
83 83 target_ref.commit_id)
84 84
85 85 return {
86 86 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
87 87 'compute_key': compute_key,
88 88 'diff_info': diff_info,
89 89 'reviewers': json_reviewers,
90 90 'rules': {},
91 91 'rules_data': {},
92 92 'rules_humanized': [],
93 93 }
94 94
95 95
96 96 def validate_default_reviewers(review_members, reviewer_rules):
97 97 """
98 98 Function to validate submitted reviewers against the saved rules
99 99 """
100 100 reviewers = []
101 101 reviewer_by_id = {}
102 102 for r in review_members:
103 103 reviewer_user_id = safe_int(r['user_id'])
104 104 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['role'], r['rules'])
105 105
106 106 reviewer_by_id[reviewer_user_id] = entry
107 107 reviewers.append(entry)
108 108
109 109 return reviewers
110 110
111 111
112 112 def validate_observers(observer_members, reviewer_rules):
113 113 return {}
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,45 +1,45 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24
25 25 from rhodecode.apps._base import RepoAppView
26 26 from rhodecode.lib.auth import (
27 27 LoginRequired, HasRepoPermissionAnyDecorator)
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 class RepoArtifactsView(RepoAppView):
33 33
34 34 def load_default_context(self):
35 35 c = self._get_local_tmpl_context(include_app_defaults=True)
36 36 c.rhodecode_repo = self.rhodecode_vcs_repo
37 37 return c
38 38
39 39 @LoginRequired()
40 40 @HasRepoPermissionAnyDecorator(
41 41 'repository.read', 'repository.write', 'repository.admin')
42 42 def repo_artifacts(self):
43 43 c = self.load_default_context()
44 44 c.active = 'artifacts'
45 45 return self._get_template_context(c)
@@ -1,64 +1,64 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from rhodecode.apps._base import RepoAppView
25 25 from rhodecode.lib.helpers import SqlPage
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 28 from rhodecode.lib.utils2 import safe_int
29 29 from rhodecode.model.repo import RepoModel
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class AuditLogsView(RepoAppView):
35 35 def load_default_context(self):
36 36 c = self._get_local_tmpl_context()
37 37 return c
38 38
39 39 @LoginRequired()
40 40 @HasRepoPermissionAnyDecorator('repository.admin')
41 41 def repo_audit_logs(self):
42 42 _ = self.request.translate
43 43 c = self.load_default_context()
44 44 c.db_repo = self.db_repo
45 45
46 46 c.active = 'audit'
47 47
48 48 p = safe_int(self.request.GET.get('page', 1), 1)
49 49
50 50 filter_term = self.request.GET.get('filter')
51 51 user_log = RepoModel().get_repo_log(c.db_repo, filter_term)
52 52
53 53 def url_generator(page_num):
54 54 query_params = {
55 55 'page': page_num
56 56 }
57 57 if filter_term:
58 58 query_params['filter'] = filter_term
59 59 return self.request.current_route_path(_query=query_params)
60 60
61 61 c.audit_logs = SqlPage(
62 62 user_log, page=p, items_per_page=10, url_maker=url_generator)
63 63 c.filter_term = filter_term
64 64 return self._get_template_context(c)
@@ -1,43 +1,43 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24
25 25 from rhodecode.apps._base import RepoAppView
26 26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 class RepoAutomationView(RepoAppView):
33 33 def load_default_context(self):
34 34 c = self._get_local_tmpl_context()
35 35 return c
36 36
37 37 @LoginRequired()
38 38 @HasRepoPermissionAnyDecorator('repository.admin')
39 39 def repo_automation(self):
40 40 c = self.load_default_context()
41 41 c.active = 'automation'
42 42
43 43 return self._get_template_context(c)
@@ -1,53 +1,53 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import logging
21 21
22 22 from pyramid.httpexceptions import HTTPNotFound
23 23
24 24 from rhodecode.apps._base import BaseReferencesView
25 25 from rhodecode.lib import ext_json
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class RepoBookmarksView(BaseReferencesView):
34 34
35 35 @LoginRequired()
36 36 @HasRepoPermissionAnyDecorator(
37 37 'repository.read', 'repository.write', 'repository.admin')
38 38 def bookmarks(self):
39 39 c = self.load_default_context()
40 40 self._prepare_and_set_clone_url(c)
41 41 c.rhodecode_repo = self.rhodecode_vcs_repo
42 42 c.repository_forks = ScmModel().get_forks(self.db_repo)
43 43
44 44 if not h.is_hg(self.db_repo):
45 45 raise HTTPNotFound()
46 46
47 47 ref_items = self.rhodecode_vcs_repo.bookmarks.items()
48 48 data = self.load_refs_context(
49 49 ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako')
50 50
51 51 c.has_references = bool(data)
52 52 c.data = ext_json.str_json(data)
53 53 return self._get_template_context(c)
@@ -1,42 +1,42 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24
25 25 from rhodecode.apps._base import RepoAppView
26 26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 class RepoSettingsBranchPermissionsView(RepoAppView):
32 32
33 33 def load_default_context(self):
34 34 c = self._get_local_tmpl_context()
35 35 return c
36 36
37 37 @LoginRequired()
38 38 @HasRepoPermissionAnyDecorator('repository.admin')
39 39 def branch_permissions(self):
40 40 c = self.load_default_context()
41 41 c.active = 'permissions_branch'
42 42 return self._get_template_context(c)
@@ -1,49 +1,49 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from rhodecode.apps._base import BaseReferencesView
25 25 from rhodecode.lib import ext_json
26 26 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
27 27 from rhodecode.model.scm import ScmModel
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 class RepoBranchesView(BaseReferencesView):
33 33
34 34 @LoginRequired()
35 35 @HasRepoPermissionAnyDecorator(
36 36 'repository.read', 'repository.write', 'repository.admin')
37 37 def branches(self):
38 38 c = self.load_default_context()
39 39 self._prepare_and_set_clone_url(c)
40 40 c.rhodecode_repo = self.rhodecode_vcs_repo
41 41 c.repository_forks = ScmModel().get_forks(self.db_repo)
42 42
43 43 ref_items = self.rhodecode_vcs_repo.branches_all.items()
44 44 data = self.load_refs_context(
45 45 ref_items=ref_items, partials_template='branches/branches_data.mako')
46 46
47 47 c.has_references = bool(data)
48 48 c.data = ext_json.str_json(data)
49 49 return self._get_template_context(c)
@@ -1,93 +1,93 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.lib.auth import (
29 29 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 30 from rhodecode.lib import helpers as h, rc_cache
31 31 from rhodecode.lib import system_info
32 32 from rhodecode.model.meta import Session
33 33 from rhodecode.model.scm import ScmModel
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class RepoCachesView(RepoAppView):
39 39 def load_default_context(self):
40 40 c = self._get_local_tmpl_context()
41 41 return c
42 42
43 43 @LoginRequired()
44 44 @HasRepoPermissionAnyDecorator('repository.admin')
45 45 def repo_caches(self):
46 46 c = self.load_default_context()
47 47 c.active = 'caches'
48 48 cached_diffs_dir = c.rhodecode_db_repo.cached_diffs_dir
49 49 c.cached_diff_count = len(c.rhodecode_db_repo.cached_diffs())
50 50 c.cached_diff_size = 0
51 51 if os.path.isdir(cached_diffs_dir):
52 52 c.cached_diff_size = system_info.get_storage_size(cached_diffs_dir)
53 53 c.shadow_repos = c.rhodecode_db_repo.shadow_repos()
54 54
55 55 cache_namespace_uid = 'cache_repo.{}'.format(self.db_repo.repo_id)
56 56 c.region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
57 57 c.backend = c.region.backend
58 58 c.repo_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
59 59
60 60 return self._get_template_context(c)
61 61
62 62 @LoginRequired()
63 63 @HasRepoPermissionAnyDecorator('repository.admin')
64 64 @CSRFRequired()
65 65 def repo_caches_purge(self):
66 66 _ = self.request.translate
67 67 c = self.load_default_context()
68 68 c.active = 'caches'
69 69 invalidated = 0
70 70
71 71 try:
72 72 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
73 73 Session().commit()
74 74 invalidated +=1
75 75 except Exception:
76 76 log.exception("Exception during cache invalidation")
77 77 h.flash(_('An error occurred during cache invalidation'),
78 78 category='error')
79 79
80 80 try:
81 81 invalidated += 1
82 82 self.rhodecode_vcs_repo.vcsserver_invalidate_cache(delete=True)
83 83 except Exception:
84 84 log.exception("Exception during vcsserver cache invalidation")
85 85 h.flash(_('An error occurred during vcsserver cache invalidation'),
86 86 category='error')
87 87
88 88 if invalidated:
89 89 h.flash(_('Cache invalidation successful. Stages {}/2').format(invalidated),
90 90 category='success')
91 91
92 92 raise HTTPFound(h.route_path(
93 93 'edit_repo_caches', repo_name=self.db_repo_name)) No newline at end of file
@@ -1,356 +1,355 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20
22 21 import logging
23 22
24 23 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
25 24
26 25 from pyramid.renderers import render
27 26 from pyramid.response import Response
28 27
29 28 from rhodecode.apps._base import RepoAppView
30 29 import rhodecode.lib.helpers as h
31 30 from rhodecode.lib import ext_json
32 31 from rhodecode.lib.auth import (
33 32 LoginRequired, HasRepoPermissionAnyDecorator)
34 33
35 34 from rhodecode.lib.ext_json import json
36 35 from rhodecode.lib.graphmod import _colored, _dagwalker
37 36 from rhodecode.lib.helpers import RepoPage
38 37 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
39 38 from rhodecode.lib.vcs.exceptions import (
40 39 RepositoryError, CommitDoesNotExistError,
41 40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
42 41
43 42 log = logging.getLogger(__name__)
44 43
45 44 DEFAULT_CHANGELOG_SIZE = 20
46 45
47 46
48 47 class RepoChangelogView(RepoAppView):
49 48
50 49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
51 50 """
52 51 This is a safe way to get commit. If an error occurs it redirects to
53 52 tip with proper message
54 53
55 54 :param commit_id: id of commit to fetch
56 55 :param redirect_after: toggle redirection
57 56 """
58 57 _ = self.request.translate
59 58
60 59 try:
61 60 return self.rhodecode_vcs_repo.get_commit(commit_id)
62 61 except EmptyRepositoryError:
63 62 if not redirect_after:
64 63 return None
65 64
66 65 h.flash(h.literal(
67 66 _('There are no commits yet')), category='warning')
68 67 raise HTTPFound(
69 68 h.route_path('repo_summary', repo_name=self.db_repo_name))
70 69
71 70 except (CommitDoesNotExistError, LookupError):
72 71 msg = _('No such commit exists for this repository')
73 72 h.flash(msg, category='error')
74 73 raise HTTPNotFound()
75 74 except RepositoryError as e:
76 75 h.flash(h.escape(safe_str(e)), category='error')
77 76 raise HTTPNotFound()
78 77
79 78 def _graph(self, repo, commits, prev_data=None, next_data=None):
80 79 """
81 80 Generates a DAG graph for repo
82 81
83 82 :param repo: repo instance
84 83 :param commits: list of commits
85 84 """
86 85 if not commits:
87 86 return json.dumps([]), json.dumps([])
88 87
89 88 def serialize(commit, parents=True):
90 89 data = dict(
91 90 raw_id=commit.raw_id,
92 91 idx=commit.idx,
93 92 branch=None,
94 93 )
95 94 if parents:
96 95 data['parents'] = [
97 96 serialize(x, parents=False) for x in commit.parents]
98 97 return data
99 98
100 99 prev_data = prev_data or []
101 100 next_data = next_data or []
102 101
103 102 current = [serialize(x) for x in commits]
104 103 commits = prev_data + current + next_data
105 104
106 105 dag = _dagwalker(repo, commits)
107 106
108 107 data = [[commit_id, vtx, edges, branch]
109 108 for commit_id, vtx, edges, branch in _colored(dag)]
110 109 return ext_json.str_json(data), ext_json.str_json(current)
111 110
112 111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
113 112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
114 113 h.flash(u'Branch {} is not found.'.format(h.escape(safe_unicode(branch_name))),
115 114 category='warning')
116 115 redirect_url = h.route_path(
117 116 'repo_commits_file', repo_name=repo_name,
118 117 commit_id=branch_name, f_path=f_path or '')
119 118 raise HTTPFound(redirect_url)
120 119
121 120 def _load_changelog_data(
122 121 self, c, collection, page, chunk_size, branch_name=None,
123 122 dynamic=False, f_path=None, commit_id=None):
124 123
125 124 def url_generator(page_num):
126 125 query_params = {
127 126 'page': page_num
128 127 }
129 128
130 129 if branch_name:
131 130 query_params.update({
132 131 'branch': branch_name
133 132 })
134 133
135 134 if f_path:
136 135 # changelog for file
137 136 return h.route_path(
138 137 'repo_commits_file',
139 138 repo_name=c.rhodecode_db_repo.repo_name,
140 139 commit_id=commit_id, f_path=f_path,
141 140 _query=query_params)
142 141 else:
143 142 return h.route_path(
144 143 'repo_commits',
145 144 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
146 145
147 146 c.total_cs = len(collection)
148 147 c.showing_commits = min(chunk_size, c.total_cs)
149 148 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
150 149 items_per_page=chunk_size, url_maker=url_generator)
151 150
152 151 c.next_page = c.pagination.next_page
153 152 c.prev_page = c.pagination.previous_page
154 153
155 154 if dynamic:
156 155 if self.request.GET.get('chunk') != 'next':
157 156 c.next_page = None
158 157 if self.request.GET.get('chunk') != 'prev':
159 158 c.prev_page = None
160 159
161 160 page_commit_ids = [x.raw_id for x in c.pagination]
162 161 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
163 162 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
164 163
165 164 def load_default_context(self):
166 165 c = self._get_local_tmpl_context(include_app_defaults=True)
167 166
168 167 c.rhodecode_repo = self.rhodecode_vcs_repo
169 168
170 169 return c
171 170
172 171 @LoginRequired()
173 172 @HasRepoPermissionAnyDecorator(
174 173 'repository.read', 'repository.write', 'repository.admin')
175 174 def repo_changelog(self):
176 175 c = self.load_default_context()
177 176
178 177 commit_id = self.request.matchdict.get('commit_id')
179 178 f_path = self._get_f_path(self.request.matchdict)
180 179 show_hidden = str2bool(self.request.GET.get('evolve'))
181 180
182 181 chunk_size = 20
183 182
184 183 c.branch_name = branch_name = self.request.GET.get('branch') or ''
185 184 c.book_name = book_name = self.request.GET.get('bookmark') or ''
186 185 c.f_path = f_path
187 186 c.commit_id = commit_id
188 187 c.show_hidden = show_hidden
189 188
190 189 hist_limit = safe_int(self.request.GET.get('limit')) or None
191 190
192 191 p = safe_int(self.request.GET.get('page', 1), 1)
193 192
194 193 c.selected_name = branch_name or book_name
195 194 if not commit_id and branch_name:
196 195 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
197 196
198 197 c.changelog_for_path = f_path
199 198 pre_load = self.get_commit_preload_attrs()
200 199
201 200 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
202 201
203 202 try:
204 203 if f_path:
205 204 log.debug('generating changelog for path %s', f_path)
206 205 # get the history for the file !
207 206 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
208 207
209 208 try:
210 209 collection = base_commit.get_path_history(
211 210 f_path, limit=hist_limit, pre_load=pre_load)
212 211 if collection and partial_xhr:
213 212 # for ajax call we remove first one since we're looking
214 213 # at it right now in the context of a file commit
215 214 collection.pop(0)
216 215 except (NodeDoesNotExistError, CommitError):
217 216 # this node is not present at tip!
218 217 try:
219 218 commit = self._get_commit_or_redirect(commit_id)
220 219 collection = commit.get_path_history(f_path)
221 220 except RepositoryError as e:
222 221 h.flash(safe_str(e), category='warning')
223 222 redirect_url = h.route_path(
224 223 'repo_commits', repo_name=self.db_repo_name)
225 224 raise HTTPFound(redirect_url)
226 225 collection = list(reversed(collection))
227 226 else:
228 227 collection = self.rhodecode_vcs_repo.get_commits(
229 228 branch_name=branch_name, show_hidden=show_hidden,
230 229 pre_load=pre_load, translate_tags=False)
231 230
232 231 self._load_changelog_data(
233 232 c, collection, p, chunk_size, c.branch_name,
234 233 f_path=f_path, commit_id=commit_id)
235 234
236 235 except EmptyRepositoryError as e:
237 236 h.flash(h.escape(safe_str(e)), category='warning')
238 237 raise HTTPFound(
239 238 h.route_path('repo_summary', repo_name=self.db_repo_name))
240 239 except HTTPFound:
241 240 raise
242 241 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
243 242 log.exception(safe_str(e))
244 243 h.flash(h.escape(safe_str(e)), category='error')
245 244
246 245 if commit_id:
247 246 # from single commit page, we redirect to main commits
248 247 raise HTTPFound(
249 248 h.route_path('repo_commits', repo_name=self.db_repo_name))
250 249 else:
251 250 # otherwise we redirect to summary
252 251 raise HTTPFound(
253 252 h.route_path('repo_summary', repo_name=self.db_repo_name))
254 253
255 254
256 255
257 256 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
258 257 # case when loading dynamic file history in file view
259 258 # loading from ajax, we don't want the first result, it's popped
260 259 # in the code above
261 260 html = render(
262 261 'rhodecode:templates/commits/changelog_file_history.mako',
263 262 self._get_template_context(c), self.request)
264 263 return Response(html)
265 264
266 265 commit_ids = []
267 266 if not f_path:
268 267 # only load graph data when not in file history mode
269 268 commit_ids = c.pagination
270 269
271 270 c.graph_data, c.graph_commits = self._graph(
272 271 self.rhodecode_vcs_repo, commit_ids)
273 272
274 273 return self._get_template_context(c)
275 274
276 275 @LoginRequired()
277 276 @HasRepoPermissionAnyDecorator(
278 277 'repository.read', 'repository.write', 'repository.admin')
279 278 def repo_commits_elements(self):
280 279 c = self.load_default_context()
281 280 commit_id = self.request.matchdict.get('commit_id')
282 281 f_path = self._get_f_path(self.request.matchdict)
283 282 show_hidden = str2bool(self.request.GET.get('evolve'))
284 283
285 284 chunk_size = 20
286 285 hist_limit = safe_int(self.request.GET.get('limit')) or None
287 286
288 287 def wrap_for_error(err):
289 288 html = '<tr>' \
290 289 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
291 290 '</tr>'.format(err)
292 291 return Response(html)
293 292
294 293 c.branch_name = branch_name = self.request.GET.get('branch') or ''
295 294 c.book_name = book_name = self.request.GET.get('bookmark') or ''
296 295 c.f_path = f_path
297 296 c.commit_id = commit_id
298 297 c.show_hidden = show_hidden
299 298
300 299 c.selected_name = branch_name or book_name
301 300 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
302 301 return wrap_for_error(
303 302 safe_str('Branch: {} is not valid'.format(branch_name)))
304 303
305 304 pre_load = self.get_commit_preload_attrs()
306 305
307 306 if f_path:
308 307 try:
309 308 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
310 309 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
311 310 log.exception(safe_str(e))
312 311 raise HTTPFound(
313 312 h.route_path('repo_commits', repo_name=self.db_repo_name))
314 313
315 314 collection = base_commit.get_path_history(
316 315 f_path, limit=hist_limit, pre_load=pre_load)
317 316 collection = list(reversed(collection))
318 317 else:
319 318 collection = self.rhodecode_vcs_repo.get_commits(
320 319 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
321 320 translate_tags=False)
322 321
323 322 p = safe_int(self.request.GET.get('page', 1), 1)
324 323 try:
325 324 self._load_changelog_data(
326 325 c, collection, p, chunk_size, dynamic=True,
327 326 f_path=f_path, commit_id=commit_id)
328 327 except EmptyRepositoryError as e:
329 328 return wrap_for_error(safe_str(e))
330 329 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
331 330 log.exception('Failed to fetch commits')
332 331 return wrap_for_error(safe_str(e))
333 332
334 333 prev_data = None
335 334 next_data = None
336 335
337 336 try:
338 337 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
339 338 except json.JSONDecodeError:
340 339 prev_graph = {}
341 340
342 341 if self.request.GET.get('chunk') == 'prev':
343 342 next_data = prev_graph
344 343 elif self.request.GET.get('chunk') == 'next':
345 344 prev_data = prev_graph
346 345
347 346 commit_ids = []
348 347 if not f_path:
349 348 # only load graph data when not in file history mode
350 349 commit_ids = c.pagination
351 350
352 351 c.graph_data, c.graph_commits = self._graph(
353 352 self.rhodecode_vcs_repo, commit_ids,
354 353 prev_data=prev_data, next_data=next_data)
355 354
356 355 return self._get_template_context(c)
@@ -1,117 +1,117 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 24
25 25 from rhodecode.apps._base import BaseAppView
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.auth import (NotAnonymous, HasRepoPermissionAny)
28 28 from rhodecode.model.db import Repository
29 29 from rhodecode.model.permission import PermissionModel
30 30 from rhodecode.model.validation_schema.types import RepoNameType
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class RepoChecksView(BaseAppView):
36 36 def load_default_context(self):
37 37 c = self._get_local_tmpl_context()
38 38 return c
39 39
40 40 @NotAnonymous()
41 41 def repo_creating(self):
42 42 c = self.load_default_context()
43 43 repo_name = self.request.matchdict['repo_name']
44 44 repo_name = RepoNameType().deserialize(None, repo_name)
45 45 db_repo = Repository.get_by_repo_name(repo_name)
46 46
47 47 # check if maybe repo is already created
48 48 if db_repo and db_repo.repo_state in [Repository.STATE_CREATED]:
49 49 self.flush_permissions_on_creation(db_repo)
50 50
51 51 # re-check permissions before redirecting to prevent resource
52 52 # discovery by checking the 302 code
53 53 perm_set = ['repository.read', 'repository.write', 'repository.admin']
54 54 has_perm = HasRepoPermissionAny(*perm_set)(
55 55 db_repo.repo_name, 'Repo Creating check')
56 56 if not has_perm:
57 57 raise HTTPNotFound()
58 58
59 59 raise HTTPFound(h.route_path(
60 60 'repo_summary', repo_name=db_repo.repo_name))
61 61
62 62 c.task_id = self.request.GET.get('task_id')
63 63 c.repo_name = repo_name
64 64
65 65 return self._get_template_context(c)
66 66
67 67 @NotAnonymous()
68 68 def repo_creating_check(self):
69 69 _ = self.request.translate
70 70 task_id = self.request.GET.get('task_id')
71 71 self.load_default_context()
72 72
73 73 repo_name = self.request.matchdict['repo_name']
74 74
75 75 if task_id and task_id not in ['None']:
76 76 import rhodecode
77 77 from rhodecode.lib.celerylib.loader import celery_app, exceptions
78 78 if rhodecode.CELERY_ENABLED:
79 79 log.debug('celery: checking result for task:%s', task_id)
80 80 task = celery_app.AsyncResult(task_id)
81 81 try:
82 82 task.get(timeout=10)
83 83 except exceptions.TimeoutError:
84 84 task = None
85 85 if task and task.failed():
86 86 msg = self._log_creation_exception(task.result, repo_name)
87 87 h.flash(msg, category='error')
88 88 raise HTTPFound(h.route_path('home'), code=501)
89 89
90 90 db_repo = Repository.get_by_repo_name(repo_name)
91 91 if db_repo and db_repo.repo_state == Repository.STATE_CREATED:
92 92 if db_repo.clone_uri:
93 93 clone_uri = db_repo.clone_uri_hidden
94 94 h.flash(_('Created repository %s from %s')
95 95 % (db_repo.repo_name, clone_uri), category='success')
96 96 else:
97 97 repo_url = h.link_to(
98 98 db_repo.repo_name,
99 99 h.route_path('repo_summary', repo_name=db_repo.repo_name))
100 100 fork = db_repo.fork
101 101 if fork:
102 102 fork_name = fork.repo_name
103 103 h.flash(h.literal(_('Forked repository %s as %s')
104 104 % (fork_name, repo_url)), category='success')
105 105 else:
106 106 h.flash(h.literal(_('Created repository %s') % repo_url),
107 107 category='success')
108 108 self.flush_permissions_on_creation(db_repo)
109 109
110 110 return {'result': True}
111 111 return {'result': False}
112 112
113 113 def flush_permissions_on_creation(self, db_repo):
114 114 # repo is finished and created, we flush the permissions now
115 115 user_group_perms = db_repo.permissions(expand_from_user_groups=True)
116 116 affected_user_ids = [perm['user_id'] for perm in user_group_perms]
117 117 PermissionModel().trigger_permission_flush(affected_user_ids)
@@ -1,819 +1,818 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import logging
22 21 import collections
23 22
24 23 from pyramid.httpexceptions import (
25 24 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 25 from pyramid.renderers import render
27 26 from pyramid.response import Response
28 27
29 28 from rhodecode.apps._base import RepoAppView
30 29 from rhodecode.apps.file_store import utils as store_utils
31 30 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 31
33 32 from rhodecode.lib import diffs, codeblocks, channelstream
34 33 from rhodecode.lib.auth import (
35 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 35 from rhodecode.lib import ext_json
37 36 from collections import OrderedDict
38 37 from rhodecode.lib.diffs import (
39 38 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 39 get_diff_whitespace_flag)
41 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 41 import rhodecode.lib.helpers as h
43 42 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict, safe_str
44 43 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 44 from rhodecode.lib.vcs.exceptions import (
46 45 RepositoryError, CommitDoesNotExistError)
47 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 47 ChangesetCommentHistory
49 48 from rhodecode.model.changeset_status import ChangesetStatusModel
50 49 from rhodecode.model.comment import CommentsModel
51 50 from rhodecode.model.meta import Session
52 51 from rhodecode.model.settings import VcsSettingsModel
53 52
54 53 log = logging.getLogger(__name__)
55 54
56 55
57 56 def _update_with_GET(params, request):
58 57 for k in ['diff1', 'diff2', 'diff']:
59 58 params[k] += request.GET.getall(k)
60 59
61 60
62 61 class RepoCommitsView(RepoAppView):
63 62 def load_default_context(self):
64 63 c = self._get_local_tmpl_context(include_app_defaults=True)
65 64 c.rhodecode_repo = self.rhodecode_vcs_repo
66 65
67 66 return c
68 67
69 68 def _is_diff_cache_enabled(self, target_repo):
70 69 caching_enabled = self._get_general_setting(
71 70 target_repo, 'rhodecode_diff_cache')
72 71 log.debug('Diff caching enabled: %s', caching_enabled)
73 72 return caching_enabled
74 73
75 74 def _commit(self, commit_id_range, method):
76 75 _ = self.request.translate
77 76 c = self.load_default_context()
78 77 c.fulldiff = self.request.GET.get('fulldiff')
79 78 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
80 79
81 80 # fetch global flags of ignore ws or context lines
82 81 diff_context = get_diff_context(self.request)
83 82 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84 83
85 84 # diff_limit will cut off the whole diff if the limit is applied
86 85 # otherwise it will just hide the big files from the front-end
87 86 diff_limit = c.visual.cut_off_limit_diff
88 87 file_limit = c.visual.cut_off_limit_file
89 88
90 89 # get ranges of commit ids if preset
91 90 commit_range = commit_id_range.split('...')[:2]
92 91
93 92 try:
94 93 pre_load = ['affected_files', 'author', 'branch', 'date',
95 94 'message', 'parents']
96 95 if self.rhodecode_vcs_repo.alias == 'hg':
97 96 pre_load += ['hidden', 'obsolete', 'phase']
98 97
99 98 if len(commit_range) == 2:
100 99 commits = self.rhodecode_vcs_repo.get_commits(
101 100 start_id=commit_range[0], end_id=commit_range[1],
102 101 pre_load=pre_load, translate_tags=False)
103 102 commits = list(commits)
104 103 else:
105 104 commits = [self.rhodecode_vcs_repo.get_commit(
106 105 commit_id=commit_id_range, pre_load=pre_load)]
107 106
108 107 c.commit_ranges = commits
109 108 if not c.commit_ranges:
110 109 raise RepositoryError('The commit range returned an empty result')
111 110 except CommitDoesNotExistError as e:
112 111 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
113 112 h.flash(msg, category='error')
114 113 raise HTTPNotFound()
115 114 except Exception:
116 115 log.exception("General failure")
117 116 raise HTTPNotFound()
118 117 single_commit = len(c.commit_ranges) == 1
119 118
120 119 if redirect_to_combined and not single_commit:
121 120 source_ref = getattr(c.commit_ranges[0].parents[0]
122 121 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
123 122 target_ref = c.commit_ranges[-1].raw_id
124 123 next_url = h.route_path(
125 124 'repo_compare',
126 125 repo_name=c.repo_name,
127 126 source_ref_type='rev',
128 127 source_ref=source_ref,
129 128 target_ref_type='rev',
130 129 target_ref=target_ref)
131 130 raise HTTPFound(next_url)
132 131
133 132 c.changes = OrderedDict()
134 133 c.lines_added = 0
135 134 c.lines_deleted = 0
136 135
137 136 # auto collapse if we have more than limit
138 137 collapse_limit = diffs.DiffProcessor._collapse_commits_over
139 138 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
140 139
141 140 c.commit_statuses = ChangesetStatus.STATUSES
142 141 c.inline_comments = []
143 142 c.files = []
144 143
145 144 c.comments = []
146 145 c.unresolved_comments = []
147 146 c.resolved_comments = []
148 147
149 148 # Single commit
150 149 if single_commit:
151 150 commit = c.commit_ranges[0]
152 151 c.comments = CommentsModel().get_comments(
153 152 self.db_repo.repo_id,
154 153 revision=commit.raw_id)
155 154
156 155 # comments from PR
157 156 statuses = ChangesetStatusModel().get_statuses(
158 157 self.db_repo.repo_id, commit.raw_id,
159 158 with_revisions=True)
160 159
161 160 prs = set()
162 161 reviewers = list()
163 162 reviewers_duplicates = set() # to not have duplicates from multiple votes
164 163 for c_status in statuses:
165 164
166 165 # extract associated pull-requests from votes
167 166 if c_status.pull_request:
168 167 prs.add(c_status.pull_request)
169 168
170 169 # extract reviewers
171 170 _user_id = c_status.author.user_id
172 171 if _user_id not in reviewers_duplicates:
173 172 reviewers.append(
174 173 StrictAttributeDict({
175 174 'user': c_status.author,
176 175
177 176 # fake attributed for commit, page that we don't have
178 177 # but we share the display with PR page
179 178 'mandatory': False,
180 179 'reasons': [],
181 180 'rule_user_group_data': lambda: None
182 181 })
183 182 )
184 183 reviewers_duplicates.add(_user_id)
185 184
186 185 c.reviewers_count = len(reviewers)
187 186 c.observers_count = 0
188 187
189 188 # from associated statuses, check the pull requests, and
190 189 # show comments from them
191 190 for pr in prs:
192 191 c.comments.extend(pr.comments)
193 192
194 193 c.unresolved_comments = CommentsModel()\
195 194 .get_commit_unresolved_todos(commit.raw_id)
196 195 c.resolved_comments = CommentsModel()\
197 196 .get_commit_resolved_todos(commit.raw_id)
198 197
199 198 c.inline_comments_flat = CommentsModel()\
200 199 .get_commit_inline_comments(commit.raw_id)
201 200
202 201 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
203 202 statuses, reviewers)
204 203
205 204 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
206 205
207 206 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
208 207
209 208 for review_obj, member, reasons, mandatory, status in review_statuses:
210 209 member_reviewer = h.reviewer_as_json(
211 210 member, reasons=reasons, mandatory=mandatory, role=None,
212 211 user_group=None
213 212 )
214 213
215 214 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
216 215 member_reviewer['review_status'] = current_review_status
217 216 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
218 217 member_reviewer['allowed_to_update'] = False
219 218 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
220 219
221 220 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
222 221
223 222 # NOTE(marcink): this uses the same voting logic as in pull-requests
224 223 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
225 224 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
226 225
227 226 diff = None
228 227 # Iterate over ranges (default commit view is always one commit)
229 228 for commit in c.commit_ranges:
230 229 c.changes[commit.raw_id] = []
231 230
232 231 commit2 = commit
233 232 commit1 = commit.first_parent
234 233
235 234 if method == 'show':
236 235 inline_comments = CommentsModel().get_inline_comments(
237 236 self.db_repo.repo_id, revision=commit.raw_id)
238 237 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
239 238 inline_comments))
240 239 c.inline_comments = inline_comments
241 240
242 241 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
243 242 self.db_repo)
244 243 cache_file_path = diff_cache_exist(
245 244 cache_path, 'diff', commit.raw_id,
246 245 hide_whitespace_changes, diff_context, c.fulldiff)
247 246
248 247 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
249 248 force_recache = str2bool(self.request.GET.get('force_recache'))
250 249
251 250 cached_diff = None
252 251 if caching_enabled:
253 252 cached_diff = load_cached_diff(cache_file_path)
254 253
255 254 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
256 255 if not force_recache and has_proper_diff_cache:
257 256 diffset = cached_diff['diff']
258 257 else:
259 258 vcs_diff = self.rhodecode_vcs_repo.get_diff(
260 259 commit1, commit2,
261 260 ignore_whitespace=hide_whitespace_changes,
262 261 context=diff_context)
263 262
264 263 diff_processor = diffs.DiffProcessor(
265 264 vcs_diff, format='newdiff', diff_limit=diff_limit,
266 265 file_limit=file_limit, show_full_diff=c.fulldiff)
267 266
268 267 _parsed = diff_processor.prepare()
269 268
270 269 diffset = codeblocks.DiffSet(
271 270 repo_name=self.db_repo_name,
272 271 source_node_getter=codeblocks.diffset_node_getter(commit1),
273 272 target_node_getter=codeblocks.diffset_node_getter(commit2))
274 273
275 274 diffset = self.path_filter.render_patchset_filtered(
276 275 diffset, _parsed, commit1.raw_id, commit2.raw_id)
277 276
278 277 # save cached diff
279 278 if caching_enabled:
280 279 cache_diff(cache_file_path, diffset, None)
281 280
282 281 c.limited_diff = diffset.limited_diff
283 282 c.changes[commit.raw_id] = diffset
284 283 else:
285 284 # TODO(marcink): no cache usage here...
286 285 _diff = self.rhodecode_vcs_repo.get_diff(
287 286 commit1, commit2,
288 287 ignore_whitespace=hide_whitespace_changes, context=diff_context)
289 288 diff_processor = diffs.DiffProcessor(
290 289 _diff, format='newdiff', diff_limit=diff_limit,
291 290 file_limit=file_limit, show_full_diff=c.fulldiff)
292 291 # downloads/raw we only need RAW diff nothing else
293 292 diff = self.path_filter.get_raw_patch(diff_processor)
294 293 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
295 294
296 295 # sort comments by how they were generated
297 296 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
298 297 c.at_version_num = None
299 298
300 299 if len(c.commit_ranges) == 1:
301 300 c.commit = c.commit_ranges[0]
302 301 c.parent_tmpl = ''.join(
303 302 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
304 303
305 304 if method == 'download':
306 305 response = Response(diff)
307 306 response.content_type = 'text/plain'
308 307 response.content_disposition = (
309 308 'attachment; filename=%s.diff' % commit_id_range[:12])
310 309 return response
311 310 elif method == 'patch':
312 311 c.diff = safe_unicode(diff)
313 312 patch = render(
314 313 'rhodecode:templates/changeset/patch_changeset.mako',
315 314 self._get_template_context(c), self.request)
316 315 response = Response(patch)
317 316 response.content_type = 'text/plain'
318 317 return response
319 318 elif method == 'raw':
320 319 response = Response(diff)
321 320 response.content_type = 'text/plain'
322 321 return response
323 322 elif method == 'show':
324 323 if len(c.commit_ranges) == 1:
325 324 html = render(
326 325 'rhodecode:templates/changeset/changeset.mako',
327 326 self._get_template_context(c), self.request)
328 327 return Response(html)
329 328 else:
330 329 c.ancestor = None
331 330 c.target_repo = self.db_repo
332 331 html = render(
333 332 'rhodecode:templates/changeset/changeset_range.mako',
334 333 self._get_template_context(c), self.request)
335 334 return Response(html)
336 335
337 336 raise HTTPBadRequest()
338 337
339 338 @LoginRequired()
340 339 @HasRepoPermissionAnyDecorator(
341 340 'repository.read', 'repository.write', 'repository.admin')
342 341 def repo_commit_show(self):
343 342 commit_id = self.request.matchdict['commit_id']
344 343 return self._commit(commit_id, method='show')
345 344
346 345 @LoginRequired()
347 346 @HasRepoPermissionAnyDecorator(
348 347 'repository.read', 'repository.write', 'repository.admin')
349 348 def repo_commit_raw(self):
350 349 commit_id = self.request.matchdict['commit_id']
351 350 return self._commit(commit_id, method='raw')
352 351
353 352 @LoginRequired()
354 353 @HasRepoPermissionAnyDecorator(
355 354 'repository.read', 'repository.write', 'repository.admin')
356 355 def repo_commit_patch(self):
357 356 commit_id = self.request.matchdict['commit_id']
358 357 return self._commit(commit_id, method='patch')
359 358
360 359 @LoginRequired()
361 360 @HasRepoPermissionAnyDecorator(
362 361 'repository.read', 'repository.write', 'repository.admin')
363 362 def repo_commit_download(self):
364 363 commit_id = self.request.matchdict['commit_id']
365 364 return self._commit(commit_id, method='download')
366 365
367 366 def _commit_comments_create(self, commit_id, comments):
368 367 _ = self.request.translate
369 368 data = {}
370 369 if not comments:
371 370 return
372 371
373 372 commit = self.db_repo.get_commit(commit_id)
374 373
375 374 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
376 375 for entry in comments:
377 376 c = self.load_default_context()
378 377 comment_type = entry['comment_type']
379 378 text = entry['text']
380 379 status = entry['status']
381 380 is_draft = str2bool(entry['is_draft'])
382 381 resolves_comment_id = entry['resolves_comment_id']
383 382 f_path = entry['f_path']
384 383 line_no = entry['line']
385 384 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
386 385
387 386 if status:
388 387 text = text or (_('Status change %(transition_icon)s %(status)s')
389 388 % {'transition_icon': '>',
390 389 'status': ChangesetStatus.get_status_lbl(status)})
391 390
392 391 comment = CommentsModel().create(
393 392 text=text,
394 393 repo=self.db_repo.repo_id,
395 394 user=self._rhodecode_db_user.user_id,
396 395 commit_id=commit_id,
397 396 f_path=f_path,
398 397 line_no=line_no,
399 398 status_change=(ChangesetStatus.get_status_lbl(status)
400 399 if status else None),
401 400 status_change_type=status,
402 401 comment_type=comment_type,
403 402 is_draft=is_draft,
404 403 resolves_comment_id=resolves_comment_id,
405 404 auth_user=self._rhodecode_user,
406 405 send_email=not is_draft, # skip notification for draft comments
407 406 )
408 407 is_inline = comment.is_inline
409 408
410 409 # get status if set !
411 410 if status:
412 411 # `dont_allow_on_closed_pull_request = True` means
413 412 # if latest status was from pull request and it's closed
414 413 # disallow changing status !
415 414
416 415 try:
417 416 ChangesetStatusModel().set_status(
418 417 self.db_repo.repo_id,
419 418 status,
420 419 self._rhodecode_db_user.user_id,
421 420 comment,
422 421 revision=commit_id,
423 422 dont_allow_on_closed_pull_request=True
424 423 )
425 424 except StatusChangeOnClosedPullRequestError:
426 425 msg = _('Changing the status of a commit associated with '
427 426 'a closed pull request is not allowed')
428 427 log.exception(msg)
429 428 h.flash(msg, category='warning')
430 429 raise HTTPFound(h.route_path(
431 430 'repo_commit', repo_name=self.db_repo_name,
432 431 commit_id=commit_id))
433 432
434 433 Session().flush()
435 434 # this is somehow required to get access to some relationship
436 435 # loaded on comment
437 436 Session().refresh(comment)
438 437
439 438 # skip notifications for drafts
440 439 if not is_draft:
441 440 CommentsModel().trigger_commit_comment_hook(
442 441 self.db_repo, self._rhodecode_user, 'create',
443 442 data={'comment': comment, 'commit': commit})
444 443
445 444 comment_id = comment.comment_id
446 445 data[comment_id] = {
447 446 'target_id': target_elem_id
448 447 }
449 448 Session().flush()
450 449
451 450 c.co = comment
452 451 c.at_version_num = 0
453 452 c.is_new = True
454 453 rendered_comment = render(
455 454 'rhodecode:templates/changeset/changeset_comment_block.mako',
456 455 self._get_template_context(c), self.request)
457 456
458 457 data[comment_id].update(comment.get_dict())
459 458 data[comment_id].update({'rendered_text': rendered_comment})
460 459
461 460 # finalize, commit and redirect
462 461 Session().commit()
463 462
464 463 # skip channelstream for draft comments
465 464 if not all_drafts:
466 465 comment_broadcast_channel = channelstream.comment_channel(
467 466 self.db_repo_name, commit_obj=commit)
468 467
469 468 comment_data = data
470 469 posted_comment_type = 'inline' if is_inline else 'general'
471 470 if len(data) == 1:
472 471 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
473 472 else:
474 473 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
475 474
476 475 channelstream.comment_channelstream_push(
477 476 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
478 477 comment_data=comment_data)
479 478
480 479 return data
481 480
482 481 @LoginRequired()
483 482 @NotAnonymous()
484 483 @HasRepoPermissionAnyDecorator(
485 484 'repository.read', 'repository.write', 'repository.admin')
486 485 @CSRFRequired()
487 486 def repo_commit_comment_create(self):
488 487 _ = self.request.translate
489 488 commit_id = self.request.matchdict['commit_id']
490 489
491 490 multi_commit_ids = []
492 491 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
493 492 if _commit_id not in ['', None, EmptyCommit.raw_id]:
494 493 if _commit_id not in multi_commit_ids:
495 494 multi_commit_ids.append(_commit_id)
496 495
497 496 commit_ids = multi_commit_ids or [commit_id]
498 497
499 498 data = []
500 499 # Multiple comments for each passed commit id
501 500 for current_id in filter(None, commit_ids):
502 501 comment_data = {
503 502 'comment_type': self.request.POST.get('comment_type'),
504 503 'text': self.request.POST.get('text'),
505 504 'status': self.request.POST.get('changeset_status', None),
506 505 'is_draft': self.request.POST.get('draft'),
507 506 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
508 507 'close_pull_request': self.request.POST.get('close_pull_request'),
509 508 'f_path': self.request.POST.get('f_path'),
510 509 'line': self.request.POST.get('line'),
511 510 }
512 511 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
513 512 data.append(comment)
514 513
515 514 return data if len(data) > 1 else data[0]
516 515
517 516 @LoginRequired()
518 517 @NotAnonymous()
519 518 @HasRepoPermissionAnyDecorator(
520 519 'repository.read', 'repository.write', 'repository.admin')
521 520 @CSRFRequired()
522 521 def repo_commit_comment_preview(self):
523 522 # Technically a CSRF token is not needed as no state changes with this
524 523 # call. However, as this is a POST is better to have it, so automated
525 524 # tools don't flag it as potential CSRF.
526 525 # Post is required because the payload could be bigger than the maximum
527 526 # allowed by GET.
528 527
529 528 text = self.request.POST.get('text')
530 529 renderer = self.request.POST.get('renderer') or 'rst'
531 530 if text:
532 531 return h.render(text, renderer=renderer, mentions=True,
533 532 repo_name=self.db_repo_name)
534 533 return ''
535 534
536 535 @LoginRequired()
537 536 @HasRepoPermissionAnyDecorator(
538 537 'repository.read', 'repository.write', 'repository.admin')
539 538 @CSRFRequired()
540 539 def repo_commit_comment_history_view(self):
541 540 c = self.load_default_context()
542 541 comment_id = self.request.matchdict['comment_id']
543 542 comment_history_id = self.request.matchdict['comment_history_id']
544 543
545 544 comment = ChangesetComment.get_or_404(comment_id)
546 545 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
547 546 if comment.draft and not comment_owner:
548 547 # if we see draft comments history, we only allow this for owner
549 548 raise HTTPNotFound()
550 549
551 550 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
552 551 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
553 552
554 553 if is_repo_comment:
555 554 c.comment_history = comment_history
556 555
557 556 rendered_comment = render(
558 557 'rhodecode:templates/changeset/comment_history.mako',
559 558 self._get_template_context(c), self.request)
560 559 return rendered_comment
561 560 else:
562 561 log.warning('No permissions for user %s to show comment_history_id: %s',
563 562 self._rhodecode_db_user, comment_history_id)
564 563 raise HTTPNotFound()
565 564
566 565 @LoginRequired()
567 566 @NotAnonymous()
568 567 @HasRepoPermissionAnyDecorator(
569 568 'repository.read', 'repository.write', 'repository.admin')
570 569 @CSRFRequired()
571 570 def repo_commit_comment_attachment_upload(self):
572 571 c = self.load_default_context()
573 572 upload_key = 'attachment'
574 573
575 574 file_obj = self.request.POST.get(upload_key)
576 575
577 576 if file_obj is None:
578 577 self.request.response.status = 400
579 578 return {'store_fid': None,
580 579 'access_path': None,
581 580 'error': '{} data field is missing'.format(upload_key)}
582 581
583 582 if not hasattr(file_obj, 'filename'):
584 583 self.request.response.status = 400
585 584 return {'store_fid': None,
586 585 'access_path': None,
587 586 'error': 'filename cannot be read from the data field'}
588 587
589 588 filename = file_obj.filename
590 589 file_display_name = filename
591 590
592 591 metadata = {
593 592 'user_uploaded': {'username': self._rhodecode_user.username,
594 593 'user_id': self._rhodecode_user.user_id,
595 594 'ip': self._rhodecode_user.ip_addr}}
596 595
597 596 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
598 597 allowed_extensions = [
599 598 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
600 599 '.pptx', '.txt', '.xlsx', '.zip']
601 600 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
602 601
603 602 try:
604 603 storage = store_utils.get_file_storage(self.request.registry.settings)
605 604 store_uid, metadata = storage.save_file(
606 605 file_obj.file, filename, extra_metadata=metadata,
607 606 extensions=allowed_extensions, max_filesize=max_file_size)
608 607 except FileNotAllowedException:
609 608 self.request.response.status = 400
610 609 permitted_extensions = ', '.join(allowed_extensions)
611 610 error_msg = 'File `{}` is not allowed. ' \
612 611 'Only following extensions are permitted: {}'.format(
613 612 filename, permitted_extensions)
614 613 return {'store_fid': None,
615 614 'access_path': None,
616 615 'error': error_msg}
617 616 except FileOverSizeException:
618 617 self.request.response.status = 400
619 618 limit_mb = h.format_byte_size_binary(max_file_size)
620 619 return {'store_fid': None,
621 620 'access_path': None,
622 621 'error': 'File {} is exceeding allowed limit of {}.'.format(
623 622 filename, limit_mb)}
624 623
625 624 try:
626 625 entry = FileStore.create(
627 626 file_uid=store_uid, filename=metadata["filename"],
628 627 file_hash=metadata["sha256"], file_size=metadata["size"],
629 628 file_display_name=file_display_name,
630 629 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
631 630 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
632 631 scope_repo_id=self.db_repo.repo_id
633 632 )
634 633 Session().add(entry)
635 634 Session().commit()
636 635 log.debug('Stored upload in DB as %s', entry)
637 636 except Exception:
638 637 log.exception('Failed to store file %s', filename)
639 638 self.request.response.status = 400
640 639 return {'store_fid': None,
641 640 'access_path': None,
642 641 'error': 'File {} failed to store in DB.'.format(filename)}
643 642
644 643 Session().commit()
645 644
646 645 return {
647 646 'store_fid': store_uid,
648 647 'access_path': h.route_path(
649 648 'download_file', fid=store_uid),
650 649 'fqn_access_path': h.route_url(
651 650 'download_file', fid=store_uid),
652 651 'repo_access_path': h.route_path(
653 652 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
654 653 'repo_fqn_access_path': h.route_url(
655 654 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
656 655 }
657 656
658 657 @LoginRequired()
659 658 @NotAnonymous()
660 659 @HasRepoPermissionAnyDecorator(
661 660 'repository.read', 'repository.write', 'repository.admin')
662 661 @CSRFRequired()
663 662 def repo_commit_comment_delete(self):
664 663 commit_id = self.request.matchdict['commit_id']
665 664 comment_id = self.request.matchdict['comment_id']
666 665
667 666 comment = ChangesetComment.get_or_404(comment_id)
668 667 if not comment:
669 668 log.debug('Comment with id:%s not found, skipping', comment_id)
670 669 # comment already deleted in another call probably
671 670 return True
672 671
673 672 if comment.immutable:
674 673 # don't allow deleting comments that are immutable
675 674 raise HTTPForbidden()
676 675
677 676 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
678 677 super_admin = h.HasPermissionAny('hg.admin')()
679 678 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
680 679 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
681 680 comment_repo_admin = is_repo_admin and is_repo_comment
682 681
683 682 if comment.draft and not comment_owner:
684 683 # We never allow to delete draft comments for other than owners
685 684 raise HTTPNotFound()
686 685
687 686 if super_admin or comment_owner or comment_repo_admin:
688 687 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
689 688 Session().commit()
690 689 return True
691 690 else:
692 691 log.warning('No permissions for user %s to delete comment_id: %s',
693 692 self._rhodecode_db_user, comment_id)
694 693 raise HTTPNotFound()
695 694
696 695 @LoginRequired()
697 696 @NotAnonymous()
698 697 @HasRepoPermissionAnyDecorator(
699 698 'repository.read', 'repository.write', 'repository.admin')
700 699 @CSRFRequired()
701 700 def repo_commit_comment_edit(self):
702 701 self.load_default_context()
703 702
704 703 commit_id = self.request.matchdict['commit_id']
705 704 comment_id = self.request.matchdict['comment_id']
706 705 comment = ChangesetComment.get_or_404(comment_id)
707 706
708 707 if comment.immutable:
709 708 # don't allow deleting comments that are immutable
710 709 raise HTTPForbidden()
711 710
712 711 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
713 712 super_admin = h.HasPermissionAny('hg.admin')()
714 713 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
715 714 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
716 715 comment_repo_admin = is_repo_admin and is_repo_comment
717 716
718 717 if super_admin or comment_owner or comment_repo_admin:
719 718 text = self.request.POST.get('text')
720 719 version = self.request.POST.get('version')
721 720 if text == comment.text:
722 721 log.warning(
723 722 'Comment(repo): '
724 723 'Trying to create new version '
725 724 'with the same comment body {}'.format(
726 725 comment_id,
727 726 )
728 727 )
729 728 raise HTTPNotFound()
730 729
731 730 if version.isdigit():
732 731 version = int(version)
733 732 else:
734 733 log.warning(
735 734 'Comment(repo): Wrong version type {} {} '
736 735 'for comment {}'.format(
737 736 version,
738 737 type(version),
739 738 comment_id,
740 739 )
741 740 )
742 741 raise HTTPNotFound()
743 742
744 743 try:
745 744 comment_history = CommentsModel().edit(
746 745 comment_id=comment_id,
747 746 text=text,
748 747 auth_user=self._rhodecode_user,
749 748 version=version,
750 749 )
751 750 except CommentVersionMismatch:
752 751 raise HTTPConflict()
753 752
754 753 if not comment_history:
755 754 raise HTTPNotFound()
756 755
757 756 if not comment.draft:
758 757 commit = self.db_repo.get_commit(commit_id)
759 758 CommentsModel().trigger_commit_comment_hook(
760 759 self.db_repo, self._rhodecode_user, 'edit',
761 760 data={'comment': comment, 'commit': commit})
762 761
763 762 Session().commit()
764 763 return {
765 764 'comment_history_id': comment_history.comment_history_id,
766 765 'comment_id': comment.comment_id,
767 766 'comment_version': comment_history.version,
768 767 'comment_author_username': comment_history.author.username,
769 768 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
770 769 'comment_created_on': h.age_component(comment_history.created_on,
771 770 time_is_local=True),
772 771 }
773 772 else:
774 773 log.warning('No permissions for user %s to edit comment_id: %s',
775 774 self._rhodecode_db_user, comment_id)
776 775 raise HTTPNotFound()
777 776
778 777 @LoginRequired()
779 778 @HasRepoPermissionAnyDecorator(
780 779 'repository.read', 'repository.write', 'repository.admin')
781 780 def repo_commit_data(self):
782 781 commit_id = self.request.matchdict['commit_id']
783 782 self.load_default_context()
784 783
785 784 try:
786 785 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
787 786 except CommitDoesNotExistError as e:
788 787 return EmptyCommit(message=str(e))
789 788
790 789 @LoginRequired()
791 790 @HasRepoPermissionAnyDecorator(
792 791 'repository.read', 'repository.write', 'repository.admin')
793 792 def repo_commit_children(self):
794 793 commit_id = self.request.matchdict['commit_id']
795 794 self.load_default_context()
796 795
797 796 try:
798 797 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
799 798 children = commit.children
800 799 except CommitDoesNotExistError:
801 800 children = []
802 801
803 802 result = {"results": children}
804 803 return result
805 804
806 805 @LoginRequired()
807 806 @HasRepoPermissionAnyDecorator(
808 807 'repository.read', 'repository.write', 'repository.admin')
809 808 def repo_commit_parents(self):
810 809 commit_id = self.request.matchdict['commit_id']
811 810 self.load_default_context()
812 811
813 812 try:
814 813 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
815 814 parents = commit.parents
816 815 except CommitDoesNotExistError:
817 816 parents = []
818 817 result = {"results": parents}
819 818 return result
@@ -1,305 +1,305 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 25
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 34 from rhodecode.lib.utils import safe_str
35 35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
37 37 from rhodecode.lib.vcs.exceptions import (
38 38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 39 NodeDoesNotExistError)
40 40 from rhodecode.model.db import Repository, ChangesetStatus
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class RepoCompareView(RepoAppView):
46 46 def load_default_context(self):
47 47 c = self._get_local_tmpl_context(include_app_defaults=True)
48 48 c.rhodecode_repo = self.rhodecode_vcs_repo
49 49 return c
50 50
51 51 def _get_commit_or_redirect(
52 52 self, ref, ref_type, repo, redirect_after=True, partial=False):
53 53 """
54 54 This is a safe way to get a commit. If an error occurs it
55 55 redirects to a commit with a proper message. If partial is set
56 56 then it does not do redirect raise and throws an exception instead.
57 57 """
58 58 _ = self.request.translate
59 59 try:
60 60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 61 except EmptyRepositoryError:
62 62 if not redirect_after:
63 63 return repo.scm_instance().EMPTY_COMMIT
64 64 h.flash(h.literal(_('There are no commits yet')),
65 65 category='warning')
66 66 if not partial:
67 67 raise HTTPFound(
68 68 h.route_path('repo_summary', repo_name=repo.repo_name))
69 69 raise HTTPBadRequest()
70 70
71 71 except RepositoryError as e:
72 72 log.exception(safe_str(e))
73 73 h.flash(h.escape(safe_str(e)), category='warning')
74 74 if not partial:
75 75 raise HTTPFound(
76 76 h.route_path('repo_summary', repo_name=repo.repo_name))
77 77 raise HTTPBadRequest()
78 78
79 79 @LoginRequired()
80 80 @HasRepoPermissionAnyDecorator(
81 81 'repository.read', 'repository.write', 'repository.admin')
82 82 def compare_select(self):
83 83 _ = self.request.translate
84 84 c = self.load_default_context()
85 85
86 86 source_repo = self.db_repo_name
87 87 target_repo = self.request.GET.get('target_repo', source_repo)
88 88 c.source_repo = Repository.get_by_repo_name(source_repo)
89 89 c.target_repo = Repository.get_by_repo_name(target_repo)
90 90
91 91 if c.source_repo is None or c.target_repo is None:
92 92 raise HTTPNotFound()
93 93
94 94 c.compare_home = True
95 95 c.commit_ranges = []
96 96 c.collapse_all_commits = False
97 97 c.diffset = None
98 98 c.limited_diff = False
99 99 c.source_ref = c.target_ref = _('Select commit')
100 100 c.source_ref_type = ""
101 101 c.target_ref_type = ""
102 102 c.commit_statuses = ChangesetStatus.STATUSES
103 103 c.preview_mode = False
104 104 c.file_path = None
105 105
106 106 return self._get_template_context(c)
107 107
108 108 @LoginRequired()
109 109 @HasRepoPermissionAnyDecorator(
110 110 'repository.read', 'repository.write', 'repository.admin')
111 111 def compare(self):
112 112 _ = self.request.translate
113 113 c = self.load_default_context()
114 114
115 115 source_ref_type = self.request.matchdict['source_ref_type']
116 116 source_ref = self.request.matchdict['source_ref']
117 117 target_ref_type = self.request.matchdict['target_ref_type']
118 118 target_ref = self.request.matchdict['target_ref']
119 119
120 120 # source_ref will be evaluated in source_repo
121 121 source_repo_name = self.db_repo_name
122 122 source_path, source_id = parse_path_ref(source_ref)
123 123
124 124 # target_ref will be evaluated in target_repo
125 125 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
126 126 target_path, target_id = parse_path_ref(
127 127 target_ref, default_path=self.request.GET.get('f_path', ''))
128 128
129 129 # if merge is True
130 130 # Show what changes since the shared ancestor commit of target/source
131 131 # the source would get if it was merged with target. Only commits
132 132 # which are in target but not in source will be shown.
133 133 merge = str2bool(self.request.GET.get('merge'))
134 134 # if merge is False
135 135 # Show a raw diff of source/target refs even if no ancestor exists
136 136
137 137 # c.fulldiff disables cut_off_limit
138 138 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
139 139
140 140 # fetch global flags of ignore ws or context lines
141 141 diff_context = diffs.get_diff_context(self.request)
142 142 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
143 143
144 144 c.file_path = target_path
145 145 c.commit_statuses = ChangesetStatus.STATUSES
146 146
147 147 # if partial, returns just compare_commits.html (commits log)
148 148 partial = self.request.is_xhr
149 149
150 150 # swap url for compare_diff page
151 151 c.swap_url = h.route_path(
152 152 'repo_compare',
153 153 repo_name=target_repo_name,
154 154 source_ref_type=target_ref_type,
155 155 source_ref=target_ref,
156 156 target_repo=source_repo_name,
157 157 target_ref_type=source_ref_type,
158 158 target_ref=source_ref,
159 159 _query=dict(merge=merge and '1' or '', f_path=target_path))
160 160
161 161 source_repo = Repository.get_by_repo_name(source_repo_name)
162 162 target_repo = Repository.get_by_repo_name(target_repo_name)
163 163
164 164 if source_repo is None:
165 165 log.error('Could not find the source repo: {}'
166 166 .format(source_repo_name))
167 167 h.flash(_('Could not find the source repo: `{}`')
168 168 .format(h.escape(source_repo_name)), category='error')
169 169 raise HTTPFound(
170 170 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
171 171
172 172 if target_repo is None:
173 173 log.error('Could not find the target repo: {}'
174 174 .format(source_repo_name))
175 175 h.flash(_('Could not find the target repo: `{}`')
176 176 .format(h.escape(target_repo_name)), category='error')
177 177 raise HTTPFound(
178 178 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
179 179
180 180 source_scm = source_repo.scm_instance()
181 181 target_scm = target_repo.scm_instance()
182 182
183 183 source_alias = source_scm.alias
184 184 target_alias = target_scm.alias
185 185 if source_alias != target_alias:
186 186 msg = _('The comparison of two different kinds of remote repos '
187 187 'is not available')
188 188 log.error(msg)
189 189 h.flash(msg, category='error')
190 190 raise HTTPFound(
191 191 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
192 192
193 193 source_commit = self._get_commit_or_redirect(
194 194 ref=source_id, ref_type=source_ref_type, repo=source_repo,
195 195 partial=partial)
196 196 target_commit = self._get_commit_or_redirect(
197 197 ref=target_id, ref_type=target_ref_type, repo=target_repo,
198 198 partial=partial)
199 199
200 200 c.compare_home = False
201 201 c.source_repo = source_repo
202 202 c.target_repo = target_repo
203 203 c.source_ref = source_ref
204 204 c.target_ref = target_ref
205 205 c.source_ref_type = source_ref_type
206 206 c.target_ref_type = target_ref_type
207 207
208 208 pre_load = ["author", "date", "message", "branch"]
209 209 c.ancestor = None
210 210
211 211 try:
212 212 c.commit_ranges = source_scm.compare(
213 213 source_commit.raw_id, target_commit.raw_id,
214 214 target_scm, merge, pre_load=pre_load) or []
215 215 if merge:
216 216 c.ancestor = source_scm.get_common_ancestor(
217 217 source_commit.raw_id, target_commit.raw_id, target_scm)
218 218 except RepositoryRequirementError:
219 219 msg = _('Could not compare repos with different '
220 220 'large file settings')
221 221 log.error(msg)
222 222 if partial:
223 223 return Response(msg)
224 224 h.flash(msg, category='error')
225 225 raise HTTPFound(
226 226 h.route_path('repo_compare_select',
227 227 repo_name=self.db_repo_name))
228 228
229 229 c.statuses = self.db_repo.statuses(
230 230 [x.raw_id for x in c.commit_ranges])
231 231
232 232 # auto collapse if we have more than limit
233 233 collapse_limit = diffs.DiffProcessor._collapse_commits_over
234 234 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
235 235
236 236 if partial: # for PR ajax commits loader
237 237 if not c.ancestor:
238 238 return Response('') # cannot merge if there is no ancestor
239 239
240 240 html = render(
241 241 'rhodecode:templates/compare/compare_commits.mako',
242 242 self._get_template_context(c), self.request)
243 243 return Response(html)
244 244
245 245 if c.ancestor:
246 246 # case we want a simple diff without incoming commits,
247 247 # previewing what will be merged.
248 248 # Make the diff on target repo (which is known to have target_ref)
249 249 log.debug('Using ancestor %s as source_ref instead of %s',
250 250 c.ancestor, source_ref)
251 251 source_repo = target_repo
252 252 source_commit = target_repo.get_commit(commit_id=c.ancestor)
253 253
254 254 # diff_limit will cut off the whole diff if the limit is applied
255 255 # otherwise it will just hide the big files from the front-end
256 256 diff_limit = c.visual.cut_off_limit_diff
257 257 file_limit = c.visual.cut_off_limit_file
258 258
259 259 log.debug('calculating diff between '
260 260 'source_ref:%s and target_ref:%s for repo `%s`',
261 261 source_commit, target_commit,
262 262 safe_unicode(source_repo.scm_instance().path))
263 263
264 264 if source_commit.repository != target_commit.repository:
265 265 msg = _(
266 266 "Repositories unrelated. "
267 267 "Cannot compare commit %(commit1)s from repository %(repo1)s "
268 268 "with commit %(commit2)s from repository %(repo2)s.") % {
269 269 'commit1': h.show_id(source_commit),
270 270 'repo1': source_repo.repo_name,
271 271 'commit2': h.show_id(target_commit),
272 272 'repo2': target_repo.repo_name,
273 273 }
274 274 h.flash(msg, category='error')
275 275 raise HTTPFound(
276 276 h.route_path('repo_compare_select',
277 277 repo_name=self.db_repo_name))
278 278
279 279 txt_diff = source_repo.scm_instance().get_diff(
280 280 commit1=source_commit, commit2=target_commit,
281 281 path=target_path, path1=source_path,
282 282 ignore_whitespace=hide_whitespace_changes, context=diff_context)
283 283
284 284 diff_processor = diffs.DiffProcessor(
285 285 txt_diff, format='newdiff', diff_limit=diff_limit,
286 286 file_limit=file_limit, show_full_diff=c.fulldiff)
287 287 _parsed = diff_processor.prepare()
288 288
289 289 diffset = codeblocks.DiffSet(
290 290 repo_name=source_repo.repo_name,
291 291 source_node_getter=codeblocks.diffset_node_getter(source_commit),
292 292 target_repo_name=self.db_repo_name,
293 293 target_node_getter=codeblocks.diffset_node_getter(target_commit),
294 294 )
295 295 c.diffset = self.path_filter.render_patchset_filtered(
296 296 diffset, _parsed, source_ref, target_ref)
297 297
298 298 c.preview_mode = merge
299 299 c.source_commit = source_commit
300 300 c.target_commit = target_commit
301 301
302 302 html = render(
303 303 'rhodecode:templates/compare/compare_diff.mako',
304 304 self._get_template_context(c), self.request)
305 305 return Response(html) No newline at end of file
@@ -1,210 +1,210 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import pytz
21 21 import logging
22 22
23 23 from pyramid.response import Response
24 24
25 25 from rhodecode.apps._base import RepoAppView
26 26 from rhodecode.lib.feedgenerator import Rss201rev2Feed, Atom1Feed
27 27 from rhodecode.lib import audit_logger
28 28 from rhodecode.lib import rc_cache
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, HasRepoPermissionAnyDecorator)
32 32 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
33 33 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
34 34 from rhodecode.model.db import UserApiKeys, CacheKey
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class RepoFeedView(RepoAppView):
40 40 def load_default_context(self):
41 41 c = self._get_local_tmpl_context()
42 42 self._load_defaults()
43 43 return c
44 44
45 45 def _get_config(self):
46 46 import rhodecode
47 47 config = rhodecode.CONFIG
48 48
49 49 return {
50 50 'language': 'en-us',
51 51 'feed_ttl': '5', # TTL of feed,
52 52 'feed_include_diff':
53 53 str2bool(config.get('rss_include_diff', False)),
54 54 'feed_items_per_page':
55 55 safe_int(config.get('rss_items_per_page', 20)),
56 56 'feed_diff_limit':
57 57 # we need to protect from parsing huge diffs here other way
58 58 # we can kill the server
59 59 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
60 60 }
61 61
62 62 def _load_defaults(self):
63 63 _ = self.request.translate
64 64 config = self._get_config()
65 65 # common values for feeds
66 66 self.description = _('Changes on %s repository')
67 67 self.title = _('%s %s feed') % (self.db_repo_name, '%s')
68 68 self.language = config["language"]
69 69 self.ttl = config["feed_ttl"]
70 70 self.feed_include_diff = config['feed_include_diff']
71 71 self.feed_diff_limit = config['feed_diff_limit']
72 72 self.feed_items_per_page = config['feed_items_per_page']
73 73
74 74 def _changes(self, commit):
75 75 diff_processor = DiffProcessor(
76 76 commit.diff(), diff_limit=self.feed_diff_limit)
77 77 _parsed = diff_processor.prepare(inline_diff=False)
78 78 limited_diff = isinstance(_parsed, LimitedDiffContainer)
79 79
80 80 return diff_processor, _parsed, limited_diff
81 81
82 82 def _get_title(self, commit):
83 83 return h.chop_at_smart(commit.message, '\n', suffix_if_chopped='...')
84 84
85 85 def _get_description(self, commit):
86 86 _renderer = self.request.get_partial_renderer(
87 87 'rhodecode:templates/feed/atom_feed_entry.mako')
88 88 diff_processor, parsed_diff, limited_diff = self._changes(commit)
89 89 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
90 90 return _renderer(
91 91 'body',
92 92 commit=commit,
93 93 parsed_diff=filtered_parsed_diff,
94 94 limited_diff=limited_diff,
95 95 feed_include_diff=self.feed_include_diff,
96 96 diff_processor=diff_processor,
97 97 has_hidden_changes=has_hidden_changes
98 98 )
99 99
100 100 def _set_timezone(self, date, tzinfo=pytz.utc):
101 101 if not getattr(date, "tzinfo", None):
102 102 date.replace(tzinfo=tzinfo)
103 103 return date
104 104
105 105 def _get_commits(self):
106 106 pre_load = ['author', 'branch', 'date', 'message', 'parents']
107 107 if self.rhodecode_vcs_repo.is_empty():
108 108 return []
109 109
110 110 collection = self.rhodecode_vcs_repo.get_commits(
111 111 branch_name=None, show_hidden=False, pre_load=pre_load,
112 112 translate_tags=False)
113 113
114 114 return list(collection[-self.feed_items_per_page:])
115 115
116 116 def uid(self, repo_id, commit_id):
117 117 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
118 118
119 119 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
120 120 @HasRepoPermissionAnyDecorator(
121 121 'repository.read', 'repository.write', 'repository.admin')
122 122 def atom(self):
123 123 """
124 124 Produce an atom-1.0 feed via feedgenerator module
125 125 """
126 126 self.load_default_context()
127 127 force_recache = self.get_recache_flag()
128 128
129 129 cache_namespace_uid = 'cache_repo_feed.{}'.format(self.db_repo.repo_id)
130 130 condition = not (self.path_filter.is_enabled or force_recache)
131 131 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
132 132
133 133 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
134 134 condition=condition)
135 135 def generate_atom_feed(repo_id, _repo_name, _commit_id, _feed_type):
136 136 feed = Atom1Feed(
137 137 title=self.title % 'atom',
138 138 link=h.route_url('repo_summary', repo_name=_repo_name),
139 139 description=self.description % _repo_name,
140 140 language=self.language,
141 141 ttl=self.ttl
142 142 )
143 143
144 144 for commit in reversed(self._get_commits()):
145 145 date = self._set_timezone(commit.date)
146 146 feed.add_item(
147 147 unique_id=self.uid(repo_id, commit.raw_id),
148 148 title=self._get_title(commit),
149 149 author_name=commit.author,
150 150 description=self._get_description(commit),
151 151 link=h.route_url(
152 152 'repo_commit', repo_name=_repo_name,
153 153 commit_id=commit.raw_id),
154 154 pubdate=date,)
155 155
156 156 return feed.content_type, feed.writeString('utf-8')
157 157
158 158 commit_id = self.db_repo.changeset_cache.get('raw_id')
159 159 content_type, feed = generate_atom_feed(
160 160 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'atom')
161 161
162 162 response = Response(feed)
163 163 response.content_type = content_type
164 164 return response
165 165
166 166 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
167 167 @HasRepoPermissionAnyDecorator(
168 168 'repository.read', 'repository.write', 'repository.admin')
169 169 def rss(self):
170 170 """
171 171 Produce an rss2 feed via feedgenerator module
172 172 """
173 173 self.load_default_context()
174 174 force_recache = self.get_recache_flag()
175 175
176 176 cache_namespace_uid = 'cache_repo_feed.{}'.format(self.db_repo.repo_id)
177 177 condition = not (self.path_filter.is_enabled or force_recache)
178 178 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
179 179
180 180 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
181 181 condition=condition)
182 182 def generate_rss_feed(repo_id, _repo_name, _commit_id, _feed_type):
183 183 feed = Rss201rev2Feed(
184 184 title=self.title % 'rss',
185 185 link=h.route_url('repo_summary', repo_name=_repo_name),
186 186 description=self.description % _repo_name,
187 187 language=self.language,
188 188 ttl=self.ttl
189 189 )
190 190
191 191 for commit in reversed(self._get_commits()):
192 192 date = self._set_timezone(commit.date)
193 193 feed.add_item(
194 194 unique_id=self.uid(repo_id, commit.raw_id),
195 195 title=self._get_title(commit),
196 196 author_name=commit.author,
197 197 description=self._get_description(commit),
198 198 link=h.route_url(
199 199 'repo_commit', repo_name=_repo_name,
200 200 commit_id=commit.raw_id),
201 201 pubdate=date,)
202 202 return feed.content_type, feed.writeString('utf-8')
203 203
204 204 commit_id = self.db_repo.changeset_cache.get('raw_id')
205 205 content_type, feed = generate_rss_feed(
206 206 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'rss')
207 207
208 208 response = Response(feed)
209 209 response.content_type = content_type
210 210 return response
@@ -1,1583 +1,1583 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27 import urllib.request, urllib.parse, urllib.error
28 28 import pathlib
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.view_utils import parse_path_ref
42 42 from rhodecode.lib.exceptions import NonRelativePathError
43 43 from rhodecode.lib.codeblocks import (
44 44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 45 from rhodecode.lib.utils2 import (
46 46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1)
47 47 from rhodecode.lib.auth import (
48 48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 49 from rhodecode.lib.vcs import path as vcspath
50 50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 51 from rhodecode.lib.vcs.conf import settings
52 52 from rhodecode.lib.vcs.nodes import FileNode
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 56 NodeDoesNotExistError, CommitError, NodeError)
57 57
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.db import Repository
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class RepoFilesView(RepoAppView):
65 65
66 66 @staticmethod
67 67 def adjust_file_path_for_svn(f_path, repo):
68 68 """
69 69 Computes the relative path of `f_path`.
70 70
71 71 This is mainly based on prefix matching of the recognized tags and
72 72 branches in the underlying repository.
73 73 """
74 74 tags_and_branches = itertools.chain(
75 75 repo.branches.iterkeys(),
76 76 repo.tags.iterkeys())
77 77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78 78
79 79 for name in tags_and_branches:
80 80 if f_path.startswith('{}/'.format(name)):
81 81 f_path = vcspath.relpath(f_path, name)
82 82 break
83 83 return f_path
84 84
85 85 def load_default_context(self):
86 86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 88 c.enable_downloads = self.db_repo.enable_downloads
89 89 return c
90 90
91 91 def _ensure_not_locked(self, commit_id='tip'):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id=commit_id)
103 103 raise HTTPFound(files_url)
104 104
105 105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 106 _ = self.request.translate
107 107
108 108 if not is_head:
109 109 message = _('Cannot modify file. '
110 110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 111 h.flash(message, category='warning')
112 112
113 113 if json_mode:
114 114 return message
115 115
116 116 files_url = h.route_path(
117 117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 118 f_path=f_path)
119 119 raise HTTPFound(files_url)
120 120
121 121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 122 _ = self.request.translate
123 123
124 124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 128 h.escape(branch_name), h.escape(rule))
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
132 132 return message
133 133
134 134 files_url = h.route_path(
135 135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136 136
137 137 raise HTTPFound(files_url)
138 138
139 139 def _get_commit_and_path(self):
140 140 default_commit_id = self.db_repo.landing_ref_name
141 141 default_f_path = '/'
142 142
143 143 commit_id = self.request.matchdict.get(
144 144 'commit_id', default_commit_id)
145 145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 146 return commit_id, f_path
147 147
148 148 def _get_default_encoding(self, c):
149 149 enc_list = getattr(c, 'default_encodings', [])
150 150 return enc_list[0] if enc_list else 'UTF-8'
151 151
152 152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 153 """
154 154 This is a safe way to get commit. If an error occurs it redirects to
155 155 tip with proper message
156 156
157 157 :param commit_id: id of commit to fetch
158 158 :param redirect_after: toggle redirection
159 159 """
160 160 _ = self.request.translate
161 161
162 162 try:
163 163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 164 except EmptyRepositoryError:
165 165 if not redirect_after:
166 166 return None
167 167
168 168 _url = h.route_path(
169 169 'repo_files_add_file',
170 170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171 171
172 172 if h.HasRepoPermissionAny(
173 173 'repository.write', 'repository.admin')(self.db_repo_name):
174 174 add_new = h.link_to(
175 175 _('Click here to add a new file.'), _url, class_="alert-link")
176 176 else:
177 177 add_new = ""
178 178
179 179 h.flash(h.literal(
180 180 _('There are no files yet. %s') % add_new), category='warning')
181 181 raise HTTPFound(
182 182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183 183
184 184 except (CommitDoesNotExistError, LookupError) as e:
185 185 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
186 186 h.flash(msg, category='error')
187 187 raise HTTPNotFound()
188 188 except RepositoryError as e:
189 189 h.flash(h.escape(safe_str(e)), category='error')
190 190 raise HTTPNotFound()
191 191
192 192 def _get_filenode_or_redirect(self, commit_obj, path):
193 193 """
194 194 Returns file_node, if error occurs or given path is directory,
195 195 it'll redirect to top level path
196 196 """
197 197 _ = self.request.translate
198 198
199 199 try:
200 200 file_node = commit_obj.get_node(path)
201 201 if file_node.is_dir():
202 202 raise RepositoryError('The given path is a directory')
203 203 except CommitDoesNotExistError:
204 204 log.exception('No such commit exists for this repository')
205 205 h.flash(_('No such commit exists for this repository'), category='error')
206 206 raise HTTPNotFound()
207 207 except RepositoryError as e:
208 208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 209 h.flash(h.escape(safe_str(e)), category='error')
210 210 raise HTTPNotFound()
211 211
212 212 return file_node
213 213
214 214 def _is_valid_head(self, commit_id, repo, landing_ref):
215 215 branch_name = sha_commit_id = ''
216 216 is_head = False
217 217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218 218
219 219 for _branch_name, branch_commit_id in repo.branches.items():
220 220 # simple case we pass in branch name, it's a HEAD
221 221 if commit_id == _branch_name:
222 222 is_head = True
223 223 branch_name = _branch_name
224 224 sha_commit_id = branch_commit_id
225 225 break
226 226 # case when we pass in full sha commit_id, which is a head
227 227 elif commit_id == branch_commit_id:
228 228 is_head = True
229 229 branch_name = _branch_name
230 230 sha_commit_id = branch_commit_id
231 231 break
232 232
233 233 if h.is_svn(repo) and not repo.is_empty():
234 234 # Note: Subversion only has one head.
235 235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 236 is_head = True
237 237 return branch_name, sha_commit_id, is_head
238 238
239 239 # checked branches, means we only need to try to get the branch/commit_sha
240 240 if repo.is_empty():
241 241 is_head = True
242 242 branch_name = landing_ref
243 243 sha_commit_id = EmptyCommit().raw_id
244 244 else:
245 245 commit = repo.get_commit(commit_id=commit_id)
246 246 if commit:
247 247 branch_name = commit.branch
248 248 sha_commit_id = commit.raw_id
249 249
250 250 return branch_name, sha_commit_id, is_head
251 251
252 252 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
253 253
254 254 repo_id = self.db_repo.repo_id
255 255 force_recache = self.get_recache_flag()
256 256
257 257 cache_seconds = safe_int(
258 258 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
259 259 cache_on = not force_recache and cache_seconds > 0
260 260 log.debug(
261 261 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
262 262 'with caching: %s[TTL: %ss]' % (
263 263 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
264 264
265 265 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
266 266 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
267 267
268 268 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
269 269 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
270 270 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
271 271 ver, _repo_id, _commit_id, _f_path)
272 272
273 273 c.full_load = _full_load
274 274 return render(
275 275 'rhodecode:templates/files/files_browser_tree.mako',
276 276 self._get_template_context(c), self.request, _at_rev)
277 277
278 278 return compute_file_tree(
279 279 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
280 280 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
281 281
282 282 def _get_archive_spec(self, fname):
283 283 log.debug('Detecting archive spec for: `%s`', fname)
284 284
285 285 fileformat = None
286 286 ext = None
287 287 content_type = None
288 288 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
289 289
290 290 if fname.endswith(extension):
291 291 fileformat = a_type
292 292 log.debug('archive is of type: %s', fileformat)
293 293 ext = extension
294 294 break
295 295
296 296 if not fileformat:
297 297 raise ValueError()
298 298
299 299 # left over part of whole fname is the commit
300 300 commit_id = fname[:-len(ext)]
301 301
302 302 return commit_id, ext, fileformat, content_type
303 303
304 304 def create_pure_path(self, *parts):
305 305 # Split paths and sanitize them, removing any ../ etc
306 306 sanitized_path = [
307 307 x for x in pathlib.PurePath(*parts).parts
308 308 if x not in ['.', '..']]
309 309
310 310 pure_path = pathlib.PurePath(*sanitized_path)
311 311 return pure_path
312 312
313 313 def _is_lf_enabled(self, target_repo):
314 314 lf_enabled = False
315 315
316 316 lf_key_for_vcs_map = {
317 317 'hg': 'extensions_largefiles',
318 318 'git': 'vcs_git_lfs_enabled'
319 319 }
320 320
321 321 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
322 322
323 323 if lf_key_for_vcs:
324 324 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
325 325
326 326 return lf_enabled
327 327
328 328 def _get_archive_name(self, db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
329 329 # original backward compat name of archive
330 330 clean_name = safe_str(db_repo_name.replace('/', '_'))
331 331
332 332 # e.g vcsserver.zip
333 333 # e.g vcsserver-abcdefgh.zip
334 334 # e.g vcsserver-abcdefgh-defghijk.zip
335 335 archive_name = '{}{}{}{}{}{}'.format(
336 336 clean_name,
337 337 '-sub' if subrepos else '',
338 338 commit_sha,
339 339 '-{}'.format('plain') if not with_hash else '',
340 340 '-{}'.format(path_sha) if path_sha else '',
341 341 ext)
342 342 return archive_name
343 343
344 344 @LoginRequired()
345 345 @HasRepoPermissionAnyDecorator(
346 346 'repository.read', 'repository.write', 'repository.admin')
347 347 def repo_archivefile(self):
348 348 # archive cache config
349 349 from rhodecode import CONFIG
350 350 _ = self.request.translate
351 351 self.load_default_context()
352 352 default_at_path = '/'
353 353 fname = self.request.matchdict['fname']
354 354 subrepos = self.request.GET.get('subrepos') == 'true'
355 355 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
356 356 at_path = self.request.GET.get('at_path') or default_at_path
357 357
358 358 if not self.db_repo.enable_downloads:
359 359 return Response(_('Downloads disabled'))
360 360
361 361 try:
362 362 commit_id, ext, fileformat, content_type = \
363 363 self._get_archive_spec(fname)
364 364 except ValueError:
365 365 return Response(_('Unknown archive type for: `{}`').format(
366 366 h.escape(fname)))
367 367
368 368 try:
369 369 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
370 370 except CommitDoesNotExistError:
371 371 return Response(_('Unknown commit_id {}').format(
372 372 h.escape(commit_id)))
373 373 except EmptyRepositoryError:
374 374 return Response(_('Empty repository'))
375 375
376 376 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
377 377 if commit_id != commit.raw_id:
378 378 fname='{}{}'.format(commit.raw_id, ext)
379 379 raise HTTPFound(self.request.current_route_path(fname=fname))
380 380
381 381 try:
382 382 at_path = commit.get_node(at_path).path or default_at_path
383 383 except Exception:
384 384 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
385 385
386 386 # path sha is part of subdir
387 387 path_sha = ''
388 388 if at_path != default_at_path:
389 389 path_sha = sha1(at_path)[:8]
390 390 short_sha = '-{}'.format(safe_str(commit.short_id))
391 391 # used for cache etc
392 392 archive_name = self._get_archive_name(
393 393 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
394 394 path_sha=path_sha, with_hash=with_hash)
395 395
396 396 if not with_hash:
397 397 short_sha = ''
398 398 path_sha = ''
399 399
400 400 # what end client gets served
401 401 response_archive_name = self._get_archive_name(
402 402 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
403 403 path_sha=path_sha, with_hash=with_hash)
404 404 # remove extension from our archive directory name
405 405 archive_dir_name = response_archive_name[:-len(ext)]
406 406
407 407 use_cached_archive = False
408 408 archive_cache_dir = CONFIG.get('archive_cache_dir')
409 409 archive_cache_enabled = archive_cache_dir and not self.request.GET.get('no_cache')
410 410 cached_archive_path = None
411 411
412 412 if archive_cache_enabled:
413 413 # check if we it's ok to write, and re-create the archive cache
414 414 if not os.path.isdir(CONFIG['archive_cache_dir']):
415 415 os.makedirs(CONFIG['archive_cache_dir'])
416 416
417 417 cached_archive_path = os.path.join(
418 418 CONFIG['archive_cache_dir'], archive_name)
419 419 if os.path.isfile(cached_archive_path):
420 420 log.debug('Found cached archive in %s', cached_archive_path)
421 421 fd, archive = None, cached_archive_path
422 422 use_cached_archive = True
423 423 else:
424 424 log.debug('Archive %s is not yet cached', archive_name)
425 425
426 426 # generate new archive, as previous was not found in the cache
427 427 if not use_cached_archive:
428 428 _dir = os.path.abspath(archive_cache_dir) if archive_cache_dir else None
429 429 fd, archive = tempfile.mkstemp(dir=_dir)
430 430 log.debug('Creating new temp archive in %s', archive)
431 431 try:
432 432 commit.archive_repo(archive, archive_dir_name=archive_dir_name,
433 433 kind=fileformat, subrepos=subrepos,
434 434 archive_at_path=at_path)
435 435 except ImproperArchiveTypeError:
436 436 return _('Unknown archive type')
437 437 if archive_cache_enabled:
438 438 # if we generated the archive and we have cache enabled
439 439 # let's use this for future
440 440 log.debug('Storing new archive in %s', cached_archive_path)
441 441 shutil.move(archive, cached_archive_path)
442 442 archive = cached_archive_path
443 443
444 444 # store download action
445 445 audit_logger.store_web(
446 446 'repo.archive.download', action_data={
447 447 'user_agent': self.request.user_agent,
448 448 'archive_name': archive_name,
449 449 'archive_spec': fname,
450 450 'archive_cached': use_cached_archive},
451 451 user=self._rhodecode_user,
452 452 repo=self.db_repo,
453 453 commit=True
454 454 )
455 455
456 456 def get_chunked_archive(archive_path):
457 457 with open(archive_path, 'rb') as stream:
458 458 while True:
459 459 data = stream.read(16 * 1024)
460 460 if not data:
461 461 if fd: # fd means we used temporary file
462 462 os.close(fd)
463 463 if not archive_cache_enabled:
464 464 log.debug('Destroying temp archive %s', archive_path)
465 465 os.remove(archive_path)
466 466 break
467 467 yield data
468 468
469 469 response = Response(app_iter=get_chunked_archive(archive))
470 470 response.content_disposition = str('attachment; filename=%s' % response_archive_name)
471 471 response.content_type = str(content_type)
472 472
473 473 return response
474 474
475 475 def _get_file_node(self, commit_id, f_path):
476 476 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
477 477 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
478 478 try:
479 479 node = commit.get_node(f_path)
480 480 if node.is_dir():
481 481 raise NodeError('%s path is a %s not a file'
482 482 % (node, type(node)))
483 483 except NodeDoesNotExistError:
484 484 commit = EmptyCommit(
485 485 commit_id=commit_id,
486 486 idx=commit.idx,
487 487 repo=commit.repository,
488 488 alias=commit.repository.alias,
489 489 message=commit.message,
490 490 author=commit.author,
491 491 date=commit.date)
492 492 node = FileNode(f_path, '', commit=commit)
493 493 else:
494 494 commit = EmptyCommit(
495 495 repo=self.rhodecode_vcs_repo,
496 496 alias=self.rhodecode_vcs_repo.alias)
497 497 node = FileNode(f_path, '', commit=commit)
498 498 return node
499 499
500 500 @LoginRequired()
501 501 @HasRepoPermissionAnyDecorator(
502 502 'repository.read', 'repository.write', 'repository.admin')
503 503 def repo_files_diff(self):
504 504 c = self.load_default_context()
505 505 f_path = self._get_f_path(self.request.matchdict)
506 506 diff1 = self.request.GET.get('diff1', '')
507 507 diff2 = self.request.GET.get('diff2', '')
508 508
509 509 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
510 510
511 511 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
512 512 line_context = self.request.GET.get('context', 3)
513 513
514 514 if not any((diff1, diff2)):
515 515 h.flash(
516 516 'Need query parameter "diff1" or "diff2" to generate a diff.',
517 517 category='error')
518 518 raise HTTPBadRequest()
519 519
520 520 c.action = self.request.GET.get('diff')
521 521 if c.action not in ['download', 'raw']:
522 522 compare_url = h.route_path(
523 523 'repo_compare',
524 524 repo_name=self.db_repo_name,
525 525 source_ref_type='rev',
526 526 source_ref=diff1,
527 527 target_repo=self.db_repo_name,
528 528 target_ref_type='rev',
529 529 target_ref=diff2,
530 530 _query=dict(f_path=f_path))
531 531 # redirect to new view if we render diff
532 532 raise HTTPFound(compare_url)
533 533
534 534 try:
535 535 node1 = self._get_file_node(diff1, path1)
536 536 node2 = self._get_file_node(diff2, f_path)
537 537 except (RepositoryError, NodeError):
538 538 log.exception("Exception while trying to get node from repository")
539 539 raise HTTPFound(
540 540 h.route_path('repo_files', repo_name=self.db_repo_name,
541 541 commit_id='tip', f_path=f_path))
542 542
543 543 if all(isinstance(node.commit, EmptyCommit)
544 544 for node in (node1, node2)):
545 545 raise HTTPNotFound()
546 546
547 547 c.commit_1 = node1.commit
548 548 c.commit_2 = node2.commit
549 549
550 550 if c.action == 'download':
551 551 _diff = diffs.get_gitdiff(node1, node2,
552 552 ignore_whitespace=ignore_whitespace,
553 553 context=line_context)
554 554 diff = diffs.DiffProcessor(_diff, format='gitdiff')
555 555
556 556 response = Response(self.path_filter.get_raw_patch(diff))
557 557 response.content_type = 'text/plain'
558 558 response.content_disposition = (
559 559 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
560 560 )
561 561 charset = self._get_default_encoding(c)
562 562 if charset:
563 563 response.charset = charset
564 564 return response
565 565
566 566 elif c.action == 'raw':
567 567 _diff = diffs.get_gitdiff(node1, node2,
568 568 ignore_whitespace=ignore_whitespace,
569 569 context=line_context)
570 570 diff = diffs.DiffProcessor(_diff, format='gitdiff')
571 571
572 572 response = Response(self.path_filter.get_raw_patch(diff))
573 573 response.content_type = 'text/plain'
574 574 charset = self._get_default_encoding(c)
575 575 if charset:
576 576 response.charset = charset
577 577 return response
578 578
579 579 # in case we ever end up here
580 580 raise HTTPNotFound()
581 581
582 582 @LoginRequired()
583 583 @HasRepoPermissionAnyDecorator(
584 584 'repository.read', 'repository.write', 'repository.admin')
585 585 def repo_files_diff_2way_redirect(self):
586 586 """
587 587 Kept only to make OLD links work
588 588 """
589 589 f_path = self._get_f_path_unchecked(self.request.matchdict)
590 590 diff1 = self.request.GET.get('diff1', '')
591 591 diff2 = self.request.GET.get('diff2', '')
592 592
593 593 if not any((diff1, diff2)):
594 594 h.flash(
595 595 'Need query parameter "diff1" or "diff2" to generate a diff.',
596 596 category='error')
597 597 raise HTTPBadRequest()
598 598
599 599 compare_url = h.route_path(
600 600 'repo_compare',
601 601 repo_name=self.db_repo_name,
602 602 source_ref_type='rev',
603 603 source_ref=diff1,
604 604 target_ref_type='rev',
605 605 target_ref=diff2,
606 606 _query=dict(f_path=f_path, diffmode='sideside',
607 607 target_repo=self.db_repo_name,))
608 608 raise HTTPFound(compare_url)
609 609
610 610 @LoginRequired()
611 611 def repo_files_default_commit_redirect(self):
612 612 """
613 613 Special page that redirects to the landing page of files based on the default
614 614 commit for repository
615 615 """
616 616 c = self.load_default_context()
617 617 ref_name = c.rhodecode_db_repo.landing_ref_name
618 618 landing_url = h.repo_files_by_ref_url(
619 619 c.rhodecode_db_repo.repo_name,
620 620 c.rhodecode_db_repo.repo_type,
621 621 f_path='',
622 622 ref_name=ref_name,
623 623 commit_id='tip',
624 624 query=dict(at=ref_name)
625 625 )
626 626
627 627 raise HTTPFound(landing_url)
628 628
629 629 @LoginRequired()
630 630 @HasRepoPermissionAnyDecorator(
631 631 'repository.read', 'repository.write', 'repository.admin')
632 632 def repo_files(self):
633 633 c = self.load_default_context()
634 634
635 635 view_name = getattr(self.request.matched_route, 'name', None)
636 636
637 637 c.annotate = view_name == 'repo_files:annotated'
638 638 # default is false, but .rst/.md files later are auto rendered, we can
639 639 # overwrite auto rendering by setting this GET flag
640 640 c.renderer = view_name == 'repo_files:rendered' or \
641 641 not self.request.GET.get('no-render', False)
642 642
643 643 commit_id, f_path = self._get_commit_and_path()
644 644
645 645 c.commit = self._get_commit_or_redirect(commit_id)
646 646 c.branch = self.request.GET.get('branch', None)
647 647 c.f_path = f_path
648 648 at_rev = self.request.GET.get('at')
649 649
650 650 # prev link
651 651 try:
652 652 prev_commit = c.commit.prev(c.branch)
653 653 c.prev_commit = prev_commit
654 654 c.url_prev = h.route_path(
655 655 'repo_files', repo_name=self.db_repo_name,
656 656 commit_id=prev_commit.raw_id, f_path=f_path)
657 657 if c.branch:
658 658 c.url_prev += '?branch=%s' % c.branch
659 659 except (CommitDoesNotExistError, VCSError):
660 660 c.url_prev = '#'
661 661 c.prev_commit = EmptyCommit()
662 662
663 663 # next link
664 664 try:
665 665 next_commit = c.commit.next(c.branch)
666 666 c.next_commit = next_commit
667 667 c.url_next = h.route_path(
668 668 'repo_files', repo_name=self.db_repo_name,
669 669 commit_id=next_commit.raw_id, f_path=f_path)
670 670 if c.branch:
671 671 c.url_next += '?branch=%s' % c.branch
672 672 except (CommitDoesNotExistError, VCSError):
673 673 c.url_next = '#'
674 674 c.next_commit = EmptyCommit()
675 675
676 676 # files or dirs
677 677 try:
678 678 c.file = c.commit.get_node(f_path)
679 679
680 680 c.file_author = True
681 681 c.file_tree = ''
682 682
683 683 # load file content
684 684 if c.file.is_file():
685 685 c.lf_node = {}
686 686
687 687 has_lf_enabled = self._is_lf_enabled(self.db_repo)
688 688 if has_lf_enabled:
689 689 c.lf_node = c.file.get_largefile_node()
690 690
691 691 c.file_source_page = 'true'
692 692 c.file_last_commit = c.file.last_commit
693 693
694 694 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
695 695
696 696 if not (c.file_size_too_big or c.file.is_binary):
697 697 if c.annotate: # annotation has precedence over renderer
698 698 c.annotated_lines = filenode_as_annotated_lines_tokens(
699 699 c.file
700 700 )
701 701 else:
702 702 c.renderer = (
703 703 c.renderer and h.renderer_from_filename(c.file.path)
704 704 )
705 705 if not c.renderer:
706 706 c.lines = filenode_as_lines_tokens(c.file)
707 707
708 708 _branch_name, _sha_commit_id, is_head = \
709 709 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
710 710 landing_ref=self.db_repo.landing_ref_name)
711 711 c.on_branch_head = is_head
712 712
713 713 branch = c.commit.branch if (
714 714 c.commit.branch and '/' not in c.commit.branch) else None
715 715 c.branch_or_raw_id = branch or c.commit.raw_id
716 716 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
717 717
718 718 author = c.file_last_commit.author
719 719 c.authors = [[
720 720 h.email(author),
721 721 h.person(author, 'username_or_name_or_email'),
722 722 1
723 723 ]]
724 724
725 725 else: # load tree content at path
726 726 c.file_source_page = 'false'
727 727 c.authors = []
728 728 # this loads a simple tree without metadata to speed things up
729 729 # later via ajax we call repo_nodetree_full and fetch whole
730 730 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
731 731
732 732 c.readme_data, c.readme_file = \
733 733 self._get_readme_data(self.db_repo, c.visual.default_renderer,
734 734 c.commit.raw_id, f_path)
735 735
736 736 except RepositoryError as e:
737 737 h.flash(h.escape(safe_str(e)), category='error')
738 738 raise HTTPNotFound()
739 739
740 740 if self.request.environ.get('HTTP_X_PJAX'):
741 741 html = render('rhodecode:templates/files/files_pjax.mako',
742 742 self._get_template_context(c), self.request)
743 743 else:
744 744 html = render('rhodecode:templates/files/files.mako',
745 745 self._get_template_context(c), self.request)
746 746 return Response(html)
747 747
748 748 @HasRepoPermissionAnyDecorator(
749 749 'repository.read', 'repository.write', 'repository.admin')
750 750 def repo_files_annotated_previous(self):
751 751 self.load_default_context()
752 752
753 753 commit_id, f_path = self._get_commit_and_path()
754 754 commit = self._get_commit_or_redirect(commit_id)
755 755 prev_commit_id = commit.raw_id
756 756 line_anchor = self.request.GET.get('line_anchor')
757 757 is_file = False
758 758 try:
759 759 _file = commit.get_node(f_path)
760 760 is_file = _file.is_file()
761 761 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
762 762 pass
763 763
764 764 if is_file:
765 765 history = commit.get_path_history(f_path)
766 766 prev_commit_id = history[1].raw_id \
767 767 if len(history) > 1 else prev_commit_id
768 768 prev_url = h.route_path(
769 769 'repo_files:annotated', repo_name=self.db_repo_name,
770 770 commit_id=prev_commit_id, f_path=f_path,
771 771 _anchor='L{}'.format(line_anchor))
772 772
773 773 raise HTTPFound(prev_url)
774 774
775 775 @LoginRequired()
776 776 @HasRepoPermissionAnyDecorator(
777 777 'repository.read', 'repository.write', 'repository.admin')
778 778 def repo_nodetree_full(self):
779 779 """
780 780 Returns rendered html of file tree that contains commit date,
781 781 author, commit_id for the specified combination of
782 782 repo, commit_id and file path
783 783 """
784 784 c = self.load_default_context()
785 785
786 786 commit_id, f_path = self._get_commit_and_path()
787 787 commit = self._get_commit_or_redirect(commit_id)
788 788 try:
789 789 dir_node = commit.get_node(f_path)
790 790 except RepositoryError as e:
791 791 return Response('error: {}'.format(h.escape(safe_str(e))))
792 792
793 793 if dir_node.is_file():
794 794 return Response('')
795 795
796 796 c.file = dir_node
797 797 c.commit = commit
798 798 at_rev = self.request.GET.get('at')
799 799
800 800 html = self._get_tree_at_commit(
801 801 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
802 802
803 803 return Response(html)
804 804
805 805 def _get_attachement_headers(self, f_path):
806 806 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
807 807 safe_path = f_name.replace('"', '\\"')
808 808 encoded_path = urllib.parse.quote(f_name)
809 809
810 810 return "attachment; " \
811 811 "filename=\"{}\"; " \
812 812 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
813 813
814 814 @LoginRequired()
815 815 @HasRepoPermissionAnyDecorator(
816 816 'repository.read', 'repository.write', 'repository.admin')
817 817 def repo_file_raw(self):
818 818 """
819 819 Action for show as raw, some mimetypes are "rendered",
820 820 those include images, icons.
821 821 """
822 822 c = self.load_default_context()
823 823
824 824 commit_id, f_path = self._get_commit_and_path()
825 825 commit = self._get_commit_or_redirect(commit_id)
826 826 file_node = self._get_filenode_or_redirect(commit, f_path)
827 827
828 828 raw_mimetype_mapping = {
829 829 # map original mimetype to a mimetype used for "show as raw"
830 830 # you can also provide a content-disposition to override the
831 831 # default "attachment" disposition.
832 832 # orig_type: (new_type, new_dispo)
833 833
834 834 # show images inline:
835 835 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
836 836 # for example render an SVG with javascript inside or even render
837 837 # HTML.
838 838 'image/x-icon': ('image/x-icon', 'inline'),
839 839 'image/png': ('image/png', 'inline'),
840 840 'image/gif': ('image/gif', 'inline'),
841 841 'image/jpeg': ('image/jpeg', 'inline'),
842 842 'application/pdf': ('application/pdf', 'inline'),
843 843 }
844 844
845 845 mimetype = file_node.mimetype
846 846 try:
847 847 mimetype, disposition = raw_mimetype_mapping[mimetype]
848 848 except KeyError:
849 849 # we don't know anything special about this, handle it safely
850 850 if file_node.is_binary:
851 851 # do same as download raw for binary files
852 852 mimetype, disposition = 'application/octet-stream', 'attachment'
853 853 else:
854 854 # do not just use the original mimetype, but force text/plain,
855 855 # otherwise it would serve text/html and that might be unsafe.
856 856 # Note: underlying vcs library fakes text/plain mimetype if the
857 857 # mimetype can not be determined and it thinks it is not
858 858 # binary.This might lead to erroneous text display in some
859 859 # cases, but helps in other cases, like with text files
860 860 # without extension.
861 861 mimetype, disposition = 'text/plain', 'inline'
862 862
863 863 if disposition == 'attachment':
864 864 disposition = self._get_attachement_headers(f_path)
865 865
866 866 stream_content = file_node.stream_bytes()
867 867
868 868 response = Response(app_iter=stream_content)
869 869 response.content_disposition = disposition
870 870 response.content_type = mimetype
871 871
872 872 charset = self._get_default_encoding(c)
873 873 if charset:
874 874 response.charset = charset
875 875
876 876 return response
877 877
878 878 @LoginRequired()
879 879 @HasRepoPermissionAnyDecorator(
880 880 'repository.read', 'repository.write', 'repository.admin')
881 881 def repo_file_download(self):
882 882 c = self.load_default_context()
883 883
884 884 commit_id, f_path = self._get_commit_and_path()
885 885 commit = self._get_commit_or_redirect(commit_id)
886 886 file_node = self._get_filenode_or_redirect(commit, f_path)
887 887
888 888 if self.request.GET.get('lf'):
889 889 # only if lf get flag is passed, we download this file
890 890 # as LFS/Largefile
891 891 lf_node = file_node.get_largefile_node()
892 892 if lf_node:
893 893 # overwrite our pointer with the REAL large-file
894 894 file_node = lf_node
895 895
896 896 disposition = self._get_attachement_headers(f_path)
897 897
898 898 stream_content = file_node.stream_bytes()
899 899
900 900 response = Response(app_iter=stream_content)
901 901 response.content_disposition = disposition
902 902 response.content_type = file_node.mimetype
903 903
904 904 charset = self._get_default_encoding(c)
905 905 if charset:
906 906 response.charset = charset
907 907
908 908 return response
909 909
910 910 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
911 911
912 912 cache_seconds = safe_int(
913 913 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
914 914 cache_on = cache_seconds > 0
915 915 log.debug(
916 916 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
917 917 'with caching: %s[TTL: %ss]' % (
918 918 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
919 919
920 920 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
921 921 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
922 922
923 923 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
924 924 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
925 925 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
926 926 _repo_id, commit_id, f_path)
927 927 try:
928 928 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
929 929 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
930 930 log.exception(safe_str(e))
931 931 h.flash(h.escape(safe_str(e)), category='error')
932 932 raise HTTPFound(h.route_path(
933 933 'repo_files', repo_name=self.db_repo_name,
934 934 commit_id='tip', f_path='/'))
935 935
936 936 return _d + _f
937 937
938 938 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
939 939 commit_id, f_path)
940 940 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
941 941
942 942 @LoginRequired()
943 943 @HasRepoPermissionAnyDecorator(
944 944 'repository.read', 'repository.write', 'repository.admin')
945 945 def repo_nodelist(self):
946 946 self.load_default_context()
947 947
948 948 commit_id, f_path = self._get_commit_and_path()
949 949 commit = self._get_commit_or_redirect(commit_id)
950 950
951 951 metadata = self._get_nodelist_at_commit(
952 952 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
953 953 return {'nodes': metadata}
954 954
955 955 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
956 956 items = []
957 957 for name, commit_id in branches_or_tags.items():
958 958 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
959 959 items.append((sym_ref, name, ref_type))
960 960 return items
961 961
962 962 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
963 963 return commit_id
964 964
965 965 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
966 966 return commit_id
967 967
968 968 # NOTE(dan): old code we used in "diff" mode compare
969 969 new_f_path = vcspath.join(name, f_path)
970 970 return u'%s@%s' % (new_f_path, commit_id)
971 971
972 972 def _get_node_history(self, commit_obj, f_path, commits=None):
973 973 """
974 974 get commit history for given node
975 975
976 976 :param commit_obj: commit to calculate history
977 977 :param f_path: path for node to calculate history for
978 978 :param commits: if passed don't calculate history and take
979 979 commits defined in this list
980 980 """
981 981 _ = self.request.translate
982 982
983 983 # calculate history based on tip
984 984 tip = self.rhodecode_vcs_repo.get_commit()
985 985 if commits is None:
986 986 pre_load = ["author", "branch"]
987 987 try:
988 988 commits = tip.get_path_history(f_path, pre_load=pre_load)
989 989 except (NodeDoesNotExistError, CommitError):
990 990 # this node is not present at tip!
991 991 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
992 992
993 993 history = []
994 994 commits_group = ([], _("Changesets"))
995 995 for commit in commits:
996 996 branch = ' (%s)' % commit.branch if commit.branch else ''
997 997 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
998 998 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
999 999 history.append(commits_group)
1000 1000
1001 1001 symbolic_reference = self._symbolic_reference
1002 1002
1003 1003 if self.rhodecode_vcs_repo.alias == 'svn':
1004 1004 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1005 1005 f_path, self.rhodecode_vcs_repo)
1006 1006 if adjusted_f_path != f_path:
1007 1007 log.debug(
1008 1008 'Recognized svn tag or branch in file "%s", using svn '
1009 1009 'specific symbolic references', f_path)
1010 1010 f_path = adjusted_f_path
1011 1011 symbolic_reference = self._symbolic_reference_svn
1012 1012
1013 1013 branches = self._create_references(
1014 1014 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1015 1015 branches_group = (branches, _("Branches"))
1016 1016
1017 1017 tags = self._create_references(
1018 1018 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1019 1019 tags_group = (tags, _("Tags"))
1020 1020
1021 1021 history.append(branches_group)
1022 1022 history.append(tags_group)
1023 1023
1024 1024 return history, commits
1025 1025
1026 1026 @LoginRequired()
1027 1027 @HasRepoPermissionAnyDecorator(
1028 1028 'repository.read', 'repository.write', 'repository.admin')
1029 1029 def repo_file_history(self):
1030 1030 self.load_default_context()
1031 1031
1032 1032 commit_id, f_path = self._get_commit_and_path()
1033 1033 commit = self._get_commit_or_redirect(commit_id)
1034 1034 file_node = self._get_filenode_or_redirect(commit, f_path)
1035 1035
1036 1036 if file_node.is_file():
1037 1037 file_history, _hist = self._get_node_history(commit, f_path)
1038 1038
1039 1039 res = []
1040 1040 for section_items, section in file_history:
1041 1041 items = []
1042 1042 for obj_id, obj_text, obj_type in section_items:
1043 1043 at_rev = ''
1044 1044 if obj_type in ['branch', 'bookmark', 'tag']:
1045 1045 at_rev = obj_text
1046 1046 entry = {
1047 1047 'id': obj_id,
1048 1048 'text': obj_text,
1049 1049 'type': obj_type,
1050 1050 'at_rev': at_rev
1051 1051 }
1052 1052
1053 1053 items.append(entry)
1054 1054
1055 1055 res.append({
1056 1056 'text': section,
1057 1057 'children': items
1058 1058 })
1059 1059
1060 1060 data = {
1061 1061 'more': False,
1062 1062 'results': res
1063 1063 }
1064 1064 return data
1065 1065
1066 1066 log.warning('Cannot fetch history for directory')
1067 1067 raise HTTPBadRequest()
1068 1068
1069 1069 @LoginRequired()
1070 1070 @HasRepoPermissionAnyDecorator(
1071 1071 'repository.read', 'repository.write', 'repository.admin')
1072 1072 def repo_file_authors(self):
1073 1073 c = self.load_default_context()
1074 1074
1075 1075 commit_id, f_path = self._get_commit_and_path()
1076 1076 commit = self._get_commit_or_redirect(commit_id)
1077 1077 file_node = self._get_filenode_or_redirect(commit, f_path)
1078 1078
1079 1079 if not file_node.is_file():
1080 1080 raise HTTPBadRequest()
1081 1081
1082 1082 c.file_last_commit = file_node.last_commit
1083 1083 if self.request.GET.get('annotate') == '1':
1084 1084 # use _hist from annotation if annotation mode is on
1085 1085 commit_ids = set(x[1] for x in file_node.annotate)
1086 1086 _hist = (
1087 1087 self.rhodecode_vcs_repo.get_commit(commit_id)
1088 1088 for commit_id in commit_ids)
1089 1089 else:
1090 1090 _f_history, _hist = self._get_node_history(commit, f_path)
1091 1091 c.file_author = False
1092 1092
1093 1093 unique = collections.OrderedDict()
1094 1094 for commit in _hist:
1095 1095 author = commit.author
1096 1096 if author not in unique:
1097 1097 unique[commit.author] = [
1098 1098 h.email(author),
1099 1099 h.person(author, 'username_or_name_or_email'),
1100 1100 1 # counter
1101 1101 ]
1102 1102
1103 1103 else:
1104 1104 # increase counter
1105 1105 unique[commit.author][2] += 1
1106 1106
1107 1107 c.authors = [val for val in unique.values()]
1108 1108
1109 1109 return self._get_template_context(c)
1110 1110
1111 1111 @LoginRequired()
1112 1112 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1113 1113 def repo_files_check_head(self):
1114 1114 self.load_default_context()
1115 1115
1116 1116 commit_id, f_path = self._get_commit_and_path()
1117 1117 _branch_name, _sha_commit_id, is_head = \
1118 1118 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1119 1119 landing_ref=self.db_repo.landing_ref_name)
1120 1120
1121 1121 new_path = self.request.POST.get('path')
1122 1122 operation = self.request.POST.get('operation')
1123 1123 path_exist = ''
1124 1124
1125 1125 if new_path and operation in ['create', 'upload']:
1126 1126 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1127 1127 try:
1128 1128 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1129 1129 # NOTE(dan): construct whole path without leading /
1130 1130 file_node = commit_obj.get_node(new_f_path)
1131 1131 if file_node is not None:
1132 1132 path_exist = new_f_path
1133 1133 except EmptyRepositoryError:
1134 1134 pass
1135 1135 except Exception:
1136 1136 pass
1137 1137
1138 1138 return {
1139 1139 'branch': _branch_name,
1140 1140 'sha': _sha_commit_id,
1141 1141 'is_head': is_head,
1142 1142 'path_exists': path_exist
1143 1143 }
1144 1144
1145 1145 @LoginRequired()
1146 1146 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1147 1147 def repo_files_remove_file(self):
1148 1148 _ = self.request.translate
1149 1149 c = self.load_default_context()
1150 1150 commit_id, f_path = self._get_commit_and_path()
1151 1151
1152 1152 self._ensure_not_locked()
1153 1153 _branch_name, _sha_commit_id, is_head = \
1154 1154 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1155 1155 landing_ref=self.db_repo.landing_ref_name)
1156 1156
1157 1157 self.forbid_non_head(is_head, f_path)
1158 1158 self.check_branch_permission(_branch_name)
1159 1159
1160 1160 c.commit = self._get_commit_or_redirect(commit_id)
1161 1161 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1162 1162
1163 1163 c.default_message = _(
1164 1164 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1165 1165 c.f_path = f_path
1166 1166
1167 1167 return self._get_template_context(c)
1168 1168
1169 1169 @LoginRequired()
1170 1170 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1171 1171 @CSRFRequired()
1172 1172 def repo_files_delete_file(self):
1173 1173 _ = self.request.translate
1174 1174
1175 1175 c = self.load_default_context()
1176 1176 commit_id, f_path = self._get_commit_and_path()
1177 1177
1178 1178 self._ensure_not_locked()
1179 1179 _branch_name, _sha_commit_id, is_head = \
1180 1180 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1181 1181 landing_ref=self.db_repo.landing_ref_name)
1182 1182
1183 1183 self.forbid_non_head(is_head, f_path)
1184 1184 self.check_branch_permission(_branch_name)
1185 1185
1186 1186 c.commit = self._get_commit_or_redirect(commit_id)
1187 1187 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1188 1188
1189 1189 c.default_message = _(
1190 1190 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1191 1191 c.f_path = f_path
1192 1192 node_path = f_path
1193 1193 author = self._rhodecode_db_user.full_contact
1194 1194 message = self.request.POST.get('message') or c.default_message
1195 1195 try:
1196 1196 nodes = {
1197 1197 node_path: {
1198 1198 'content': ''
1199 1199 }
1200 1200 }
1201 1201 ScmModel().delete_nodes(
1202 1202 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1203 1203 message=message,
1204 1204 nodes=nodes,
1205 1205 parent_commit=c.commit,
1206 1206 author=author,
1207 1207 )
1208 1208
1209 1209 h.flash(
1210 1210 _('Successfully deleted file `{}`').format(
1211 1211 h.escape(f_path)), category='success')
1212 1212 except Exception:
1213 1213 log.exception('Error during commit operation')
1214 1214 h.flash(_('Error occurred during commit'), category='error')
1215 1215 raise HTTPFound(
1216 1216 h.route_path('repo_commit', repo_name=self.db_repo_name,
1217 1217 commit_id='tip'))
1218 1218
1219 1219 @LoginRequired()
1220 1220 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1221 1221 def repo_files_edit_file(self):
1222 1222 _ = self.request.translate
1223 1223 c = self.load_default_context()
1224 1224 commit_id, f_path = self._get_commit_and_path()
1225 1225
1226 1226 self._ensure_not_locked()
1227 1227 _branch_name, _sha_commit_id, is_head = \
1228 1228 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1229 1229 landing_ref=self.db_repo.landing_ref_name)
1230 1230
1231 1231 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1232 1232 self.check_branch_permission(_branch_name, commit_id=commit_id)
1233 1233
1234 1234 c.commit = self._get_commit_or_redirect(commit_id)
1235 1235 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1236 1236
1237 1237 if c.file.is_binary:
1238 1238 files_url = h.route_path(
1239 1239 'repo_files',
1240 1240 repo_name=self.db_repo_name,
1241 1241 commit_id=c.commit.raw_id, f_path=f_path)
1242 1242 raise HTTPFound(files_url)
1243 1243
1244 1244 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1245 1245 c.f_path = f_path
1246 1246
1247 1247 return self._get_template_context(c)
1248 1248
1249 1249 @LoginRequired()
1250 1250 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1251 1251 @CSRFRequired()
1252 1252 def repo_files_update_file(self):
1253 1253 _ = self.request.translate
1254 1254 c = self.load_default_context()
1255 1255 commit_id, f_path = self._get_commit_and_path()
1256 1256
1257 1257 self._ensure_not_locked()
1258 1258
1259 1259 c.commit = self._get_commit_or_redirect(commit_id)
1260 1260 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1261 1261
1262 1262 if c.file.is_binary:
1263 1263 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1264 1264 commit_id=c.commit.raw_id, f_path=f_path))
1265 1265
1266 1266 _branch_name, _sha_commit_id, is_head = \
1267 1267 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1268 1268 landing_ref=self.db_repo.landing_ref_name)
1269 1269
1270 1270 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1271 1271 self.check_branch_permission(_branch_name, commit_id=commit_id)
1272 1272
1273 1273 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1274 1274 c.f_path = f_path
1275 1275
1276 1276 old_content = c.file.content
1277 1277 sl = old_content.splitlines(1)
1278 1278 first_line = sl[0] if sl else ''
1279 1279
1280 1280 r_post = self.request.POST
1281 1281 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1282 1282 line_ending_mode = detect_mode(first_line, 0)
1283 1283 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1284 1284
1285 1285 message = r_post.get('message') or c.default_message
1286 1286 org_node_path = c.file.unicode_path
1287 1287 filename = r_post['filename']
1288 1288
1289 1289 root_path = c.file.dir_path
1290 1290 pure_path = self.create_pure_path(root_path, filename)
1291 1291 node_path = pure_path.as_posix()
1292 1292
1293 1293 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1294 1294 commit_id=commit_id)
1295 1295 if content == old_content and node_path == org_node_path:
1296 1296 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1297 1297 category='warning')
1298 1298 raise HTTPFound(default_redirect_url)
1299 1299
1300 1300 try:
1301 1301 mapping = {
1302 1302 org_node_path: {
1303 1303 'org_filename': org_node_path,
1304 1304 'filename': node_path,
1305 1305 'content': content,
1306 1306 'lexer': '',
1307 1307 'op': 'mod',
1308 1308 'mode': c.file.mode
1309 1309 }
1310 1310 }
1311 1311
1312 1312 commit = ScmModel().update_nodes(
1313 1313 user=self._rhodecode_db_user.user_id,
1314 1314 repo=self.db_repo,
1315 1315 message=message,
1316 1316 nodes=mapping,
1317 1317 parent_commit=c.commit,
1318 1318 )
1319 1319
1320 1320 h.flash(_('Successfully committed changes to file `{}`').format(
1321 1321 h.escape(f_path)), category='success')
1322 1322 default_redirect_url = h.route_path(
1323 1323 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1324 1324
1325 1325 except Exception:
1326 1326 log.exception('Error occurred during commit')
1327 1327 h.flash(_('Error occurred during commit'), category='error')
1328 1328
1329 1329 raise HTTPFound(default_redirect_url)
1330 1330
1331 1331 @LoginRequired()
1332 1332 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1333 1333 def repo_files_add_file(self):
1334 1334 _ = self.request.translate
1335 1335 c = self.load_default_context()
1336 1336 commit_id, f_path = self._get_commit_and_path()
1337 1337
1338 1338 self._ensure_not_locked()
1339 1339
1340 1340 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1341 1341 if c.commit is None:
1342 1342 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1343 1343
1344 1344 if self.rhodecode_vcs_repo.is_empty():
1345 1345 # for empty repository we cannot check for current branch, we rely on
1346 1346 # c.commit.branch instead
1347 1347 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1348 1348 else:
1349 1349 _branch_name, _sha_commit_id, is_head = \
1350 1350 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1351 1351 landing_ref=self.db_repo.landing_ref_name)
1352 1352
1353 1353 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1354 1354 self.check_branch_permission(_branch_name, commit_id=commit_id)
1355 1355
1356 1356 c.default_message = (_('Added file via RhodeCode Enterprise'))
1357 1357 c.f_path = f_path.lstrip('/') # ensure not relative path
1358 1358
1359 1359 return self._get_template_context(c)
1360 1360
1361 1361 @LoginRequired()
1362 1362 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1363 1363 @CSRFRequired()
1364 1364 def repo_files_create_file(self):
1365 1365 _ = self.request.translate
1366 1366 c = self.load_default_context()
1367 1367 commit_id, f_path = self._get_commit_and_path()
1368 1368
1369 1369 self._ensure_not_locked()
1370 1370
1371 1371 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1372 1372 if c.commit is None:
1373 1373 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1374 1374
1375 1375 # calculate redirect URL
1376 1376 if self.rhodecode_vcs_repo.is_empty():
1377 1377 default_redirect_url = h.route_path(
1378 1378 'repo_summary', repo_name=self.db_repo_name)
1379 1379 else:
1380 1380 default_redirect_url = h.route_path(
1381 1381 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1382 1382
1383 1383 if self.rhodecode_vcs_repo.is_empty():
1384 1384 # for empty repository we cannot check for current branch, we rely on
1385 1385 # c.commit.branch instead
1386 1386 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1387 1387 else:
1388 1388 _branch_name, _sha_commit_id, is_head = \
1389 1389 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1390 1390 landing_ref=self.db_repo.landing_ref_name)
1391 1391
1392 1392 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1393 1393 self.check_branch_permission(_branch_name, commit_id=commit_id)
1394 1394
1395 1395 c.default_message = (_('Added file via RhodeCode Enterprise'))
1396 1396 c.f_path = f_path
1397 1397
1398 1398 r_post = self.request.POST
1399 1399 message = r_post.get('message') or c.default_message
1400 1400 filename = r_post.get('filename')
1401 1401 unix_mode = 0
1402 1402
1403 1403 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1404 1404
1405 1405 if not filename:
1406 1406 # If there's no commit, redirect to repo summary
1407 1407 if type(c.commit) is EmptyCommit:
1408 1408 redirect_url = h.route_path(
1409 1409 'repo_summary', repo_name=self.db_repo_name)
1410 1410 else:
1411 1411 redirect_url = default_redirect_url
1412 1412 h.flash(_('No filename specified'), category='warning')
1413 1413 raise HTTPFound(redirect_url)
1414 1414
1415 1415 root_path = f_path
1416 1416 pure_path = self.create_pure_path(root_path, filename)
1417 1417 node_path = pure_path.as_posix().lstrip('/')
1418 1418
1419 1419 author = self._rhodecode_db_user.full_contact
1420 1420 nodes = {
1421 1421 node_path: {
1422 1422 'content': content
1423 1423 }
1424 1424 }
1425 1425
1426 1426 try:
1427 1427
1428 1428 commit = ScmModel().create_nodes(
1429 1429 user=self._rhodecode_db_user.user_id,
1430 1430 repo=self.db_repo,
1431 1431 message=message,
1432 1432 nodes=nodes,
1433 1433 parent_commit=c.commit,
1434 1434 author=author,
1435 1435 )
1436 1436
1437 1437 h.flash(_('Successfully committed new file `{}`').format(
1438 1438 h.escape(node_path)), category='success')
1439 1439
1440 1440 default_redirect_url = h.route_path(
1441 1441 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1442 1442
1443 1443 except NonRelativePathError:
1444 1444 log.exception('Non Relative path found')
1445 1445 h.flash(_('The location specified must be a relative path and must not '
1446 1446 'contain .. in the path'), category='warning')
1447 1447 raise HTTPFound(default_redirect_url)
1448 1448 except (NodeError, NodeAlreadyExistsError) as e:
1449 1449 h.flash(h.escape(safe_str(e)), category='error')
1450 1450 except Exception:
1451 1451 log.exception('Error occurred during commit')
1452 1452 h.flash(_('Error occurred during commit'), category='error')
1453 1453
1454 1454 raise HTTPFound(default_redirect_url)
1455 1455
1456 1456 @LoginRequired()
1457 1457 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1458 1458 @CSRFRequired()
1459 1459 def repo_files_upload_file(self):
1460 1460 _ = self.request.translate
1461 1461 c = self.load_default_context()
1462 1462 commit_id, f_path = self._get_commit_and_path()
1463 1463
1464 1464 self._ensure_not_locked()
1465 1465
1466 1466 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1467 1467 if c.commit is None:
1468 1468 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1469 1469
1470 1470 # calculate redirect URL
1471 1471 if self.rhodecode_vcs_repo.is_empty():
1472 1472 default_redirect_url = h.route_path(
1473 1473 'repo_summary', repo_name=self.db_repo_name)
1474 1474 else:
1475 1475 default_redirect_url = h.route_path(
1476 1476 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1477 1477
1478 1478 if self.rhodecode_vcs_repo.is_empty():
1479 1479 # for empty repository we cannot check for current branch, we rely on
1480 1480 # c.commit.branch instead
1481 1481 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1482 1482 else:
1483 1483 _branch_name, _sha_commit_id, is_head = \
1484 1484 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1485 1485 landing_ref=self.db_repo.landing_ref_name)
1486 1486
1487 1487 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1488 1488 if error:
1489 1489 return {
1490 1490 'error': error,
1491 1491 'redirect_url': default_redirect_url
1492 1492 }
1493 1493 error = self.check_branch_permission(_branch_name, json_mode=True)
1494 1494 if error:
1495 1495 return {
1496 1496 'error': error,
1497 1497 'redirect_url': default_redirect_url
1498 1498 }
1499 1499
1500 1500 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1501 1501 c.f_path = f_path
1502 1502
1503 1503 r_post = self.request.POST
1504 1504
1505 1505 message = c.default_message
1506 1506 user_message = r_post.getall('message')
1507 1507 if isinstance(user_message, list) and user_message:
1508 1508 # we take the first from duplicated results if it's not empty
1509 1509 message = user_message[0] if user_message[0] else message
1510 1510
1511 1511 nodes = {}
1512 1512
1513 1513 for file_obj in r_post.getall('files_upload') or []:
1514 1514 content = file_obj.file
1515 1515 filename = file_obj.filename
1516 1516
1517 1517 root_path = f_path
1518 1518 pure_path = self.create_pure_path(root_path, filename)
1519 1519 node_path = pure_path.as_posix().lstrip('/')
1520 1520
1521 1521 nodes[node_path] = {
1522 1522 'content': content
1523 1523 }
1524 1524
1525 1525 if not nodes:
1526 1526 error = 'missing files'
1527 1527 return {
1528 1528 'error': error,
1529 1529 'redirect_url': default_redirect_url
1530 1530 }
1531 1531
1532 1532 author = self._rhodecode_db_user.full_contact
1533 1533
1534 1534 try:
1535 1535 commit = ScmModel().create_nodes(
1536 1536 user=self._rhodecode_db_user.user_id,
1537 1537 repo=self.db_repo,
1538 1538 message=message,
1539 1539 nodes=nodes,
1540 1540 parent_commit=c.commit,
1541 1541 author=author,
1542 1542 )
1543 1543 if len(nodes) == 1:
1544 1544 flash_message = _('Successfully committed {} new files').format(len(nodes))
1545 1545 else:
1546 1546 flash_message = _('Successfully committed 1 new file')
1547 1547
1548 1548 h.flash(flash_message, category='success')
1549 1549
1550 1550 default_redirect_url = h.route_path(
1551 1551 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1552 1552
1553 1553 except NonRelativePathError:
1554 1554 log.exception('Non Relative path found')
1555 1555 error = _('The location specified must be a relative path and must not '
1556 1556 'contain .. in the path')
1557 1557 h.flash(error, category='warning')
1558 1558
1559 1559 return {
1560 1560 'error': error,
1561 1561 'redirect_url': default_redirect_url
1562 1562 }
1563 1563 except (NodeError, NodeAlreadyExistsError) as e:
1564 1564 error = h.escape(e)
1565 1565 h.flash(error, category='error')
1566 1566
1567 1567 return {
1568 1568 'error': error,
1569 1569 'redirect_url': default_redirect_url
1570 1570 }
1571 1571 except Exception:
1572 1572 log.exception('Error occurred during commit')
1573 1573 error = _('Error occurred during commit')
1574 1574 h.flash(error, category='error')
1575 1575 return {
1576 1576 'error': error,
1577 1577 'redirect_url': default_redirect_url
1578 1578 }
1579 1579
1580 1580 return {
1581 1581 'error': None,
1582 1582 'redirect_url': default_redirect_url
1583 1583 }
@@ -1,254 +1,254 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
35 35 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
36 36 import rhodecode.lib.helpers as h
37 37 from rhodecode.lib.celerylib.utils import get_task_id
38 38 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.repo import RepoModel
41 41 from rhodecode.model.forms import RepoForkForm
42 42 from rhodecode.model.scm import ScmModel, RepoGroupList
43 43 from rhodecode.lib.utils2 import safe_int, safe_unicode
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class RepoForksView(RepoAppView, DataGridAppView):
49 49
50 50 def load_default_context(self):
51 51 c = self._get_local_tmpl_context(include_app_defaults=True)
52 52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 53
54 54 acl_groups = RepoGroupList(
55 55 RepoGroup.query().all(),
56 56 perm_set=['group.write', 'group.admin'])
57 57 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
58 58 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
59 59
60 60 c.personal_repo_group = c.rhodecode_user.personal_repo_group
61 61
62 62 return c
63 63
64 64 @LoginRequired()
65 65 @HasRepoPermissionAnyDecorator(
66 66 'repository.read', 'repository.write', 'repository.admin')
67 67 def repo_forks_show_all(self):
68 68 c = self.load_default_context()
69 69 return self._get_template_context(c)
70 70
71 71 @LoginRequired()
72 72 @HasRepoPermissionAnyDecorator(
73 73 'repository.read', 'repository.write', 'repository.admin')
74 74 def repo_forks_data(self):
75 75 _ = self.request.translate
76 76 self.load_default_context()
77 77 column_map = {
78 78 'fork_name': 'repo_name',
79 79 'fork_date': 'created_on',
80 80 'last_activity': 'updated_on'
81 81 }
82 82 draw, start, limit = self._extract_chunk(self.request)
83 83 search_q, order_by, order_dir = self._extract_ordering(
84 84 self.request, column_map=column_map)
85 85
86 86 acl_check = HasRepoPermissionAny(
87 87 'repository.read', 'repository.write', 'repository.admin')
88 88 repo_id = self.db_repo.repo_id
89 89 allowed_ids = [-1]
90 90 for f in Repository.query().filter(Repository.fork_id == repo_id):
91 91 if acl_check(f.repo_name, 'get forks check'):
92 92 allowed_ids.append(f.repo_id)
93 93
94 94 forks_data_total_count = Repository.query()\
95 95 .filter(Repository.fork_id == repo_id)\
96 96 .filter(Repository.repo_id.in_(allowed_ids))\
97 97 .count()
98 98
99 99 # json generate
100 100 base_q = Repository.query()\
101 101 .filter(Repository.fork_id == repo_id)\
102 102 .filter(Repository.repo_id.in_(allowed_ids))\
103 103
104 104 if search_q:
105 105 like_expression = u'%{}%'.format(safe_unicode(search_q))
106 106 base_q = base_q.filter(or_(
107 107 Repository.repo_name.ilike(like_expression),
108 108 Repository.description.ilike(like_expression),
109 109 ))
110 110
111 111 forks_data_total_filtered_count = base_q.count()
112 112
113 113 sort_col = getattr(Repository, order_by, None)
114 114 if sort_col:
115 115 if order_dir == 'asc':
116 116 # handle null values properly to order by NULL last
117 117 if order_by in ['last_activity']:
118 118 sort_col = coalesce(sort_col, datetime.date.max)
119 119 sort_col = sort_col.asc()
120 120 else:
121 121 # handle null values properly to order by NULL last
122 122 if order_by in ['last_activity']:
123 123 sort_col = coalesce(sort_col, datetime.date.min)
124 124 sort_col = sort_col.desc()
125 125
126 126 base_q = base_q.order_by(sort_col)
127 127 base_q = base_q.offset(start).limit(limit)
128 128
129 129 fork_list = base_q.all()
130 130
131 131 def fork_actions(fork):
132 132 url_link = h.route_path(
133 133 'repo_compare',
134 134 repo_name=fork.repo_name,
135 135 source_ref_type=self.db_repo.landing_ref_type,
136 136 source_ref=self.db_repo.landing_ref_name,
137 137 target_ref_type=self.db_repo.landing_ref_type,
138 138 target_ref=self.db_repo.landing_ref_name,
139 139 _query=dict(merge=1, target_repo=f.repo_name))
140 140 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
141 141
142 142 def fork_name(fork):
143 143 return h.link_to(fork.repo_name,
144 144 h.route_path('repo_summary', repo_name=fork.repo_name))
145 145
146 146 forks_data = []
147 147 for fork in fork_list:
148 148 forks_data.append({
149 149 "username": h.gravatar_with_user(self.request, fork.user.username),
150 150 "fork_name": fork_name(fork),
151 151 "description": fork.description_safe,
152 152 "fork_date": h.age_component(fork.created_on, time_is_local=True),
153 153 "last_activity": h.format_date(fork.updated_on),
154 154 "action": fork_actions(fork),
155 155 })
156 156
157 157 data = ({
158 158 'draw': draw,
159 159 'data': forks_data,
160 160 'recordsTotal': forks_data_total_count,
161 161 'recordsFiltered': forks_data_total_filtered_count,
162 162 })
163 163
164 164 return data
165 165
166 166 @LoginRequired()
167 167 @NotAnonymous()
168 168 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
169 169 @HasRepoPermissionAnyDecorator(
170 170 'repository.read', 'repository.write', 'repository.admin')
171 171 def repo_fork_new(self):
172 172 c = self.load_default_context()
173 173
174 174 defaults = RepoModel()._get_defaults(self.db_repo_name)
175 175 # alter the description to indicate a fork
176 176 defaults['description'] = (
177 177 'fork of repository: %s \n%s' % (
178 178 defaults['repo_name'], defaults['description']))
179 179 # add suffix to fork
180 180 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
181 181
182 182 data = render('rhodecode:templates/forks/fork.mako',
183 183 self._get_template_context(c), self.request)
184 184 html = formencode.htmlfill.render(
185 185 data,
186 186 defaults=defaults,
187 187 encoding="UTF-8",
188 188 force_defaults=False
189 189 )
190 190 return Response(html)
191 191
192 192 @LoginRequired()
193 193 @NotAnonymous()
194 194 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
195 195 @HasRepoPermissionAnyDecorator(
196 196 'repository.read', 'repository.write', 'repository.admin')
197 197 @CSRFRequired()
198 198 def repo_fork_create(self):
199 199 _ = self.request.translate
200 200 c = self.load_default_context()
201 201
202 202 _form = RepoForkForm(self.request.translate,
203 203 old_data={'repo_type': self.db_repo.repo_type},
204 204 repo_groups=c.repo_groups_choices)()
205 205 post_data = dict(self.request.POST)
206 206
207 207 # forbid injecting other repo by forging a request
208 208 post_data['fork_parent_id'] = self.db_repo.repo_id
209 209 post_data['landing_rev'] = self.db_repo._landing_revision
210 210
211 211 form_result = {}
212 212 task_id = None
213 213 try:
214 214 form_result = _form.to_python(post_data)
215 215 copy_permissions = form_result.get('copy_permissions')
216 216 # create fork is done sometimes async on celery, db transaction
217 217 # management is handled there.
218 218 task = RepoModel().create_fork(
219 219 form_result, c.rhodecode_user.user_id)
220 220
221 221 task_id = get_task_id(task)
222 222 except formencode.Invalid as errors:
223 223 c.rhodecode_db_repo = self.db_repo
224 224
225 225 data = render('rhodecode:templates/forks/fork.mako',
226 226 self._get_template_context(c), self.request)
227 227 html = formencode.htmlfill.render(
228 228 data,
229 229 defaults=errors.value,
230 230 errors=errors.error_dict or {},
231 231 prefix_error=False,
232 232 encoding="UTF-8",
233 233 force_defaults=False
234 234 )
235 235 return Response(html)
236 236 except Exception:
237 237 log.exception(
238 238 u'Exception while trying to fork the repository %s', self.db_repo_name)
239 239 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
240 240 h.flash(msg, category='error')
241 241 raise HTTPFound(h.route_path('home'))
242 242
243 243 repo_name = form_result.get('repo_name_full', self.db_repo_name)
244 244
245 245 affected_user_ids = [self._rhodecode_user.user_id]
246 246 if copy_permissions:
247 247 # permission flush is done in repo creating
248 248 pass
249 249
250 250 PermissionModel().trigger_permission_flush(affected_user_ids)
251 251
252 252 raise HTTPFound(
253 253 h.route_path('repo_creating', repo_name=repo_name,
254 254 _query=dict(task_id=task_id)))
@@ -1,56 +1,56 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24
25 25 from rhodecode.apps._base import RepoAppView
26 26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 27 from rhodecode.lib import repo_maintenance
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 class RepoMaintenanceView(RepoAppView):
33 33 def load_default_context(self):
34 34 c = self._get_local_tmpl_context()
35 35 return c
36 36
37 37 @LoginRequired()
38 38 @HasRepoPermissionAnyDecorator('repository.admin')
39 39 def repo_maintenance(self):
40 40 c = self.load_default_context()
41 41 c.active = 'maintenance'
42 42 maintenance = repo_maintenance.RepoMaintenance()
43 43 c.executable_tasks = maintenance.get_tasks_for_repo(self.db_repo)
44 44 return self._get_template_context(c)
45 45
46 46 @LoginRequired()
47 47 @HasRepoPermissionAnyDecorator('repository.admin')
48 48 def repo_maintenance_execute(self):
49 49 c = self.load_default_context()
50 50 c.active = 'maintenance'
51 51 _ = self.request.translate
52 52
53 53 maintenance = repo_maintenance.RepoMaintenance()
54 54 executed_types = maintenance.execute(self.db_repo)
55 55
56 56 return executed_types
@@ -1,130 +1,130 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound
24 24
25 25 from rhodecode.apps._base import RepoAppView
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib import audit_logger
28 28 from rhodecode.lib.auth import (
29 29 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 30 from rhodecode.lib.utils2 import str2bool
31 31 from rhodecode.model.db import User
32 32 from rhodecode.model.forms import RepoPermsForm
33 33 from rhodecode.model.meta import Session
34 34 from rhodecode.model.permission import PermissionModel
35 35 from rhodecode.model.repo import RepoModel
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class RepoSettingsPermissionsView(RepoAppView):
41 41
42 42 def load_default_context(self):
43 43 c = self._get_local_tmpl_context()
44 44 return c
45 45
46 46 @LoginRequired()
47 47 @HasRepoPermissionAnyDecorator('repository.admin')
48 48 def edit_permissions(self):
49 49 _ = self.request.translate
50 50 c = self.load_default_context()
51 51 c.active = 'permissions'
52 52 if self.request.GET.get('branch_permissions'):
53 53 h.flash(_('Explicitly add user or user group with write or higher '
54 54 'permission to modify their branch permissions.'),
55 55 category='notice')
56 56 return self._get_template_context(c)
57 57
58 58 @LoginRequired()
59 59 @HasRepoPermissionAnyDecorator('repository.admin')
60 60 @CSRFRequired()
61 61 def edit_permissions_update(self):
62 62 _ = self.request.translate
63 63 c = self.load_default_context()
64 64 c.active = 'permissions'
65 65 data = self.request.POST
66 66 # store private flag outside of HTML to verify if we can modify
67 67 # default user permissions, prevents submission of FAKE post data
68 68 # into the form for private repos
69 69 data['repo_private'] = self.db_repo.private
70 70 form = RepoPermsForm(self.request.translate)().to_python(data)
71 71 changes = RepoModel().update_permissions(
72 72 self.db_repo_name, form['perm_additions'], form['perm_updates'],
73 73 form['perm_deletions'])
74 74
75 75 action_data = {
76 76 'added': changes['added'],
77 77 'updated': changes['updated'],
78 78 'deleted': changes['deleted'],
79 79 }
80 80 audit_logger.store_web(
81 81 'repo.edit.permissions', action_data=action_data,
82 82 user=self._rhodecode_user, repo=self.db_repo)
83 83
84 84 Session().commit()
85 85 h.flash(_('Repository access permissions updated'), category='success')
86 86
87 87 affected_user_ids = None
88 88 if changes.get('default_user_changed', False):
89 89 # if we change the default user, we need to flush everyone permissions
90 90 affected_user_ids = User.get_all_user_ids()
91 91 PermissionModel().flush_user_permission_caches(
92 92 changes, affected_user_ids=affected_user_ids)
93 93
94 94 raise HTTPFound(
95 95 h.route_path('edit_repo_perms', repo_name=self.db_repo_name))
96 96
97 97 @LoginRequired()
98 98 @HasRepoPermissionAnyDecorator('repository.admin')
99 99 @CSRFRequired()
100 100 def edit_permissions_set_private_repo(self):
101 101 _ = self.request.translate
102 102 self.load_default_context()
103 103
104 104 private_flag = str2bool(self.request.POST.get('private'))
105 105
106 106 try:
107 107 repo = RepoModel().get(self.db_repo.repo_id)
108 108 repo.private = private_flag
109 109 Session().add(repo)
110 110 RepoModel().grant_user_permission(
111 111 repo=self.db_repo, user=User.DEFAULT_USER, perm='repository.none'
112 112 )
113 113
114 114 Session().commit()
115 115
116 116 h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name),
117 117 category='success')
118 118 # NOTE(dan): we change repo private mode we need to notify all USERS
119 119 affected_user_ids = User.get_all_user_ids()
120 120 PermissionModel().trigger_permission_flush(affected_user_ids)
121 121
122 122 except Exception:
123 123 log.exception("Exception during update of repository")
124 124 h.flash(_('Error occurred during update of repository {}').format(
125 125 self.db_repo_name), category='error')
126 126
127 127 return {
128 128 'redirect_url': h.route_path('edit_repo_perms', repo_name=self.db_repo_name),
129 129 'private': private_flag
130 130 }
@@ -1,1877 +1,1877 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib import ext_json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist, retry
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name,
83 83 search_q=search_q, statuses=statuses,
84 84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name,
87 87 search_q=search_q, statuses=statuses)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, self._rhodecode_user.user_id,
91 91 search_q=search_q, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 94 repo_name, self._rhodecode_user.user_id,
95 95 search_q=search_q, statuses=statuses)
96 96 else:
97 97 pull_requests = PullRequestModel().get_all(
98 98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 99 statuses=statuses, offset=start, length=limit,
100 100 order_by=order_by, order_dir=order_dir)
101 101 pull_requests_total_count = PullRequestModel().count_all(
102 102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 103 opened_by=opened_by)
104 104
105 105 data = []
106 106 comments_model = CommentsModel()
107 107 for pr in pull_requests:
108 108 comments_count = comments_model.get_all_comments(
109 109 self.db_repo.repo_id, pull_request=pr,
110 110 include_drafts=False, count_only=True)
111 111
112 112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 114 if review_statuses and review_statuses[4]:
115 115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 116 my_review_status = statuses[0][1].status
117 117
118 118 data.append({
119 119 'name': _render('pullrequest_name',
120 120 pr.pull_request_id, pr.pull_request_state,
121 121 pr.work_in_progress, pr.target_repo.repo_name,
122 122 short=True),
123 123 'name_raw': pr.pull_request_id,
124 124 'status': _render('pullrequest_status',
125 125 pr.calculated_review_status()),
126 126 'my_status': _render('pullrequest_status',
127 127 my_review_status),
128 128 'title': _render('pullrequest_title', pr.title, pr.description),
129 129 'description': h.escape(pr.description),
130 130 'updated_on': _render('pullrequest_updated_on',
131 131 h.datetime_to_time(pr.updated_on),
132 132 pr.versions_count),
133 133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 134 'created_on': _render('pullrequest_updated_on',
135 135 h.datetime_to_time(pr.created_on)),
136 136 'created_on_raw': h.datetime_to_time(pr.created_on),
137 137 'state': pr.pull_request_state,
138 138 'author': _render('pullrequest_author',
139 139 pr.author.full_contact, ),
140 140 'author_raw': pr.author.full_name,
141 141 'comments': _render('pullrequest_comments', comments_count),
142 142 'comments_raw': comments_count,
143 143 'closed': pr.is_closed(),
144 144 })
145 145
146 146 data = ({
147 147 'draw': draw,
148 148 'data': data,
149 149 'recordsTotal': pull_requests_total_count,
150 150 'recordsFiltered': pull_requests_total_count,
151 151 })
152 152 return data
153 153
154 154 @LoginRequired()
155 155 @HasRepoPermissionAnyDecorator(
156 156 'repository.read', 'repository.write', 'repository.admin')
157 157 def pull_request_list(self):
158 158 c = self.load_default_context()
159 159
160 160 req_get = self.request.GET
161 161 c.source = str2bool(req_get.get('source'))
162 162 c.closed = str2bool(req_get.get('closed'))
163 163 c.my = str2bool(req_get.get('my'))
164 164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166 166
167 167 c.active = 'open'
168 168 if c.my:
169 169 c.active = 'my'
170 170 if c.closed:
171 171 c.active = 'closed'
172 172 if c.awaiting_review and not c.source:
173 173 c.active = 'awaiting'
174 174 if c.source and not c.awaiting_review:
175 175 c.active = 'source'
176 176 if c.awaiting_my_review:
177 177 c.active = 'awaiting_my'
178 178
179 179 return self._get_template_context(c)
180 180
181 181 @LoginRequired()
182 182 @HasRepoPermissionAnyDecorator(
183 183 'repository.read', 'repository.write', 'repository.admin')
184 184 def pull_request_list_data(self):
185 185 self.load_default_context()
186 186
187 187 # additional filters
188 188 req_get = self.request.GET
189 189 source = str2bool(req_get.get('source'))
190 190 closed = str2bool(req_get.get('closed'))
191 191 my = str2bool(req_get.get('my'))
192 192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194 194
195 195 filter_type = 'awaiting_review' if awaiting_review \
196 196 else 'awaiting_my_review' if awaiting_my_review \
197 197 else None
198 198
199 199 opened_by = None
200 200 if my:
201 201 opened_by = [self._rhodecode_user.user_id]
202 202
203 203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 204 if closed:
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 data = self._get_pull_requests_list(
208 208 repo_name=self.db_repo_name, source=source,
209 209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210 210
211 211 return data
212 212
213 213 def _is_diff_cache_enabled(self, target_repo):
214 214 caching_enabled = self._get_general_setting(
215 215 target_repo, 'rhodecode_diff_cache')
216 216 log.debug('Diff caching enabled: %s', caching_enabled)
217 217 return caching_enabled
218 218
219 219 def _get_diffset(self, source_repo_name, source_repo,
220 220 ancestor_commit,
221 221 source_ref_id, target_ref_id,
222 222 target_commit, source_commit, diff_limit, file_limit,
223 223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224 224
225 225 target_commit_final = target_commit
226 226 source_commit_final = source_commit
227 227
228 228 if use_ancestor:
229 229 # we might want to not use it for versions
230 230 target_ref_id = ancestor_commit.raw_id
231 231 target_commit_final = ancestor_commit
232 232
233 233 vcs_diff = PullRequestModel().get_diff(
234 234 source_repo, source_ref_id, target_ref_id,
235 235 hide_whitespace_changes, diff_context)
236 236
237 237 diff_processor = diffs.DiffProcessor(
238 238 vcs_diff, format='newdiff', diff_limit=diff_limit,
239 239 file_limit=file_limit, show_full_diff=fulldiff)
240 240
241 241 _parsed = diff_processor.prepare()
242 242
243 243 diffset = codeblocks.DiffSet(
244 244 repo_name=self.db_repo_name,
245 245 source_repo_name=source_repo_name,
246 246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
247 247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
248 248 )
249 249 diffset = self.path_filter.render_patchset_filtered(
250 250 diffset, _parsed, target_ref_id, source_ref_id)
251 251
252 252 return diffset
253 253
254 254 def _get_range_diffset(self, source_scm, source_repo,
255 255 commit1, commit2, diff_limit, file_limit,
256 256 fulldiff, hide_whitespace_changes, diff_context):
257 257 vcs_diff = source_scm.get_diff(
258 258 commit1, commit2,
259 259 ignore_whitespace=hide_whitespace_changes,
260 260 context=diff_context)
261 261
262 262 diff_processor = diffs.DiffProcessor(
263 263 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 264 file_limit=file_limit, show_full_diff=fulldiff)
265 265
266 266 _parsed = diff_processor.prepare()
267 267
268 268 diffset = codeblocks.DiffSet(
269 269 repo_name=source_repo.repo_name,
270 270 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 271 target_node_getter=codeblocks.diffset_node_getter(commit2))
272 272
273 273 diffset = self.path_filter.render_patchset_filtered(
274 274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275 275
276 276 return diffset
277 277
278 278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
279 279 comments_model = CommentsModel()
280 280
281 281 # GENERAL COMMENTS with versions #
282 282 q = comments_model._all_general_comments_of_pull_request(pull_request)
283 283 q = q.order_by(ChangesetComment.comment_id.asc())
284 284 if not include_drafts:
285 285 q = q.filter(ChangesetComment.draft == false())
286 286 general_comments = q
287 287
288 288 # pick comments we want to render at current version
289 289 c.comment_versions = comments_model.aggregate_comments(
290 290 general_comments, versions, c.at_version_num)
291 291
292 292 # INLINE COMMENTS with versions #
293 293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
294 294 q = q.order_by(ChangesetComment.comment_id.asc())
295 295 if not include_drafts:
296 296 q = q.filter(ChangesetComment.draft == false())
297 297 inline_comments = q
298 298
299 299 c.inline_versions = comments_model.aggregate_comments(
300 300 inline_comments, versions, c.at_version_num, inline=True)
301 301
302 302 # Comments inline+general
303 303 if c.at_version:
304 304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
305 305 c.comments = c.comment_versions[c.at_version_num]['display']
306 306 else:
307 307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
308 308 c.comments = c.comment_versions[c.at_version_num]['until']
309 309
310 310 return general_comments, inline_comments
311 311
312 312 @LoginRequired()
313 313 @HasRepoPermissionAnyDecorator(
314 314 'repository.read', 'repository.write', 'repository.admin')
315 315 def pull_request_show(self):
316 316 _ = self.request.translate
317 317 c = self.load_default_context()
318 318
319 319 pull_request = PullRequest.get_or_404(
320 320 self.request.matchdict['pull_request_id'])
321 321 pull_request_id = pull_request.pull_request_id
322 322
323 323 c.state_progressing = pull_request.is_state_changing()
324 324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
325 325
326 326 _new_state = {
327 327 'created': PullRequest.STATE_CREATED,
328 328 }.get(self.request.GET.get('force_state'))
329 329 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
330 330
331 331 if can_force_state and _new_state:
332 332 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
333 333 h.flash(
334 334 _('Pull Request state was force changed to `{}`').format(_new_state),
335 335 category='success')
336 336 Session().commit()
337 337
338 338 raise HTTPFound(h.route_path(
339 339 'pullrequest_show', repo_name=self.db_repo_name,
340 340 pull_request_id=pull_request_id))
341 341
342 342 version = self.request.GET.get('version')
343 343 from_version = self.request.GET.get('from_version') or version
344 344 merge_checks = self.request.GET.get('merge_checks')
345 345 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
346 346 force_refresh = str2bool(self.request.GET.get('force_refresh'))
347 347 c.range_diff_on = self.request.GET.get('range-diff') == "1"
348 348
349 349 # fetch global flags of ignore ws or context lines
350 350 diff_context = diffs.get_diff_context(self.request)
351 351 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
352 352
353 353 (pull_request_latest,
354 354 pull_request_at_ver,
355 355 pull_request_display_obj,
356 356 at_version) = PullRequestModel().get_pr_version(
357 357 pull_request_id, version=version)
358 358
359 359 pr_closed = pull_request_latest.is_closed()
360 360
361 361 if pr_closed and (version or from_version):
362 362 # not allow to browse versions for closed PR
363 363 raise HTTPFound(h.route_path(
364 364 'pullrequest_show', repo_name=self.db_repo_name,
365 365 pull_request_id=pull_request_id))
366 366
367 367 versions = pull_request_display_obj.versions()
368 368
369 369 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
370 370
371 371 # used to store per-commit range diffs
372 372 c.changes = collections.OrderedDict()
373 373
374 374 c.at_version = at_version
375 375 c.at_version_num = (at_version
376 376 if at_version and at_version != PullRequest.LATEST_VER
377 377 else None)
378 378
379 379 c.at_version_index = ChangesetComment.get_index_from_version(
380 380 c.at_version_num, versions)
381 381
382 382 (prev_pull_request_latest,
383 383 prev_pull_request_at_ver,
384 384 prev_pull_request_display_obj,
385 385 prev_at_version) = PullRequestModel().get_pr_version(
386 386 pull_request_id, version=from_version)
387 387
388 388 c.from_version = prev_at_version
389 389 c.from_version_num = (prev_at_version
390 390 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
391 391 else None)
392 392 c.from_version_index = ChangesetComment.get_index_from_version(
393 393 c.from_version_num, versions)
394 394
395 395 # define if we're in COMPARE mode or VIEW at version mode
396 396 compare = at_version != prev_at_version
397 397
398 398 # pull_requests repo_name we opened it against
399 399 # ie. target_repo must match
400 400 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
401 401 log.warning('Mismatch between the current repo: %s, and target %s',
402 402 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
403 403 raise HTTPNotFound()
404 404
405 405 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
406 406
407 407 c.pull_request = pull_request_display_obj
408 408 c.renderer = pull_request_at_ver.description_renderer or c.renderer
409 409 c.pull_request_latest = pull_request_latest
410 410
411 411 # inject latest version
412 412 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
413 413 c.versions = versions + [latest_ver]
414 414
415 415 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
416 416 c.allowed_to_change_status = False
417 417 c.allowed_to_update = False
418 418 c.allowed_to_merge = False
419 419 c.allowed_to_delete = False
420 420 c.allowed_to_comment = False
421 421 c.allowed_to_close = False
422 422 else:
423 423 can_change_status = PullRequestModel().check_user_change_status(
424 424 pull_request_at_ver, self._rhodecode_user)
425 425 c.allowed_to_change_status = can_change_status and not pr_closed
426 426
427 427 c.allowed_to_update = PullRequestModel().check_user_update(
428 428 pull_request_latest, self._rhodecode_user) and not pr_closed
429 429 c.allowed_to_merge = PullRequestModel().check_user_merge(
430 430 pull_request_latest, self._rhodecode_user) and not pr_closed
431 431 c.allowed_to_delete = PullRequestModel().check_user_delete(
432 432 pull_request_latest, self._rhodecode_user) and not pr_closed
433 433 c.allowed_to_comment = not pr_closed
434 434 c.allowed_to_close = c.allowed_to_merge and not pr_closed
435 435
436 436 c.forbid_adding_reviewers = False
437 437
438 438 if pull_request_latest.reviewer_data and \
439 439 'rules' in pull_request_latest.reviewer_data:
440 440 rules = pull_request_latest.reviewer_data['rules'] or {}
441 441 try:
442 442 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
443 443 except Exception:
444 444 pass
445 445
446 446 # check merge capabilities
447 447 _merge_check = MergeCheck.validate(
448 448 pull_request_latest, auth_user=self._rhodecode_user,
449 449 translator=self.request.translate,
450 450 force_shadow_repo_refresh=force_refresh)
451 451
452 452 c.pr_merge_errors = _merge_check.error_details
453 453 c.pr_merge_possible = not _merge_check.failed
454 454 c.pr_merge_message = _merge_check.merge_msg
455 455 c.pr_merge_source_commit = _merge_check.source_commit
456 456 c.pr_merge_target_commit = _merge_check.target_commit
457 457
458 458 c.pr_merge_info = MergeCheck.get_merge_conditions(
459 459 pull_request_latest, translator=self.request.translate)
460 460
461 461 c.pull_request_review_status = _merge_check.review_status
462 462 if merge_checks:
463 463 self.request.override_renderer = \
464 464 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
465 465 return self._get_template_context(c)
466 466
467 467 c.reviewers_count = pull_request.reviewers_count
468 468 c.observers_count = pull_request.observers_count
469 469
470 470 # reviewers and statuses
471 471 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
472 472 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
473 473 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
474 474
475 475 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
476 476 member_reviewer = h.reviewer_as_json(
477 477 member, reasons=reasons, mandatory=mandatory,
478 478 role=review_obj.role,
479 479 user_group=review_obj.rule_user_group_data()
480 480 )
481 481
482 482 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
483 483 member_reviewer['review_status'] = current_review_status
484 484 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
485 485 member_reviewer['allowed_to_update'] = c.allowed_to_update
486 486 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
487 487
488 488 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
489 489
490 490 for observer_obj, member in pull_request_at_ver.observers():
491 491 member_observer = h.reviewer_as_json(
492 492 member, reasons=[], mandatory=False,
493 493 role=observer_obj.role,
494 494 user_group=observer_obj.rule_user_group_data()
495 495 )
496 496 member_observer['allowed_to_update'] = c.allowed_to_update
497 497 c.pull_request_set_observers_data_json['observers'].append(member_observer)
498 498
499 499 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
500 500
501 501 general_comments, inline_comments = \
502 502 self.register_comments_vars(c, pull_request_latest, versions)
503 503
504 504 # TODOs
505 505 c.unresolved_comments = CommentsModel() \
506 506 .get_pull_request_unresolved_todos(pull_request_latest)
507 507 c.resolved_comments = CommentsModel() \
508 508 .get_pull_request_resolved_todos(pull_request_latest)
509 509
510 510 # Drafts
511 511 c.draft_comments = CommentsModel().get_pull_request_drafts(
512 512 self._rhodecode_db_user.user_id,
513 513 pull_request_latest)
514 514
515 515 # if we use version, then do not show later comments
516 516 # than current version
517 517 display_inline_comments = collections.defaultdict(
518 518 lambda: collections.defaultdict(list))
519 519 for co in inline_comments:
520 520 if c.at_version_num:
521 521 # pick comments that are at least UPTO given version, so we
522 522 # don't render comments for higher version
523 523 should_render = co.pull_request_version_id and \
524 524 co.pull_request_version_id <= c.at_version_num
525 525 else:
526 526 # showing all, for 'latest'
527 527 should_render = True
528 528
529 529 if should_render:
530 530 display_inline_comments[co.f_path][co.line_no].append(co)
531 531
532 532 # load diff data into template context, if we use compare mode then
533 533 # diff is calculated based on changes between versions of PR
534 534
535 535 source_repo = pull_request_at_ver.source_repo
536 536 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
537 537
538 538 target_repo = pull_request_at_ver.target_repo
539 539 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
540 540
541 541 if compare:
542 542 # in compare switch the diff base to latest commit from prev version
543 543 target_ref_id = prev_pull_request_display_obj.revisions[0]
544 544
545 545 # despite opening commits for bookmarks/branches/tags, we always
546 546 # convert this to rev to prevent changes after bookmark or branch change
547 547 c.source_ref_type = 'rev'
548 548 c.source_ref = source_ref_id
549 549
550 550 c.target_ref_type = 'rev'
551 551 c.target_ref = target_ref_id
552 552
553 553 c.source_repo = source_repo
554 554 c.target_repo = target_repo
555 555
556 556 c.commit_ranges = []
557 557 source_commit = EmptyCommit()
558 558 target_commit = EmptyCommit()
559 559 c.missing_requirements = False
560 560
561 561 source_scm = source_repo.scm_instance()
562 562 target_scm = target_repo.scm_instance()
563 563
564 564 shadow_scm = None
565 565 try:
566 566 shadow_scm = pull_request_latest.get_shadow_repo()
567 567 except Exception:
568 568 log.debug('Failed to get shadow repo', exc_info=True)
569 569 # try first the existing source_repo, and then shadow
570 570 # repo if we can obtain one
571 571 commits_source_repo = source_scm
572 572 if shadow_scm:
573 573 commits_source_repo = shadow_scm
574 574
575 575 c.commits_source_repo = commits_source_repo
576 576 c.ancestor = None # set it to None, to hide it from PR view
577 577
578 578 # empty version means latest, so we keep this to prevent
579 579 # double caching
580 580 version_normalized = version or PullRequest.LATEST_VER
581 581 from_version_normalized = from_version or PullRequest.LATEST_VER
582 582
583 583 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
584 584 cache_file_path = diff_cache_exist(
585 585 cache_path, 'pull_request', pull_request_id, version_normalized,
586 586 from_version_normalized, source_ref_id, target_ref_id,
587 587 hide_whitespace_changes, diff_context, c.fulldiff)
588 588
589 589 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
590 590 force_recache = self.get_recache_flag()
591 591
592 592 cached_diff = None
593 593 if caching_enabled:
594 594 cached_diff = load_cached_diff(cache_file_path)
595 595
596 596 has_proper_commit_cache = (
597 597 cached_diff and cached_diff.get('commits')
598 598 and len(cached_diff.get('commits', [])) == 5
599 599 and cached_diff.get('commits')[0]
600 600 and cached_diff.get('commits')[3])
601 601
602 602 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
603 603 diff_commit_cache = \
604 604 (ancestor_commit, commit_cache, missing_requirements,
605 605 source_commit, target_commit) = cached_diff['commits']
606 606 else:
607 607 # NOTE(marcink): we reach potentially unreachable errors when a PR has
608 608 # merge errors resulting in potentially hidden commits in the shadow repo.
609 609 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
610 610 and _merge_check.merge_response
611 611 maybe_unreachable = maybe_unreachable \
612 612 and _merge_check.merge_response.metadata.get('unresolved_files')
613 613 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
614 614 diff_commit_cache = \
615 615 (ancestor_commit, commit_cache, missing_requirements,
616 616 source_commit, target_commit) = self.get_commits(
617 617 commits_source_repo,
618 618 pull_request_at_ver,
619 619 source_commit,
620 620 source_ref_id,
621 621 source_scm,
622 622 target_commit,
623 623 target_ref_id,
624 624 target_scm,
625 625 maybe_unreachable=maybe_unreachable)
626 626
627 627 # register our commit range
628 628 for comm in commit_cache.values():
629 629 c.commit_ranges.append(comm)
630 630
631 631 c.missing_requirements = missing_requirements
632 632 c.ancestor_commit = ancestor_commit
633 633 c.statuses = source_repo.statuses(
634 634 [x.raw_id for x in c.commit_ranges])
635 635
636 636 # auto collapse if we have more than limit
637 637 collapse_limit = diffs.DiffProcessor._collapse_commits_over
638 638 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
639 639 c.compare_mode = compare
640 640
641 641 # diff_limit is the old behavior, will cut off the whole diff
642 642 # if the limit is applied otherwise will just hide the
643 643 # big files from the front-end
644 644 diff_limit = c.visual.cut_off_limit_diff
645 645 file_limit = c.visual.cut_off_limit_file
646 646
647 647 c.missing_commits = False
648 648 if (c.missing_requirements
649 649 or isinstance(source_commit, EmptyCommit)
650 650 or source_commit == target_commit):
651 651
652 652 c.missing_commits = True
653 653 else:
654 654 c.inline_comments = display_inline_comments
655 655
656 656 use_ancestor = True
657 657 if from_version_normalized != version_normalized:
658 658 use_ancestor = False
659 659
660 660 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
661 661 if not force_recache and has_proper_diff_cache:
662 662 c.diffset = cached_diff['diff']
663 663 else:
664 664 try:
665 665 c.diffset = self._get_diffset(
666 666 c.source_repo.repo_name, commits_source_repo,
667 667 c.ancestor_commit,
668 668 source_ref_id, target_ref_id,
669 669 target_commit, source_commit,
670 670 diff_limit, file_limit, c.fulldiff,
671 671 hide_whitespace_changes, diff_context,
672 672 use_ancestor=use_ancestor
673 673 )
674 674
675 675 # save cached diff
676 676 if caching_enabled:
677 677 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
678 678 except CommitDoesNotExistError:
679 679 log.exception('Failed to generate diffset')
680 680 c.missing_commits = True
681 681
682 682 if not c.missing_commits:
683 683
684 684 c.limited_diff = c.diffset.limited_diff
685 685
686 686 # calculate removed files that are bound to comments
687 687 comment_deleted_files = [
688 688 fname for fname in display_inline_comments
689 689 if fname not in c.diffset.file_stats]
690 690
691 691 c.deleted_files_comments = collections.defaultdict(dict)
692 692 for fname, per_line_comments in display_inline_comments.items():
693 693 if fname in comment_deleted_files:
694 694 c.deleted_files_comments[fname]['stats'] = 0
695 695 c.deleted_files_comments[fname]['comments'] = list()
696 696 for lno, comments in per_line_comments.items():
697 697 c.deleted_files_comments[fname]['comments'].extend(comments)
698 698
699 699 # maybe calculate the range diff
700 700 if c.range_diff_on:
701 701 # TODO(marcink): set whitespace/context
702 702 context_lcl = 3
703 703 ign_whitespace_lcl = False
704 704
705 705 for commit in c.commit_ranges:
706 706 commit2 = commit
707 707 commit1 = commit.first_parent
708 708
709 709 range_diff_cache_file_path = diff_cache_exist(
710 710 cache_path, 'diff', commit.raw_id,
711 711 ign_whitespace_lcl, context_lcl, c.fulldiff)
712 712
713 713 cached_diff = None
714 714 if caching_enabled:
715 715 cached_diff = load_cached_diff(range_diff_cache_file_path)
716 716
717 717 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
718 718 if not force_recache and has_proper_diff_cache:
719 719 diffset = cached_diff['diff']
720 720 else:
721 721 diffset = self._get_range_diffset(
722 722 commits_source_repo, source_repo,
723 723 commit1, commit2, diff_limit, file_limit,
724 724 c.fulldiff, ign_whitespace_lcl, context_lcl
725 725 )
726 726
727 727 # save cached diff
728 728 if caching_enabled:
729 729 cache_diff(range_diff_cache_file_path, diffset, None)
730 730
731 731 c.changes[commit.raw_id] = diffset
732 732
733 733 # this is a hack to properly display links, when creating PR, the
734 734 # compare view and others uses different notation, and
735 735 # compare_commits.mako renders links based on the target_repo.
736 736 # We need to swap that here to generate it properly on the html side
737 737 c.target_repo = c.source_repo
738 738
739 739 c.commit_statuses = ChangesetStatus.STATUSES
740 740
741 741 c.show_version_changes = not pr_closed
742 742 if c.show_version_changes:
743 743 cur_obj = pull_request_at_ver
744 744 prev_obj = prev_pull_request_at_ver
745 745
746 746 old_commit_ids = prev_obj.revisions
747 747 new_commit_ids = cur_obj.revisions
748 748 commit_changes = PullRequestModel()._calculate_commit_id_changes(
749 749 old_commit_ids, new_commit_ids)
750 750 c.commit_changes_summary = commit_changes
751 751
752 752 # calculate the diff for commits between versions
753 753 c.commit_changes = []
754 754
755 755 def mark(cs, fw):
756 756 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
757 757
758 758 for c_type, raw_id in mark(commit_changes.added, 'a') \
759 759 + mark(commit_changes.removed, 'r') \
760 760 + mark(commit_changes.common, 'c'):
761 761
762 762 if raw_id in commit_cache:
763 763 commit = commit_cache[raw_id]
764 764 else:
765 765 try:
766 766 commit = commits_source_repo.get_commit(raw_id)
767 767 except CommitDoesNotExistError:
768 768 # in case we fail extracting still use "dummy" commit
769 769 # for display in commit diff
770 770 commit = h.AttributeDict(
771 771 {'raw_id': raw_id,
772 772 'message': 'EMPTY or MISSING COMMIT'})
773 773 c.commit_changes.append([c_type, commit])
774 774
775 775 # current user review statuses for each version
776 776 c.review_versions = {}
777 777 is_reviewer = PullRequestModel().is_user_reviewer(
778 778 pull_request, self._rhodecode_user)
779 779 if is_reviewer:
780 780 for co in general_comments:
781 781 if co.author.user_id == self._rhodecode_user.user_id:
782 782 status = co.status_change
783 783 if status:
784 784 _ver_pr = status[0].comment.pull_request_version_id
785 785 c.review_versions[_ver_pr] = status[0]
786 786
787 787 return self._get_template_context(c)
788 788
789 789 def get_commits(
790 790 self, commits_source_repo, pull_request_at_ver, source_commit,
791 791 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
792 792 maybe_unreachable=False):
793 793
794 794 commit_cache = collections.OrderedDict()
795 795 missing_requirements = False
796 796
797 797 try:
798 798 pre_load = ["author", "date", "message", "branch", "parents"]
799 799
800 800 pull_request_commits = pull_request_at_ver.revisions
801 801 log.debug('Loading %s commits from %s',
802 802 len(pull_request_commits), commits_source_repo)
803 803
804 804 for rev in pull_request_commits:
805 805 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
806 806 maybe_unreachable=maybe_unreachable)
807 807 commit_cache[comm.raw_id] = comm
808 808
809 809 # Order here matters, we first need to get target, and then
810 810 # the source
811 811 target_commit = commits_source_repo.get_commit(
812 812 commit_id=safe_str(target_ref_id))
813 813
814 814 source_commit = commits_source_repo.get_commit(
815 815 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
816 816 except CommitDoesNotExistError:
817 817 log.warning('Failed to get commit from `{}` repo'.format(
818 818 commits_source_repo), exc_info=True)
819 819 except RepositoryRequirementError:
820 820 log.warning('Failed to get all required data from repo', exc_info=True)
821 821 missing_requirements = True
822 822
823 823 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
824 824
825 825 try:
826 826 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
827 827 except Exception:
828 828 ancestor_commit = None
829 829
830 830 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
831 831
832 832 def assure_not_empty_repo(self):
833 833 _ = self.request.translate
834 834
835 835 try:
836 836 self.db_repo.scm_instance().get_commit()
837 837 except EmptyRepositoryError:
838 838 h.flash(h.literal(_('There are no commits yet')),
839 839 category='warning')
840 840 raise HTTPFound(
841 841 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
842 842
843 843 @LoginRequired()
844 844 @NotAnonymous()
845 845 @HasRepoPermissionAnyDecorator(
846 846 'repository.read', 'repository.write', 'repository.admin')
847 847 def pull_request_new(self):
848 848 _ = self.request.translate
849 849 c = self.load_default_context()
850 850
851 851 self.assure_not_empty_repo()
852 852 source_repo = self.db_repo
853 853
854 854 commit_id = self.request.GET.get('commit')
855 855 branch_ref = self.request.GET.get('branch')
856 856 bookmark_ref = self.request.GET.get('bookmark')
857 857
858 858 try:
859 859 source_repo_data = PullRequestModel().generate_repo_data(
860 860 source_repo, commit_id=commit_id,
861 861 branch=branch_ref, bookmark=bookmark_ref,
862 862 translator=self.request.translate)
863 863 except CommitDoesNotExistError as e:
864 864 log.exception(e)
865 865 h.flash(_('Commit does not exist'), 'error')
866 866 raise HTTPFound(
867 867 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
868 868
869 869 default_target_repo = source_repo
870 870
871 871 if source_repo.parent and c.has_origin_repo_read_perm:
872 872 parent_vcs_obj = source_repo.parent.scm_instance()
873 873 if parent_vcs_obj and not parent_vcs_obj.is_empty():
874 874 # change default if we have a parent repo
875 875 default_target_repo = source_repo.parent
876 876
877 877 target_repo_data = PullRequestModel().generate_repo_data(
878 878 default_target_repo, translator=self.request.translate)
879 879
880 880 selected_source_ref = source_repo_data['refs']['selected_ref']
881 881 title_source_ref = ''
882 882 if selected_source_ref:
883 883 title_source_ref = selected_source_ref.split(':', 2)[1]
884 884 c.default_title = PullRequestModel().generate_pullrequest_title(
885 885 source=source_repo.repo_name,
886 886 source_ref=title_source_ref,
887 887 target=default_target_repo.repo_name
888 888 )
889 889
890 890 c.default_repo_data = {
891 891 'source_repo_name': source_repo.repo_name,
892 892 'source_refs_json': ext_json.str_json(source_repo_data),
893 893 'target_repo_name': default_target_repo.repo_name,
894 894 'target_refs_json': ext_json.str_json(target_repo_data),
895 895 }
896 896 c.default_source_ref = selected_source_ref
897 897
898 898 return self._get_template_context(c)
899 899
900 900 @LoginRequired()
901 901 @NotAnonymous()
902 902 @HasRepoPermissionAnyDecorator(
903 903 'repository.read', 'repository.write', 'repository.admin')
904 904 def pull_request_repo_refs(self):
905 905 self.load_default_context()
906 906 target_repo_name = self.request.matchdict['target_repo_name']
907 907 repo = Repository.get_by_repo_name(target_repo_name)
908 908 if not repo:
909 909 raise HTTPNotFound()
910 910
911 911 target_perm = HasRepoPermissionAny(
912 912 'repository.read', 'repository.write', 'repository.admin')(
913 913 target_repo_name)
914 914 if not target_perm:
915 915 raise HTTPNotFound()
916 916
917 917 return PullRequestModel().generate_repo_data(
918 918 repo, translator=self.request.translate)
919 919
920 920 @LoginRequired()
921 921 @NotAnonymous()
922 922 @HasRepoPermissionAnyDecorator(
923 923 'repository.read', 'repository.write', 'repository.admin')
924 924 def pullrequest_repo_targets(self):
925 925 _ = self.request.translate
926 926 filter_query = self.request.GET.get('query')
927 927
928 928 # get the parents
929 929 parent_target_repos = []
930 930 if self.db_repo.parent:
931 931 parents_query = Repository.query() \
932 932 .order_by(func.length(Repository.repo_name)) \
933 933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934 934
935 935 if filter_query:
936 936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 937 parents_query = parents_query.filter(
938 938 Repository.repo_name.ilike(ilike_expression))
939 939 parents = parents_query.limit(20).all()
940 940
941 941 for parent in parents:
942 942 parent_vcs_obj = parent.scm_instance()
943 943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 944 parent_target_repos.append(parent)
945 945
946 946 # get other forks, and repo itself
947 947 query = Repository.query() \
948 948 .order_by(func.length(Repository.repo_name)) \
949 949 .filter(
950 950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 952 ) \
953 953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954 954
955 955 if filter_query:
956 956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
957 957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958 958
959 959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 960 target_repos = query.limit(limit).all()
961 961
962 962 all_target_repos = target_repos + parent_target_repos
963 963
964 964 repos = []
965 965 # This checks permissions to the repositories
966 966 for obj in ScmModel().get_repos(all_target_repos):
967 967 repos.append({
968 968 'id': obj['name'],
969 969 'text': obj['name'],
970 970 'type': 'repo',
971 971 'repo_id': obj['dbrepo']['repo_id'],
972 972 'repo_type': obj['dbrepo']['repo_type'],
973 973 'private': obj['dbrepo']['private'],
974 974
975 975 })
976 976
977 977 data = {
978 978 'more': False,
979 979 'results': [{
980 980 'text': _('Repositories'),
981 981 'children': repos
982 982 }] if repos else []
983 983 }
984 984 return data
985 985
986 986 @classmethod
987 987 def get_comment_ids(cls, post_data):
988 988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
989 989
990 990 @LoginRequired()
991 991 @NotAnonymous()
992 992 @HasRepoPermissionAnyDecorator(
993 993 'repository.read', 'repository.write', 'repository.admin')
994 994 def pullrequest_comments(self):
995 995 self.load_default_context()
996 996
997 997 pull_request = PullRequest.get_or_404(
998 998 self.request.matchdict['pull_request_id'])
999 999 pull_request_id = pull_request.pull_request_id
1000 1000 version = self.request.GET.get('version')
1001 1001
1002 1002 _render = self.request.get_partial_renderer(
1003 1003 'rhodecode:templates/base/sidebar.mako')
1004 1004 c = _render.get_call_context()
1005 1005
1006 1006 (pull_request_latest,
1007 1007 pull_request_at_ver,
1008 1008 pull_request_display_obj,
1009 1009 at_version) = PullRequestModel().get_pr_version(
1010 1010 pull_request_id, version=version)
1011 1011 versions = pull_request_display_obj.versions()
1012 1012 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1013 1013 c.versions = versions + [latest_ver]
1014 1014
1015 1015 c.at_version = at_version
1016 1016 c.at_version_num = (at_version
1017 1017 if at_version and at_version != PullRequest.LATEST_VER
1018 1018 else None)
1019 1019
1020 1020 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1021 1021 all_comments = c.inline_comments_flat + c.comments
1022 1022
1023 1023 existing_ids = self.get_comment_ids(self.request.POST)
1024 1024 return _render('comments_table', all_comments, len(all_comments),
1025 1025 existing_ids=existing_ids)
1026 1026
1027 1027 @LoginRequired()
1028 1028 @NotAnonymous()
1029 1029 @HasRepoPermissionAnyDecorator(
1030 1030 'repository.read', 'repository.write', 'repository.admin')
1031 1031 def pullrequest_todos(self):
1032 1032 self.load_default_context()
1033 1033
1034 1034 pull_request = PullRequest.get_or_404(
1035 1035 self.request.matchdict['pull_request_id'])
1036 1036 pull_request_id = pull_request.pull_request_id
1037 1037 version = self.request.GET.get('version')
1038 1038
1039 1039 _render = self.request.get_partial_renderer(
1040 1040 'rhodecode:templates/base/sidebar.mako')
1041 1041 c = _render.get_call_context()
1042 1042 (pull_request_latest,
1043 1043 pull_request_at_ver,
1044 1044 pull_request_display_obj,
1045 1045 at_version) = PullRequestModel().get_pr_version(
1046 1046 pull_request_id, version=version)
1047 1047 versions = pull_request_display_obj.versions()
1048 1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1049 1049 c.versions = versions + [latest_ver]
1050 1050
1051 1051 c.at_version = at_version
1052 1052 c.at_version_num = (at_version
1053 1053 if at_version and at_version != PullRequest.LATEST_VER
1054 1054 else None)
1055 1055
1056 1056 c.unresolved_comments = CommentsModel() \
1057 1057 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1058 1058 c.resolved_comments = CommentsModel() \
1059 1059 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1060 1060
1061 1061 all_comments = c.unresolved_comments + c.resolved_comments
1062 1062 existing_ids = self.get_comment_ids(self.request.POST)
1063 1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1064 1064 todo_comments=True, existing_ids=existing_ids)
1065 1065
1066 1066 @LoginRequired()
1067 1067 @NotAnonymous()
1068 1068 @HasRepoPermissionAnyDecorator(
1069 1069 'repository.read', 'repository.write', 'repository.admin')
1070 1070 def pullrequest_drafts(self):
1071 1071 self.load_default_context()
1072 1072
1073 1073 pull_request = PullRequest.get_or_404(
1074 1074 self.request.matchdict['pull_request_id'])
1075 1075 pull_request_id = pull_request.pull_request_id
1076 1076 version = self.request.GET.get('version')
1077 1077
1078 1078 _render = self.request.get_partial_renderer(
1079 1079 'rhodecode:templates/base/sidebar.mako')
1080 1080 c = _render.get_call_context()
1081 1081
1082 1082 (pull_request_latest,
1083 1083 pull_request_at_ver,
1084 1084 pull_request_display_obj,
1085 1085 at_version) = PullRequestModel().get_pr_version(
1086 1086 pull_request_id, version=version)
1087 1087 versions = pull_request_display_obj.versions()
1088 1088 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1089 1089 c.versions = versions + [latest_ver]
1090 1090
1091 1091 c.at_version = at_version
1092 1092 c.at_version_num = (at_version
1093 1093 if at_version and at_version != PullRequest.LATEST_VER
1094 1094 else None)
1095 1095
1096 1096 c.draft_comments = CommentsModel() \
1097 1097 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1098 1098
1099 1099 all_comments = c.draft_comments
1100 1100
1101 1101 existing_ids = self.get_comment_ids(self.request.POST)
1102 1102 return _render('comments_table', all_comments, len(all_comments),
1103 1103 existing_ids=existing_ids, draft_comments=True)
1104 1104
1105 1105 @LoginRequired()
1106 1106 @NotAnonymous()
1107 1107 @HasRepoPermissionAnyDecorator(
1108 1108 'repository.read', 'repository.write', 'repository.admin')
1109 1109 @CSRFRequired()
1110 1110 def pull_request_create(self):
1111 1111 _ = self.request.translate
1112 1112 self.assure_not_empty_repo()
1113 1113 self.load_default_context()
1114 1114
1115 1115 controls = peppercorn.parse(self.request.POST.items())
1116 1116
1117 1117 try:
1118 1118 form = PullRequestForm(
1119 1119 self.request.translate, self.db_repo.repo_id)()
1120 1120 _form = form.to_python(controls)
1121 1121 except formencode.Invalid as errors:
1122 1122 if errors.error_dict.get('revisions'):
1123 1123 msg = 'Revisions: %s' % errors.error_dict['revisions']
1124 1124 elif errors.error_dict.get('pullrequest_title'):
1125 1125 msg = errors.error_dict.get('pullrequest_title')
1126 1126 else:
1127 1127 msg = _('Error creating pull request: {}').format(errors)
1128 1128 log.exception(msg)
1129 1129 h.flash(msg, 'error')
1130 1130
1131 1131 # would rather just go back to form ...
1132 1132 raise HTTPFound(
1133 1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1134 1134
1135 1135 source_repo = _form['source_repo']
1136 1136 source_ref = _form['source_ref']
1137 1137 target_repo = _form['target_repo']
1138 1138 target_ref = _form['target_ref']
1139 1139 commit_ids = _form['revisions'][::-1]
1140 1140 common_ancestor_id = _form['common_ancestor']
1141 1141
1142 1142 # find the ancestor for this pr
1143 1143 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1144 1144 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1145 1145
1146 1146 if not (source_db_repo or target_db_repo):
1147 1147 h.flash(_('source_repo or target repo not found'), category='error')
1148 1148 raise HTTPFound(
1149 1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1150 1150
1151 1151 # re-check permissions again here
1152 1152 # source_repo we must have read permissions
1153 1153
1154 1154 source_perm = HasRepoPermissionAny(
1155 1155 'repository.read', 'repository.write', 'repository.admin')(
1156 1156 source_db_repo.repo_name)
1157 1157 if not source_perm:
1158 1158 msg = _('Not Enough permissions to source repo `{}`.'.format(
1159 1159 source_db_repo.repo_name))
1160 1160 h.flash(msg, category='error')
1161 1161 # copy the args back to redirect
1162 1162 org_query = self.request.GET.mixed()
1163 1163 raise HTTPFound(
1164 1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1165 1165 _query=org_query))
1166 1166
1167 1167 # target repo we must have read permissions, and also later on
1168 1168 # we want to check branch permissions here
1169 1169 target_perm = HasRepoPermissionAny(
1170 1170 'repository.read', 'repository.write', 'repository.admin')(
1171 1171 target_db_repo.repo_name)
1172 1172 if not target_perm:
1173 1173 msg = _('Not Enough permissions to target repo `{}`.'.format(
1174 1174 target_db_repo.repo_name))
1175 1175 h.flash(msg, category='error')
1176 1176 # copy the args back to redirect
1177 1177 org_query = self.request.GET.mixed()
1178 1178 raise HTTPFound(
1179 1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1180 1180 _query=org_query))
1181 1181
1182 1182 source_scm = source_db_repo.scm_instance()
1183 1183 target_scm = target_db_repo.scm_instance()
1184 1184
1185 1185 source_ref_obj = unicode_to_reference(source_ref)
1186 1186 target_ref_obj = unicode_to_reference(target_ref)
1187 1187
1188 1188 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1189 1189 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1190 1190
1191 1191 ancestor = source_scm.get_common_ancestor(
1192 1192 source_commit.raw_id, target_commit.raw_id, target_scm)
1193 1193
1194 1194 # recalculate target ref based on ancestor
1195 1195 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1196 1196
1197 1197 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1198 1198 PullRequestModel().get_reviewer_functions()
1199 1199
1200 1200 # recalculate reviewers logic, to make sure we can validate this
1201 1201 reviewer_rules = get_default_reviewers_data(
1202 1202 self._rhodecode_db_user,
1203 1203 source_db_repo,
1204 1204 source_ref_obj,
1205 1205 target_db_repo,
1206 1206 target_ref_obj,
1207 1207 include_diff_info=False)
1208 1208
1209 1209 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1210 1210 observers = validate_observers(_form['observer_members'], reviewer_rules)
1211 1211
1212 1212 pullrequest_title = _form['pullrequest_title']
1213 1213 title_source_ref = source_ref_obj.name
1214 1214 if not pullrequest_title:
1215 1215 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1216 1216 source=source_repo,
1217 1217 source_ref=title_source_ref,
1218 1218 target=target_repo
1219 1219 )
1220 1220
1221 1221 description = _form['pullrequest_desc']
1222 1222 description_renderer = _form['description_renderer']
1223 1223
1224 1224 try:
1225 1225 pull_request = PullRequestModel().create(
1226 1226 created_by=self._rhodecode_user.user_id,
1227 1227 source_repo=source_repo,
1228 1228 source_ref=source_ref,
1229 1229 target_repo=target_repo,
1230 1230 target_ref=target_ref,
1231 1231 revisions=commit_ids,
1232 1232 common_ancestor_id=common_ancestor_id,
1233 1233 reviewers=reviewers,
1234 1234 observers=observers,
1235 1235 title=pullrequest_title,
1236 1236 description=description,
1237 1237 description_renderer=description_renderer,
1238 1238 reviewer_data=reviewer_rules,
1239 1239 auth_user=self._rhodecode_user
1240 1240 )
1241 1241 Session().commit()
1242 1242
1243 1243 h.flash(_('Successfully opened new pull request'),
1244 1244 category='success')
1245 1245 except Exception:
1246 1246 msg = _('Error occurred during creation of this pull request.')
1247 1247 log.exception(msg)
1248 1248 h.flash(msg, category='error')
1249 1249
1250 1250 # copy the args back to redirect
1251 1251 org_query = self.request.GET.mixed()
1252 1252 raise HTTPFound(
1253 1253 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1254 1254 _query=org_query))
1255 1255
1256 1256 raise HTTPFound(
1257 1257 h.route_path('pullrequest_show', repo_name=target_repo,
1258 1258 pull_request_id=pull_request.pull_request_id))
1259 1259
1260 1260 @LoginRequired()
1261 1261 @NotAnonymous()
1262 1262 @HasRepoPermissionAnyDecorator(
1263 1263 'repository.read', 'repository.write', 'repository.admin')
1264 1264 @CSRFRequired()
1265 1265 def pull_request_update(self):
1266 1266 pull_request = PullRequest.get_or_404(
1267 1267 self.request.matchdict['pull_request_id'])
1268 1268 _ = self.request.translate
1269 1269
1270 1270 c = self.load_default_context()
1271 1271 redirect_url = None
1272 1272 # we do this check as first, because we want to know ASAP in the flow that
1273 1273 # pr is updating currently
1274 1274 is_state_changing = pull_request.is_state_changing()
1275 1275
1276 1276 if pull_request.is_closed():
1277 1277 log.debug('update: forbidden because pull request is closed')
1278 1278 msg = _(u'Cannot update closed pull requests.')
1279 1279 h.flash(msg, category='error')
1280 1280 return {'response': True,
1281 1281 'redirect_url': redirect_url}
1282 1282
1283 1283 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1284 1284
1285 1285 # only owner or admin can update it
1286 1286 allowed_to_update = PullRequestModel().check_user_update(
1287 1287 pull_request, self._rhodecode_user)
1288 1288
1289 1289 if allowed_to_update:
1290 1290 controls = peppercorn.parse(self.request.POST.items())
1291 1291 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1292 1292 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1293 1293
1294 1294 if 'review_members' in controls:
1295 1295 self._update_reviewers(
1296 1296 c,
1297 1297 pull_request, controls['review_members'],
1298 1298 pull_request.reviewer_data,
1299 1299 PullRequestReviewers.ROLE_REVIEWER)
1300 1300 elif 'observer_members' in controls:
1301 1301 self._update_reviewers(
1302 1302 c,
1303 1303 pull_request, controls['observer_members'],
1304 1304 pull_request.reviewer_data,
1305 1305 PullRequestReviewers.ROLE_OBSERVER)
1306 1306 elif do_update_commits:
1307 1307 if is_state_changing:
1308 1308 log.debug('commits update: forbidden because pull request is in state %s',
1309 1309 pull_request.pull_request_state)
1310 1310 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1311 1311 u'Current state is: `{}`').format(
1312 1312 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1313 1313 h.flash(msg, category='error')
1314 1314 return {'response': True,
1315 1315 'redirect_url': redirect_url}
1316 1316
1317 1317 self._update_commits(c, pull_request)
1318 1318 if force_refresh:
1319 1319 redirect_url = h.route_path(
1320 1320 'pullrequest_show', repo_name=self.db_repo_name,
1321 1321 pull_request_id=pull_request.pull_request_id,
1322 1322 _query={"force_refresh": 1})
1323 1323 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1324 1324 self._edit_pull_request(pull_request)
1325 1325 else:
1326 1326 log.error('Unhandled update data.')
1327 1327 raise HTTPBadRequest()
1328 1328
1329 1329 return {'response': True,
1330 1330 'redirect_url': redirect_url}
1331 1331 raise HTTPForbidden()
1332 1332
1333 1333 def _edit_pull_request(self, pull_request):
1334 1334 """
1335 1335 Edit title and description
1336 1336 """
1337 1337 _ = self.request.translate
1338 1338
1339 1339 try:
1340 1340 PullRequestModel().edit(
1341 1341 pull_request,
1342 1342 self.request.POST.get('title'),
1343 1343 self.request.POST.get('description'),
1344 1344 self.request.POST.get('description_renderer'),
1345 1345 self._rhodecode_user)
1346 1346 except ValueError:
1347 1347 msg = _(u'Cannot update closed pull requests.')
1348 1348 h.flash(msg, category='error')
1349 1349 return
1350 1350 else:
1351 1351 Session().commit()
1352 1352
1353 1353 msg = _(u'Pull request title & description updated.')
1354 1354 h.flash(msg, category='success')
1355 1355 return
1356 1356
1357 1357 def _update_commits(self, c, pull_request):
1358 1358 _ = self.request.translate
1359 1359 log.debug('pull-request: running update commits actions')
1360 1360
1361 1361 @retry(exception=Exception, n_tries=3, delay=2)
1362 1362 def commits_update():
1363 1363 return PullRequestModel().update_commits(
1364 1364 pull_request, self._rhodecode_db_user)
1365 1365
1366 1366 with pull_request.set_state(PullRequest.STATE_UPDATING):
1367 1367 resp = commits_update() # retry x3
1368 1368
1369 1369 if resp.executed:
1370 1370
1371 1371 if resp.target_changed and resp.source_changed:
1372 1372 changed = 'target and source repositories'
1373 1373 elif resp.target_changed and not resp.source_changed:
1374 1374 changed = 'target repository'
1375 1375 elif not resp.target_changed and resp.source_changed:
1376 1376 changed = 'source repository'
1377 1377 else:
1378 1378 changed = 'nothing'
1379 1379
1380 1380 msg = _(u'Pull request updated to "{source_commit_id}" with '
1381 1381 u'{count_added} added, {count_removed} removed commits. '
1382 1382 u'Source of changes: {change_source}.')
1383 1383 msg = msg.format(
1384 1384 source_commit_id=pull_request.source_ref_parts.commit_id,
1385 1385 count_added=len(resp.changes.added),
1386 1386 count_removed=len(resp.changes.removed),
1387 1387 change_source=changed)
1388 1388 h.flash(msg, category='success')
1389 1389 channelstream.pr_update_channelstream_push(
1390 1390 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1391 1391 else:
1392 1392 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1393 1393 warning_reasons = [
1394 1394 UpdateFailureReason.NO_CHANGE,
1395 1395 UpdateFailureReason.WRONG_REF_TYPE,
1396 1396 ]
1397 1397 category = 'warning' if resp.reason in warning_reasons else 'error'
1398 1398 h.flash(msg, category=category)
1399 1399
1400 1400 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1401 1401 _ = self.request.translate
1402 1402
1403 1403 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1404 1404 PullRequestModel().get_reviewer_functions()
1405 1405
1406 1406 if role == PullRequestReviewers.ROLE_REVIEWER:
1407 1407 try:
1408 1408 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1409 1409 except ValueError as e:
1410 1410 log.error('Reviewers Validation: {}'.format(e))
1411 1411 h.flash(e, category='error')
1412 1412 return
1413 1413
1414 1414 old_calculated_status = pull_request.calculated_review_status()
1415 1415 PullRequestModel().update_reviewers(
1416 1416 pull_request, reviewers, self._rhodecode_db_user)
1417 1417
1418 1418 Session().commit()
1419 1419
1420 1420 msg = _('Pull request reviewers updated.')
1421 1421 h.flash(msg, category='success')
1422 1422 channelstream.pr_update_channelstream_push(
1423 1423 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1424 1424
1425 1425 # trigger status changed if change in reviewers changes the status
1426 1426 calculated_status = pull_request.calculated_review_status()
1427 1427 if old_calculated_status != calculated_status:
1428 1428 PullRequestModel().trigger_pull_request_hook(
1429 1429 pull_request, self._rhodecode_user, 'review_status_change',
1430 1430 data={'status': calculated_status})
1431 1431
1432 1432 elif role == PullRequestReviewers.ROLE_OBSERVER:
1433 1433 try:
1434 1434 observers = validate_observers(review_members, reviewer_rules)
1435 1435 except ValueError as e:
1436 1436 log.error('Observers Validation: {}'.format(e))
1437 1437 h.flash(e, category='error')
1438 1438 return
1439 1439
1440 1440 PullRequestModel().update_observers(
1441 1441 pull_request, observers, self._rhodecode_db_user)
1442 1442
1443 1443 Session().commit()
1444 1444 msg = _('Pull request observers updated.')
1445 1445 h.flash(msg, category='success')
1446 1446 channelstream.pr_update_channelstream_push(
1447 1447 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1448 1448
1449 1449 @LoginRequired()
1450 1450 @NotAnonymous()
1451 1451 @HasRepoPermissionAnyDecorator(
1452 1452 'repository.read', 'repository.write', 'repository.admin')
1453 1453 @CSRFRequired()
1454 1454 def pull_request_merge(self):
1455 1455 """
1456 1456 Merge will perform a server-side merge of the specified
1457 1457 pull request, if the pull request is approved and mergeable.
1458 1458 After successful merging, the pull request is automatically
1459 1459 closed, with a relevant comment.
1460 1460 """
1461 1461 pull_request = PullRequest.get_or_404(
1462 1462 self.request.matchdict['pull_request_id'])
1463 1463 _ = self.request.translate
1464 1464
1465 1465 if pull_request.is_state_changing():
1466 1466 log.debug('show: forbidden because pull request is in state %s',
1467 1467 pull_request.pull_request_state)
1468 1468 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1469 1469 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1470 1470 pull_request.pull_request_state)
1471 1471 h.flash(msg, category='error')
1472 1472 raise HTTPFound(
1473 1473 h.route_path('pullrequest_show',
1474 1474 repo_name=pull_request.target_repo.repo_name,
1475 1475 pull_request_id=pull_request.pull_request_id))
1476 1476
1477 1477 self.load_default_context()
1478 1478
1479 1479 with pull_request.set_state(PullRequest.STATE_UPDATING):
1480 1480 check = MergeCheck.validate(
1481 1481 pull_request, auth_user=self._rhodecode_user,
1482 1482 translator=self.request.translate)
1483 1483 merge_possible = not check.failed
1484 1484
1485 1485 for err_type, error_msg in check.errors:
1486 1486 h.flash(error_msg, category=err_type)
1487 1487
1488 1488 if merge_possible:
1489 1489 log.debug("Pre-conditions checked, trying to merge.")
1490 1490 extras = vcs_operation_context(
1491 1491 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1492 1492 username=self._rhodecode_db_user.username, action='push',
1493 1493 scm=pull_request.target_repo.repo_type)
1494 1494 with pull_request.set_state(PullRequest.STATE_UPDATING):
1495 1495 self._merge_pull_request(
1496 1496 pull_request, self._rhodecode_db_user, extras)
1497 1497 else:
1498 1498 log.debug("Pre-conditions failed, NOT merging.")
1499 1499
1500 1500 raise HTTPFound(
1501 1501 h.route_path('pullrequest_show',
1502 1502 repo_name=pull_request.target_repo.repo_name,
1503 1503 pull_request_id=pull_request.pull_request_id))
1504 1504
1505 1505 def _merge_pull_request(self, pull_request, user, extras):
1506 1506 _ = self.request.translate
1507 1507 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1508 1508
1509 1509 if merge_resp.executed:
1510 1510 log.debug("The merge was successful, closing the pull request.")
1511 1511 PullRequestModel().close_pull_request(
1512 1512 pull_request.pull_request_id, user)
1513 1513 Session().commit()
1514 1514 msg = _('Pull request was successfully merged and closed.')
1515 1515 h.flash(msg, category='success')
1516 1516 else:
1517 1517 log.debug(
1518 1518 "The merge was not successful. Merge response: %s", merge_resp)
1519 1519 msg = merge_resp.merge_status_message
1520 1520 h.flash(msg, category='error')
1521 1521
1522 1522 @LoginRequired()
1523 1523 @NotAnonymous()
1524 1524 @HasRepoPermissionAnyDecorator(
1525 1525 'repository.read', 'repository.write', 'repository.admin')
1526 1526 @CSRFRequired()
1527 1527 def pull_request_delete(self):
1528 1528 _ = self.request.translate
1529 1529
1530 1530 pull_request = PullRequest.get_or_404(
1531 1531 self.request.matchdict['pull_request_id'])
1532 1532 self.load_default_context()
1533 1533
1534 1534 pr_closed = pull_request.is_closed()
1535 1535 allowed_to_delete = PullRequestModel().check_user_delete(
1536 1536 pull_request, self._rhodecode_user) and not pr_closed
1537 1537
1538 1538 # only owner can delete it !
1539 1539 if allowed_to_delete:
1540 1540 PullRequestModel().delete(pull_request, self._rhodecode_user)
1541 1541 Session().commit()
1542 1542 h.flash(_('Successfully deleted pull request'),
1543 1543 category='success')
1544 1544 raise HTTPFound(h.route_path('pullrequest_show_all',
1545 1545 repo_name=self.db_repo_name))
1546 1546
1547 1547 log.warning('user %s tried to delete pull request without access',
1548 1548 self._rhodecode_user)
1549 1549 raise HTTPNotFound()
1550 1550
1551 1551 def _pull_request_comments_create(self, pull_request, comments):
1552 1552 _ = self.request.translate
1553 1553 data = {}
1554 1554 if not comments:
1555 1555 return
1556 1556 pull_request_id = pull_request.pull_request_id
1557 1557
1558 1558 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1559 1559
1560 1560 for entry in comments:
1561 1561 c = self.load_default_context()
1562 1562 comment_type = entry['comment_type']
1563 1563 text = entry['text']
1564 1564 status = entry['status']
1565 1565 is_draft = str2bool(entry['is_draft'])
1566 1566 resolves_comment_id = entry['resolves_comment_id']
1567 1567 close_pull_request = entry['close_pull_request']
1568 1568 f_path = entry['f_path']
1569 1569 line_no = entry['line']
1570 1570 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1571 1571
1572 1572 # the logic here should work like following, if we submit close
1573 1573 # pr comment, use `close_pull_request_with_comment` function
1574 1574 # else handle regular comment logic
1575 1575
1576 1576 if close_pull_request:
1577 1577 # only owner or admin or person with write permissions
1578 1578 allowed_to_close = PullRequestModel().check_user_update(
1579 1579 pull_request, self._rhodecode_user)
1580 1580 if not allowed_to_close:
1581 1581 log.debug('comment: forbidden because not allowed to close '
1582 1582 'pull request %s', pull_request_id)
1583 1583 raise HTTPForbidden()
1584 1584
1585 1585 # This also triggers `review_status_change`
1586 1586 comment, status = PullRequestModel().close_pull_request_with_comment(
1587 1587 pull_request, self._rhodecode_user, self.db_repo, message=text,
1588 1588 auth_user=self._rhodecode_user)
1589 1589 Session().flush()
1590 1590 is_inline = comment.is_inline
1591 1591
1592 1592 PullRequestModel().trigger_pull_request_hook(
1593 1593 pull_request, self._rhodecode_user, 'comment',
1594 1594 data={'comment': comment})
1595 1595
1596 1596 else:
1597 1597 # regular comment case, could be inline, or one with status.
1598 1598 # for that one we check also permissions
1599 1599 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1600 1600 allowed_to_change_status = PullRequestModel().check_user_change_status(
1601 1601 pull_request, self._rhodecode_user) and not is_draft
1602 1602
1603 1603 if status and allowed_to_change_status:
1604 1604 message = (_('Status change %(transition_icon)s %(status)s')
1605 1605 % {'transition_icon': '>',
1606 1606 'status': ChangesetStatus.get_status_lbl(status)})
1607 1607 text = text or message
1608 1608
1609 1609 comment = CommentsModel().create(
1610 1610 text=text,
1611 1611 repo=self.db_repo.repo_id,
1612 1612 user=self._rhodecode_user.user_id,
1613 1613 pull_request=pull_request,
1614 1614 f_path=f_path,
1615 1615 line_no=line_no,
1616 1616 status_change=(ChangesetStatus.get_status_lbl(status)
1617 1617 if status and allowed_to_change_status else None),
1618 1618 status_change_type=(status
1619 1619 if status and allowed_to_change_status else None),
1620 1620 comment_type=comment_type,
1621 1621 is_draft=is_draft,
1622 1622 resolves_comment_id=resolves_comment_id,
1623 1623 auth_user=self._rhodecode_user,
1624 1624 send_email=not is_draft, # skip notification for draft comments
1625 1625 )
1626 1626 is_inline = comment.is_inline
1627 1627
1628 1628 if allowed_to_change_status:
1629 1629 # calculate old status before we change it
1630 1630 old_calculated_status = pull_request.calculated_review_status()
1631 1631
1632 1632 # get status if set !
1633 1633 if status:
1634 1634 ChangesetStatusModel().set_status(
1635 1635 self.db_repo.repo_id,
1636 1636 status,
1637 1637 self._rhodecode_user.user_id,
1638 1638 comment,
1639 1639 pull_request=pull_request
1640 1640 )
1641 1641
1642 1642 Session().flush()
1643 1643 # this is somehow required to get access to some relationship
1644 1644 # loaded on comment
1645 1645 Session().refresh(comment)
1646 1646
1647 1647 # skip notifications for drafts
1648 1648 if not is_draft:
1649 1649 PullRequestModel().trigger_pull_request_hook(
1650 1650 pull_request, self._rhodecode_user, 'comment',
1651 1651 data={'comment': comment})
1652 1652
1653 1653 # we now calculate the status of pull request, and based on that
1654 1654 # calculation we set the commits status
1655 1655 calculated_status = pull_request.calculated_review_status()
1656 1656 if old_calculated_status != calculated_status:
1657 1657 PullRequestModel().trigger_pull_request_hook(
1658 1658 pull_request, self._rhodecode_user, 'review_status_change',
1659 1659 data={'status': calculated_status})
1660 1660
1661 1661 comment_id = comment.comment_id
1662 1662 data[comment_id] = {
1663 1663 'target_id': target_elem_id
1664 1664 }
1665 1665 Session().flush()
1666 1666
1667 1667 c.co = comment
1668 1668 c.at_version_num = None
1669 1669 c.is_new = True
1670 1670 rendered_comment = render(
1671 1671 'rhodecode:templates/changeset/changeset_comment_block.mako',
1672 1672 self._get_template_context(c), self.request)
1673 1673
1674 1674 data[comment_id].update(comment.get_dict())
1675 1675 data[comment_id].update({'rendered_text': rendered_comment})
1676 1676
1677 1677 Session().commit()
1678 1678
1679 1679 # skip channelstream for draft comments
1680 1680 if not all_drafts:
1681 1681 comment_broadcast_channel = channelstream.comment_channel(
1682 1682 self.db_repo_name, pull_request_obj=pull_request)
1683 1683
1684 1684 comment_data = data
1685 1685 posted_comment_type = 'inline' if is_inline else 'general'
1686 1686 if len(data) == 1:
1687 1687 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1688 1688 else:
1689 1689 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1690 1690
1691 1691 channelstream.comment_channelstream_push(
1692 1692 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1693 1693 comment_data=comment_data)
1694 1694
1695 1695 return data
1696 1696
1697 1697 @LoginRequired()
1698 1698 @NotAnonymous()
1699 1699 @HasRepoPermissionAnyDecorator(
1700 1700 'repository.read', 'repository.write', 'repository.admin')
1701 1701 @CSRFRequired()
1702 1702 def pull_request_comment_create(self):
1703 1703 _ = self.request.translate
1704 1704
1705 1705 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1706 1706
1707 1707 if pull_request.is_closed():
1708 1708 log.debug('comment: forbidden because pull request is closed')
1709 1709 raise HTTPForbidden()
1710 1710
1711 1711 allowed_to_comment = PullRequestModel().check_user_comment(
1712 1712 pull_request, self._rhodecode_user)
1713 1713 if not allowed_to_comment:
1714 1714 log.debug('comment: forbidden because pull request is from forbidden repo')
1715 1715 raise HTTPForbidden()
1716 1716
1717 1717 comment_data = {
1718 1718 'comment_type': self.request.POST.get('comment_type'),
1719 1719 'text': self.request.POST.get('text'),
1720 1720 'status': self.request.POST.get('changeset_status', None),
1721 1721 'is_draft': self.request.POST.get('draft'),
1722 1722 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1723 1723 'close_pull_request': self.request.POST.get('close_pull_request'),
1724 1724 'f_path': self.request.POST.get('f_path'),
1725 1725 'line': self.request.POST.get('line'),
1726 1726 }
1727 1727 data = self._pull_request_comments_create(pull_request, [comment_data])
1728 1728
1729 1729 return data
1730 1730
1731 1731 @LoginRequired()
1732 1732 @NotAnonymous()
1733 1733 @HasRepoPermissionAnyDecorator(
1734 1734 'repository.read', 'repository.write', 'repository.admin')
1735 1735 @CSRFRequired()
1736 1736 def pull_request_comment_delete(self):
1737 1737 pull_request = PullRequest.get_or_404(
1738 1738 self.request.matchdict['pull_request_id'])
1739 1739
1740 1740 comment = ChangesetComment.get_or_404(
1741 1741 self.request.matchdict['comment_id'])
1742 1742 comment_id = comment.comment_id
1743 1743
1744 1744 if comment.immutable:
1745 1745 # don't allow deleting comments that are immutable
1746 1746 raise HTTPForbidden()
1747 1747
1748 1748 if pull_request.is_closed():
1749 1749 log.debug('comment: forbidden because pull request is closed')
1750 1750 raise HTTPForbidden()
1751 1751
1752 1752 if not comment:
1753 1753 log.debug('Comment with id:%s not found, skipping', comment_id)
1754 1754 # comment already deleted in another call probably
1755 1755 return True
1756 1756
1757 1757 if comment.pull_request.is_closed():
1758 1758 # don't allow deleting comments on closed pull request
1759 1759 raise HTTPForbidden()
1760 1760
1761 1761 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1762 1762 super_admin = h.HasPermissionAny('hg.admin')()
1763 1763 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1764 1764 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1765 1765 comment_repo_admin = is_repo_admin and is_repo_comment
1766 1766
1767 1767 if comment.draft and not comment_owner:
1768 1768 # We never allow to delete draft comments for other than owners
1769 1769 raise HTTPNotFound()
1770 1770
1771 1771 if super_admin or comment_owner or comment_repo_admin:
1772 1772 old_calculated_status = comment.pull_request.calculated_review_status()
1773 1773 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1774 1774 Session().commit()
1775 1775 calculated_status = comment.pull_request.calculated_review_status()
1776 1776 if old_calculated_status != calculated_status:
1777 1777 PullRequestModel().trigger_pull_request_hook(
1778 1778 comment.pull_request, self._rhodecode_user, 'review_status_change',
1779 1779 data={'status': calculated_status})
1780 1780 return True
1781 1781 else:
1782 1782 log.warning('No permissions for user %s to delete comment_id: %s',
1783 1783 self._rhodecode_db_user, comment_id)
1784 1784 raise HTTPNotFound()
1785 1785
1786 1786 @LoginRequired()
1787 1787 @NotAnonymous()
1788 1788 @HasRepoPermissionAnyDecorator(
1789 1789 'repository.read', 'repository.write', 'repository.admin')
1790 1790 @CSRFRequired()
1791 1791 def pull_request_comment_edit(self):
1792 1792 self.load_default_context()
1793 1793
1794 1794 pull_request = PullRequest.get_or_404(
1795 1795 self.request.matchdict['pull_request_id']
1796 1796 )
1797 1797 comment = ChangesetComment.get_or_404(
1798 1798 self.request.matchdict['comment_id']
1799 1799 )
1800 1800 comment_id = comment.comment_id
1801 1801
1802 1802 if comment.immutable:
1803 1803 # don't allow deleting comments that are immutable
1804 1804 raise HTTPForbidden()
1805 1805
1806 1806 if pull_request.is_closed():
1807 1807 log.debug('comment: forbidden because pull request is closed')
1808 1808 raise HTTPForbidden()
1809 1809
1810 1810 if comment.pull_request.is_closed():
1811 1811 # don't allow deleting comments on closed pull request
1812 1812 raise HTTPForbidden()
1813 1813
1814 1814 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1815 1815 super_admin = h.HasPermissionAny('hg.admin')()
1816 1816 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1817 1817 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1818 1818 comment_repo_admin = is_repo_admin and is_repo_comment
1819 1819
1820 1820 if super_admin or comment_owner or comment_repo_admin:
1821 1821 text = self.request.POST.get('text')
1822 1822 version = self.request.POST.get('version')
1823 1823 if text == comment.text:
1824 1824 log.warning(
1825 1825 'Comment(PR): '
1826 1826 'Trying to create new version '
1827 1827 'with the same comment body {}'.format(
1828 1828 comment_id,
1829 1829 )
1830 1830 )
1831 1831 raise HTTPNotFound()
1832 1832
1833 1833 if version.isdigit():
1834 1834 version = int(version)
1835 1835 else:
1836 1836 log.warning(
1837 1837 'Comment(PR): Wrong version type {} {} '
1838 1838 'for comment {}'.format(
1839 1839 version,
1840 1840 type(version),
1841 1841 comment_id,
1842 1842 )
1843 1843 )
1844 1844 raise HTTPNotFound()
1845 1845
1846 1846 try:
1847 1847 comment_history = CommentsModel().edit(
1848 1848 comment_id=comment_id,
1849 1849 text=text,
1850 1850 auth_user=self._rhodecode_user,
1851 1851 version=version,
1852 1852 )
1853 1853 except CommentVersionMismatch:
1854 1854 raise HTTPConflict()
1855 1855
1856 1856 if not comment_history:
1857 1857 raise HTTPNotFound()
1858 1858
1859 1859 Session().commit()
1860 1860 if not comment.draft:
1861 1861 PullRequestModel().trigger_pull_request_hook(
1862 1862 pull_request, self._rhodecode_user, 'comment_edit',
1863 1863 data={'comment': comment})
1864 1864
1865 1865 return {
1866 1866 'comment_history_id': comment_history.comment_history_id,
1867 1867 'comment_id': comment.comment_id,
1868 1868 'comment_version': comment_history.version,
1869 1869 'comment_author_username': comment_history.author.username,
1870 1870 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1871 1871 'comment_created_on': h.age_component(comment_history.created_on,
1872 1872 time_is_local=True),
1873 1873 }
1874 1874 else:
1875 1875 log.warning('No permissions for user %s to edit comment_id: %s',
1876 1876 self._rhodecode_db_user, comment_id)
1877 1877 raise HTTPNotFound()
@@ -1,83 +1,83 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24
25 25 from rhodecode.apps._base import RepoAppView
26 26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 28 from rhodecode.lib.vcs.backends.base import Reference
29 29 from rhodecode.model.db import Repository
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class RepoReviewRulesView(RepoAppView):
35 35 def load_default_context(self):
36 36 c = self._get_local_tmpl_context()
37 37 return c
38 38
39 39 @LoginRequired()
40 40 @HasRepoPermissionAnyDecorator('repository.admin')
41 41 def repo_review_rules(self):
42 42 c = self.load_default_context()
43 43 c.active = 'reviewers'
44 44
45 45 return self._get_template_context(c)
46 46
47 47 @LoginRequired()
48 48 @HasRepoPermissionAnyDecorator(
49 49 'repository.read', 'repository.write', 'repository.admin')
50 50 def repo_default_reviewers_data(self):
51 51 self.load_default_context()
52 52
53 53 request = self.request
54 54 source_repo = self.db_repo
55 55 source_repo_name = source_repo.repo_name
56 56 target_repo_name = request.GET.get('target_repo', source_repo_name)
57 57 target_repo = Repository.get_by_repo_name(target_repo_name)
58 58
59 59 current_user = request.user.get_instance()
60 60
61 61 source_commit_id = request.GET['source_ref']
62 62 source_type = request.GET['source_ref_type']
63 63 source_name = request.GET['source_ref_name']
64 64
65 65 target_commit_id = request.GET['target_ref']
66 66 target_type = request.GET['target_ref_type']
67 67 target_name = request.GET['target_ref_name']
68 68
69 69 try:
70 70 review_data = get_default_reviewers_data(
71 71 current_user,
72 72 source_repo,
73 73 Reference(source_type, source_name, source_commit_id),
74 74 target_repo,
75 75 Reference(target_type, target_name, target_commit_id)
76 76 )
77 77 except ValueError:
78 78 # No common ancestor
79 79 msg = "No Common ancestor found between target and source reference"
80 80 log.exception(msg)
81 81 return {'diff_info': {'error': msg}}
82 82
83 83 return review_data
@@ -1,251 +1,251 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import deform
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode import events
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.forms import RcForm
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 33 from rhodecode.model.db import RepositoryField, RepoGroup, Repository, User
34 34 from rhodecode.model.meta import Session
35 35 from rhodecode.model.permission import PermissionModel
36 36 from rhodecode.model.repo import RepoModel
37 37 from rhodecode.model.scm import RepoGroupList, ScmModel
38 38 from rhodecode.model.validation_schema.schemas import repo_schema
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class RepoSettingsView(RepoAppView):
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47
48 48 acl_groups = RepoGroupList(
49 49 RepoGroup.query().all(),
50 50 perm_set=['group.write', 'group.admin'])
51 51 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
52 52 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
53 53
54 54 # in case someone no longer have a group.write access to a repository
55 55 # pre fill the list with this entry, we don't care if this is the same
56 56 # but it will allow saving repo data properly.
57 57 repo_group = self.db_repo.group
58 58 if repo_group and repo_group.group_id not in c.repo_groups_choices:
59 59 c.repo_groups_choices.append(repo_group.group_id)
60 60 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
61 61
62 62 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
63 63 # we might be in missing requirement state, so we load things
64 64 # without touching scm_instance()
65 65 c.landing_revs_choices, c.landing_revs = \
66 66 ScmModel().get_repo_landing_revs(self.request.translate)
67 67 else:
68 68 c.landing_revs_choices, c.landing_revs = \
69 69 ScmModel().get_repo_landing_revs(
70 70 self.request.translate, self.db_repo)
71 71
72 72 c.personal_repo_group = c.auth_user.personal_repo_group
73 73 c.repo_fields = RepositoryField.query()\
74 74 .filter(RepositoryField.repository == self.db_repo).all()
75 75 return c
76 76
77 77 def _get_schema(self, c, old_values=None):
78 78 return repo_schema.RepoSettingsSchema().bind(
79 79 repo_type=self.db_repo.repo_type,
80 80 repo_type_options=[self.db_repo.repo_type],
81 81 repo_ref_options=c.landing_revs_choices,
82 82 repo_ref_items=c.landing_revs,
83 83 repo_repo_group_options=c.repo_groups_choices,
84 84 repo_repo_group_items=c.repo_groups,
85 85 # user caller
86 86 user=self._rhodecode_user,
87 87 old_values=old_values
88 88 )
89 89
90 90 @LoginRequired()
91 91 @HasRepoPermissionAnyDecorator('repository.admin')
92 92 def edit_settings(self):
93 93 c = self.load_default_context()
94 94 c.active = 'settings'
95 95
96 96 defaults = RepoModel()._get_defaults(self.db_repo_name)
97 97 defaults['repo_owner'] = defaults['user']
98 98 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
99 99
100 100 schema = self._get_schema(c)
101 101 c.form = RcForm(schema, appstruct=defaults)
102 102 return self._get_template_context(c)
103 103
104 104 @LoginRequired()
105 105 @HasRepoPermissionAnyDecorator('repository.admin')
106 106 @CSRFRequired()
107 107 def edit_settings_update(self):
108 108 _ = self.request.translate
109 109 c = self.load_default_context()
110 110 c.active = 'settings'
111 111 old_repo_name = self.db_repo_name
112 112
113 113 old_values = self.db_repo.get_api_data()
114 114 schema = self._get_schema(c, old_values=old_values)
115 115
116 116 c.form = RcForm(schema)
117 117 pstruct = self.request.POST.items()
118 118 pstruct.append(('repo_type', self.db_repo.repo_type))
119 119 try:
120 120 schema_data = c.form.validate(pstruct)
121 121 except deform.ValidationFailure as err_form:
122 122 return self._get_template_context(c)
123 123
124 124 # data is now VALID, proceed with updates
125 125 # save validated data back into the updates dict
126 126 validated_updates = dict(
127 127 repo_name=schema_data['repo_group']['repo_name_without_group'],
128 128 repo_group=schema_data['repo_group']['repo_group_id'],
129 129
130 130 user=schema_data['repo_owner'],
131 131 repo_description=schema_data['repo_description'],
132 132 repo_private=schema_data['repo_private'],
133 133 clone_uri=schema_data['repo_clone_uri'],
134 134 push_uri=schema_data['repo_push_uri'],
135 135 repo_landing_rev=schema_data['repo_landing_commit_ref'],
136 136 repo_enable_statistics=schema_data['repo_enable_statistics'],
137 137 repo_enable_locking=schema_data['repo_enable_locking'],
138 138 repo_enable_downloads=schema_data['repo_enable_downloads'],
139 139 )
140 140 # detect if SYNC URI changed, if we get OLD means we keep old values
141 141 if schema_data['repo_clone_uri_change'] == 'OLD':
142 142 validated_updates['clone_uri'] = self.db_repo.clone_uri
143 143
144 144 if schema_data['repo_push_uri_change'] == 'OLD':
145 145 validated_updates['push_uri'] = self.db_repo.push_uri
146 146
147 147 # use the new full name for redirect
148 148 new_repo_name = schema_data['repo_group']['repo_name_with_group']
149 149
150 150 # save extra fields into our validated data
151 151 for key, value in pstruct:
152 152 if key.startswith(RepositoryField.PREFIX):
153 153 validated_updates[key] = value
154 154
155 155 try:
156 156 RepoModel().update(self.db_repo, **validated_updates)
157 157 ScmModel().mark_for_invalidation(new_repo_name)
158 158
159 159 audit_logger.store_web(
160 160 'repo.edit', action_data={'old_data': old_values},
161 161 user=self._rhodecode_user, repo=self.db_repo)
162 162
163 163 Session().commit()
164 164
165 165 h.flash(_('Repository `{}` updated successfully').format(old_repo_name),
166 166 category='success')
167 167 except Exception:
168 168 log.exception("Exception during update of repository")
169 169 h.flash(_('Error occurred during update of repository {}').format(
170 170 old_repo_name), category='error')
171 171
172 172 name_changed = old_repo_name != new_repo_name
173 173 if name_changed:
174 174 current_perms = self.db_repo.permissions(expand_from_user_groups=True)
175 175 affected_user_ids = [perm['user_id'] for perm in current_perms]
176 176
177 177 # NOTE(marcink): also add owner maybe it has changed
178 178 owner = User.get_by_username(schema_data['repo_owner'])
179 179 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
180 180 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
181 181 PermissionModel().trigger_permission_flush(affected_user_ids)
182 182
183 183 raise HTTPFound(
184 184 h.route_path('edit_repo', repo_name=new_repo_name))
185 185
186 186 @LoginRequired()
187 187 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
188 188 def toggle_locking(self):
189 189 """
190 190 Toggle locking of repository by simple GET call to url
191 191 """
192 192 _ = self.request.translate
193 193 repo = self.db_repo
194 194
195 195 try:
196 196 if repo.enable_locking:
197 197 if repo.locked[0]:
198 198 Repository.unlock(repo)
199 199 action = _('Unlocked')
200 200 else:
201 201 Repository.lock(
202 202 repo, self._rhodecode_user.user_id,
203 203 lock_reason=Repository.LOCK_WEB)
204 204 action = _('Locked')
205 205
206 206 h.flash(_('Repository has been %s') % action,
207 207 category='success')
208 208 except Exception:
209 209 log.exception("Exception during unlocking")
210 210 h.flash(_('An error occurred during unlocking'),
211 211 category='error')
212 212 raise HTTPFound(
213 213 h.route_path('repo_summary', repo_name=self.db_repo_name))
214 214
215 215 @LoginRequired()
216 216 @HasRepoPermissionAnyDecorator('repository.admin')
217 217 def edit_statistics_form(self):
218 218 c = self.load_default_context()
219 219
220 220 if self.db_repo.stats:
221 221 # this is on what revision we ended up so we add +1 for count
222 222 last_rev = self.db_repo.stats.stat_on_revision + 1
223 223 else:
224 224 last_rev = 0
225 225
226 226 c.active = 'statistics'
227 227 c.stats_revision = last_rev
228 228 c.repo_last_rev = self.rhodecode_vcs_repo.count()
229 229
230 230 if last_rev == 0 or c.repo_last_rev == 0:
231 231 c.stats_percentage = 0
232 232 else:
233 233 c.stats_percentage = '%.2f' % (
234 234 (float((last_rev)) / c.repo_last_rev) * 100)
235 235 return self._get_template_context(c)
236 236
237 237 @LoginRequired()
238 238 @HasRepoPermissionAnyDecorator('repository.admin')
239 239 @CSRFRequired()
240 240 def repo_statistics_reset(self):
241 241 _ = self.request.translate
242 242
243 243 try:
244 244 RepoModel().delete_stats(self.db_repo_name)
245 245 Session().commit()
246 246 except Exception:
247 247 log.exception('Edit statistics failure')
248 248 h.flash(_('An error occurred during deletion of repository stats'),
249 249 category='error')
250 250 raise HTTPFound(
251 251 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
@@ -1,304 +1,304 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25 from packaging.version import Version
26 26
27 27 from rhodecode import events
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired,
33 33 HasRepoPermissionAny)
34 34 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
35 35 from rhodecode.lib.utils2 import safe_int
36 36 from rhodecode.lib.vcs import RepositoryError
37 37 from rhodecode.model.db import Session, UserFollowing, User, Repository
38 38 from rhodecode.model.permission import PermissionModel
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.scm import ScmModel
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class RepoSettingsAdvancedView(RepoAppView):
46 46
47 47 def load_default_context(self):
48 48 c = self._get_local_tmpl_context()
49 49 return c
50 50
51 51 def _get_users_with_permissions(self):
52 52 user_permissions = {}
53 53 for perm in self.db_repo.permissions():
54 54 user_permissions[perm.user_id] = perm
55 55
56 56 return user_permissions
57 57
58 58 @LoginRequired()
59 59 @HasRepoPermissionAnyDecorator('repository.admin')
60 60 def edit_advanced(self):
61 61 _ = self.request.translate
62 62 c = self.load_default_context()
63 63 c.active = 'advanced'
64 64
65 65 c.default_user_id = User.get_default_user_id()
66 66 c.in_public_journal = UserFollowing.query() \
67 67 .filter(UserFollowing.user_id == c.default_user_id) \
68 68 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
69 69
70 70 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
71 71 c.hooks_outdated = False
72 72
73 73 try:
74 74 if Version(c.ver_info_dict['pre_version']) < Version(c.rhodecode_version):
75 75 c.hooks_outdated = True
76 76 except Exception:
77 77 pass
78 78
79 79 # update commit cache if GET flag is present
80 80 if self.request.GET.get('update_commit_cache'):
81 81 self.db_repo.update_commit_cache()
82 82 h.flash(_('updated commit cache'), category='success')
83 83
84 84 return self._get_template_context(c)
85 85
86 86 @LoginRequired()
87 87 @HasRepoPermissionAnyDecorator('repository.admin')
88 88 @CSRFRequired()
89 89 def edit_advanced_archive(self):
90 90 """
91 91 Archives the repository. It will become read-only, and not visible in search
92 92 or other queries. But still visible for super-admins.
93 93 """
94 94
95 95 _ = self.request.translate
96 96
97 97 try:
98 98 old_data = self.db_repo.get_api_data()
99 99 RepoModel().archive(self.db_repo)
100 100
101 101 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
102 102 audit_logger.store_web(
103 103 'repo.archive', action_data={'old_data': old_data},
104 104 user=self._rhodecode_user, repo=repo)
105 105
106 106 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
107 107 h.flash(
108 108 _('Archived repository `%s`') % self.db_repo_name,
109 109 category='success')
110 110 Session().commit()
111 111 except Exception:
112 112 log.exception("Exception during archiving of repository")
113 113 h.flash(_('An error occurred during archiving of `%s`')
114 114 % self.db_repo_name, category='error')
115 115 # redirect to advanced for more deletion options
116 116 raise HTTPFound(
117 117 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
118 118 _anchor='advanced-archive'))
119 119
120 120 # flush permissions for all users defined in permissions
121 121 affected_user_ids = self._get_users_with_permissions().keys()
122 122 PermissionModel().trigger_permission_flush(affected_user_ids)
123 123
124 124 raise HTTPFound(h.route_path('home'))
125 125
126 126 @LoginRequired()
127 127 @HasRepoPermissionAnyDecorator('repository.admin')
128 128 @CSRFRequired()
129 129 def edit_advanced_delete(self):
130 130 """
131 131 Deletes the repository, or shows warnings if deletion is not possible
132 132 because of attached forks or other errors.
133 133 """
134 134 _ = self.request.translate
135 135 handle_forks = self.request.POST.get('forks', None)
136 136 if handle_forks == 'detach_forks':
137 137 handle_forks = 'detach'
138 138 elif handle_forks == 'delete_forks':
139 139 handle_forks = 'delete'
140 140
141 141 try:
142 142 old_data = self.db_repo.get_api_data()
143 143 RepoModel().delete(self.db_repo, forks=handle_forks)
144 144
145 145 _forks = self.db_repo.forks.count()
146 146 if _forks and handle_forks:
147 147 if handle_forks == 'detach_forks':
148 148 h.flash(_('Detached %s forks') % _forks, category='success')
149 149 elif handle_forks == 'delete_forks':
150 150 h.flash(_('Deleted %s forks') % _forks, category='success')
151 151
152 152 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
153 153 audit_logger.store_web(
154 154 'repo.delete', action_data={'old_data': old_data},
155 155 user=self._rhodecode_user, repo=repo)
156 156
157 157 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
158 158 h.flash(
159 159 _('Deleted repository `%s`') % self.db_repo_name,
160 160 category='success')
161 161 Session().commit()
162 162 except AttachedForksError:
163 163 repo_advanced_url = h.route_path(
164 164 'edit_repo_advanced', repo_name=self.db_repo_name,
165 165 _anchor='advanced-delete')
166 166 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
167 167 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
168 168 'Try using {delete_or_detach} option.')
169 169 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
170 170 category='warning')
171 171
172 172 # redirect to advanced for forks handle action ?
173 173 raise HTTPFound(repo_advanced_url)
174 174
175 175 except AttachedPullRequestsError:
176 176 repo_advanced_url = h.route_path(
177 177 'edit_repo_advanced', repo_name=self.db_repo_name,
178 178 _anchor='advanced-delete')
179 179 attached_prs = len(self.db_repo.pull_requests_source +
180 180 self.db_repo.pull_requests_target)
181 181 h.flash(
182 182 _('Cannot delete `{repo}` it still contains {num} attached pull requests. '
183 183 'Consider archiving the repository instead.').format(
184 184 repo=self.db_repo_name, num=attached_prs), category='warning')
185 185
186 186 # redirect to advanced for forks handle action ?
187 187 raise HTTPFound(repo_advanced_url)
188 188
189 189 except Exception:
190 190 log.exception("Exception during deletion of repository")
191 191 h.flash(_('An error occurred during deletion of `%s`')
192 192 % self.db_repo_name, category='error')
193 193 # redirect to advanced for more deletion options
194 194 raise HTTPFound(
195 195 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
196 196 _anchor='advanced-delete'))
197 197
198 198 raise HTTPFound(h.route_path('home'))
199 199
200 200 @LoginRequired()
201 201 @HasRepoPermissionAnyDecorator('repository.admin')
202 202 @CSRFRequired()
203 203 def edit_advanced_journal(self):
204 204 """
205 205 Set's this repository to be visible in public journal,
206 206 in other words making default user to follow this repo
207 207 """
208 208 _ = self.request.translate
209 209
210 210 try:
211 211 user_id = User.get_default_user_id()
212 212 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
213 213 h.flash(_('Updated repository visibility in public journal'),
214 214 category='success')
215 215 Session().commit()
216 216 except Exception:
217 217 h.flash(_('An error occurred during setting this '
218 218 'repository in public journal'),
219 219 category='error')
220 220
221 221 raise HTTPFound(
222 222 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
223 223
224 224 @LoginRequired()
225 225 @HasRepoPermissionAnyDecorator('repository.admin')
226 226 @CSRFRequired()
227 227 def edit_advanced_fork(self):
228 228 """
229 229 Mark given repository as a fork of another
230 230 """
231 231 _ = self.request.translate
232 232
233 233 new_fork_id = safe_int(self.request.POST.get('id_fork_of'))
234 234
235 235 # valid repo, re-check permissions
236 236 if new_fork_id:
237 237 repo = Repository.get(new_fork_id)
238 238 # ensure we have at least read access to the repo we mark
239 239 perm_check = HasRepoPermissionAny(
240 240 'repository.read', 'repository.write', 'repository.admin')
241 241
242 242 if repo and perm_check(repo_name=repo.repo_name):
243 243 new_fork_id = repo.repo_id
244 244 else:
245 245 new_fork_id = None
246 246
247 247 try:
248 248 repo = ScmModel().mark_as_fork(
249 249 self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
250 250 fork = repo.fork.repo_name if repo.fork else _('Nothing')
251 251 Session().commit()
252 252 h.flash(
253 253 _('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
254 254 category='success')
255 255 except RepositoryError as e:
256 256 log.exception("Repository Error occurred")
257 257 h.flash(str(e), category='error')
258 258 except Exception:
259 259 log.exception("Exception while editing fork")
260 260 h.flash(_('An error occurred during this operation'),
261 261 category='error')
262 262
263 263 raise HTTPFound(
264 264 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
265 265
266 266 @LoginRequired()
267 267 @HasRepoPermissionAnyDecorator('repository.admin')
268 268 @CSRFRequired()
269 269 def edit_advanced_toggle_locking(self):
270 270 """
271 271 Toggle locking of repository
272 272 """
273 273 _ = self.request.translate
274 274 set_lock = self.request.POST.get('set_lock')
275 275 set_unlock = self.request.POST.get('set_unlock')
276 276
277 277 try:
278 278 if set_lock:
279 279 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
280 280 lock_reason=Repository.LOCK_WEB)
281 281 h.flash(_('Locked repository'), category='success')
282 282 elif set_unlock:
283 283 Repository.unlock(self.db_repo)
284 284 h.flash(_('Unlocked repository'), category='success')
285 285 except Exception as e:
286 286 log.exception("Exception during unlocking")
287 287 h.flash(_('An error occurred during unlocking'), category='error')
288 288
289 289 raise HTTPFound(
290 290 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
291 291
292 292 @LoginRequired()
293 293 @HasRepoPermissionAnyDecorator('repository.admin')
294 294 def edit_advanced_install_hooks(self):
295 295 """
296 296 Install Hooks for repository
297 297 """
298 298 _ = self.request.translate
299 299 self.load_default_context()
300 300 self.rhodecode_vcs_repo.install_hooks(force=True)
301 301 h.flash(_('installed updated hooks into this repository'),
302 302 category='success')
303 303 raise HTTPFound(
304 304 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@@ -1,102 +1,102 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.lib import audit_logger
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 33 from rhodecode.model.db import RepositoryField
34 34 from rhodecode.model.forms import RepoFieldForm
35 35 from rhodecode.model.meta import Session
36 36 from rhodecode.model.repo import RepoModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class RepoSettingsFieldsView(RepoAppView):
42 42 def load_default_context(self):
43 43 c = self._get_local_tmpl_context()
44 44
45 45
46 46 return c
47 47
48 48 @LoginRequired()
49 49 @HasRepoPermissionAnyDecorator('repository.admin')
50 50 def repo_field_edit(self):
51 51 c = self.load_default_context()
52 52
53 53 c.active = 'fields'
54 54 c.repo_fields = RepositoryField.query() \
55 55 .filter(RepositoryField.repository == self.db_repo).all()
56 56
57 57 return self._get_template_context(c)
58 58
59 59 @LoginRequired()
60 60 @HasRepoPermissionAnyDecorator('repository.admin')
61 61 @CSRFRequired()
62 62 def repo_field_create(self):
63 63 _ = self.request.translate
64 64
65 65 try:
66 66 form = RepoFieldForm(self.request.translate)()
67 67 form_result = form.to_python(dict(self.request.POST))
68 68 RepoModel().add_repo_field(
69 69 self.db_repo_name,
70 70 form_result['new_field_key'],
71 71 field_type=form_result['new_field_type'],
72 72 field_value=form_result['new_field_value'],
73 73 field_label=form_result['new_field_label'],
74 74 field_desc=form_result['new_field_desc'])
75 75
76 76 Session().commit()
77 77 except Exception as e:
78 78 log.exception("Exception creating field")
79 79 msg = _('An error occurred during creation of field')
80 80 if isinstance(e, formencode.Invalid):
81 81 msg += ". " + e.msg
82 82 h.flash(msg, category='error')
83 83
84 84 raise HTTPFound(
85 85 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
86 86
87 87 @LoginRequired()
88 88 @HasRepoPermissionAnyDecorator('repository.admin')
89 89 @CSRFRequired()
90 90 def repo_field_delete(self):
91 91 _ = self.request.translate
92 92 field = RepositoryField.get_or_404(self.request.matchdict['field_id'])
93 93 try:
94 94 RepoModel().delete_repo_field(self.db_repo_name, field.field_key)
95 95 Session().commit()
96 96 except Exception:
97 97 log.exception('Exception during removal of field')
98 98 msg = _('An error occurred during removal of field')
99 99 h.flash(msg, category='error')
100 100
101 101 raise HTTPFound(
102 102 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
@@ -1,125 +1,125 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 24
25 25 import formencode
26 26
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.lib import audit_logger
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
32 32 from rhodecode.model.forms import IssueTrackerPatternsForm
33 33 from rhodecode.model.meta import Session
34 34 from rhodecode.model.settings import SettingsModel
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class RepoSettingsIssueTrackersView(RepoAppView):
40 40 def load_default_context(self):
41 41 c = self._get_local_tmpl_context()
42 42 return c
43 43
44 44 @LoginRequired()
45 45 @HasRepoPermissionAnyDecorator('repository.admin')
46 46 def repo_issuetracker(self):
47 47 c = self.load_default_context()
48 48 c.active = 'issuetracker'
49 49 c.data = 'data'
50 50
51 51 c.settings_model = self.db_repo_patterns
52 52 c.global_patterns = c.settings_model.get_global_settings()
53 53 c.repo_patterns = c.settings_model.get_repo_settings()
54 54
55 55 return self._get_template_context(c)
56 56
57 57 @LoginRequired()
58 58 @HasRepoPermissionAnyDecorator('repository.admin')
59 59 @CSRFRequired()
60 60 def repo_issuetracker_test(self):
61 61 return h.urlify_commit_message(
62 62 self.request.POST.get('test_text', ''),
63 63 self.db_repo_name)
64 64
65 65 @LoginRequired()
66 66 @HasRepoPermissionAnyDecorator('repository.admin')
67 67 @CSRFRequired()
68 68 def repo_issuetracker_delete(self):
69 69 _ = self.request.translate
70 70 uid = self.request.POST.get('uid')
71 71 repo_settings = self.db_repo_patterns
72 72 try:
73 73 repo_settings.delete_entries(uid)
74 74 except Exception:
75 75 h.flash(_('Error occurred during deleting issue tracker entry'),
76 76 category='error')
77 77 raise HTTPNotFound()
78 78
79 79 SettingsModel().invalidate_settings_cache()
80 80 h.flash(_('Removed issue tracker entry.'), category='success')
81 81
82 82 return {'deleted': uid}
83 83
84 84 def _update_patterns(self, form, repo_settings):
85 85 for uid in form['delete_patterns']:
86 86 repo_settings.delete_entries(uid)
87 87
88 88 for pattern_data in form['patterns']:
89 89 for setting_key, pattern, type_ in pattern_data:
90 90 sett = repo_settings.create_or_update_setting(
91 91 setting_key, pattern.strip(), type_)
92 92 Session().add(sett)
93 93
94 94 Session().commit()
95 95
96 96 @LoginRequired()
97 97 @HasRepoPermissionAnyDecorator('repository.admin')
98 98 @CSRFRequired()
99 99 def repo_issuetracker_update(self):
100 100 _ = self.request.translate
101 101 # Save inheritance
102 102 repo_settings = self.db_repo_patterns
103 103 inherited = (
104 104 self.request.POST.get('inherit_global_issuetracker') == "inherited")
105 105 repo_settings.inherit_global_settings = inherited
106 106 Session().commit()
107 107
108 108 try:
109 109 form = IssueTrackerPatternsForm(self.request.translate)().to_python(self.request.POST)
110 110 except formencode.Invalid as errors:
111 111 log.exception('Failed to add new pattern')
112 112 error = errors
113 113 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
114 114 category='error')
115 115 raise HTTPFound(
116 116 h.route_path('edit_repo_issuetracker',
117 117 repo_name=self.db_repo_name))
118 118
119 119 if form:
120 120 self._update_patterns(form, repo_settings)
121 121
122 122 h.flash(_('Updated issue tracker entries'), category='success')
123 123 raise HTTPFound(
124 124 h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name))
125 125
@@ -1,64 +1,64 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound
24 24
25 25
26 26 from rhodecode.apps._base import RepoAppView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.auth import (
29 29 LoginRequired, CSRFRequired, HasRepoPermissionAnyDecorator)
30 30 from rhodecode.model.scm import ScmModel
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class RepoSettingsRemoteView(RepoAppView):
36 36 def load_default_context(self):
37 37 c = self._get_local_tmpl_context()
38 38 return c
39 39
40 40 @LoginRequired()
41 41 @HasRepoPermissionAnyDecorator('repository.admin')
42 42 def repo_remote_edit_form(self):
43 43 c = self.load_default_context()
44 44 c.active = 'remote'
45 45
46 46 return self._get_template_context(c)
47 47
48 48 @LoginRequired()
49 49 @HasRepoPermissionAnyDecorator('repository.admin')
50 50 @CSRFRequired()
51 51 def repo_remote_pull_changes(self):
52 52 _ = self.request.translate
53 53 self.load_default_context()
54 54
55 55 try:
56 56 ScmModel().pull_changes(
57 57 self.db_repo_name, self._rhodecode_user.username)
58 58 h.flash(_('Pulled from remote location'), category='success')
59 59 except Exception:
60 60 log.exception("Exception during pull from remote")
61 61 h.flash(_('An error occurred during pull from remote location'),
62 62 category='error')
63 63 raise HTTPFound(
64 64 h.route_path('edit_repo_remote', repo_name=self.db_repo_name))
@@ -1,159 +1,159 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25 from pyramid.httpexceptions import HTTPFound, HTTPBadRequest
26 26 from pyramid.response import Response
27 27 from pyramid.renderers import render
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 33 from rhodecode.model.forms import RepoVcsSettingsForm
34 34 from rhodecode.model.meta import Session
35 35 from rhodecode.model.settings import VcsSettingsModel, SettingNotFound
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class RepoSettingsVcsView(RepoAppView):
41 41 def load_default_context(self):
42 42 c = self._get_local_tmpl_context()
43 43
44 44
45 45 return c
46 46
47 47 def _vcs_form_defaults(self, repo_name):
48 48 model = VcsSettingsModel(repo=repo_name)
49 49 global_defaults = model.get_global_settings()
50 50
51 51 repo_defaults = {}
52 52 repo_defaults.update(global_defaults)
53 53 repo_defaults.update(model.get_repo_settings())
54 54
55 55 global_defaults = {
56 56 '{}_inherited'.format(k): global_defaults[k]
57 57 for k in global_defaults}
58 58
59 59 defaults = {
60 60 'inherit_global_settings': model.inherit_global_settings
61 61 }
62 62 defaults.update(global_defaults)
63 63 defaults.update(repo_defaults)
64 64 defaults.update({
65 65 'new_svn_branch': '',
66 66 'new_svn_tag': '',
67 67 })
68 68 return defaults
69 69
70 70 @LoginRequired()
71 71 @HasRepoPermissionAnyDecorator('repository.admin')
72 72 def repo_vcs_settings(self):
73 73 c = self.load_default_context()
74 74 model = VcsSettingsModel(repo=self.db_repo_name)
75 75
76 76 c.active = 'vcs'
77 77 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
78 78 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
79 79 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
80 80 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
81 81
82 82 defaults = self._vcs_form_defaults(self.db_repo_name)
83 83 c.inherit_global_settings = defaults['inherit_global_settings']
84 84
85 85 data = render('rhodecode:templates/admin/repos/repo_edit.mako',
86 86 self._get_template_context(c), self.request)
87 87 html = formencode.htmlfill.render(
88 88 data,
89 89 defaults=defaults,
90 90 encoding="UTF-8",
91 91 force_defaults=False
92 92 )
93 93 return Response(html)
94 94
95 95 @LoginRequired()
96 96 @HasRepoPermissionAnyDecorator('repository.admin')
97 97 @CSRFRequired()
98 98 def repo_settings_vcs_update(self):
99 99 _ = self.request.translate
100 100 c = self.load_default_context()
101 101 c.active = 'vcs'
102 102
103 103 model = VcsSettingsModel(repo=self.db_repo_name)
104 104 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
105 105 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
106 106 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
107 107 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
108 108
109 109 defaults = self._vcs_form_defaults(self.db_repo_name)
110 110 c.inherit_global_settings = defaults['inherit_global_settings']
111 111
112 112 application_form = RepoVcsSettingsForm(self.request.translate, self.db_repo_name)()
113 113 try:
114 114 form_result = application_form.to_python(dict(self.request.POST))
115 115 except formencode.Invalid as errors:
116 116 h.flash(_("Some form inputs contain invalid data."),
117 117 category='error')
118 118
119 119 data = render('rhodecode:templates/admin/repos/repo_edit.mako',
120 120 self._get_template_context(c), self.request)
121 121 html = formencode.htmlfill.render(
122 122 data,
123 123 defaults=errors.value,
124 124 errors=errors.error_dict or {},
125 125 encoding="UTF-8",
126 126 force_defaults=False
127 127 )
128 128 return Response(html)
129 129
130 130 try:
131 131 inherit_global_settings = form_result['inherit_global_settings']
132 132 model.create_or_update_repo_settings(
133 133 form_result, inherit_global_settings=inherit_global_settings)
134 134 Session().commit()
135 135 h.flash(_('Updated VCS settings'), category='success')
136 136 except Exception:
137 137 log.exception("Exception while updating settings")
138 138 h.flash(
139 139 _('Error occurred during updating repository VCS settings'),
140 140 category='error')
141 141
142 142 raise HTTPFound(
143 143 h.route_path('edit_repo_vcs', repo_name=self.db_repo_name))
144 144
145 145 @LoginRequired()
146 146 @HasRepoPermissionAnyDecorator('repository.admin')
147 147 @CSRFRequired()
148 148 def repo_settings_delete_svn_pattern(self):
149 149 self.load_default_context()
150 150 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
151 151 model = VcsSettingsModel(repo=self.db_repo_name)
152 152 try:
153 153 model.delete_repo_svn_pattern(delete_pattern_id)
154 154 except SettingNotFound:
155 155 log.exception('Failed to delete SVN pattern')
156 156 raise HTTPBadRequest()
157 157
158 158 Session().commit()
159 159 return True
@@ -1,104 +1,104 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from rhodecode.apps._base import RepoAppView
25 25 from rhodecode.lib import audit_logger
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.auth import (
28 28 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
29 29 from rhodecode.lib.ext_json import json
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class RepoStripView(RepoAppView):
35 35 def load_default_context(self):
36 36 c = self._get_local_tmpl_context()
37 37
38 38
39 39 return c
40 40
41 41 @LoginRequired()
42 42 @HasRepoPermissionAnyDecorator('repository.admin')
43 43 def strip(self):
44 44 c = self.load_default_context()
45 45 c.active = 'strip'
46 46 c.strip_limit = 10
47 47
48 48 return self._get_template_context(c)
49 49
50 50 @LoginRequired()
51 51 @HasRepoPermissionAnyDecorator('repository.admin')
52 52 @CSRFRequired()
53 53 def strip_check(self):
54 54 from rhodecode.lib.vcs.backends.base import EmptyCommit
55 55 data = {}
56 56 rp = self.request.POST
57 57 for i in range(1, 11):
58 58 chset = 'changeset_id-%d' % (i,)
59 59 check = rp.get(chset)
60 60
61 61 if check:
62 62 data[i] = self.db_repo.get_commit(rp[chset])
63 63 if isinstance(data[i], EmptyCommit):
64 64 data[i] = {'rev': None, 'commit': h.escape(rp[chset])}
65 65 else:
66 66 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch,
67 67 'author': h.escape(data[i].author),
68 68 'comment': h.escape(data[i].message)}
69 69 else:
70 70 break
71 71 return data
72 72
73 73 @LoginRequired()
74 74 @HasRepoPermissionAnyDecorator('repository.admin')
75 75 @CSRFRequired()
76 76 def strip_execute(self):
77 77 from rhodecode.model.scm import ScmModel
78 78
79 79 c = self.load_default_context()
80 80 user = self._rhodecode_user
81 81 rp = self.request.POST
82 82 data = {}
83 83 for idx in rp:
84 84 commit = json.loads(rp[idx])
85 85 # If someone put two times the same branch
86 86 if commit['branch'] in data.keys():
87 87 continue
88 88 try:
89 89 ScmModel().strip(
90 90 repo=self.db_repo,
91 91 commit_id=commit['rev'], branch=commit['branch'])
92 92 log.info('Stripped commit %s from repo `%s` by %s',
93 93 commit['rev'], self.db_repo_name, user)
94 94 data[commit['rev']] = True
95 95
96 96 audit_logger.store_web(
97 97 'repo.commit.strip', action_data={'commit_id': commit['rev']},
98 98 repo=self.db_repo, user=self._rhodecode_user, commit=True)
99 99
100 100 except Exception as e:
101 101 data[commit['rev']] = False
102 102 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s',
103 103 commit['rev'], self.db_repo_name, user, e.message)
104 104 return data
@@ -1,274 +1,274 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import string
23 23 import time
24 24
25 25 import rhodecode
26 26
27 27
28 28
29 29 from rhodecode.lib.view_utils import get_format_ref_id
30 30 from rhodecode.apps._base import RepoAppView
31 31 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
32 32 from rhodecode.lib import helpers as h, rc_cache
33 33 from rhodecode.lib.utils2 import safe_str, safe_int
34 34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 37 from rhodecode.lib.vcs.exceptions import (
38 38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
39 39 from rhodecode.model.db import Statistics, CacheKey, User
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.scm import ScmModel
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class RepoSummaryView(RepoAppView):
47 47
48 48 def load_default_context(self):
49 49 c = self._get_local_tmpl_context(include_app_defaults=True)
50 50 c.rhodecode_repo = None
51 51 if not c.repository_requirements_missing:
52 52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 53 return c
54 54
55 55 def _load_commits_context(self, c):
56 56 p = safe_int(self.request.GET.get('page'), 1)
57 57 size = safe_int(self.request.GET.get('size'), 10)
58 58
59 59 def url_generator(page_num):
60 60 query_params = {
61 61 'page': page_num,
62 62 'size': size
63 63 }
64 64 return h.route_path(
65 65 'repo_summary_commits',
66 66 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
67 67
68 68 pre_load = self.get_commit_preload_attrs()
69 69
70 70 try:
71 71 collection = self.rhodecode_vcs_repo.get_commits(
72 72 pre_load=pre_load, translate_tags=False)
73 73 except EmptyRepositoryError:
74 74 collection = self.rhodecode_vcs_repo
75 75
76 76 c.repo_commits = h.RepoPage(
77 77 collection, page=p, items_per_page=size, url_maker=url_generator)
78 78 page_ids = [x.raw_id for x in c.repo_commits]
79 79 c.comments = self.db_repo.get_comments(page_ids)
80 80 c.statuses = self.db_repo.statuses(page_ids)
81 81
82 82 @LoginRequired()
83 83 @HasRepoPermissionAnyDecorator(
84 84 'repository.read', 'repository.write', 'repository.admin')
85 85 def summary_commits(self):
86 86 c = self.load_default_context()
87 87 self._prepare_and_set_clone_url(c)
88 88 self._load_commits_context(c)
89 89 return self._get_template_context(c)
90 90
91 91 @LoginRequired()
92 92 @HasRepoPermissionAnyDecorator(
93 93 'repository.read', 'repository.write', 'repository.admin')
94 94 def summary(self):
95 95 c = self.load_default_context()
96 96
97 97 # Prepare the clone URL
98 98 self._prepare_and_set_clone_url(c)
99 99
100 100 # If enabled, get statistics data
101 101 c.show_stats = bool(self.db_repo.enable_statistics)
102 102
103 103 stats = Session().query(Statistics) \
104 104 .filter(Statistics.repository == self.db_repo) \
105 105 .scalar()
106 106
107 107 c.stats_percentage = 0
108 108
109 109 if stats and stats.languages:
110 110 c.no_data = False is self.db_repo.enable_statistics
111 111 lang_stats_d = json.loads(stats.languages)
112 112
113 113 # Sort first by decreasing count and second by the file extension,
114 114 # so we have a consistent output.
115 115 lang_stats_items = sorted(lang_stats_d.items(),
116 116 key=lambda k: (-k[1], k[0]))[:10]
117 117 lang_stats = [(x, {"count": y,
118 118 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
119 119 for x, y in lang_stats_items]
120 120
121 121 c.trending_languages = json.dumps(lang_stats)
122 122 else:
123 123 c.no_data = True
124 124 c.trending_languages = json.dumps({})
125 125
126 126 scm_model = ScmModel()
127 127 c.enable_downloads = self.db_repo.enable_downloads
128 128 c.repository_followers = scm_model.get_followers(self.db_repo)
129 129 c.repository_forks = scm_model.get_forks(self.db_repo)
130 130
131 131 # first interaction with the VCS instance after here...
132 132 if c.repository_requirements_missing:
133 133 self.request.override_renderer = \
134 134 'rhodecode:templates/summary/missing_requirements.mako'
135 135 return self._get_template_context(c)
136 136
137 137 c.readme_data, c.readme_file = \
138 138 self._get_readme_data(self.db_repo, c.visual.default_renderer)
139 139
140 140 # loads the summary commits template context
141 141 self._load_commits_context(c)
142 142
143 143 return self._get_template_context(c)
144 144
145 145 @LoginRequired()
146 146 @HasRepoPermissionAnyDecorator(
147 147 'repository.read', 'repository.write', 'repository.admin')
148 148 def repo_stats(self):
149 149 show_stats = bool(self.db_repo.enable_statistics)
150 150 repo_id = self.db_repo.repo_id
151 151
152 152 landing_commit = self.db_repo.get_landing_commit()
153 153 if isinstance(landing_commit, EmptyCommit):
154 154 return {'size': 0, 'code_stats': {}}
155 155
156 156 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
157 157 cache_on = cache_seconds > 0
158 158
159 159 log.debug(
160 160 'Computing REPO STATS for repo_id %s commit_id `%s` '
161 161 'with caching: %s[TTL: %ss]' % (
162 162 repo_id, landing_commit, cache_on, cache_seconds or 0))
163 163
164 164 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
165 165 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
166 166
167 167 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
168 168 condition=cache_on)
169 169 def compute_stats(repo_id, commit_id, _show_stats):
170 170 code_stats = {}
171 171 size = 0
172 172 try:
173 173 commit = self.db_repo.get_commit(commit_id)
174 174
175 175 for node in commit.get_filenodes_generator():
176 176 size += node.size
177 177 if not _show_stats:
178 178 continue
179 179 ext = node.extension.lower()
180 180 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
181 181 if ext_info:
182 182 if ext in code_stats:
183 183 code_stats[ext]['count'] += 1
184 184 else:
185 185 code_stats[ext] = {"count": 1, "desc": ext_info}
186 186 except (EmptyRepositoryError, CommitDoesNotExistError):
187 187 pass
188 188 return {'size': h.format_byte_size_binary(size),
189 189 'code_stats': code_stats}
190 190
191 191 stats = compute_stats(self.db_repo.repo_id, landing_commit.raw_id, show_stats)
192 192 return stats
193 193
194 194 @LoginRequired()
195 195 @HasRepoPermissionAnyDecorator(
196 196 'repository.read', 'repository.write', 'repository.admin')
197 197 def repo_refs_data(self):
198 198 _ = self.request.translate
199 199 self.load_default_context()
200 200
201 201 repo = self.rhodecode_vcs_repo
202 202 refs_to_create = [
203 203 (_("Branch"), repo.branches, 'branch'),
204 204 (_("Tag"), repo.tags, 'tag'),
205 205 (_("Bookmark"), repo.bookmarks, 'book'),
206 206 ]
207 207 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
208 208 data = {
209 209 'more': False,
210 210 'results': res
211 211 }
212 212 return data
213 213
214 214 @LoginRequired()
215 215 @HasRepoPermissionAnyDecorator(
216 216 'repository.read', 'repository.write', 'repository.admin')
217 217 def repo_refs_changelog_data(self):
218 218 _ = self.request.translate
219 219 self.load_default_context()
220 220
221 221 repo = self.rhodecode_vcs_repo
222 222
223 223 refs_to_create = [
224 224 (_("Branches"), repo.branches, 'branch'),
225 225 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
226 226 # TODO: enable when vcs can handle bookmarks filters
227 227 # (_("Bookmarks"), repo.bookmarks, "book"),
228 228 ]
229 229 res = self._create_reference_data(
230 230 repo, self.db_repo_name, refs_to_create)
231 231 data = {
232 232 'more': False,
233 233 'results': res
234 234 }
235 235 return data
236 236
237 237 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
238 238 format_ref_id = get_format_ref_id(repo)
239 239
240 240 result = []
241 241 for title, refs, ref_type in refs_to_create:
242 242 if refs:
243 243 result.append({
244 244 'text': title,
245 245 'children': self._create_reference_items(
246 246 repo, full_repo_name, refs, ref_type,
247 247 format_ref_id),
248 248 })
249 249 return result
250 250
251 251 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
252 252 result = []
253 253 is_svn = h.is_svn(repo)
254 254 for ref_name, raw_id in refs.items():
255 255 files_url = self._create_files_url(
256 256 repo, full_repo_name, ref_name, raw_id, is_svn)
257 257 result.append({
258 258 'text': ref_name,
259 259 'id': format_ref_id(ref_name, raw_id),
260 260 'raw_id': raw_id,
261 261 'type': ref_type,
262 262 'files_url': files_url,
263 263 'idx': 0,
264 264 })
265 265 return result
266 266
267 267 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
268 268 use_commit_id = '/' in ref_name or is_svn
269 269 return h.route_path(
270 270 'repo_files',
271 271 repo_name=full_repo_name,
272 272 f_path=ref_name if is_svn else '',
273 273 commit_id=raw_id if use_commit_id else ref_name,
274 274 _query=dict(at=ref_name))
@@ -1,48 +1,48 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from rhodecode.apps._base import BaseReferencesView
24 24 from rhodecode.lib import ext_json
25 25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
26 26 from rhodecode.model.scm import ScmModel
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 class RepoTagsView(BaseReferencesView):
32 32
33 33 @LoginRequired()
34 34 @HasRepoPermissionAnyDecorator(
35 35 'repository.read', 'repository.write', 'repository.admin')
36 36 def tags(self):
37 37 c = self.load_default_context()
38 38 self._prepare_and_set_clone_url(c)
39 39 c.rhodecode_repo = self.rhodecode_vcs_repo
40 40 c.repository_forks = ScmModel().get_forks(self.db_repo)
41 41
42 42 ref_items = self.rhodecode_vcs_repo.tags.items()
43 43 data = self.load_refs_context(
44 44 ref_items=ref_items, partials_template='tags/tags_data.mako')
45 45
46 46 c.has_references = bool(data)
47 47 c.data = ext_json.str_json(data)
48 48 return self._get_template_context(c)
@@ -1,62 +1,62 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import ADMIN_PREFIX
21 21
22 22
23 23 def includeme(config):
24 24 from rhodecode.apps.search.views import (
25 25 SearchView, SearchRepoView, SearchRepoGroupView)
26 26
27 27 config.add_route(
28 28 name='search',
29 29 pattern=ADMIN_PREFIX + '/search')
30 30 config.add_view(
31 31 SearchView,
32 32 attr='search',
33 33 route_name='search', request_method='GET',
34 34 renderer='rhodecode:templates/search/search.mako')
35 35
36 36 config.add_route(
37 37 name='search_repo',
38 38 pattern='/{repo_name:.*?[^/]}/_search', repo_route=True)
39 39 config.add_view(
40 40 SearchRepoView,
41 41 attr='search_repo',
42 42 route_name='search_repo', request_method='GET',
43 43 renderer='rhodecode:templates/search/search.mako')
44 44
45 45 config.add_route(
46 46 name='search_repo_alt',
47 47 pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
48 48 config.add_view(
49 49 SearchRepoView,
50 50 attr='search_repo',
51 51 route_name='search_repo_alt', request_method='GET',
52 52 renderer='rhodecode:templates/search/search.mako')
53 53
54 54 config.add_route(
55 55 name='search_repo_group',
56 56 pattern='/{repo_group_name:.*?[^/]}/_search',
57 57 repo_group_route=True)
58 58 config.add_view(
59 59 SearchRepoGroupView,
60 60 attr='search_repo_group',
61 61 route_name='search_repo_group', request_method='GET',
62 62 renderer='rhodecode:templates/search/search.mako')
@@ -1,202 +1,201 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import os
22 21
23 22 import mock
24 23 import pytest
25 24 from whoosh import query
26 25
27 26 from rhodecode.tests import (
28 27 TestController, route_path_generator, HG_REPO,
29 28 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
30 29 from rhodecode.tests.utils import AssertResponse
31 30
32 31
33 32 def route_path(name, params=None, **kwargs):
34 33 from rhodecode.apps._base import ADMIN_PREFIX
35 34 url_defs = {
36 35 'search':
37 36 ADMIN_PREFIX + '/search',
38 37 'search_repo':
39 38 '/{repo_name}/search',
40 39 }
41 40 return route_path_generator(url_defs, name=name, params=params, **kwargs)
42 41
43 42
44 43 class TestSearchController(TestController):
45 44
46 45 def test_index(self):
47 46 self.log_user()
48 47 response = self.app.get(route_path('search'))
49 48 assert_response = response.assert_response()
50 49 assert_response.one_element_exists('input#q')
51 50
52 51 def test_search_files_empty_search(self):
53 52 if os.path.isdir(self.index_location):
54 53 pytest.skip('skipped due to existing index')
55 54 else:
56 55 self.log_user()
57 56 response = self.app.get(route_path('search'),
58 57 {'q': HG_REPO})
59 58 response.mustcontain('There is no index to search in. '
60 59 'Please run whoosh indexer')
61 60
62 61 def test_search_validation(self):
63 62 self.log_user()
64 63 response = self.app.get(route_path('search'),
65 64 {'q': query, 'type': 'content', 'page_limit': 1000})
66 65
67 66 response.mustcontain(
68 67 'page_limit - 1000 is greater than maximum value 500')
69 68
70 69 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
71 70 ('todo', 23, [
72 71 'vcs/backends/hg/inmemory.py',
73 72 'vcs/tests/test_git.py']),
74 73 ('extension:rst installation', 6, [
75 74 'docs/index.rst',
76 75 'docs/installation.rst']),
77 76 ('def repo', 87, [
78 77 'vcs/tests/test_git.py',
79 78 'vcs/tests/test_changesets.py']),
80 79 ('repository:%s def test' % HG_REPO, 18, [
81 80 'vcs/tests/test_git.py',
82 81 'vcs/tests/test_changesets.py']),
83 82 ('"def main"', 9, [
84 83 'vcs/__init__.py',
85 84 'vcs/tests/__init__.py',
86 85 'vcs/utils/progressbar.py']),
87 86 ('owner:test_admin', 358, [
88 87 'vcs/tests/base.py',
89 88 'MANIFEST.in',
90 89 'vcs/utils/termcolors.py',
91 90 'docs/theme/ADC/static/documentation.png']),
92 91 ('owner:test_admin def main', 72, [
93 92 'vcs/__init__.py',
94 93 'vcs/tests/test_utils_filesize.py',
95 94 'vcs/tests/test_cli.py']),
96 95 ('owner:michał test', 0, []),
97 96 ])
98 97 def test_search_files(self, query, expected_hits, expected_paths):
99 98 self.log_user()
100 99 response = self.app.get(route_path('search'),
101 100 {'q': query, 'type': 'content', 'page_limit': 500})
102 101
103 102 response.mustcontain('%s results' % expected_hits)
104 103 for path in expected_paths:
105 104 response.mustcontain(path)
106 105
107 106 @pytest.mark.parametrize("query, expected_hits, expected_commits", [
108 107 ('bother to ask where to fetch repo during tests', 3, [
109 108 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1'),
110 109 ('git', 'c6eb379775c578a95dad8ddab53f963b80894850'),
111 110 ('svn', '98')]),
112 111 ('michał', 0, []),
113 112 ('changed:tests/utils.py', 36, [
114 113 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1')]),
115 114 ('changed:vcs/utils/archivers.py', 11, [
116 115 ('hg', '25213a5fbb048dff8ba65d21e466a835536e5b70'),
117 116 ('hg', '47aedd538bf616eedcb0e7d630ea476df0e159c7'),
118 117 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
119 118 ('hg', '04ad456aefd6461aea24f90b63954b6b1ce07b3e'),
120 119 ('git', 'c994f0de03b2a0aa848a04fc2c0d7e737dba31fc'),
121 120 ('git', 'd1f898326327e20524fe22417c22d71064fe54a1'),
122 121 ('git', 'fe568b4081755c12abf6ba673ba777fc02a415f3'),
123 122 ('git', 'bafe786f0d8c2ff7da5c1dcfcfa577de0b5e92f1')]),
124 123 ('added:README.rst', 3, [
125 124 ('hg', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'),
126 125 ('git', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
127 126 ('svn', '8')]),
128 127 ('changed:lazy.py', 15, [
129 128 ('hg', 'eaa291c5e6ae6126a203059de9854ccf7b5baa12'),
130 129 ('git', '17438a11f72b93f56d0e08e7d1fa79a378578a82'),
131 130 ('svn', '82'),
132 131 ('svn', '262'),
133 132 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
134 133 ('git', '33fa3223355104431402a888fa77a4e9956feb3e')
135 134 ]),
136 135 ('author:marcin@python-blog.com '
137 136 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
138 137 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
139 138 ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
140 139 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
141 140 ('b986218b', 1, [
142 141 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
143 142 ])
144 143 def test_search_commit_messages(
145 144 self, query, expected_hits, expected_commits, enabled_backends):
146 145 self.log_user()
147 146 response = self.app.get(route_path('search'),
148 147 {'q': query, 'type': 'commit', 'page_limit': 500})
149 148
150 149 response.mustcontain('%s results' % expected_hits)
151 150 for backend, commit_id in expected_commits:
152 151 if backend in enabled_backends:
153 152 response.mustcontain(commit_id)
154 153
155 154 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
156 155 ('readme.rst', 3, []),
157 156 ('test*', 75, []),
158 157 ('*model*', 1, []),
159 158 ('extension:rst', 48, []),
160 159 ('extension:rst api', 24, []),
161 160 ])
162 161 def test_search_file_paths(self, query, expected_hits, expected_paths):
163 162 self.log_user()
164 163 response = self.app.get(route_path('search'),
165 164 {'q': query, 'type': 'path', 'page_limit': 500})
166 165
167 166 response.mustcontain('%s results' % expected_hits)
168 167 for path in expected_paths:
169 168 response.mustcontain(path)
170 169
171 170 def test_search_commit_message_specific_repo(self, backend):
172 171 self.log_user()
173 172 response = self.app.get(
174 173 route_path('search_repo',repo_name=backend.repo_name),
175 174 {'q': 'bother to ask where to fetch repo during tests',
176 175 'type': 'commit'})
177 176
178 177 response.mustcontain('1 results')
179 178
180 179 def test_filters_are_not_applied_for_admin_user(self):
181 180 self.log_user()
182 181 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
183 182 self.app.get(route_path('search'),
184 183 {'q': 'test query', 'type': 'commit'})
185 184 assert search_mock.call_count == 1
186 185 _, kwargs = search_mock.call_args
187 186 assert kwargs['filter'] is None
188 187
189 188 def test_filters_are_applied_for_normal_user(self, enabled_backends):
190 189 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
191 190 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
192 191 self.app.get(route_path('search'),
193 192 {'q': 'test query', 'type': 'commit'})
194 193 assert search_mock.call_count == 1
195 194 _, kwargs = search_mock.call_args
196 195 assert isinstance(kwargs['filter'], query.Or)
197 196 expected_repositories = [
198 197 'vcs_test_{}'.format(b) for b in enabled_backends]
199 198 queried_repositories = [
200 199 name for type_, name in kwargs['filter'].all_terms()]
201 200 for repository in expected_repositories:
202 201 assert repository in queried_repositories
@@ -1,166 +1,166 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import urllib.request, urllib.parse, urllib.error
23 23
24 24 from webhelpers2.html.tools import update_params
25 25
26 26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
27 27 from rhodecode.lib.auth import (
28 28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
29 29 from rhodecode.lib.helpers import Page
30 30 from rhodecode.lib.utils2 import safe_str
31 31 from rhodecode.lib.index import searcher_from_config
32 32 from rhodecode.model import validation_schema
33 33 from rhodecode.model.validation_schema.schemas import search_schema
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
39 39 searcher = searcher_from_config(request.registry.settings)
40 40 formatted_results = []
41 41 execution_time = ''
42 42
43 43 schema = search_schema.SearchParamsSchema()
44 44 search_tags = []
45 45 search_params = {}
46 46 errors = []
47 47
48 48 try:
49 49 search_params = schema.deserialize(
50 50 dict(
51 51 search_query=request.GET.get('q'),
52 52 search_type=request.GET.get('type'),
53 53 search_sort=request.GET.get('sort'),
54 54 search_max_lines=request.GET.get('max_lines'),
55 55 page_limit=request.GET.get('page_limit'),
56 56 requested_page=request.GET.get('page'),
57 57 )
58 58 )
59 59 except validation_schema.Invalid as e:
60 60 errors = e.children
61 61
62 62 def url_generator(page_num):
63 63
64 64 query_params = {
65 65 'page': page_num,
66 66 'q': safe_str(search_query),
67 67 'type': safe_str(search_type),
68 68 'max_lines': search_max_lines,
69 69 'sort': search_sort
70 70 }
71 71
72 72 return '?' + urllib.parse.urlencode(query_params)
73 73
74 74 c = tmpl_context
75 75 search_query = search_params.get('search_query')
76 76 search_type = search_params.get('search_type')
77 77 search_sort = search_params.get('search_sort')
78 78 search_max_lines = search_params.get('search_max_lines')
79 79 if search_params.get('search_query'):
80 80 page_limit = search_params['page_limit']
81 81 requested_page = search_params['requested_page']
82 82
83 83 try:
84 84 search_result = searcher.search(
85 85 search_query, search_type, c.auth_user, repo_name, repo_group_name,
86 86 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
87 87
88 88 formatted_results = Page(
89 89 search_result['results'], page=requested_page,
90 90 item_count=search_result['count'],
91 91 items_per_page=page_limit, url_maker=url_generator)
92 92 finally:
93 93 searcher.cleanup()
94 94
95 95 search_tags = searcher.extract_search_tags(search_query)
96 96
97 97 if not search_result['error']:
98 98 execution_time = '%s results (%.4f seconds)' % (
99 99 search_result['count'],
100 100 search_result['runtime'])
101 101 elif not errors:
102 102 node = schema['search_query']
103 103 errors = [
104 104 validation_schema.Invalid(node, search_result['error'])]
105 105
106 106 c.perm_user = c.auth_user
107 107 c.repo_name = repo_name
108 108 c.repo_group_name = repo_group_name
109 109 c.errors = errors
110 110 c.formatted_results = formatted_results
111 111 c.runtime = execution_time
112 112 c.cur_query = search_query
113 113 c.search_type = search_type
114 114 c.searcher = searcher
115 115 c.search_tags = search_tags
116 116
117 117 direction, sort_field = searcher.get_sort(search_type, search_sort)
118 118 sort_definition = searcher.sort_def(search_type, direction, sort_field)
119 119 c.sort = ''
120 120 c.sort_tag = None
121 121 c.sort_tag_dir = direction
122 122 if sort_definition:
123 123 c.sort = '{}:{}'.format(direction, sort_field)
124 124 c.sort_tag = sort_field
125 125
126 126
127 127 class SearchView(BaseAppView):
128 128 def load_default_context(self):
129 129 c = self._get_local_tmpl_context()
130 130 return c
131 131
132 132 @LoginRequired()
133 133 def search(self):
134 134 c = self.load_default_context()
135 135 perform_search(self.request, c)
136 136 return self._get_template_context(c)
137 137
138 138
139 139 class SearchRepoView(RepoAppView):
140 140 def load_default_context(self):
141 141 c = self._get_local_tmpl_context()
142 142 c.active = 'search'
143 143 return c
144 144
145 145 @LoginRequired()
146 146 @HasRepoPermissionAnyDecorator(
147 147 'repository.read', 'repository.write', 'repository.admin')
148 148 def search_repo(self):
149 149 c = self.load_default_context()
150 150 perform_search(self.request, c, repo_name=self.db_repo_name)
151 151 return self._get_template_context(c)
152 152
153 153
154 154 class SearchRepoGroupView(RepoGroupAppView):
155 155 def load_default_context(self):
156 156 c = self._get_local_tmpl_context()
157 157 c.active = 'search'
158 158 return c
159 159
160 160 @LoginRequired()
161 161 @HasRepoGroupPermissionAnyDecorator(
162 162 'group.read', 'group.write', 'group.admin')
163 163 def search_repo_group(self):
164 164 c = self.load_default_context()
165 165 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
166 166 return self._get_template_context(c)
@@ -1,61 +1,61 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from . import config_keys
24 24 from .events import SshKeyFileChangeEvent
25 25 from .subscribers import generate_ssh_authorized_keys_file_subscriber
26 26
27 27 from rhodecode.config.settings_maker import SettingsMaker
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 def _sanitize_settings_and_apply_defaults(settings):
33 33 """
34 34 Set defaults, convert to python types and validate settings.
35 35 """
36 36 settings_maker = SettingsMaker(settings)
37 37
38 38 settings_maker.make_setting(config_keys.generate_authorized_keyfile, False, parser='bool')
39 39 settings_maker.make_setting(config_keys.wrapper_allow_shell, False, parser='bool')
40 40 settings_maker.make_setting(config_keys.enable_debug_logging, False, parser='bool')
41 41 settings_maker.make_setting(config_keys.ssh_key_generator_enabled, True, parser='bool')
42 42
43 43 settings_maker.make_setting(config_keys.authorized_keys_file_path, '~/.ssh/authorized_keys_rhodecode')
44 44 settings_maker.make_setting(config_keys.wrapper_cmd, '')
45 45 settings_maker.make_setting(config_keys.authorized_keys_line_ssh_opts, '')
46 46
47 47 settings_maker.make_setting(config_keys.ssh_hg_bin, '~/.rccontrol/vcsserver-1/profile/bin/hg')
48 48 settings_maker.make_setting(config_keys.ssh_git_bin, '~/.rccontrol/vcsserver-1/profile/bin/git')
49 49 settings_maker.make_setting(config_keys.ssh_svn_bin, '~/.rccontrol/vcsserver-1/profile/bin/svnserve')
50 50
51 51 settings_maker.env_expand()
52 52
53 53
54 54 def includeme(config):
55 55 settings = config.registry.settings
56 56 _sanitize_settings_and_apply_defaults(settings)
57 57
58 58 # if we have enable generation of file, subscribe to event
59 59 if settings[config_keys.generate_authorized_keyfile]:
60 60 config.add_subscriber(
61 61 generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent)
@@ -1,34 +1,34 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 # Definition of setting keys used to configure this module. Defined here to
23 23 # avoid repetition of keys throughout the module.
24 24 generate_authorized_keyfile = 'ssh.generate_authorized_keyfile'
25 25 authorized_keys_file_path = 'ssh.authorized_keys_file_path'
26 26 authorized_keys_line_ssh_opts = 'ssh.authorized_keys_ssh_opts'
27 27 ssh_key_generator_enabled = 'ssh.enable_ui_key_generator'
28 28 wrapper_cmd = 'ssh.wrapper_cmd'
29 29 wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell'
30 30 enable_debug_logging = 'ssh.enable_debug_logging'
31 31
32 32 ssh_hg_bin = 'ssh.executable.hg'
33 33 ssh_git_bin = 'ssh.executable.git'
34 34 ssh_svn_bin = 'ssh.executable.svn'
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,264 +1,264 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import re
23 23 import logging
24 24 import datetime
25 25 import configparser
26 26 from sqlalchemy import Table
27 27
28 28 from rhodecode.lib.utils2 import AttributeDict
29 29 from rhodecode.model.scm import ScmModel
30 30
31 31 from .hg import MercurialServer
32 32 from .git import GitServer
33 33 from .svn import SubversionServer
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class SshWrapper(object):
38 38 hg_cmd_pat = re.compile(r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$')
39 39 git_cmd_pat = re.compile(r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
40 40 svn_cmd_pat = re.compile(r'^svnserve -t')
41 41
42 42 def __init__(self, command, connection_info, mode,
43 43 user, user_id, key_id: int, shell, ini_path: str, env):
44 44 self.command = command
45 45 self.connection_info = connection_info
46 46 self.mode = mode
47 47 self.username = user
48 48 self.user_id = user_id
49 49 self.key_id = key_id
50 50 self.shell = shell
51 51 self.ini_path = ini_path
52 52 self.env = env
53 53
54 54 self.config = self.parse_config(ini_path)
55 55 self.server_impl = None
56 56
57 57 def parse_config(self, config_path):
58 58 parser = configparser.ConfigParser()
59 59 parser.read(config_path)
60 60 return parser
61 61
62 62 def update_key_access_time(self, key_id):
63 63 from rhodecode.model.meta import raw_query_executor, Base
64 64
65 65 table = Table('user_ssh_keys', Base.metadata, autoload=False)
66 66 stmt = (
67 67 table.update()
68 68 .where(table.c.ssh_key_id == key_id)
69 69 .values(accessed_on=datetime.datetime.utcnow())
70 70 .returning(table.c.accessed_on, table.c.ssh_key_fingerprint)
71 71 )
72 72
73 73 scalar_res = None
74 74 with raw_query_executor() as session:
75 75 result = session.execute(stmt)
76 76 if result.rowcount:
77 77 scalar_res = result.first()
78 78
79 79 if scalar_res:
80 80 atime, ssh_key_fingerprint = scalar_res
81 81 log.debug('Update key id:`%s` fingerprint:`%s` access time',
82 82 key_id, ssh_key_fingerprint)
83 83
84 84 def get_user(self, user_id):
85 85 user = AttributeDict()
86 86 # lazy load db imports
87 87 from rhodecode.model.db import User
88 88 dbuser = User.get(user_id)
89 89 if not dbuser:
90 90 return None
91 91 user.user_id = dbuser.user_id
92 92 user.username = dbuser.username
93 93 user.auth_user = dbuser.AuthUser()
94 94 return user
95 95
96 96 def get_connection_info(self):
97 97 """
98 98 connection_info
99 99
100 100 Identifies the client and server ends of the connection.
101 101 The variable contains four space-separated values: client IP address,
102 102 client port number, server IP address, and server port number.
103 103 """
104 104 conn = dict(
105 105 client_ip=None,
106 106 client_port=None,
107 107 server_ip=None,
108 108 server_port=None,
109 109 )
110 110
111 111 info = self.connection_info.split(' ')
112 112 if len(info) == 4:
113 113 conn['client_ip'] = info[0]
114 114 conn['client_port'] = info[1]
115 115 conn['server_ip'] = info[2]
116 116 conn['server_port'] = info[3]
117 117
118 118 return conn
119 119
120 120 def maybe_translate_repo_uid(self, repo_name):
121 121 _org_name = repo_name
122 122 if _org_name.startswith('_'):
123 123 # remove format of _ID/subrepo
124 124 _org_name = _org_name.split('/', 1)[0]
125 125
126 126 if repo_name.startswith('_'):
127 127 from rhodecode.model.repo import RepoModel
128 128 org_repo_name = repo_name
129 129 log.debug('translating UID repo %s', org_repo_name)
130 130 by_id_match = RepoModel().get_repo_by_id(repo_name)
131 131 if by_id_match:
132 132 repo_name = by_id_match.repo_name
133 133 log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name)
134 134
135 135 return repo_name, _org_name
136 136
137 137 def get_repo_details(self, mode):
138 138 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
139 139 repo_name = None
140 140
141 141 hg_match = self.hg_cmd_pat.match(self.command)
142 142 if hg_match is not None:
143 143 vcs_type = 'hg'
144 144 repo_id = hg_match.group(1).strip('/')
145 145 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
146 146 return vcs_type, repo_name, mode
147 147
148 148 git_match = self.git_cmd_pat.match(self.command)
149 149 if git_match is not None:
150 150 mode = git_match.group(1)
151 151 vcs_type = 'git'
152 152 repo_id = git_match.group(2).strip('/')
153 153 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
154 154 return vcs_type, repo_name, mode
155 155
156 156 svn_match = self.svn_cmd_pat.match(self.command)
157 157 if svn_match is not None:
158 158 vcs_type = 'svn'
159 159 # Repo name should be extracted from the input stream, we're unable to
160 160 # extract it at this point in execution
161 161 return vcs_type, repo_name, mode
162 162
163 163 return vcs_type, repo_name, mode
164 164
165 165 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
166 166 store = ScmModel().repos_path
167 167
168 168 check_branch_perms = False
169 169 detect_force_push = False
170 170
171 171 if branch_permissions:
172 172 check_branch_perms = True
173 173 detect_force_push = True
174 174
175 175 log.debug(
176 176 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
177 177 vcs, mode, repo, check_branch_perms)
178 178
179 179 # detect if we have to check branch permissions
180 180 extras = {
181 181 'detect_force_push': detect_force_push,
182 182 'check_branch_perms': check_branch_perms,
183 183 }
184 184
185 185 if vcs == 'hg':
186 186 server = MercurialServer(
187 187 store=store, ini_path=self.ini_path,
188 188 repo_name=repo, user=user,
189 189 user_permissions=permissions, config=self.config, env=self.env)
190 190 self.server_impl = server
191 191 return server.run(tunnel_extras=extras)
192 192
193 193 elif vcs == 'git':
194 194 server = GitServer(
195 195 store=store, ini_path=self.ini_path,
196 196 repo_name=repo, repo_mode=mode, user=user,
197 197 user_permissions=permissions, config=self.config, env=self.env)
198 198 self.server_impl = server
199 199 return server.run(tunnel_extras=extras)
200 200
201 201 elif vcs == 'svn':
202 202 server = SubversionServer(
203 203 store=store, ini_path=self.ini_path,
204 204 repo_name=None, user=user,
205 205 user_permissions=permissions, config=self.config, env=self.env)
206 206 self.server_impl = server
207 207 return server.run(tunnel_extras=extras)
208 208
209 209 else:
210 210 raise Exception('Unrecognised VCS: {}'.format(vcs))
211 211
212 212 def wrap(self):
213 213 mode = self.mode
214 214 username = self.username
215 215 user_id = self.user_id
216 216 key_id = self.key_id
217 217 shell = self.shell
218 218
219 219 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
220 220
221 221 log.debug(
222 222 'Mode: `%s` User: `name:%s : id:%s` Shell: `%s` SSH Command: `\"%s\"` '
223 223 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
224 224 mode, username, user_id, shell, self.command,
225 225 scm_detected, scm_mode, scm_repo)
226 226
227 227 log.debug('SSH Connection info %s', self.get_connection_info())
228 228
229 229 # update last access time for this key
230 230 if key_id:
231 231 self.update_key_access_time(key_id)
232 232
233 233 if shell and self.command is None:
234 234 log.info('Dropping to shell, no command given and shell is allowed')
235 235 os.execl('/bin/bash', '-l')
236 236 exit_code = 1
237 237
238 238 elif scm_detected:
239 239 user = self.get_user(user_id)
240 240 if not user:
241 241 log.warning('User with id %s not found', user_id)
242 242 exit_code = -1
243 243 return exit_code
244 244
245 245 auth_user = user.auth_user
246 246 permissions = auth_user.permissions['repositories']
247 247 repo_branch_permissions = auth_user.get_branch_permissions(scm_repo)
248 248 try:
249 249 exit_code, is_updated = self.serve(
250 250 scm_detected, scm_repo, scm_mode, user, permissions,
251 251 repo_branch_permissions)
252 252 except Exception:
253 253 log.exception('Error occurred during execution of SshWrapper')
254 254 exit_code = -1
255 255
256 256 elif self.command is None and shell is False:
257 257 log.error('No Command given.')
258 258 exit_code = -1
259 259
260 260 else:
261 261 log.error('Unhandled Command: "%s" Aborting.', self.command)
262 262 exit_code = -1
263 263
264 264 return exit_code
@@ -1,163 +1,163 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import json
24 24 import logging
25 25
26 26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
27 27 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class VcsServer(object):
34 34 repo_user_agent = None # set in child classes
35 35 _path = None # set executable path for hg/git/svn binary
36 36 backend = None # set in child classes
37 37 tunnel = None # subprocess handling tunnel
38 38 write_perms = ['repository.admin', 'repository.write']
39 39 read_perms = ['repository.read', 'repository.admin', 'repository.write']
40 40
41 41 def __init__(self, user, user_permissions, config, env):
42 42 self.user = user
43 43 self.user_permissions = user_permissions
44 44 self.config = config
45 45 self.env = env
46 46 self.stdin = sys.stdin
47 47
48 48 self.repo_name = None
49 49 self.repo_mode = None
50 50 self.store = ''
51 51 self.ini_path = ''
52 52
53 53 def _invalidate_cache(self, repo_name):
54 54 """
55 55 Set's cache for this repository for invalidation on next access
56 56
57 57 :param repo_name: full repo name, also a cache key
58 58 """
59 59 ScmModel().mark_for_invalidation(repo_name)
60 60
61 61 def has_write_perm(self):
62 62 permission = self.user_permissions.get(self.repo_name)
63 63 if permission in ['repository.write', 'repository.admin']:
64 64 return True
65 65
66 66 return False
67 67
68 68 def _check_permissions(self, action):
69 69 permission = self.user_permissions.get(self.repo_name)
70 70 log.debug('permission for %s on %s are: %s',
71 71 self.user, self.repo_name, permission)
72 72
73 73 if not permission:
74 74 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
75 75 self.user, self.repo_name)
76 76 return -2
77 77
78 78 if action == 'pull':
79 79 if permission in self.read_perms:
80 80 log.info(
81 81 'READ Permissions for User "%s" detected to repo "%s"!',
82 82 self.user, self.repo_name)
83 83 return 0
84 84 else:
85 85 if permission in self.write_perms:
86 86 log.info(
87 87 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
88 88 self.user, self.repo_name)
89 89 return 0
90 90
91 91 log.error('Cannot properly fetch or verify user `%s` permissions. '
92 92 'Permissions: %s, vcs action: %s',
93 93 self.user, permission, action)
94 94 return -2
95 95
96 96 def update_environment(self, action, extras=None):
97 97
98 98 scm_data = {
99 99 'ip': os.environ['SSH_CLIENT'].split()[0],
100 100 'username': self.user.username,
101 101 'user_id': self.user.user_id,
102 102 'action': action,
103 103 'repository': self.repo_name,
104 104 'scm': self.backend,
105 105 'config': self.ini_path,
106 106 'repo_store': self.store,
107 107 'make_lock': None,
108 108 'locked_by': [None, None],
109 109 'server_url': None,
110 110 'user_agent': '{}/ssh-user-agent'.format(self.repo_user_agent),
111 111 'hooks': ['push', 'pull'],
112 112 'hooks_module': 'rhodecode.lib.hooks_daemon',
113 113 'is_shadow_repo': False,
114 114 'detect_force_push': False,
115 115 'check_branch_perms': False,
116 116
117 117 'SSH': True,
118 118 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
119 119 }
120 120 if extras:
121 121 scm_data.update(extras)
122 122 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
123 123
124 124 def get_root_store(self):
125 125 root_store = self.store
126 126 if not root_store.endswith('/'):
127 127 # always append trailing slash
128 128 root_store = root_store + '/'
129 129 return root_store
130 130
131 131 def _handle_tunnel(self, extras):
132 132 # pre-auth
133 133 action = 'pull'
134 134 exit_code = self._check_permissions(action)
135 135 if exit_code:
136 136 return exit_code, False
137 137
138 138 req = self.env['request']
139 139 server_url = req.host_url + req.script_name
140 140 extras['server_url'] = server_url
141 141
142 142 log.debug('Using %s binaries from path %s', self.backend, self._path)
143 143 exit_code = self.tunnel.run(extras)
144 144
145 145 return exit_code, action == "push"
146 146
147 147 def run(self, tunnel_extras=None):
148 148 tunnel_extras = tunnel_extras or {}
149 149 extras = {}
150 150 extras.update(tunnel_extras)
151 151
152 152 callback_daemon, extras = prepare_callback_daemon(
153 153 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
154 154 host=vcs_settings.HOOKS_HOST,
155 155 use_direct_calls=False)
156 156
157 157 with callback_daemon:
158 158 try:
159 159 return self._handle_tunnel(extras)
160 160 finally:
161 161 log.debug('Running cleanup with cache invalidation')
162 162 if self.repo_name:
163 163 self._invalidate_cache(self.repo_name)
@@ -1,75 +1,75 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24
25 25 from .base import VcsServer
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 class GitTunnelWrapper(object):
31 31 process = None
32 32
33 33 def __init__(self, server):
34 34 self.server = server
35 35 self.stdin = sys.stdin
36 36 self.stdout = sys.stdout
37 37
38 38 def create_hooks_env(self):
39 39 pass
40 40
41 41 def command(self):
42 42 root = self.server.get_root_store()
43 43 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
44 44 root=root, git_path=self.server.git_path,
45 45 mode=self.server.repo_mode, repo_name=self.server.repo_name)
46 46 log.debug("Final CMD: %s", command)
47 47 return command
48 48
49 49 def run(self, extras):
50 50 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
51 51 exit_code = self.server._check_permissions(action)
52 52 if exit_code:
53 53 return exit_code
54 54
55 55 self.server.update_environment(action=action, extras=extras)
56 56 self.create_hooks_env()
57 57 return os.system(self.command())
58 58
59 59
60 60 class GitServer(VcsServer):
61 61 backend = 'git'
62 62 repo_user_agent = 'git'
63 63
64 64 def __init__(self, store, ini_path, repo_name, repo_mode,
65 65 user, user_permissions, config, env):
66 66 super(GitServer, self).\
67 67 __init__(user, user_permissions, config, env)
68 68
69 69 self.store = store
70 70 self.ini_path = ini_path
71 71 self.repo_name = repo_name
72 72 self._path = self.git_path = config.get('app:main', 'ssh.executable.git')
73 73
74 74 self.repo_mode = repo_mode
75 75 self.tunnel = GitTunnelWrapper(server=self)
@@ -1,148 +1,148 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24 import tempfile
25 25 import textwrap
26 26 import collections
27 27 from .base import VcsServer
28 28 from rhodecode.model.db import RhodeCodeUi
29 29 from rhodecode.model.settings import VcsSettingsModel
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class MercurialTunnelWrapper(object):
35 35 process = None
36 36
37 37 def __init__(self, server):
38 38 self.server = server
39 39 self.stdin = sys.stdin
40 40 self.stdout = sys.stdout
41 41 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp(prefix='hgrc_rhodecode_')
42 42
43 43 def create_hooks_env(self):
44 44 repo_name = self.server.repo_name
45 45 hg_flags = self.server.config_to_hgrc(repo_name)
46 46
47 47 content = textwrap.dedent(
48 48 '''
49 49 # RhodeCode SSH hooks version=2.0.0
50 50 {custom}
51 51 '''
52 52 ).format(custom='\n'.join(hg_flags))
53 53
54 54 root = self.server.get_root_store()
55 55 hgrc_custom = os.path.join(root, repo_name, '.hg', 'hgrc_rhodecode')
56 56 hgrc_main = os.path.join(root, repo_name, '.hg', 'hgrc')
57 57
58 58 # cleanup custom hgrc file
59 59 if os.path.isfile(hgrc_custom):
60 60 with open(hgrc_custom, 'wb') as f:
61 61 f.write('')
62 62 log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
63 63
64 64 # write temp
65 65 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
66 66 hooks_env_file.write(content)
67 67
68 68 return self.hooks_env_path
69 69
70 70 def remove_configs(self):
71 71 os.remove(self.hooks_env_path)
72 72
73 73 def command(self, hgrc_path):
74 74 root = self.server.get_root_store()
75 75
76 76 command = (
77 77 "cd {root}; HGRCPATH={hgrc} {hg_path} -R {root}{repo_name} "
78 78 "serve --stdio".format(
79 79 root=root, hg_path=self.server.hg_path,
80 80 repo_name=self.server.repo_name, hgrc=hgrc_path))
81 81 log.debug("Final CMD: %s", command)
82 82 return command
83 83
84 84 def run(self, extras):
85 85 # at this point we cannot tell, we do further ACL checks
86 86 # inside the hooks
87 87 action = '?'
88 88 # permissions are check via `pre_push_ssh_auth` hook
89 89 self.server.update_environment(action=action, extras=extras)
90 90 custom_hgrc_file = self.create_hooks_env()
91 91
92 92 try:
93 93 return os.system(self.command(custom_hgrc_file))
94 94 finally:
95 95 self.remove_configs()
96 96
97 97
98 98 class MercurialServer(VcsServer):
99 99 backend = 'hg'
100 100 repo_user_agent = 'mercurial'
101 101 cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
102 102
103 103 def __init__(self, store, ini_path, repo_name, user, user_permissions, config, env):
104 104 super(MercurialServer, self).__init__(user, user_permissions, config, env)
105 105
106 106 self.store = store
107 107 self.ini_path = ini_path
108 108 self.repo_name = repo_name
109 109 self._path = self.hg_path = config.get('app:main', 'ssh.executable.hg')
110 110 self.tunnel = MercurialTunnelWrapper(server=self)
111 111
112 112 def config_to_hgrc(self, repo_name):
113 113 ui_sections = collections.defaultdict(list)
114 114 ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None)
115 115
116 116 # write default hooks
117 117 default_hooks = [
118 118 ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'),
119 119 ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'),
120 120 ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'),
121 121
122 122 ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'),
123 123 ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'),
124 124 ]
125 125
126 126 for k, v in default_hooks:
127 127 ui_sections['hooks'].append((k, v))
128 128
129 129 for entry in ui:
130 130 if not entry.active:
131 131 continue
132 132 sec = entry.section
133 133 key = entry.key
134 134
135 135 if sec in self.cli_flags:
136 136 # we want only custom hooks, so we skip builtins
137 137 if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN:
138 138 continue
139 139
140 140 ui_sections[sec].append([key, entry.value])
141 141
142 142 flags = []
143 143 for _sec, key_val in ui_sections.items():
144 144 flags.append(' ')
145 145 flags.append('[{}]'.format(_sec))
146 146 for key, val in key_val:
147 147 flags.append('{}= {}'.format(key, val))
148 148 return flags
@@ -1,81 +1,81 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24
25 25 import click
26 26
27 27 from pyramid.paster import setup_logging
28 28
29 29 from rhodecode.lib.pyramid_utils import bootstrap
30 30 from .backends import SshWrapper
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 def setup_custom_logging(ini_path, debug):
36 36 if debug:
37 37 # enabled rhodecode.ini controlled logging setup
38 38 setup_logging(ini_path)
39 39 else:
40 40 # configure logging in a mode that doesn't print anything.
41 41 # in case of regularly configured logging it gets printed out back
42 42 # to the client doing an SSH command.
43 43 logger = logging.getLogger('')
44 44 null = logging.NullHandler()
45 45 # add the handler to the root logger
46 46 logger.handlers = [null]
47 47
48 48
49 49 @click.command()
50 50 @click.argument('ini_path', type=click.Path(exists=True))
51 51 @click.option(
52 52 '--mode', '-m', required=False, default='auto',
53 53 type=click.Choice(['auto', 'vcs', 'git', 'hg', 'svn', 'test']),
54 54 help='mode of operation')
55 55 @click.option('--user', help='Username for which the command will be executed')
56 56 @click.option('--user-id', help='User ID for which the command will be executed')
57 57 @click.option('--key-id', help='ID of the key from the database')
58 58 @click.option('--shell', '-s', is_flag=True, help='Allow Shell')
59 59 @click.option('--debug', is_flag=True, help='Enabled detailed output logging')
60 60 def main(ini_path, mode, user, user_id, key_id, shell, debug):
61 61 setup_custom_logging(ini_path, debug)
62 62
63 63 command = os.environ.get('SSH_ORIGINAL_COMMAND', '')
64 64 if not command and mode not in ['test']:
65 65 raise ValueError(
66 66 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.'
67 67 'Please make sure this is set and available during execution '
68 68 'of this script.')
69 69 connection_info = os.environ.get('SSH_CONNECTION', '')
70 70
71 71 with bootstrap(ini_path, env={'RC_CMD_SSH_WRAPPER': '1'}) as env:
72 72 try:
73 73 ssh_wrapper = SshWrapper(
74 74 command, connection_info, mode,
75 75 user, user_id, key_id, shell, ini_path, env)
76 76 except Exception:
77 77 log.exception('Failed to execute SshWrapper')
78 78 sys.exit(-5)
79 79
80 80 return_code = ssh_wrapper.wrap()
81 81 sys.exit(return_code)
@@ -1,36 +1,36 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from .utils import generate_ssh_authorized_keys_file
25 25
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 def generate_ssh_authorized_keys_file_subscriber(event):
31 31 """
32 32 Subscriber to the `SshKeyFileChangeEvent`. This triggers the
33 33 automatic generation of authorized_keys file on any change in
34 34 ssh keys management
35 35 """
36 36 generate_ssh_authorized_keys_file(event.request.registry)
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,70 +1,70 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import pytest
23 23 import configparser
24 24
25 25 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper
26 26 from rhodecode.lib.utils2 import AttributeDict
27 27
28 28
29 29 @pytest.fixture()
30 30 def dummy_conf_file(tmpdir):
31 31 conf = configparser.ConfigParser()
32 32 conf.add_section('app:main')
33 33 conf.set('app:main', 'ssh.executable.hg', '/usr/bin/hg')
34 34 conf.set('app:main', 'ssh.executable.git', '/usr/bin/git')
35 35 conf.set('app:main', 'ssh.executable.svn', '/usr/bin/svnserve')
36 36
37 37 f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini')
38 38 with open(f_path, 'wb') as f:
39 39 conf.write(f)
40 40
41 41 return os.path.join(f_path)
42 42
43 43
44 44 def plain_dummy_env():
45 45 return {
46 46 'request':
47 47 AttributeDict(host_url='http://localhost', script_name='/')
48 48 }
49 49
50 50
51 51 @pytest.fixture()
52 52 def dummy_env():
53 53 return plain_dummy_env()
54 54
55 55
56 56 def plain_dummy_user():
57 57 return AttributeDict(username='test_user')
58 58
59 59
60 60 @pytest.fixture()
61 61 def dummy_user():
62 62 return plain_dummy_user()
63 63
64 64
65 65 @pytest.fixture()
66 66 def ssh_wrapper(app, dummy_conf_file, dummy_env):
67 67 conn_info = '127.0.0.1 22 10.0.0.1 443'
68 68 return SshWrapper(
69 69 'random command', conn_info, 'auto', 'admin', '1', key_id='1',
70 70 shell=False, ini_path=dummy_conf_file, env=dummy_env)
@@ -1,156 +1,156 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import json
22 22 import os
23 23
24 24 import mock
25 25 import pytest
26 26
27 27 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
28 28 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
29 29
30 30
31 31 class GitServerCreator(object):
32 32 root = '/tmp/repo/path/'
33 33 git_path = '/usr/local/bin/git'
34 34 config_data = {
35 35 'app:main': {
36 36 'ssh.executable.git': git_path,
37 37 'vcs.hooks.protocol': 'http',
38 38 }
39 39 }
40 40 repo_name = 'test_git'
41 41 repo_mode = 'receive-pack'
42 42 user = plain_dummy_user()
43 43
44 44 def __init__(self):
45 45 def config_get(part, key):
46 46 return self.config_data.get(part, {}).get(key)
47 47 self.config_mock = mock.Mock()
48 48 self.config_mock.get = mock.Mock(side_effect=config_get)
49 49
50 50 def create(self, **kwargs):
51 51 parameters = {
52 52 'store': self.root,
53 53 'ini_path': '',
54 54 'user': self.user,
55 55 'repo_name': self.repo_name,
56 56 'repo_mode': self.repo_mode,
57 57 'user_permissions': {
58 58 self.repo_name: 'repository.admin'
59 59 },
60 60 'config': self.config_mock,
61 61 'env': plain_dummy_env()
62 62 }
63 63 parameters.update(kwargs)
64 64 server = GitServer(**parameters)
65 65 return server
66 66
67 67
68 68 @pytest.fixture()
69 69 def git_server(app):
70 70 return GitServerCreator()
71 71
72 72
73 73 class TestGitServer(object):
74 74
75 75 def test_command(self, git_server):
76 76 server = git_server.create()
77 77 expected_command = (
78 78 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
79 79 root=git_server.root, git_path=git_server.git_path,
80 80 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
81 81 )
82 82 assert expected_command == server.tunnel.command()
83 83
84 84 @pytest.mark.parametrize('permissions, action, code', [
85 85 ({}, 'pull', -2),
86 86 ({'test_git': 'repository.read'}, 'pull', 0),
87 87 ({'test_git': 'repository.read'}, 'push', -2),
88 88 ({'test_git': 'repository.write'}, 'push', 0),
89 89 ({'test_git': 'repository.admin'}, 'push', 0),
90 90
91 91 ])
92 92 def test_permission_checks(self, git_server, permissions, action, code):
93 93 server = git_server.create(user_permissions=permissions)
94 94 result = server._check_permissions(action)
95 95 assert result is code
96 96
97 97 @pytest.mark.parametrize('permissions, value', [
98 98 ({}, False),
99 99 ({'test_git': 'repository.read'}, False),
100 100 ({'test_git': 'repository.write'}, True),
101 101 ({'test_git': 'repository.admin'}, True),
102 102
103 103 ])
104 104 def test_has_write_permissions(self, git_server, permissions, value):
105 105 server = git_server.create(user_permissions=permissions)
106 106 result = server.has_write_perm()
107 107 assert result is value
108 108
109 109 def test_run_returns_executes_command(self, git_server):
110 110 server = git_server.create()
111 111 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
112 112
113 113 os.environ['SSH_CLIENT'] = '127.0.0.1'
114 114 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
115 115 _patch.return_value = 0
116 116 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
117 117 exit_code = server.run()
118 118
119 119 assert exit_code == (0, False)
120 120
121 121 @pytest.mark.parametrize(
122 122 'repo_mode, action', [
123 123 ['receive-pack', 'push'],
124 124 ['upload-pack', 'pull']
125 125 ])
126 126 def test_update_environment(self, git_server, repo_mode, action):
127 127 server = git_server.create(repo_mode=repo_mode)
128 128 store = server.store
129 129
130 130 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
131 131 with mock.patch('os.putenv') as putenv_mock:
132 132 server.update_environment(action)
133 133
134 134 expected_data = {
135 135 'username': git_server.user.username,
136 136 'user_id': git_server.user.user_id,
137 137 'scm': 'git',
138 138 'repository': git_server.repo_name,
139 139 'make_lock': None,
140 140 'action': action,
141 141 'ip': '10.10.10.10',
142 142 'locked_by': [None, None],
143 143 'config': '',
144 144 'repo_store': store,
145 145 'server_url': None,
146 146 'hooks': ['push', 'pull'],
147 147 'is_shadow_repo': False,
148 148 'hooks_module': 'rhodecode.lib.hooks_daemon',
149 149 'check_branch_perms': False,
150 150 'detect_force_push': False,
151 151 'user_agent': u'git/ssh-user-agent',
152 152 'SSH': True,
153 153 'SSH_PERMISSIONS': 'repository.admin',
154 154 }
155 155 args, kwargs = putenv_mock.call_args
156 156 assert json.loads(args[1]) == expected_data
@@ -1,120 +1,120 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import mock
23 23 import pytest
24 24
25 25 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialServer
26 26 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
27 27
28 28
29 29 class MercurialServerCreator(object):
30 30 root = '/tmp/repo/path/'
31 31 hg_path = '/usr/local/bin/hg'
32 32
33 33 config_data = {
34 34 'app:main': {
35 35 'ssh.executable.hg': hg_path,
36 36 'vcs.hooks.protocol': 'http',
37 37 }
38 38 }
39 39 repo_name = 'test_hg'
40 40 user = plain_dummy_user()
41 41
42 42 def __init__(self):
43 43 def config_get(part, key):
44 44 return self.config_data.get(part, {}).get(key)
45 45 self.config_mock = mock.Mock()
46 46 self.config_mock.get = mock.Mock(side_effect=config_get)
47 47
48 48 def create(self, **kwargs):
49 49 parameters = {
50 50 'store': self.root,
51 51 'ini_path': '',
52 52 'user': self.user,
53 53 'repo_name': self.repo_name,
54 54 'user_permissions': {
55 55 'test_hg': 'repository.admin'
56 56 },
57 57 'config': self.config_mock,
58 58 'env': plain_dummy_env()
59 59 }
60 60 parameters.update(kwargs)
61 61 server = MercurialServer(**parameters)
62 62 return server
63 63
64 64
65 65 @pytest.fixture()
66 66 def hg_server(app):
67 67 return MercurialServerCreator()
68 68
69 69
70 70 class TestMercurialServer(object):
71 71
72 72 def test_command(self, hg_server, tmpdir):
73 73 server = hg_server.create()
74 74 custom_hgrc = os.path.join(str(tmpdir), 'hgrc')
75 75 expected_command = (
76 76 'cd {root}; HGRCPATH={custom_hgrc} {hg_path} -R {root}{repo_name} serve --stdio'.format(
77 77 root=hg_server.root, custom_hgrc=custom_hgrc, hg_path=hg_server.hg_path,
78 78 repo_name=hg_server.repo_name)
79 79 )
80 80 server_command = server.tunnel.command(custom_hgrc)
81 81 assert expected_command == server_command
82 82
83 83 @pytest.mark.parametrize('permissions, action, code', [
84 84 ({}, 'pull', -2),
85 85 ({'test_hg': 'repository.read'}, 'pull', 0),
86 86 ({'test_hg': 'repository.read'}, 'push', -2),
87 87 ({'test_hg': 'repository.write'}, 'push', 0),
88 88 ({'test_hg': 'repository.admin'}, 'push', 0),
89 89
90 90 ])
91 91 def test_permission_checks(self, hg_server, permissions, action, code):
92 92 server = hg_server.create(user_permissions=permissions)
93 93 result = server._check_permissions(action)
94 94 assert result is code
95 95
96 96 @pytest.mark.parametrize('permissions, value', [
97 97 ({}, False),
98 98 ({'test_hg': 'repository.read'}, False),
99 99 ({'test_hg': 'repository.write'}, True),
100 100 ({'test_hg': 'repository.admin'}, True),
101 101
102 102 ])
103 103 def test_has_write_permissions(self, hg_server, permissions, value):
104 104 server = hg_server.create(user_permissions=permissions)
105 105 result = server.has_write_perm()
106 106 assert result is value
107 107
108 108 def test_run_returns_executes_command(self, hg_server):
109 109 server = hg_server.create()
110 110 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialTunnelWrapper
111 111 os.environ['SSH_CLIENT'] = '127.0.0.1'
112 112 with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch:
113 113 _patch.return_value = 0
114 114 with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'):
115 115 exit_code = server.run()
116 116
117 117 assert exit_code == (0, False)
118 118
119 119
120 120
@@ -1,207 +1,207 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import os
21 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
25 25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
26 26
27 27
28 28 class SubversionServerCreator(object):
29 29 root = '/tmp/repo/path/'
30 30 svn_path = '/usr/local/bin/svnserve'
31 31 config_data = {
32 32 'app:main': {
33 33 'ssh.executable.svn': svn_path,
34 34 'vcs.hooks.protocol': 'http',
35 35 }
36 36 }
37 37 repo_name = 'test-svn'
38 38 user = plain_dummy_user()
39 39
40 40 def __init__(self):
41 41 def config_get(part, key):
42 42 return self.config_data.get(part, {}).get(key)
43 43 self.config_mock = mock.Mock()
44 44 self.config_mock.get = mock.Mock(side_effect=config_get)
45 45
46 46 def create(self, **kwargs):
47 47 parameters = {
48 48 'store': self.root,
49 49 'repo_name': self.repo_name,
50 50 'ini_path': '',
51 51 'user': self.user,
52 52 'user_permissions': {
53 53 self.repo_name: 'repository.admin'
54 54 },
55 55 'config': self.config_mock,
56 56 'env': plain_dummy_env()
57 57 }
58 58
59 59 parameters.update(kwargs)
60 60 server = SubversionServer(**parameters)
61 61 return server
62 62
63 63
64 64 @pytest.fixture()
65 65 def svn_server(app):
66 66 return SubversionServerCreator()
67 67
68 68
69 69 class TestSubversionServer(object):
70 70 def test_command(self, svn_server):
71 71 server = svn_server.create()
72 72 expected_command = [
73 73 svn_server.svn_path, '-t',
74 74 '--config-file', server.tunnel.svn_conf_path,
75 75 '--tunnel-user', svn_server.user.username,
76 76 '-r', svn_server.root
77 77 ]
78 78
79 79 assert expected_command == server.tunnel.command()
80 80
81 81 @pytest.mark.parametrize('permissions, action, code', [
82 82 ({}, 'pull', -2),
83 83 ({'test-svn': 'repository.read'}, 'pull', 0),
84 84 ({'test-svn': 'repository.read'}, 'push', -2),
85 85 ({'test-svn': 'repository.write'}, 'push', 0),
86 86 ({'test-svn': 'repository.admin'}, 'push', 0),
87 87
88 88 ])
89 89 def test_permission_checks(self, svn_server, permissions, action, code):
90 90 server = svn_server.create(user_permissions=permissions)
91 91 result = server._check_permissions(action)
92 92 assert result is code
93 93
94 94 @pytest.mark.parametrize('permissions, access_paths, expected_match', [
95 95 # not matched repository name
96 96 ({
97 97 'test-svn': ''
98 98 }, ['test-svn-1', 'test-svn-1/subpath'],
99 99 None),
100 100
101 101 # exact match
102 102 ({
103 103 'test-svn': ''
104 104 },
105 105 ['test-svn'],
106 106 'test-svn'),
107 107
108 108 # subdir commits
109 109 ({
110 110 'test-svn': ''
111 111 },
112 112 ['test-svn/foo',
113 113 'test-svn/foo/test-svn',
114 114 'test-svn/trunk/development.txt',
115 115 ],
116 116 'test-svn'),
117 117
118 118 # subgroups + similar patterns
119 119 ({
120 120 'test-svn': '',
121 121 'test-svn-1': '',
122 122 'test-svn-subgroup/test-svn': '',
123 123
124 124 },
125 125 ['test-svn-1',
126 126 'test-svn-1/foo/test-svn',
127 127 'test-svn-1/test-svn',
128 128 ],
129 129 'test-svn-1'),
130 130
131 131 # subgroups + similar patterns
132 132 ({
133 133 'test-svn-1': '',
134 134 'test-svn-10': '',
135 135 'test-svn-100': '',
136 136 },
137 137 ['test-svn-10',
138 138 'test-svn-10/foo/test-svn',
139 139 'test-svn-10/test-svn',
140 140 ],
141 141 'test-svn-10'),
142 142
143 143 # subgroups + similar patterns
144 144 ({
145 145 'name': '',
146 146 'nameContains': '',
147 147 'nameContainsThis': '',
148 148 },
149 149 ['nameContains',
150 150 'nameContains/This',
151 151 'nameContains/This/test-svn',
152 152 ],
153 153 'nameContains'),
154 154
155 155 # subgroups + similar patterns
156 156 ({
157 157 'test-svn': '',
158 158 'test-svn-1': '',
159 159 'test-svn-subgroup/test-svn': '',
160 160
161 161 },
162 162 ['test-svn-subgroup/test-svn',
163 163 'test-svn-subgroup/test-svn/foo/test-svn',
164 164 'test-svn-subgroup/test-svn/trunk/example.txt',
165 165 ],
166 166 'test-svn-subgroup/test-svn'),
167 167 ])
168 168 def test_repo_extraction_on_subdir(self, svn_server, permissions, access_paths, expected_match):
169 169 server = svn_server.create(user_permissions=permissions)
170 170 for path in access_paths:
171 171 repo_name = server.tunnel._match_repo_name(path)
172 172 assert repo_name == expected_match
173 173
174 174 def test_run_returns_executes_command(self, svn_server):
175 175 server = svn_server.create()
176 176 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
177 177 os.environ['SSH_CLIENT'] = '127.0.0.1'
178 178 with mock.patch.object(
179 179 SubversionTunnelWrapper, 'get_first_client_response',
180 180 return_value={'url': 'http://server/test-svn'}):
181 181 with mock.patch.object(
182 182 SubversionTunnelWrapper, 'patch_first_client_response',
183 183 return_value=0):
184 184 with mock.patch.object(
185 185 SubversionTunnelWrapper, 'sync',
186 186 return_value=0):
187 187 with mock.patch.object(
188 188 SubversionTunnelWrapper, 'command',
189 189 return_value=['date']):
190 190
191 191 exit_code = server.run()
192 192 # SVN has this differently configured, and we get in our mock env
193 193 # None as return code
194 194 assert exit_code == (None, False)
195 195
196 196 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
197 197 server = svn_server.create()
198 198 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
199 199 with mock.patch.object(
200 200 SubversionTunnelWrapper, 'command',
201 201 return_value=['date']):
202 202 with mock.patch.object(
203 203 SubversionTunnelWrapper, 'get_first_client_response',
204 204 return_value=None):
205 205 exit_code = server.run()
206 206
207 207 assert exit_code == (1, False)
@@ -1,71 +1,71 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import pytest
23 23 import mock
24 24
25 25 from rhodecode.apps.ssh_support import utils
26 26 from rhodecode.lib.utils2 import AttributeDict
27 27
28 28
29 29 class TestSshKeyFileGeneration(object):
30 30 @pytest.mark.parametrize('ssh_wrapper_cmd', ['/tmp/sshwrapper.py'])
31 31 @pytest.mark.parametrize('allow_shell', [True, False])
32 32 @pytest.mark.parametrize('debug', [True, False])
33 33 @pytest.mark.parametrize('ssh_opts', [None, 'mycustom,option'])
34 34 def test_write_keyfile(self, tmpdir, ssh_wrapper_cmd, allow_shell, debug, ssh_opts):
35 35
36 36 authorized_keys_file_path = os.path.join(str(tmpdir), 'authorized_keys')
37 37
38 38 def keys():
39 39 return [
40 40 AttributeDict({'user': AttributeDict(username='admin'),
41 41 'ssh_key_data': 'ssh-rsa ADMIN_KEY'}),
42 42 AttributeDict({'user': AttributeDict(username='user'),
43 43 'ssh_key_data': 'ssh-rsa USER_KEY'}),
44 44 ]
45 45 with mock.patch('rhodecode.apps.ssh_support.utils.get_all_active_keys',
46 46 return_value=keys()):
47 47 with mock.patch.dict('rhodecode.CONFIG', {'__file__': '/tmp/file.ini'}):
48 48 utils._generate_ssh_authorized_keys_file(
49 49 authorized_keys_file_path, ssh_wrapper_cmd,
50 50 allow_shell, ssh_opts, debug
51 51 )
52 52
53 53 assert os.path.isfile(authorized_keys_file_path)
54 54 with open(authorized_keys_file_path) as f:
55 55 content = f.read()
56 56
57 57 assert 'command="/tmp/sshwrapper.py' in content
58 58 assert 'This file is managed by RhodeCode, ' \
59 59 'please do not edit it manually.' in content
60 60
61 61 if allow_shell:
62 62 assert '--shell' in content
63 63
64 64 if debug:
65 65 assert '--debug' in content
66 66
67 67 assert '--user' in content
68 68 assert '--user-id' in content
69 69
70 70 if ssh_opts:
71 71 assert ssh_opts in content
@@ -1,54 +1,54 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23
24 24 class TestSSHWrapper(object):
25 25
26 26 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, ssh_wrapper):
27 27 with pytest.raises(Exception) as exc_info:
28 28 ssh_wrapper.serve(
29 29 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
30 30 permissions={}, branch_permissions={})
31 31 assert str(exc_info.value) == 'Unrecognised VCS: microsoft-tfs'
32 32
33 33 def test_parse_config(self, ssh_wrapper):
34 34 config = ssh_wrapper.parse_config(ssh_wrapper.ini_path)
35 35 assert config
36 36
37 37 def test_get_connection_info(self, ssh_wrapper):
38 38 conn_info = ssh_wrapper.get_connection_info()
39 39 assert {'client_ip': '127.0.0.1',
40 40 'client_port': '22',
41 41 'server_ip': '10.0.0.1',
42 42 'server_port': '443'} == conn_info
43 43
44 44 @pytest.mark.parametrize('command, vcs', [
45 45 ('xxx', None),
46 46 ('svnserve -t', 'svn'),
47 47 ('hg -R repo serve --stdio', 'hg'),
48 48 ('git-receive-pack \'repo.git\'', 'git'),
49 49
50 50 ])
51 51 def test_get_repo_details(self, ssh_wrapper, command, vcs):
52 52 ssh_wrapper.command = command
53 53 vcs_type, repo_name, mode = ssh_wrapper.get_repo_details(mode='auto')
54 54 assert vcs_type == vcs
@@ -1,133 +1,133 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import stat
23 23 import logging
24 24 import tempfile
25 25 import datetime
26 26
27 27 from . import config_keys
28 28 from rhodecode.model.db import true, joinedload, User, UserSshKeys
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33 HEADER = \
34 34 "# This file is managed by RhodeCode, please do not edit it manually. # \n" \
35 35 "# Current entries: {}, create date: UTC:{}.\n"
36 36
37 37 # Default SSH options for authorized_keys file, can be override via .ini
38 38 SSH_OPTS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
39 39
40 40
41 41 def get_all_active_keys():
42 42 result = UserSshKeys.query() \
43 43 .options(joinedload(UserSshKeys.user)) \
44 44 .filter(UserSshKeys.user != User.get_default_user()) \
45 45 .filter(User.active == true()) \
46 46 .all()
47 47 return result
48 48
49 49
50 50 def _generate_ssh_authorized_keys_file(
51 51 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts, debug):
52 52 import rhodecode
53 53
54 54 authorized_keys_file_path = os.path.abspath(
55 55 os.path.expanduser(authorized_keys_file_path))
56 56 tmp_file_dir = os.path.dirname(authorized_keys_file_path)
57 57
58 58 all_active_keys = get_all_active_keys()
59 59
60 60 if allow_shell:
61 61 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
62 62 if debug:
63 63 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --debug'
64 64
65 65 if not os.path.isfile(authorized_keys_file_path):
66 66 log.debug('Creating file at %s', authorized_keys_file_path)
67 67 with open(authorized_keys_file_path, 'w'):
68 68 pass
69 69
70 70 if not os.access(authorized_keys_file_path, os.R_OK):
71 71 raise OSError('Access to file {} is without read access'.format(
72 72 authorized_keys_file_path))
73 73
74 74 line_tmpl = '{ssh_opts},command="{wrapper_command} {ini_path} --user-id={user_id} --user={user} --key-id={user_key_id}" {key}\n'
75 75
76 76 fd, tmp_authorized_keys = tempfile.mkstemp(
77 77 '.authorized_keys_write_operation',
78 78 dir=tmp_file_dir)
79 79
80 80 now = datetime.datetime.utcnow().isoformat()
81 81 keys_file = os.fdopen(fd, 'wb')
82 82 keys_file.write(HEADER.format(len(all_active_keys), now))
83 83 ini_path = rhodecode.CONFIG['__file__']
84 84
85 85 for user_key in all_active_keys:
86 86 username = user_key.user.username
87 87 user_id = user_key.user.user_id
88 88 # replace all newline from ends and inside
89 89 safe_key_data = user_key.ssh_key_data\
90 90 .strip()\
91 91 .replace('\n', ' ') \
92 92 .replace('\t', ' ') \
93 93 .replace('\r', ' ')
94 94
95 95 line = line_tmpl.format(
96 96 ssh_opts=ssh_opts or SSH_OPTS,
97 97 wrapper_command=ssh_wrapper_cmd,
98 98 ini_path=ini_path,
99 99 user_id=user_id,
100 100 user=username,
101 101 user_key_id=user_key.ssh_key_id,
102 102 key=safe_key_data)
103 103
104 104 keys_file.write(line)
105 105 log.debug('addkey: Key added for user: `%s`', username)
106 106 keys_file.close()
107 107
108 108 # Explicitly setting read-only permissions to authorized_keys
109 109 os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
110 110 # Rename is atomic operation
111 111 os.rename(tmp_authorized_keys, authorized_keys_file_path)
112 112
113 113
114 114 def generate_ssh_authorized_keys_file(registry):
115 115 log.info('Generating new authorized key file')
116 116
117 117 authorized_keys_file_path = registry.settings.get(
118 118 config_keys.authorized_keys_file_path)
119 119
120 120 ssh_wrapper_cmd = registry.settings.get(
121 121 config_keys.wrapper_cmd)
122 122 allow_shell = registry.settings.get(
123 123 config_keys.wrapper_allow_shell)
124 124 ssh_opts = registry.settings.get(
125 125 config_keys.authorized_keys_line_ssh_opts)
126 126 debug = registry.settings.get(
127 127 config_keys.enable_debug_logging)
128 128
129 129 _generate_ssh_authorized_keys_file(
130 130 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts,
131 131 debug)
132 132
133 133 return 0
@@ -1,90 +1,90 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import os
21 21 import logging
22 22
23 23 # Do not use `from rhodecode import events` here, it will be overridden by the
24 24 # events module in this package due to pythons import mechanism.
25 25 from rhodecode.events import RepoGroupEvent
26 26 from rhodecode.subscribers import AsyncSubprocessSubscriber
27 27 from rhodecode.config.settings_maker import SettingsMaker
28 28
29 29 from .events import ModDavSvnConfigChange
30 30 from .subscribers import generate_config_subscriber
31 31 from . import config_keys
32 32
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def _sanitize_settings_and_apply_defaults(settings):
38 38 """
39 39 Set defaults, convert to python types and validate settings.
40 40 """
41 41 settings_maker = SettingsMaker(settings)
42 42 settings_maker.make_setting(config_keys.generate_config, False, parser='bool')
43 43 settings_maker.make_setting(config_keys.list_parent_path, True, parser='bool')
44 44 settings_maker.make_setting(config_keys.reload_timeout, 10, parser='bool')
45 45 settings_maker.make_setting(config_keys.config_file_path, '')
46 46 settings_maker.make_setting(config_keys.location_root, '/')
47 47 settings_maker.make_setting(config_keys.reload_command, '')
48 48 settings_maker.make_setting(config_keys.template, '')
49 49
50 50 settings_maker.env_expand()
51 51
52 52 # Convert negative timeout values to zero.
53 53 if settings[config_keys.reload_timeout] < 0:
54 54 settings[config_keys.reload_timeout] = 0
55 55
56 56 # Append path separator to location root.
57 57 settings[config_keys.location_root] = _append_path_sep(
58 58 settings[config_keys.location_root])
59 59
60 60 # Validate settings.
61 61 if settings[config_keys.generate_config]:
62 62 assert len(settings[config_keys.config_file_path]) > 0
63 63
64 64
65 65 def _append_path_sep(path):
66 66 """
67 67 Append the path separator if missing.
68 68 """
69 69 if isinstance(path, str) and not path.endswith(os.path.sep):
70 70 path += os.path.sep
71 71 return path
72 72
73 73
74 74 def includeme(config):
75 75 settings = config.registry.settings
76 76 _sanitize_settings_and_apply_defaults(settings)
77 77
78 78 if settings[config_keys.generate_config]:
79 79 # Add subscriber to generate the Apache mod dav svn configuration on
80 80 # repository group events.
81 81 config.add_subscriber(generate_config_subscriber, RepoGroupEvent)
82 82
83 83 # If a reload command is set add a subscriber to execute it on
84 84 # configuration changes.
85 85 reload_cmd = settings[config_keys.reload_command]
86 86 if reload_cmd:
87 87 reload_timeout = settings[config_keys.reload_timeout] or None
88 88 reload_subscriber = AsyncSubprocessSubscriber(
89 89 cmd=reload_cmd, timeout=reload_timeout)
90 90 config.add_subscriber(reload_subscriber, ModDavSvnConfigChange)
@@ -1,30 +1,30 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 # Definition of setting keys used to configure this module. Defined here to
23 23 # avoid repetition of keys throughout the module.
24 24 config_file_path = 'svn.proxy.config_file_path'
25 25 generate_config = 'svn.proxy.generate_config'
26 26 list_parent_path = 'svn.proxy.list_parent_path'
27 27 location_root = 'svn.proxy.location_root'
28 28 reload_command = 'svn.proxy.reload_cmd'
29 29 reload_timeout = 'svn.proxy.reload_timeout'
30 30 template = 'svn.proxy.config_template'
@@ -1,40 +1,40 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from .utils import generate_mod_dav_svn_config
25 25
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 def generate_config_subscriber(event):
31 31 """
32 32 Subscriber to the `rhodcode.events.RepoGroupEvent`. This triggers the
33 33 automatic generation of mod_dav_svn config file on repository group
34 34 changes.
35 35 """
36 36 try:
37 37 generate_mod_dav_svn_config(event.request.registry)
38 38 except Exception:
39 39 log.exception(
40 40 'Exception while generating subversion mod_dav_svn configuration.')
@@ -1,121 +1,121 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import os
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.apps.svn_support import utils
27 27
28 28
29 29 @pytest.mark.usefixtures('config_stub')
30 30 class TestModDavSvnConfig(object):
31 31
32 32 @classmethod
33 33 def setup_class(cls):
34 34 cls.location_root = u'/location/root/çµäö'
35 35 cls.parent_path_root = u'/parent/path/çµäö'
36 36 cls.realm = u'Dummy Realm (äöüçµ)'
37 37
38 38 @classmethod
39 39 def get_repo_group_mocks(cls, count=1):
40 40 repo_groups = []
41 41 for num in range(0, count):
42 42 full_path = u'/path/to/RepöGröúp-°µ {}'.format(num)
43 43 repo_group_mock = mock.MagicMock()
44 44 repo_group_mock.full_path = full_path
45 45 repo_group_mock.full_path_splitted = full_path.split('/')
46 46 repo_groups.append(repo_group_mock)
47 47 return repo_groups
48 48
49 49 def assert_root_location_directive(self, config):
50 50 pattern = u'<Location "{location}">'.format(
51 51 location=self.location_root)
52 52 assert len(re.findall(pattern, config)) == 1
53 53
54 54 def assert_group_location_directive(self, config, group_path):
55 55 pattern = u'<Location "{location}{group_path}">'.format(
56 56 location=self.location_root, group_path=group_path)
57 57 assert len(re.findall(pattern, config)) == 1
58 58
59 59 def test_render_mod_dav_svn_config(self):
60 60 repo_groups = self.get_repo_group_mocks(count=10)
61 61 generated_config = utils._render_mod_dav_svn_config(
62 62 parent_path_root=self.parent_path_root,
63 63 list_parent_path=True,
64 64 location_root=self.location_root,
65 65 repo_groups=repo_groups,
66 66 realm=self.realm,
67 67 use_ssl=True,
68 68 template=''
69 69 )
70 70 # Assert that one location directive exists for each repository group.
71 71 for group in repo_groups:
72 72 self.assert_group_location_directive(
73 73 generated_config, group.full_path)
74 74
75 75 # Assert that the root location directive exists.
76 76 self.assert_root_location_directive(generated_config)
77 77
78 78 def test_render_mod_dav_svn_config_with_alternative_template(self, tmpdir):
79 79 repo_groups = self.get_repo_group_mocks(count=10)
80 80 test_file_path = os.path.join(str(tmpdir), 'example.mako')
81 81 with open(test_file_path, 'wb') as f:
82 82 f.write('TEST_EXAMPLE\n')
83 83
84 84 generated_config = utils._render_mod_dav_svn_config(
85 85 parent_path_root=self.parent_path_root,
86 86 list_parent_path=True,
87 87 location_root=self.location_root,
88 88 repo_groups=repo_groups,
89 89 realm=self.realm,
90 90 use_ssl=True,
91 91 template=test_file_path
92 92 )
93 93 assert 'TEST_EXAMPLE' in generated_config
94 94
95 95 @pytest.mark.parametrize('list_parent_path', [True, False])
96 96 @pytest.mark.parametrize('use_ssl', [True, False])
97 97 def test_list_parent_path(self, list_parent_path, use_ssl):
98 98 generated_config = utils._render_mod_dav_svn_config(
99 99 parent_path_root=self.parent_path_root,
100 100 list_parent_path=list_parent_path,
101 101 location_root=self.location_root,
102 102 repo_groups=self.get_repo_group_mocks(count=10),
103 103 realm=self.realm,
104 104 use_ssl=use_ssl,
105 105 template=''
106 106 )
107 107
108 108 # Assert that correct configuration directive is present.
109 109 if list_parent_path:
110 110 assert not re.search('SVNListParentPath\s+Off', generated_config)
111 111 assert re.search('SVNListParentPath\s+On', generated_config)
112 112 else:
113 113 assert re.search('SVNListParentPath\s+Off', generated_config)
114 114 assert not re.search('SVNListParentPath\s+On', generated_config)
115 115
116 116 if use_ssl:
117 117 assert 'RequestHeader edit Destination ^https: http: early' \
118 118 in generated_config
119 119 else:
120 120 assert '#RequestHeader edit Destination ^https: http: early' \
121 121 in generated_config
@@ -1,99 +1,99 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import codecs
22 22 import logging
23 23 import os
24 24 from pyramid.renderers import render
25 25
26 26 from rhodecode.events import trigger
27 27 from rhodecode.lib.utils import get_rhodecode_realm, get_rhodecode_base_path
28 28 from rhodecode.lib.utils2 import str2bool
29 29 from rhodecode.model.db import RepoGroup
30 30
31 31 from . import config_keys
32 32 from .events import ModDavSvnConfigChange
33 33
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def write_mod_dav_svn_config(settings):
39 39 use_ssl = str2bool(settings['force_https'])
40 40 file_path = settings[config_keys.config_file_path]
41 41 config = _render_mod_dav_svn_config(
42 42 use_ssl=use_ssl,
43 43 parent_path_root=get_rhodecode_base_path(),
44 44 list_parent_path=settings[config_keys.list_parent_path],
45 45 location_root=settings[config_keys.location_root],
46 46 repo_groups=RepoGroup.get_all_repo_groups(),
47 47 realm=get_rhodecode_realm(), template=settings[config_keys.template])
48 48 _write_mod_dav_svn_config(config, file_path)
49 49 return file_path
50 50
51 51
52 52 def generate_mod_dav_svn_config(registry):
53 53 """
54 54 Generate the configuration file for use with subversion's mod_dav_svn
55 55 module. The configuration has to contain a <Location> block for each
56 56 available repository group because the mod_dav_svn module does not support
57 57 repositories organized in sub folders.
58 58 """
59 59 settings = registry.settings
60 60 file_path = write_mod_dav_svn_config(settings)
61 61
62 62 # Trigger an event on mod dav svn configuration change.
63 63 trigger(ModDavSvnConfigChange(), registry)
64 64 return file_path
65 65
66 66
67 67 def _render_mod_dav_svn_config(
68 68 parent_path_root, list_parent_path, location_root, repo_groups, realm,
69 69 use_ssl, template):
70 70 """
71 71 Render mod_dav_svn configuration to string.
72 72 """
73 73 repo_group_paths = []
74 74 for repo_group in repo_groups:
75 75 group_path = repo_group.full_path_splitted
76 76 location = os.path.join(location_root, *group_path)
77 77 parent_path = os.path.join(parent_path_root, *group_path)
78 78 repo_group_paths.append((location, parent_path))
79 79
80 80 context = {
81 81 'location_root': location_root,
82 82 'parent_path_root': parent_path_root,
83 83 'repo_group_paths': repo_group_paths,
84 84 'svn_list_parent_path': list_parent_path,
85 85 'rhodecode_realm': realm,
86 86 'use_https': use_ssl,
87 87 }
88 88 template = template or \
89 89 'rhodecode:apps/svn_support/templates/mod-dav-svn.conf.mako'
90 90 # Render the configuration template to string.
91 91 return render(template, context)
92 92
93 93
94 94 def _write_mod_dav_svn_config(config, filepath):
95 95 """
96 96 Write mod_dav_svn config to file.
97 97 """
98 98 with codecs.open(filepath, 'w', encoding='utf-8') as f:
99 99 f.write(config)
@@ -1,161 +1,161 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps._base.navigation import NavigationRegistry
23 23 from rhodecode.apps._base import ADMIN_PREFIX
24 24 from rhodecode.lib.utils2 import str2bool
25 25
26 26
27 27 def admin_routes(config):
28 28 """
29 29 User groups /_admin prefixed routes
30 30 """
31 31 from rhodecode.apps.user_group.views import UserGroupsView
32 32
33 33 config.add_route(
34 34 name='user_group_members_data',
35 35 pattern='/user_groups/{user_group_id:\d+}/members',
36 36 user_group_route=True)
37 37 config.add_view(
38 38 UserGroupsView,
39 39 attr='user_group_members',
40 40 route_name='user_group_members_data', request_method='GET',
41 41 renderer='json_ext', xhr=True)
42 42
43 43 # user groups perms
44 44 config.add_route(
45 45 name='edit_user_group_perms_summary',
46 46 pattern='/user_groups/{user_group_id:\d+}/edit/permissions_summary',
47 47 user_group_route=True)
48 48 config.add_view(
49 49 UserGroupsView,
50 50 attr='user_group_perms_summary',
51 51 route_name='edit_user_group_perms_summary', request_method='GET',
52 52 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
53 53
54 54 config.add_route(
55 55 name='edit_user_group_perms_summary_json',
56 56 pattern='/user_groups/{user_group_id:\d+}/edit/permissions_summary/json',
57 57 user_group_route=True)
58 58 config.add_view(
59 59 UserGroupsView,
60 60 attr='user_group_perms_summary_json',
61 61 route_name='edit_user_group_perms_summary_json', request_method='GET',
62 62 renderer='json_ext')
63 63
64 64 # user groups edit
65 65 config.add_route(
66 66 name='edit_user_group',
67 67 pattern='/user_groups/{user_group_id:\d+}/edit',
68 68 user_group_route=True)
69 69 config.add_view(
70 70 UserGroupsView,
71 71 attr='user_group_edit',
72 72 route_name='edit_user_group', request_method='GET',
73 73 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
74 74
75 75 # user groups update
76 76 config.add_route(
77 77 name='user_groups_update',
78 78 pattern='/user_groups/{user_group_id:\d+}/update',
79 79 user_group_route=True)
80 80 config.add_view(
81 81 UserGroupsView,
82 82 attr='user_group_update',
83 83 route_name='user_groups_update', request_method='POST',
84 84 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
85 85
86 86 config.add_route(
87 87 name='edit_user_group_global_perms',
88 88 pattern='/user_groups/{user_group_id:\d+}/edit/global_permissions',
89 89 user_group_route=True)
90 90 config.add_view(
91 91 UserGroupsView,
92 92 attr='user_group_global_perms_edit',
93 93 route_name='edit_user_group_global_perms', request_method='GET',
94 94 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
95 95
96 96 config.add_route(
97 97 name='edit_user_group_global_perms_update',
98 98 pattern='/user_groups/{user_group_id:\d+}/edit/global_permissions/update',
99 99 user_group_route=True)
100 100 config.add_view(
101 101 UserGroupsView,
102 102 attr='user_group_global_perms_update',
103 103 route_name='edit_user_group_global_perms_update', request_method='POST',
104 104 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
105 105
106 106 config.add_route(
107 107 name='edit_user_group_perms',
108 108 pattern='/user_groups/{user_group_id:\d+}/edit/permissions',
109 109 user_group_route=True)
110 110 config.add_view(
111 111 UserGroupsView,
112 112 attr='user_group_edit_perms',
113 113 route_name='edit_user_group_perms', request_method='GET',
114 114 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
115 115
116 116 config.add_route(
117 117 name='edit_user_group_perms_update',
118 118 pattern='/user_groups/{user_group_id:\d+}/edit/permissions/update',
119 119 user_group_route=True)
120 120 config.add_view(
121 121 UserGroupsView,
122 122 attr='user_group_update_perms',
123 123 route_name='edit_user_group_perms_update', request_method='POST',
124 124 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
125 125
126 126 config.add_route(
127 127 name='edit_user_group_advanced',
128 128 pattern='/user_groups/{user_group_id:\d+}/edit/advanced',
129 129 user_group_route=True)
130 130 config.add_view(
131 131 UserGroupsView,
132 132 attr='user_group_edit_advanced',
133 133 route_name='edit_user_group_advanced', request_method='GET',
134 134 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
135 135
136 136 config.add_route(
137 137 name='edit_user_group_advanced_sync',
138 138 pattern='/user_groups/{user_group_id:\d+}/edit/advanced/sync',
139 139 user_group_route=True)
140 140 config.add_view(
141 141 UserGroupsView,
142 142 attr='user_group_edit_advanced_set_synchronization',
143 143 route_name='edit_user_group_advanced_sync', request_method='POST',
144 144 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
145 145
146 146 # user groups delete
147 147 config.add_route(
148 148 name='user_groups_delete',
149 149 pattern='/user_groups/{user_group_id:\d+}/delete',
150 150 user_group_route=True)
151 151 config.add_view(
152 152 UserGroupsView,
153 153 attr='user_group_delete',
154 154 route_name='user_groups_delete', request_method='POST',
155 155 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
156 156
157 157
158 158 def includeme(config):
159 159 # main admin routes
160 160 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
161 161
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/\
@@ -1,254 +1,253 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests import (
24 23 TestController, assert_session_flash, TEST_USER_ADMIN_LOGIN)
25 24 from rhodecode.model.db import UserGroup
26 25 from rhodecode.model.meta import Session
27 26 from rhodecode.tests.fixture import Fixture
28 27
29 28 fixture = Fixture()
30 29
31 30
32 31 def route_path(name, params=None, **kwargs):
33 32 import urllib.request, urllib.parse, urllib.error
34 33 from rhodecode.apps._base import ADMIN_PREFIX
35 34
36 35 base_url = {
37 36 'user_groups': ADMIN_PREFIX + '/user_groups',
38 37 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
39 38 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
40 39 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
41 40 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
42 41 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
43 42 'edit_user_group_advanced_sync': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/advanced/sync',
44 43 'edit_user_group_global_perms_update': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/global_permissions/update',
45 44 'user_groups_update': ADMIN_PREFIX + '/user_groups/{user_group_id}/update',
46 45 'user_groups_delete': ADMIN_PREFIX + '/user_groups/{user_group_id}/delete',
47 46
48 47 }[name].format(**kwargs)
49 48
50 49 if params:
51 50 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 51 return base_url
53 52
54 53
55 54 class TestUserGroupsView(TestController):
56 55
57 56 def test_set_synchronization(self, user_util):
58 57 self.log_user()
59 58 user_group_name = user_util.create_user_group().users_group_name
60 59
61 60 group = Session().query(UserGroup).filter(
62 61 UserGroup.users_group_name == user_group_name).one()
63 62
64 63 assert group.group_data.get('extern_type') is None
65 64
66 65 # enable
67 66 self.app.post(
68 67 route_path('edit_user_group_advanced_sync',
69 68 user_group_id=group.users_group_id),
70 69 params={'csrf_token': self.csrf_token}, status=302)
71 70
72 71 group = Session().query(UserGroup).filter(
73 72 UserGroup.users_group_name == user_group_name).one()
74 73 assert group.group_data.get('extern_type') == 'manual'
75 74 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
76 75
77 76 # disable
78 77 self.app.post(
79 78 route_path('edit_user_group_advanced_sync',
80 79 user_group_id=group.users_group_id),
81 80 params={'csrf_token': self.csrf_token}, status=302)
82 81
83 82 group = Session().query(UserGroup).filter(
84 83 UserGroup.users_group_name == user_group_name).one()
85 84 assert group.group_data.get('extern_type') is None
86 85 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
87 86
88 87 def test_delete_user_group(self, user_util):
89 88 self.log_user()
90 89 user_group_id = user_util.create_user_group().users_group_id
91 90
92 91 group = Session().query(UserGroup).filter(
93 92 UserGroup.users_group_id == user_group_id).one()
94 93
95 94 self.app.post(
96 95 route_path('user_groups_delete', user_group_id=group.users_group_id),
97 96 params={'csrf_token': self.csrf_token})
98 97
99 98 group = Session().query(UserGroup).filter(
100 99 UserGroup.users_group_id == user_group_id).scalar()
101 100
102 101 assert group is None
103 102
104 103 @pytest.mark.parametrize('repo_create, repo_create_write, user_group_create, repo_group_create, fork_create, inherit_default_permissions, expect_error, expect_form_error', [
105 104 ('hg.create.none', 'hg.create.write_on_repogroup.false', 'hg.usergroup.create.false', 'hg.repogroup.create.false', 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
106 105 ('hg.create.repository', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, False),
107 106 ('hg.create.XXX', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, True),
108 107 ('', '', '', '', '', '', True, False),
109 108 ])
110 109 def test_global_permissions_on_user_group(
111 110 self, repo_create, repo_create_write, user_group_create,
112 111 repo_group_create, fork_create, expect_error, expect_form_error,
113 112 inherit_default_permissions, user_util):
114 113
115 114 self.log_user()
116 115 user_group = user_util.create_user_group()
117 116
118 117 user_group_name = user_group.users_group_name
119 118 user_group_id = user_group.users_group_id
120 119
121 120 # ENABLE REPO CREATE ON A GROUP
122 121 perm_params = {
123 122 'inherit_default_permissions': False,
124 123 'default_repo_create': repo_create,
125 124 'default_repo_create_on_write': repo_create_write,
126 125 'default_user_group_create': user_group_create,
127 126 'default_repo_group_create': repo_group_create,
128 127 'default_fork_create': fork_create,
129 128 'default_inherit_default_permissions': inherit_default_permissions,
130 129
131 130 'csrf_token': self.csrf_token,
132 131 }
133 132 response = self.app.post(
134 133 route_path('edit_user_group_global_perms_update',
135 134 user_group_id=user_group_id),
136 135 params=perm_params)
137 136
138 137 if expect_form_error:
139 138 assert response.status_int == 200
140 139 response.mustcontain('Value must be one of')
141 140 else:
142 141 if expect_error:
143 142 msg = 'An error occurred during permissions saving'
144 143 else:
145 144 msg = 'User Group global permissions updated successfully'
146 145 ug = UserGroup.get_by_group_name(user_group_name)
147 146 del perm_params['csrf_token']
148 147 del perm_params['inherit_default_permissions']
149 148 assert perm_params == ug.get_default_perms()
150 149 assert_session_flash(response, msg)
151 150
152 151 def test_edit_view(self, user_util):
153 152 self.log_user()
154 153
155 154 user_group = user_util.create_user_group()
156 155 self.app.get(
157 156 route_path('edit_user_group',
158 157 user_group_id=user_group.users_group_id),
159 158 status=200)
160 159
161 160 def test_update_user_group(self, user_util):
162 161 user = self.log_user()
163 162
164 163 user_group = user_util.create_user_group()
165 164 users_group_id = user_group.users_group_id
166 165 new_name = user_group.users_group_name + '_CHANGE'
167 166
168 167 params = [
169 168 ('users_group_active', False),
170 169 ('user_group_description', 'DESC'),
171 170 ('users_group_name', new_name),
172 171 ('user', user['username']),
173 172 ('csrf_token', self.csrf_token),
174 173 ('__start__', 'user_group_members:sequence'),
175 174 ('__start__', 'member:mapping'),
176 175 ('member_user_id', user['user_id']),
177 176 ('type', 'existing'),
178 177 ('__end__', 'member:mapping'),
179 178 ('__end__', 'user_group_members:sequence'),
180 179 ]
181 180
182 181 self.app.post(
183 182 route_path('user_groups_update',
184 183 user_group_id=users_group_id),
185 184 params=params,
186 185 status=302)
187 186
188 187 user_group = UserGroup.get(users_group_id)
189 188 assert user_group
190 189
191 190 assert user_group.users_group_name == new_name
192 191 assert user_group.user_group_description == 'DESC'
193 192 assert user_group.users_group_active == False
194 193
195 194 def test_update_user_group_name_conflicts(self, user_util):
196 195 self.log_user()
197 196 user_group_old = user_util.create_user_group()
198 197 new_name = user_group_old.users_group_name
199 198
200 199 user_group = user_util.create_user_group()
201 200
202 201 params = dict(
203 202 users_group_active=False,
204 203 user_group_description='DESC',
205 204 users_group_name=new_name,
206 205 csrf_token=self.csrf_token)
207 206
208 207 response = self.app.post(
209 208 route_path('user_groups_update',
210 209 user_group_id=user_group.users_group_id),
211 210 params=params,
212 211 status=200)
213 212
214 213 response.mustcontain('User group `{}` already exists'.format(
215 214 new_name))
216 215
217 216 def test_update_members_from_user_ids(self, user_regular):
218 217 uid = user_regular.user_id
219 218 username = user_regular.username
220 219 self.log_user()
221 220
222 221 user_group = fixture.create_user_group('test_gr_ids')
223 222 assert user_group.members == []
224 223 assert user_group.user != user_regular
225 224 expected_active_state = not user_group.users_group_active
226 225
227 226 form_data = [
228 227 ('csrf_token', self.csrf_token),
229 228 ('user', username),
230 229 ('users_group_name', 'changed_name'),
231 230 ('users_group_active', expected_active_state),
232 231 ('user_group_description', 'changed_description'),
233 232
234 233 ('__start__', 'user_group_members:sequence'),
235 234 ('__start__', 'member:mapping'),
236 235 ('member_user_id', uid),
237 236 ('type', 'existing'),
238 237 ('__end__', 'member:mapping'),
239 238 ('__end__', 'user_group_members:sequence'),
240 239 ]
241 240 ugid = user_group.users_group_id
242 241 self.app.post(
243 242 route_path('user_groups_update', user_group_id=ugid), form_data)
244 243
245 244 user_group = UserGroup.get(ugid)
246 245 assert user_group
247 246
248 247 assert user_group.members[0].user_id == uid
249 248 assert user_group.user_id == uid
250 249 assert 'changed_name' in user_group.users_group_name
251 250 assert 'changed_description' in user_group.user_group_description
252 251 assert user_group.users_group_active == expected_active_state
253 252
254 253 fixture.destroy_user_group(user_group)
@@ -1,80 +1,79 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.tests.utils import permission_update_data_generator
24 23
25 24
26 25 def route_path(name, params=None, **kwargs):
27 26 import urllib.request, urllib.parse, urllib.error
28 27 from rhodecode.apps._base import ADMIN_PREFIX
29 28
30 29 base_url = {
31 30 'edit_user_group_perms':
32 31 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions',
33 32 'edit_user_group_perms_update':
34 33 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions/update',
35 34 }[name].format(**kwargs)
36 35
37 36 if params:
38 37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 38 return base_url
40 39
41 40
42 41 @pytest.mark.usefixtures("app")
43 42 class TestUserGroupPermissionsView(object):
44 43
45 44 def test_edit_perms_view(self, user_util, autologin_user):
46 45 user_group = user_util.create_user_group()
47 46 self.app.get(
48 47 route_path('edit_user_group_perms',
49 48 user_group_id=user_group.users_group_id), status=200)
50 49
51 50 def test_update_permissions(self, csrf_token, user_util):
52 51 user_group = user_util.create_user_group()
53 52 user_group_id = user_group.users_group_id
54 53 user = user_util.create_user()
55 54 user_id = user.user_id
56 55 username = user.username
57 56
58 57 # grant new
59 58 form_data = permission_update_data_generator(
60 59 csrf_token,
61 60 default='usergroup.write',
62 61 grant=[(user_id, 'usergroup.write', username, 'user')])
63 62
64 63 response = self.app.post(
65 64 route_path('edit_user_group_perms_update',
66 65 user_group_id=user_group_id), form_data).follow()
67 66
68 67 assert 'User Group permissions updated' in response
69 68
70 69 # revoke given
71 70 form_data = permission_update_data_generator(
72 71 csrf_token,
73 72 default='usergroup.read',
74 73 revoke=[(user_id, 'user')])
75 74
76 75 response = self.app.post(
77 76 route_path('edit_user_group_perms_update',
78 77 user_group_id=user_group_id), form_data).follow()
79 78
80 79 assert 'User Group permissions updated' in response
@@ -1,514 +1,514 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import peppercorn
24 24 import formencode
25 25 import formencode.htmlfill
26 26 from pyramid.httpexceptions import HTTPFound
27 27
28 28 from pyramid.response import Response
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
32 32 from rhodecode.lib.exceptions import (
33 33 RepoGroupAssignmentError, UserGroupAssignedException)
34 34 from rhodecode.model.forms import (
35 35 UserGroupPermsForm, UserGroupForm, UserIndividualPermissionsForm,
36 36 UserPermissionsForm)
37 37 from rhodecode.model.permission import PermissionModel
38 38
39 39 from rhodecode.apps._base import UserGroupAppView
40 40 from rhodecode.lib.auth import (
41 41 LoginRequired, HasUserGroupPermissionAnyDecorator, CSRFRequired)
42 42 from rhodecode.lib import helpers as h, audit_logger
43 43 from rhodecode.lib.utils2 import str2bool, safe_int
44 44 from rhodecode.model.db import User, UserGroup
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.user_group import UserGroupModel
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class UserGroupsView(UserGroupAppView):
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55
56 56 PermissionModel().set_global_permission_choices(
57 57 c, gettext_translator=self.request.translate)
58 58
59 59 return c
60 60
61 61 @LoginRequired()
62 62 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
63 63 def user_group_members(self):
64 64 """
65 65 Return members of given user group
66 66 """
67 67 self.load_default_context()
68 68 user_group = self.db_user_group
69 69 group_members_obj = sorted((x.user for x in user_group.members),
70 70 key=lambda u: u.username.lower())
71 71
72 72 group_members = [
73 73 {
74 74 'id': user.user_id,
75 75 'first_name': user.first_name,
76 76 'last_name': user.last_name,
77 77 'username': user.username,
78 78 'icon_link': h.gravatar_url(user.email, 30),
79 79 'value_display': h.person(user.email),
80 80 'value': user.username,
81 81 'value_type': 'user',
82 82 'active': user.active,
83 83 }
84 84 for user in group_members_obj
85 85 ]
86 86
87 87 return {
88 88 'members': group_members
89 89 }
90 90
91 91 @LoginRequired()
92 92 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
93 93 def user_group_perms_summary(self):
94 94 c = self.load_default_context()
95 95 c.user_group = self.db_user_group
96 96 c.active = 'perms_summary'
97 97 c.permissions = UserGroupModel().get_perms_summary(
98 98 c.user_group.users_group_id)
99 99 return self._get_template_context(c)
100 100
101 101 @LoginRequired()
102 102 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
103 103 def user_group_perms_summary_json(self):
104 104 self.load_default_context()
105 105 user_group = self.db_user_group
106 106 return UserGroupModel().get_perms_summary(user_group.users_group_id)
107 107
108 108 def _revoke_perms_on_yourself(self, form_result):
109 109 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
110 110 form_result['perm_updates'])
111 111 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
112 112 form_result['perm_additions'])
113 113 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
114 114 form_result['perm_deletions'])
115 115 admin_perm = 'usergroup.admin'
116 116 if _updates and _updates[0][1] != admin_perm or \
117 117 _additions and _additions[0][1] != admin_perm or \
118 118 _deletions and _deletions[0][1] != admin_perm:
119 119 return True
120 120 return False
121 121
122 122 @LoginRequired()
123 123 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
124 124 @CSRFRequired()
125 125 def user_group_update(self):
126 126 _ = self.request.translate
127 127
128 128 user_group = self.db_user_group
129 129 user_group_id = user_group.users_group_id
130 130
131 131 old_user_group_name = self.db_user_group_name
132 132 new_user_group_name = old_user_group_name
133 133
134 134 c = self.load_default_context()
135 135 c.user_group = user_group
136 136 c.group_members_obj = [x.user for x in c.user_group.members]
137 137 c.group_members_obj.sort(key=lambda u: u.username.lower())
138 138 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
139 139 c.active = 'settings'
140 140
141 141 users_group_form = UserGroupForm(
142 142 self.request.translate, edit=True,
143 143 old_data=c.user_group.get_dict(), allow_disabled=True)()
144 144
145 145 old_values = c.user_group.get_api_data()
146 146
147 147 try:
148 148 form_result = users_group_form.to_python(self.request.POST)
149 149 pstruct = peppercorn.parse(self.request.POST.items())
150 150 form_result['users_group_members'] = pstruct['user_group_members']
151 151
152 152 user_group, added_members, removed_members = \
153 153 UserGroupModel().update(c.user_group, form_result)
154 154 new_user_group_name = form_result['users_group_name']
155 155
156 156 for user_id in added_members:
157 157 user = User.get(user_id)
158 158 user_data = user.get_api_data()
159 159 audit_logger.store_web(
160 160 'user_group.edit.member.add',
161 161 action_data={'user': user_data, 'old_data': old_values},
162 162 user=self._rhodecode_user)
163 163
164 164 for user_id in removed_members:
165 165 user = User.get(user_id)
166 166 user_data = user.get_api_data()
167 167 audit_logger.store_web(
168 168 'user_group.edit.member.delete',
169 169 action_data={'user': user_data, 'old_data': old_values},
170 170 user=self._rhodecode_user)
171 171
172 172 audit_logger.store_web(
173 173 'user_group.edit', action_data={'old_data': old_values},
174 174 user=self._rhodecode_user)
175 175
176 176 h.flash(_('Updated user group %s') % new_user_group_name,
177 177 category='success')
178 178
179 179 affected_user_ids = []
180 180 for user_id in added_members + removed_members:
181 181 affected_user_ids.append(user_id)
182 182
183 183 name_changed = old_user_group_name != new_user_group_name
184 184 if name_changed:
185 185 owner = User.get_by_username(form_result['user'])
186 186 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
187 187 affected_user_ids.append(self._rhodecode_user.user_id)
188 188 affected_user_ids.append(owner_id)
189 189
190 190 PermissionModel().trigger_permission_flush(affected_user_ids)
191 191
192 192 Session().commit()
193 193 except formencode.Invalid as errors:
194 194 defaults = errors.value
195 195 e = errors.error_dict or {}
196 196
197 197 data = render(
198 198 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
199 199 self._get_template_context(c), self.request)
200 200 html = formencode.htmlfill.render(
201 201 data,
202 202 defaults=defaults,
203 203 errors=e,
204 204 prefix_error=False,
205 205 encoding="UTF-8",
206 206 force_defaults=False
207 207 )
208 208 return Response(html)
209 209
210 210 except Exception:
211 211 log.exception("Exception during update of user group")
212 212 h.flash(_('Error occurred during update of user group %s')
213 213 % new_user_group_name, category='error')
214 214
215 215 raise HTTPFound(
216 216 h.route_path('edit_user_group', user_group_id=user_group_id))
217 217
218 218 @LoginRequired()
219 219 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
220 220 @CSRFRequired()
221 221 def user_group_delete(self):
222 222 _ = self.request.translate
223 223 user_group = self.db_user_group
224 224
225 225 self.load_default_context()
226 226 force = str2bool(self.request.POST.get('force'))
227 227
228 228 old_values = user_group.get_api_data()
229 229 try:
230 230 UserGroupModel().delete(user_group, force=force)
231 231 audit_logger.store_web(
232 232 'user.delete', action_data={'old_data': old_values},
233 233 user=self._rhodecode_user)
234 234 Session().commit()
235 235 h.flash(_('Successfully deleted user group'), category='success')
236 236 except UserGroupAssignedException as e:
237 237 h.flash(str(e), category='error')
238 238 except Exception:
239 239 log.exception("Exception during deletion of user group")
240 240 h.flash(_('An error occurred during deletion of user group'),
241 241 category='error')
242 242 raise HTTPFound(h.route_path('user_groups'))
243 243
244 244 @LoginRequired()
245 245 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
246 246 def user_group_edit(self):
247 247 user_group = self.db_user_group
248 248
249 249 c = self.load_default_context()
250 250 c.user_group = user_group
251 251 c.group_members_obj = [x.user for x in c.user_group.members]
252 252 c.group_members_obj.sort(key=lambda u: u.username.lower())
253 253 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
254 254
255 255 c.active = 'settings'
256 256
257 257 defaults = user_group.get_dict()
258 258 # fill owner
259 259 if user_group.user:
260 260 defaults.update({'user': user_group.user.username})
261 261 else:
262 262 replacement_user = User.get_first_super_admin().username
263 263 defaults.update({'user': replacement_user})
264 264
265 265 data = render(
266 266 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
267 267 self._get_template_context(c), self.request)
268 268 html = formencode.htmlfill.render(
269 269 data,
270 270 defaults=defaults,
271 271 encoding="UTF-8",
272 272 force_defaults=False
273 273 )
274 274 return Response(html)
275 275
276 276 @LoginRequired()
277 277 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
278 278 def user_group_edit_perms(self):
279 279 user_group = self.db_user_group
280 280 c = self.load_default_context()
281 281 c.user_group = user_group
282 282 c.active = 'perms'
283 283
284 284 defaults = {}
285 285 # fill user group users
286 286 for p in c.user_group.user_user_group_to_perm:
287 287 defaults.update({'u_perm_%s' % p.user.user_id:
288 288 p.permission.permission_name})
289 289
290 290 for p in c.user_group.user_group_user_group_to_perm:
291 291 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
292 292 p.permission.permission_name})
293 293
294 294 data = render(
295 295 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
296 296 self._get_template_context(c), self.request)
297 297 html = formencode.htmlfill.render(
298 298 data,
299 299 defaults=defaults,
300 300 encoding="UTF-8",
301 301 force_defaults=False
302 302 )
303 303 return Response(html)
304 304
305 305 @LoginRequired()
306 306 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
307 307 @CSRFRequired()
308 308 def user_group_update_perms(self):
309 309 """
310 310 grant permission for given user group
311 311 """
312 312 _ = self.request.translate
313 313
314 314 user_group = self.db_user_group
315 315 user_group_id = user_group.users_group_id
316 316 c = self.load_default_context()
317 317 c.user_group = user_group
318 318 form = UserGroupPermsForm(self.request.translate)().to_python(self.request.POST)
319 319
320 320 if not self._rhodecode_user.is_admin:
321 321 if self._revoke_perms_on_yourself(form):
322 322 msg = _('Cannot change permission for yourself as admin')
323 323 h.flash(msg, category='warning')
324 324 raise HTTPFound(
325 325 h.route_path('edit_user_group_perms',
326 326 user_group_id=user_group_id))
327 327
328 328 try:
329 329 changes = UserGroupModel().update_permissions(
330 330 user_group,
331 331 form['perm_additions'], form['perm_updates'],
332 332 form['perm_deletions'])
333 333
334 334 except RepoGroupAssignmentError:
335 335 h.flash(_('Target group cannot be the same'), category='error')
336 336 raise HTTPFound(
337 337 h.route_path('edit_user_group_perms',
338 338 user_group_id=user_group_id))
339 339
340 340 action_data = {
341 341 'added': changes['added'],
342 342 'updated': changes['updated'],
343 343 'deleted': changes['deleted'],
344 344 }
345 345 audit_logger.store_web(
346 346 'user_group.edit.permissions', action_data=action_data,
347 347 user=self._rhodecode_user)
348 348
349 349 Session().commit()
350 350 h.flash(_('User Group permissions updated'), category='success')
351 351
352 352 affected_user_ids = []
353 353 for change in changes['added'] + changes['updated'] + changes['deleted']:
354 354 if change['type'] == 'user':
355 355 affected_user_ids.append(change['id'])
356 356 if change['type'] == 'user_group':
357 357 user_group = UserGroup.get(safe_int(change['id']))
358 358 if user_group:
359 359 group_members_ids = [x.user_id for x in user_group.members]
360 360 affected_user_ids.extend(group_members_ids)
361 361
362 362 PermissionModel().trigger_permission_flush(affected_user_ids)
363 363
364 364 raise HTTPFound(
365 365 h.route_path('edit_user_group_perms', user_group_id=user_group_id))
366 366
367 367 @LoginRequired()
368 368 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
369 369 def user_group_global_perms_edit(self):
370 370 user_group = self.db_user_group
371 371 c = self.load_default_context()
372 372 c.user_group = user_group
373 373 c.active = 'global_perms'
374 374
375 375 c.default_user = User.get_default_user()
376 376 defaults = c.user_group.get_dict()
377 377 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
378 378 defaults.update(c.user_group.get_default_perms())
379 379
380 380 data = render(
381 381 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
382 382 self._get_template_context(c), self.request)
383 383 html = formencode.htmlfill.render(
384 384 data,
385 385 defaults=defaults,
386 386 encoding="UTF-8",
387 387 force_defaults=False
388 388 )
389 389 return Response(html)
390 390
391 391 @LoginRequired()
392 392 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
393 393 @CSRFRequired()
394 394 def user_group_global_perms_update(self):
395 395 _ = self.request.translate
396 396 user_group = self.db_user_group
397 397 user_group_id = self.db_user_group.users_group_id
398 398
399 399 c = self.load_default_context()
400 400 c.user_group = user_group
401 401 c.active = 'global_perms'
402 402
403 403 try:
404 404 # first stage that verifies the checkbox
405 405 _form = UserIndividualPermissionsForm(self.request.translate)
406 406 form_result = _form.to_python(dict(self.request.POST))
407 407 inherit_perms = form_result['inherit_default_permissions']
408 408 user_group.inherit_default_permissions = inherit_perms
409 409 Session().add(user_group)
410 410
411 411 if not inherit_perms:
412 412 # only update the individual ones if we un check the flag
413 413 _form = UserPermissionsForm(
414 414 self.request.translate,
415 415 [x[0] for x in c.repo_create_choices],
416 416 [x[0] for x in c.repo_create_on_write_choices],
417 417 [x[0] for x in c.repo_group_create_choices],
418 418 [x[0] for x in c.user_group_create_choices],
419 419 [x[0] for x in c.fork_choices],
420 420 [x[0] for x in c.inherit_default_permission_choices])()
421 421
422 422 form_result = _form.to_python(dict(self.request.POST))
423 423 form_result.update(
424 424 {'perm_user_group_id': user_group.users_group_id})
425 425
426 426 PermissionModel().update_user_group_permissions(form_result)
427 427
428 428 Session().commit()
429 429 h.flash(_('User Group global permissions updated successfully'),
430 430 category='success')
431 431
432 432 except formencode.Invalid as errors:
433 433 defaults = errors.value
434 434
435 435 data = render(
436 436 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
437 437 self._get_template_context(c), self.request)
438 438 html = formencode.htmlfill.render(
439 439 data,
440 440 defaults=defaults,
441 441 errors=errors.error_dict or {},
442 442 prefix_error=False,
443 443 encoding="UTF-8",
444 444 force_defaults=False
445 445 )
446 446 return Response(html)
447 447 except Exception:
448 448 log.exception("Exception during permissions saving")
449 449 h.flash(_('An error occurred during permissions saving'),
450 450 category='error')
451 451
452 452 raise HTTPFound(
453 453 h.route_path('edit_user_group_global_perms',
454 454 user_group_id=user_group_id))
455 455
456 456 @LoginRequired()
457 457 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
458 458 def user_group_edit_advanced(self):
459 459 user_group = self.db_user_group
460 460
461 461 c = self.load_default_context()
462 462 c.user_group = user_group
463 463 c.active = 'advanced'
464 464 c.group_members_obj = sorted(
465 465 (x.user for x in c.user_group.members),
466 466 key=lambda u: u.username.lower())
467 467
468 468 c.group_to_repos = sorted(
469 469 (x.repository for x in c.user_group.users_group_repo_to_perm),
470 470 key=lambda u: u.repo_name.lower())
471 471
472 472 c.group_to_repo_groups = sorted(
473 473 (x.group for x in c.user_group.users_group_repo_group_to_perm),
474 474 key=lambda u: u.group_name.lower())
475 475
476 476 c.group_to_review_rules = sorted(
477 477 (x.users_group for x in c.user_group.user_group_review_rules),
478 478 key=lambda u: u.users_group_name.lower())
479 479
480 480 return self._get_template_context(c)
481 481
482 482 @LoginRequired()
483 483 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
484 484 @CSRFRequired()
485 485 def user_group_edit_advanced_set_synchronization(self):
486 486 _ = self.request.translate
487 487 user_group = self.db_user_group
488 488 user_group_id = user_group.users_group_id
489 489
490 490 existing = user_group.group_data.get('extern_type')
491 491
492 492 if existing:
493 493 new_state = user_group.group_data
494 494 new_state['extern_type'] = None
495 495 else:
496 496 new_state = user_group.group_data
497 497 new_state['extern_type'] = 'manual'
498 498 new_state['extern_type_set_by'] = self._rhodecode_user.username
499 499
500 500 try:
501 501 user_group.group_data = new_state
502 502 Session().add(user_group)
503 503 Session().commit()
504 504
505 505 h.flash(_('User Group synchronization updated successfully'),
506 506 category='success')
507 507 except Exception:
508 508 log.exception("Exception during sync settings saving")
509 509 h.flash(_('An error occurred during synchronization update'),
510 510 category='error')
511 511
512 512 raise HTTPFound(
513 513 h.route_path('edit_user_group_advanced',
514 514 user_group_id=user_group_id))
@@ -1,32 +1,32 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 def includeme(config):
23 23 from rhodecode.apps.user_group_profile.views import UserGroupProfileView
24 24
25 25 config.add_route(
26 26 name='user_group_profile',
27 27 pattern='/_profile_user_group/{user_group_name}')
28 28 config.add_view(
29 29 UserGroupProfileView,
30 30 attr='user_group_profile',
31 31 route_name='user_group_profile', request_method='GET',
32 32 renderer='rhodecode:templates/user_group/user_group.mako')
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,76 +1,75 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19 from rhodecode.model.user_group import UserGroupModel
21 20 from rhodecode.tests import (
22 21 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
23 22 from rhodecode.tests.fixture import Fixture
24 23 from rhodecode.tests.utils import AssertResponse
25 24
26 25 fixture = Fixture()
27 26
28 27
29 28 def route_path(name, **kwargs):
30 29 return '/_profile_user_group/{user_group_name}'.format(**kwargs)
31 30
32 31
33 32 class TestUsersController(TestController):
34 33
35 34 def test_user_group_profile(self, user_util):
36 35 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
37 36 user, usergroup = user_util.create_user_with_group()
38 37
39 38 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
40 39 response.mustcontain(usergroup.users_group_name)
41 40 response.mustcontain(user.username)
42 41
43 42 def test_user_can_check_own_group(self, user_util):
44 43 user = user_util.create_user(
45 44 TEST_USER_REGULAR_LOGIN, password=TEST_USER_REGULAR_PASS, email='testme@rhodecode.org')
46 45 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
47 46 usergroup = user_util.create_user_group(owner=user)
48 47 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
49 48 response.mustcontain(usergroup.users_group_name)
50 49 response.mustcontain(user.username)
51 50
52 51 def test_user_can_not_check_other_group(self, user_util):
53 52 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
54 53 user_group = user_util.create_user_group()
55 54 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.none')
56 55 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=404)
57 56 assert response.status_code == 404
58 57
59 58 def test_another_user_can_check_if_he_is_in_group(self, user_util):
60 59 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
61 60 user = user_util.create_user(
62 61 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
63 62 user_group = user_util.create_user_group()
64 63 UserGroupModel().add_user_to_group(user_group, user)
65 64 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.read')
66 65 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name))
67 66 response.mustcontain(user_group.users_group_name)
68 67 response.mustcontain(user.username)
69 68
70 69 def test_with_anonymous_user(self, user_util):
71 70 user = user_util.create_user(
72 71 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
73 72 user_group = user_util.create_user_group()
74 73 UserGroupModel().add_user_to_group(user_group, user)
75 74 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=302)
76 75 assert response.status_code == 302 No newline at end of file
@@ -1,50 +1,50 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPNotFound
24 24
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.lib.auth import HasUserGroupPermissionAnyDecorator, LoginRequired, NotAnonymous
28 28 from rhodecode.model.db import UserGroup, User
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class UserGroupProfileView(BaseAppView):
35 35
36 36 @LoginRequired()
37 37 @NotAnonymous()
38 38 @HasUserGroupPermissionAnyDecorator('usergroup.read', 'usergroup.write', 'usergroup.admin',)
39 39 def user_group_profile(self):
40 40 c = self._get_local_tmpl_context()
41 41 c.active = 'profile'
42 42 self.db_user_group_name = self.request.matchdict.get('user_group_name')
43 43 c.user_group = UserGroup().get_by_group_name(self.db_user_group_name)
44 44 if not c.user_group:
45 45 raise HTTPNotFound()
46 46 group_members_obj = sorted((x.user for x in c.user_group.members),
47 47 key=lambda u: u.username.lower())
48 48 c.group_members = group_members_obj
49 49 c.anonymous = self._rhodecode_user.username == User.DEFAULT_USER
50 50 return self._get_template_context(c)
@@ -1,32 +1,32 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 def includeme(config):
23 23 from rhodecode.apps.user_profile.views import UserProfileView
24 24
25 25 config.add_route(
26 26 name='user_profile',
27 27 pattern='/_profiles/{username}')
28 28 config.add_view(
29 29 UserProfileView,
30 30 attr='user_profile',
31 31 route_name='user_profile', request_method='GET',
32 32 renderer='rhodecode:templates/users/user.mako')
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,75 +1,74 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21
23 22 from rhodecode.model.db import User
24 23 from rhodecode.tests import (
25 24 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
26 25 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
27 26 from rhodecode.tests.fixture import Fixture
28 27 from rhodecode.tests.utils import AssertResponse
29 28
30 29 fixture = Fixture()
31 30
32 31
33 32 def route_path(name, **kwargs):
34 33 return '/_profiles/{username}'.format(**kwargs)
35 34
36 35
37 36 class TestUsersController(TestController):
38 37
39 38 def test_user_profile(self, user_util):
40 39 edit_link_css = '.user-profile .panel-edit'
41 40 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
42 41 user = user_util.create_user(
43 42 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
44 43 username = user.username
45 44
46 45 response = self.app.get(route_path('user_profile', username=username))
47 46 response.mustcontain('testme')
48 47 response.mustcontain('testme@rhodecode.org')
49 48 assert_response = response.assert_response()
50 49 assert_response.no_element_exists(edit_link_css)
51 50
52 51 # edit should be available to superadmin users
53 52 self.logout_user()
54 53 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
55 54 response = self.app.get(route_path('user_profile', username=username))
56 55 assert_response = response.assert_response()
57 56 assert_response.element_contains(edit_link_css, 'Edit')
58 57
59 58 def test_user_profile_not_available(self, user_util):
60 59 user = user_util.create_user()
61 60 username = user.username
62 61
63 62 # not logged in, redirect
64 63 self.app.get(route_path('user_profile', username=username), status=302)
65 64
66 65 self.log_user()
67 66 # after log-in show
68 67 self.app.get(route_path('user_profile', username=username), status=200)
69 68
70 69 # default user, not allowed to show it
71 70 self.app.get(
72 71 route_path('user_profile', username=User.DEFAULT_USER), status=404)
73 72
74 73 # actual 404
75 74 self.app.get(route_path('user_profile', username='unknown'), status=404)
@@ -1,49 +1,49 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPNotFound
24 24
25 25 from rhodecode.apps._base import BaseAppView
26 26 from rhodecode.lib.auth import LoginRequired, NotAnonymous
27 27
28 28 from rhodecode.model.db import User
29 29 from rhodecode.model.user import UserModel
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class UserProfileView(BaseAppView):
35 35
36 36 @LoginRequired()
37 37 @NotAnonymous()
38 38 def user_profile(self):
39 39 # register local template context
40 40 c = self._get_local_tmpl_context()
41 41 c.active = 'user_profile'
42 42
43 43 username = self.request.matchdict.get('username')
44 44
45 45 c.user = UserModel().get_by_username(username)
46 46 if not c.user or c.user.username == User.DEFAULT_USER:
47 47 raise HTTPNotFound()
48 48
49 49 return self._get_template_context(c)
General Comments 0
You need to be logged in to leave comments. Login now