##// END OF EJS Templates
apps: removed utf8 marker
super-admin -
r5053:8da271d0 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -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 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now