##// END OF EJS Templates
path-permissions: Handle scm_instance() returning None
idlsoft -
r2621:2fdb0c6c default
parent child Browse files
Show More
@@ -1,622 +1,625 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 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
26 26
27 27 from rhodecode.lib import helpers as h, diffs
28 28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 30 from rhodecode.model import repo
31 31 from rhodecode.model import repo_group
32 32 from rhodecode.model import user_group
33 33 from rhodecode.model import user
34 34 from rhodecode.model.db import User
35 35 from rhodecode.model.scm import ScmModel
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 ADMIN_PREFIX = '/_admin'
41 41 STATIC_FILE_PREFIX = '/_static'
42 42
43 43 URL_NAME_REQUIREMENTS = {
44 44 # group name can have a slash in them, but they must not end with a slash
45 45 'group_name': r'.*?[^/]',
46 46 'repo_group_name': r'.*?[^/]',
47 47 # repo names can have a slash in them, but they must not end with a slash
48 48 'repo_name': r'.*?[^/]',
49 49 # file path eats up everything at the end
50 50 'f_path': r'.*',
51 51 # reference types
52 52 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
53 53 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
54 54 }
55 55
56 56
57 57 def add_route_with_slash(config,name, pattern, **kw):
58 58 config.add_route(name, pattern, **kw)
59 59 if not pattern.endswith('/'):
60 60 config.add_route(name + '_slash', pattern + '/', **kw)
61 61
62 62
63 63 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
64 64 """
65 65 Adds regex requirements to pyramid routes using a mapping dict
66 66 e.g::
67 67 add_route_requirements('{repo_name}/settings')
68 68 """
69 69 for key, regex in requirements.items():
70 70 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
71 71 return route_path
72 72
73 73
74 74 def get_format_ref_id(repo):
75 75 """Returns a `repo` specific reference formatter function"""
76 76 if h.is_svn(repo):
77 77 return _format_ref_id_svn
78 78 else:
79 79 return _format_ref_id
80 80
81 81
82 82 def _format_ref_id(name, raw_id):
83 83 """Default formatting of a given reference `name`"""
84 84 return name
85 85
86 86
87 87 def _format_ref_id_svn(name, raw_id):
88 88 """Special way of formatting a reference for Subversion including path"""
89 89 return '%s@%s' % (name, raw_id)
90 90
91 91
92 92 class TemplateArgs(StrictAttributeDict):
93 93 pass
94 94
95 95
96 96 class BaseAppView(object):
97 97
98 98 def __init__(self, context, request):
99 99 self.request = request
100 100 self.context = context
101 101 self.session = request.session
102 102 self._rhodecode_user = request.user # auth user
103 103 self._rhodecode_db_user = self._rhodecode_user.get_instance()
104 104 self._maybe_needs_password_change(
105 105 request.matched_route.name, self._rhodecode_db_user)
106 106
107 107 def _maybe_needs_password_change(self, view_name, user_obj):
108 108 log.debug('Checking if user %s needs password change on view %s',
109 109 user_obj, view_name)
110 110 skip_user_views = [
111 111 'logout', 'login',
112 112 'my_account_password', 'my_account_password_update'
113 113 ]
114 114
115 115 if not user_obj:
116 116 return
117 117
118 118 if user_obj.username == User.DEFAULT_USER:
119 119 return
120 120
121 121 now = time.time()
122 122 should_change = user_obj.user_data.get('force_password_change')
123 123 change_after = safe_int(should_change) or 0
124 124 if should_change and now > change_after:
125 125 log.debug('User %s requires password change', user_obj)
126 126 h.flash('You are required to change your password', 'warning',
127 127 ignore_duplicate=True)
128 128
129 129 if view_name not in skip_user_views:
130 130 raise HTTPFound(
131 131 self.request.route_path('my_account_password'))
132 132
133 133 def _log_creation_exception(self, e, repo_name):
134 134 _ = self.request.translate
135 135 reason = None
136 136 if len(e.args) == 2:
137 137 reason = e.args[1]
138 138
139 139 if reason == 'INVALID_CERTIFICATE':
140 140 log.exception(
141 141 'Exception creating a repository: invalid certificate')
142 142 msg = (_('Error creating repository %s: invalid certificate')
143 143 % repo_name)
144 144 else:
145 145 log.exception("Exception creating a repository")
146 146 msg = (_('Error creating repository %s')
147 147 % repo_name)
148 148 return msg
149 149
150 150 def _get_local_tmpl_context(self, include_app_defaults=True):
151 151 c = TemplateArgs()
152 152 c.auth_user = self.request.user
153 153 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
154 154 c.rhodecode_user = self.request.user
155 155
156 156 if include_app_defaults:
157 157 from rhodecode.lib.base import attach_context_attributes
158 158 attach_context_attributes(c, self.request, self.request.user.user_id)
159 159
160 160 return c
161 161
162 162 def _get_template_context(self, tmpl_args, **kwargs):
163 163
164 164 local_tmpl_args = {
165 165 'defaults': {},
166 166 'errors': {},
167 167 'c': tmpl_args
168 168 }
169 169 local_tmpl_args.update(kwargs)
170 170 return local_tmpl_args
171 171
172 172 def load_default_context(self):
173 173 """
174 174 example:
175 175
176 176 def load_default_context(self):
177 177 c = self._get_local_tmpl_context()
178 178 c.custom_var = 'foobar'
179 179
180 180 return c
181 181 """
182 182 raise NotImplementedError('Needs implementation in view class')
183 183
184 184
185 185 class RepoAppView(BaseAppView):
186 186
187 187 def __init__(self, context, request):
188 188 super(RepoAppView, self).__init__(context, request)
189 189 self.db_repo = request.db_repo
190 190 self.db_repo_name = self.db_repo.repo_name
191 191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
192 192
193 193 def _handle_missing_requirements(self, error):
194 194 log.error(
195 195 'Requirements are missing for repository %s: %s',
196 196 self.db_repo_name, error.message)
197 197
198 198 def _get_local_tmpl_context(self, include_app_defaults=True):
199 199 _ = self.request.translate
200 200 c = super(RepoAppView, self)._get_local_tmpl_context(
201 201 include_app_defaults=include_app_defaults)
202 202
203 203 # register common vars for this type of view
204 204 c.rhodecode_db_repo = self.db_repo
205 205 c.repo_name = self.db_repo_name
206 206 c.repository_pull_requests = self.db_repo_pull_requests
207 207
208 208 c.repository_requirements_missing = False
209 209 try:
210 210 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
211 if self.rhodecode_vcs_repo:
211 212 self.path_filter = PathFilter(self.rhodecode_vcs_repo.get_path_permissions(c.auth_user.username))
213 else:
214 self.path_filter = PathFilter(None)
212 215 except RepositoryRequirementError as e:
213 216 c.repository_requirements_missing = True
214 217 self._handle_missing_requirements(e)
215 218 self.rhodecode_vcs_repo = None
216 219 self.path_filter = None
217 220
218 221 c.path_filter = self.path_filter # used by atom_feed_entry.mako
219 222
220 223 if (not c.repository_requirements_missing
221 224 and self.rhodecode_vcs_repo is None):
222 225 # unable to fetch this repo as vcs instance, report back to user
223 226 h.flash(_(
224 227 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
225 228 "Please check if it exist, or is not damaged.") %
226 229 {'repo_name': c.repo_name},
227 230 category='error', ignore_duplicate=True)
228 231 raise HTTPFound(h.route_path('home'))
229 232
230 233 return c
231 234
232 235 def _get_f_path_unchecked(self, matchdict, default=None):
233 236 """
234 237 Should only be used by redirects, everything else should call _get_f_path
235 238 """
236 239 f_path = matchdict.get('f_path')
237 240 if f_path:
238 241 # fix for multiple initial slashes that causes errors for GIT
239 242 return f_path.lstrip('/')
240 243
241 244 return default
242 245
243 246 def _get_f_path(self, matchdict, default=None):
244 247 return self.path_filter.assert_path_permissions(self._get_f_path_unchecked(matchdict, default))
245 248
246 249
247 250 class PathFilter(object):
248 251
249 252 # Expects and instance of BasePathPermissionChecker or None
250 253 def __init__(self, permission_checker):
251 254 self.permission_checker = permission_checker
252 255
253 256 def assert_path_permissions(self, path):
254 257 if path and self.permission_checker and not self.permission_checker.has_access(path):
255 258 raise HTTPForbidden()
256 259 return path
257 260
258 261 def filter_patchset(self, patchset):
259 262 if not self.permission_checker or not patchset:
260 263 return patchset, False
261 264 had_filtered = False
262 265 filtered_patchset = []
263 266 for patch in patchset:
264 267 filename = patch.get('filename', None)
265 268 if not filename or self.permission_checker.has_access(filename):
266 269 filtered_patchset.append(patch)
267 270 else:
268 271 had_filtered = True
269 272 if had_filtered:
270 273 if isinstance(patchset, diffs.LimitedDiffContainer):
271 274 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
272 275 return filtered_patchset, True
273 276 else:
274 277 return patchset, False
275 278
276 279 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
277 280 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
278 281 result = diffset.render_patchset(filtered_patchset, source_ref=source_ref, target_ref=target_ref)
279 282 result.has_hidden_changes = has_hidden_changes
280 283 return result
281 284
282 285 def get_raw_patch(self, diff_processor):
283 286 if self.permission_checker is None:
284 287 return diff_processor.as_raw()
285 288 elif self.permission_checker.has_full_access:
286 289 return diff_processor.as_raw()
287 290 else:
288 291 return '# Repository has user-specific filters, raw patch generation is disabled.'
289 292
290 293 @property
291 294 def is_enabled(self):
292 295 return self.permission_checker is not None
293 296
294 297
295 298 class RepoGroupAppView(BaseAppView):
296 299 def __init__(self, context, request):
297 300 super(RepoGroupAppView, self).__init__(context, request)
298 301 self.db_repo_group = request.db_repo_group
299 302 self.db_repo_group_name = self.db_repo_group.group_name
300 303
301 304 def _revoke_perms_on_yourself(self, form_result):
302 305 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
303 306 form_result['perm_updates'])
304 307 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
305 308 form_result['perm_additions'])
306 309 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
307 310 form_result['perm_deletions'])
308 311 admin_perm = 'group.admin'
309 312 if _updates and _updates[0][1] != admin_perm or \
310 313 _additions and _additions[0][1] != admin_perm or \
311 314 _deletions and _deletions[0][1] != admin_perm:
312 315 return True
313 316 return False
314 317
315 318
316 319 class UserGroupAppView(BaseAppView):
317 320 def __init__(self, context, request):
318 321 super(UserGroupAppView, self).__init__(context, request)
319 322 self.db_user_group = request.db_user_group
320 323 self.db_user_group_name = self.db_user_group.users_group_name
321 324
322 325
323 326 class UserAppView(BaseAppView):
324 327 def __init__(self, context, request):
325 328 super(UserAppView, self).__init__(context, request)
326 329 self.db_user = request.db_user
327 330 self.db_user_id = self.db_user.user_id
328 331
329 332 _ = self.request.translate
330 333 if not request.db_user_supports_default:
331 334 if self.db_user.username == User.DEFAULT_USER:
332 335 h.flash(_("Editing user `{}` is disabled.".format(
333 336 User.DEFAULT_USER)), category='warning')
334 337 raise HTTPFound(h.route_path('users'))
335 338
336 339
337 340 class DataGridAppView(object):
338 341 """
339 342 Common class to have re-usable grid rendering components
340 343 """
341 344
342 345 def _extract_ordering(self, request, column_map=None):
343 346 column_map = column_map or {}
344 347 column_index = safe_int(request.GET.get('order[0][column]'))
345 348 order_dir = request.GET.get(
346 349 'order[0][dir]', 'desc')
347 350 order_by = request.GET.get(
348 351 'columns[%s][data][sort]' % column_index, 'name_raw')
349 352
350 353 # translate datatable to DB columns
351 354 order_by = column_map.get(order_by) or order_by
352 355
353 356 search_q = request.GET.get('search[value]')
354 357 return search_q, order_by, order_dir
355 358
356 359 def _extract_chunk(self, request):
357 360 start = safe_int(request.GET.get('start'), 0)
358 361 length = safe_int(request.GET.get('length'), 25)
359 362 draw = safe_int(request.GET.get('draw'))
360 363 return draw, start, length
361 364
362 365 def _get_order_col(self, order_by, model):
363 366 if isinstance(order_by, basestring):
364 367 try:
365 368 return operator.attrgetter(order_by)(model)
366 369 except AttributeError:
367 370 return None
368 371 else:
369 372 return order_by
370 373
371 374
372 375 class BaseReferencesView(RepoAppView):
373 376 """
374 377 Base for reference view for branches, tags and bookmarks.
375 378 """
376 379 def load_default_context(self):
377 380 c = self._get_local_tmpl_context()
378 381
379 382
380 383 return c
381 384
382 385 def load_refs_context(self, ref_items, partials_template):
383 386 _render = self.request.get_partial_renderer(partials_template)
384 387 pre_load = ["author", "date", "message"]
385 388
386 389 is_svn = h.is_svn(self.rhodecode_vcs_repo)
387 390 is_hg = h.is_hg(self.rhodecode_vcs_repo)
388 391
389 392 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
390 393
391 394 closed_refs = {}
392 395 if is_hg:
393 396 closed_refs = self.rhodecode_vcs_repo.branches_closed
394 397
395 398 data = []
396 399 for ref_name, commit_id in ref_items:
397 400 commit = self.rhodecode_vcs_repo.get_commit(
398 401 commit_id=commit_id, pre_load=pre_load)
399 402 closed = ref_name in closed_refs
400 403
401 404 # TODO: johbo: Unify generation of reference links
402 405 use_commit_id = '/' in ref_name or is_svn
403 406
404 407 if use_commit_id:
405 408 files_url = h.route_path(
406 409 'repo_files',
407 410 repo_name=self.db_repo_name,
408 411 f_path=ref_name if is_svn else '',
409 412 commit_id=commit_id)
410 413
411 414 else:
412 415 files_url = h.route_path(
413 416 'repo_files',
414 417 repo_name=self.db_repo_name,
415 418 f_path=ref_name if is_svn else '',
416 419 commit_id=ref_name,
417 420 _query=dict(at=ref_name))
418 421
419 422 data.append({
420 423 "name": _render('name', ref_name, files_url, closed),
421 424 "name_raw": ref_name,
422 425 "date": _render('date', commit.date),
423 426 "date_raw": datetime_to_time(commit.date),
424 427 "author": _render('author', commit.author),
425 428 "commit": _render(
426 429 'commit', commit.message, commit.raw_id, commit.idx),
427 430 "commit_raw": commit.idx,
428 431 "compare": _render(
429 432 'compare', format_ref_id(ref_name, commit.raw_id)),
430 433 })
431 434
432 435 return data
433 436
434 437
435 438 class RepoRoutePredicate(object):
436 439 def __init__(self, val, config):
437 440 self.val = val
438 441
439 442 def text(self):
440 443 return 'repo_route = %s' % self.val
441 444
442 445 phash = text
443 446
444 447 def __call__(self, info, request):
445 448
446 449 if hasattr(request, 'vcs_call'):
447 450 # skip vcs calls
448 451 return
449 452
450 453 repo_name = info['match']['repo_name']
451 454 repo_model = repo.RepoModel()
452 455 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
453 456
454 457 def redirect_if_creating(db_repo):
455 458 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
456 459 raise HTTPFound(
457 460 request.route_path('repo_creating',
458 461 repo_name=db_repo.repo_name))
459 462
460 463 if by_name_match:
461 464 # register this as request object we can re-use later
462 465 request.db_repo = by_name_match
463 466 redirect_if_creating(by_name_match)
464 467 return True
465 468
466 469 by_id_match = repo_model.get_repo_by_id(repo_name)
467 470 if by_id_match:
468 471 request.db_repo = by_id_match
469 472 redirect_if_creating(by_id_match)
470 473 return True
471 474
472 475 return False
473 476
474 477
475 478 class RepoTypeRoutePredicate(object):
476 479 def __init__(self, val, config):
477 480 self.val = val or ['hg', 'git', 'svn']
478 481
479 482 def text(self):
480 483 return 'repo_accepted_type = %s' % self.val
481 484
482 485 phash = text
483 486
484 487 def __call__(self, info, request):
485 488 if hasattr(request, 'vcs_call'):
486 489 # skip vcs calls
487 490 return
488 491
489 492 rhodecode_db_repo = request.db_repo
490 493
491 494 log.debug(
492 495 '%s checking repo type for %s in %s',
493 496 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
494 497
495 498 if rhodecode_db_repo.repo_type in self.val:
496 499 return True
497 500 else:
498 501 log.warning('Current view is not supported for repo type:%s',
499 502 rhodecode_db_repo.repo_type)
500 503 #
501 504 # h.flash(h.literal(
502 505 # _('Action not supported for %s.' % rhodecode_repo.alias)),
503 506 # category='warning')
504 507 # return redirect(
505 508 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
506 509
507 510 return False
508 511
509 512
510 513 class RepoGroupRoutePredicate(object):
511 514 def __init__(self, val, config):
512 515 self.val = val
513 516
514 517 def text(self):
515 518 return 'repo_group_route = %s' % self.val
516 519
517 520 phash = text
518 521
519 522 def __call__(self, info, request):
520 523 if hasattr(request, 'vcs_call'):
521 524 # skip vcs calls
522 525 return
523 526
524 527 repo_group_name = info['match']['repo_group_name']
525 528 repo_group_model = repo_group.RepoGroupModel()
526 529 by_name_match = repo_group_model.get_by_group_name(
527 530 repo_group_name, cache=True)
528 531
529 532 if by_name_match:
530 533 # register this as request object we can re-use later
531 534 request.db_repo_group = by_name_match
532 535 return True
533 536
534 537 return False
535 538
536 539
537 540 class UserGroupRoutePredicate(object):
538 541 def __init__(self, val, config):
539 542 self.val = val
540 543
541 544 def text(self):
542 545 return 'user_group_route = %s' % self.val
543 546
544 547 phash = text
545 548
546 549 def __call__(self, info, request):
547 550 if hasattr(request, 'vcs_call'):
548 551 # skip vcs calls
549 552 return
550 553
551 554 user_group_id = info['match']['user_group_id']
552 555 user_group_model = user_group.UserGroup()
553 556 by_id_match = user_group_model.get(
554 557 user_group_id, cache=True)
555 558
556 559 if by_id_match:
557 560 # register this as request object we can re-use later
558 561 request.db_user_group = by_id_match
559 562 return True
560 563
561 564 return False
562 565
563 566
564 567 class UserRoutePredicateBase(object):
565 568 supports_default = None
566 569
567 570 def __init__(self, val, config):
568 571 self.val = val
569 572
570 573 def text(self):
571 574 raise NotImplementedError()
572 575
573 576 def __call__(self, info, request):
574 577 if hasattr(request, 'vcs_call'):
575 578 # skip vcs calls
576 579 return
577 580
578 581 user_id = info['match']['user_id']
579 582 user_model = user.User()
580 583 by_id_match = user_model.get(
581 584 user_id, cache=True)
582 585
583 586 if by_id_match:
584 587 # register this as request object we can re-use later
585 588 request.db_user = by_id_match
586 589 request.db_user_supports_default = self.supports_default
587 590 return True
588 591
589 592 return False
590 593
591 594
592 595 class UserRoutePredicate(UserRoutePredicateBase):
593 596 supports_default = False
594 597
595 598 def text(self):
596 599 return 'user_route = %s' % self.val
597 600
598 601 phash = text
599 602
600 603
601 604 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
602 605 supports_default = True
603 606
604 607 def text(self):
605 608 return 'user_with_default_route = %s' % self.val
606 609
607 610 phash = text
608 611
609 612
610 613 def includeme(config):
611 614 config.add_route_predicate(
612 615 'repo_route', RepoRoutePredicate)
613 616 config.add_route_predicate(
614 617 'repo_accepted_types', RepoTypeRoutePredicate)
615 618 config.add_route_predicate(
616 619 'repo_group_route', RepoGroupRoutePredicate)
617 620 config.add_route_predicate(
618 621 'user_group_route', UserGroupRoutePredicate)
619 622 config.add_route_predicate(
620 623 'user_route_with_default', UserRouteWithDefaultPredicate)
621 624 config.add_route_predicate(
622 625 'user_route', UserRoutePredicate) No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now