##// END OF EJS Templates
fix(2fa): fixed redirect loop in workflow when password reset was done.
super-admin -
r5370:6dc425cb default
parent child Browse files
Show More
@@ -1,986 +1,987 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import time
20 20 import logging
21 21 import operator
22 22
23 23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
24 24
25 25 from rhodecode.lib import helpers as h, diffs, rc_cache
26 26 from rhodecode.lib.str_utils import safe_str
27 27 from rhodecode.lib.utils import repo_name_slug
28 28 from rhodecode.lib.utils2 import (
29 29 StrictAttributeDict,
30 30 str2bool,
31 31 safe_int,
32 32 datetime_to_time,
33 33 )
34 34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
35 35 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 36 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
37 37 from rhodecode.model import repo
38 38 from rhodecode.model import repo_group
39 39 from rhodecode.model import user_group
40 40 from rhodecode.model import user
41 41 from rhodecode.model.db import User
42 42 from rhodecode.model.scm import ScmModel
43 43 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
44 44 from rhodecode.model.repo import ReadmeFinder
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 ADMIN_PREFIX: str = "/_admin"
50 50 STATIC_FILE_PREFIX: str = "/_static"
51 51
52 52 URL_NAME_REQUIREMENTS = {
53 53 # group name can have a slash in them, but they must not end with a slash
54 54 "group_name": r".*?[^/]",
55 55 "repo_group_name": r".*?[^/]",
56 56 # repo names can have a slash in them, but they must not end with a slash
57 57 "repo_name": r".*?[^/]",
58 58 # file path eats up everything at the end
59 59 "f_path": r".*",
60 60 # reference types
61 61 "source_ref_type": r"(branch|book|tag|rev|\%\(source_ref_type\)s)",
62 62 "target_ref_type": r"(branch|book|tag|rev|\%\(target_ref_type\)s)",
63 63 }
64 64
65 65
66 66 def add_route_with_slash(config, name, pattern, **kw):
67 67 config.add_route(name, pattern, **kw)
68 68 if not pattern.endswith("/"):
69 69 config.add_route(name + "_slash", pattern + "/", **kw)
70 70
71 71
72 72 def add_route_requirements(route_path, requirements=None):
73 73 """
74 74 Adds regex requirements to pyramid routes using a mapping dict
75 75 e.g::
76 76 add_route_requirements('{repo_name}/settings')
77 77 """
78 78 requirements = requirements or URL_NAME_REQUIREMENTS
79 79 for key, regex in list(requirements.items()):
80 80 route_path = route_path.replace("{%s}" % key, "{%s:%s}" % (key, regex))
81 81 return route_path
82 82
83 83
84 84 def get_format_ref_id(repo):
85 85 """Returns a `repo` specific reference formatter function"""
86 86 if h.is_svn(repo):
87 87 return _format_ref_id_svn
88 88 else:
89 89 return _format_ref_id
90 90
91 91
92 92 def _format_ref_id(name, raw_id):
93 93 """Default formatting of a given reference `name`"""
94 94 return name
95 95
96 96
97 97 def _format_ref_id_svn(name, raw_id):
98 98 """Special way of formatting a reference for Subversion including path"""
99 99 return f"{name}@{raw_id}"
100 100
101 101
102 102 class TemplateArgs(StrictAttributeDict):
103 103 pass
104 104
105 105
106 106 class BaseAppView(object):
107 107 DONT_CHECKOUT_VIEWS = ["channelstream_connect", "ops_ping"]
108 108 EXTRA_VIEWS_TO_IGNORE = ['login', 'register', 'logout']
109 109 SETUP_2FA_VIEW = 'setup_2fa'
110 110 VERIFY_2FA_VIEW = 'check_2fa'
111 111
112 112 def __init__(self, context, request):
113 113 self.request = request
114 114 self.context = context
115 115 self.session = request.session
116 116 if not hasattr(request, "user"):
117 117 # NOTE(marcink): edge case, we ended up in matched route
118 118 # but probably of web-app context, e.g API CALL/VCS CALL
119 119 if hasattr(request, "vcs_call") or hasattr(request, "rpc_method"):
120 120 log.warning("Unable to process request `%s` in this scope", request)
121 121 raise HTTPBadRequest()
122 122
123 123 self._rhodecode_user = request.user # auth user
124 124 self._rhodecode_db_user = self._rhodecode_user.get_instance()
125 125 self.user_data = self._rhodecode_db_user.user_data if self._rhodecode_db_user else {}
126 126 self._maybe_needs_password_change(
127 127 request.matched_route.name, self._rhodecode_db_user
128 128 )
129 129 self._maybe_needs_2fa_configuration(
130 130 request.matched_route.name, self._rhodecode_db_user
131 131 )
132 132 self._maybe_needs_2fa_check(
133 133 request.matched_route.name, self._rhodecode_db_user
134 134 )
135 135
136 136 def _maybe_needs_password_change(self, view_name, user_obj):
137 137 if view_name in self.DONT_CHECKOUT_VIEWS:
138 138 return
139 139
140 140 log.debug(
141 141 "Checking if user %s needs password change on view %s", user_obj, view_name
142 142 )
143 143
144 144 skip_user_views = [
145 145 "logout",
146 146 "login",
147 "check_2fa",
147 148 "my_account_password",
148 149 "my_account_password_update",
149 150 ]
150 151
151 152 if not user_obj:
152 153 return
153 154
154 155 if user_obj.username == User.DEFAULT_USER:
155 156 return
156 157
157 158 now = time.time()
158 159 should_change = self.user_data.get("force_password_change")
159 160 change_after = safe_int(should_change) or 0
160 161 if should_change and now > change_after:
161 162 log.debug("User %s requires password change", user_obj)
162 163 h.flash(
163 164 "You are required to change your password",
164 165 "warning",
165 166 ignore_duplicate=True,
166 167 )
167 168
168 169 if view_name not in skip_user_views:
169 170 raise HTTPFound(self.request.route_path("my_account_password"))
170 171
171 172 def _maybe_needs_2fa_configuration(self, view_name, user_obj):
172 173 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
173 174 return
174 175
175 176 if not user_obj:
176 177 return
177 178
178 179 if user_obj.has_forced_2fa and user_obj.extern_type != 'rhodecode':
179 180 return
180 181
181 182 if user_obj.needs_2fa_configure and view_name != self.SETUP_2FA_VIEW:
182 183 h.flash(
183 184 "You are required to configure 2FA",
184 185 "warning",
185 186 ignore_duplicate=False,
186 187 )
187 188 raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW))
188 189
189 190 def _maybe_needs_2fa_check(self, view_name, user_obj):
190 191 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
191 192 return
192 193
193 194 if not user_obj:
194 195 return
195 196
196 197 if user_obj.has_check_2fa_flag and view_name != self.VERIFY_2FA_VIEW:
197 198 raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW))
198 199
199 200 def _log_creation_exception(self, e, repo_name):
200 201 _ = self.request.translate
201 202 reason = None
202 203 if len(e.args) == 2:
203 204 reason = e.args[1]
204 205
205 206 if reason == "INVALID_CERTIFICATE":
206 207 log.exception("Exception creating a repository: invalid certificate")
207 208 msg = _("Error creating repository %s: invalid certificate") % repo_name
208 209 else:
209 210 log.exception("Exception creating a repository")
210 211 msg = _("Error creating repository %s") % repo_name
211 212 return msg
212 213
213 214 def _get_local_tmpl_context(self, include_app_defaults=True):
214 215 c = TemplateArgs()
215 216 c.auth_user = self.request.user
216 217 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
217 218 c.rhodecode_user = self.request.user
218 219
219 220 if include_app_defaults:
220 221 from rhodecode.lib.base import attach_context_attributes
221 222
222 223 attach_context_attributes(c, self.request, self.request.user.user_id)
223 224
224 225 c.is_super_admin = c.auth_user.is_admin
225 226
226 227 c.can_create_repo = c.is_super_admin
227 228 c.can_create_repo_group = c.is_super_admin
228 229 c.can_create_user_group = c.is_super_admin
229 230
230 231 c.is_delegated_admin = False
231 232
232 233 if not c.auth_user.is_default and not c.is_super_admin:
233 234 c.can_create_repo = h.HasPermissionAny("hg.create.repository")(
234 235 user=self.request.user
235 236 )
236 237 repositories = c.auth_user.repositories_admin or c.can_create_repo
237 238
238 239 c.can_create_repo_group = h.HasPermissionAny("hg.repogroup.create.true")(
239 240 user=self.request.user
240 241 )
241 242 repository_groups = (
242 243 c.auth_user.repository_groups_admin or c.can_create_repo_group
243 244 )
244 245
245 246 c.can_create_user_group = h.HasPermissionAny("hg.usergroup.create.true")(
246 247 user=self.request.user
247 248 )
248 249 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
249 250 # delegated admin can create, or manage some objects
250 251 c.is_delegated_admin = repositories or repository_groups or user_groups
251 252 return c
252 253
253 254 def _get_template_context(self, tmpl_args, **kwargs):
254 255 local_tmpl_args = {"defaults": {}, "errors": {}, "c": tmpl_args}
255 256 local_tmpl_args.update(kwargs)
256 257 return local_tmpl_args
257 258
258 259 def load_default_context(self):
259 260 """
260 261 example:
261 262
262 263 def load_default_context(self):
263 264 c = self._get_local_tmpl_context()
264 265 c.custom_var = 'foobar'
265 266
266 267 return c
267 268 """
268 269 raise NotImplementedError("Needs implementation in view class")
269 270
270 271
271 272 class RepoAppView(BaseAppView):
272 273 def __init__(self, context, request):
273 274 super().__init__(context, request)
274 275 self.db_repo = request.db_repo
275 276 self.db_repo_name = self.db_repo.repo_name
276 277 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
277 278 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
278 279 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
279 280
280 281 def _handle_missing_requirements(self, error):
281 282 log.error(
282 283 "Requirements are missing for repository %s: %s",
283 284 self.db_repo_name,
284 285 safe_str(error),
285 286 )
286 287
287 288 def _prepare_and_set_clone_url(self, c):
288 289 username = ""
289 290 if self._rhodecode_user.username != User.DEFAULT_USER:
290 291 username = self._rhodecode_user.username
291 292
292 293 _def_clone_uri = c.clone_uri_tmpl
293 294 _def_clone_uri_id = c.clone_uri_id_tmpl
294 295 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
295 296
296 297 c.clone_repo_url = self.db_repo.clone_url(
297 298 user=username, uri_tmpl=_def_clone_uri
298 299 )
299 300 c.clone_repo_url_id = self.db_repo.clone_url(
300 301 user=username, uri_tmpl=_def_clone_uri_id
301 302 )
302 303 c.clone_repo_url_ssh = self.db_repo.clone_url(
303 304 uri_tmpl=_def_clone_uri_ssh, ssh=True
304 305 )
305 306
306 307 def _get_local_tmpl_context(self, include_app_defaults=True):
307 308 _ = self.request.translate
308 309 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
309 310
310 311 # register common vars for this type of view
311 312 c.rhodecode_db_repo = self.db_repo
312 313 c.repo_name = self.db_repo_name
313 314 c.repository_pull_requests = self.db_repo_pull_requests
314 315 c.repository_artifacts = self.db_repo_artifacts
315 316 c.repository_is_user_following = ScmModel().is_following_repo(
316 317 self.db_repo_name, self._rhodecode_user.user_id
317 318 )
318 319 self.path_filter = PathFilter(None)
319 320
320 321 c.repository_requirements_missing = {}
321 322 try:
322 323 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
323 324 # NOTE(marcink):
324 325 # comparison to None since if it's an object __bool__ is expensive to
325 326 # calculate
326 327 if self.rhodecode_vcs_repo is not None:
327 328 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
328 329 c.auth_user.username
329 330 )
330 331 self.path_filter = PathFilter(path_perms)
331 332 except RepositoryRequirementError as e:
332 333 c.repository_requirements_missing = {"error": str(e)}
333 334 self._handle_missing_requirements(e)
334 335 self.rhodecode_vcs_repo = None
335 336
336 337 c.path_filter = self.path_filter # used by atom_feed_entry.mako
337 338
338 339 if self.rhodecode_vcs_repo is None:
339 340 # unable to fetch this repo as vcs instance, report back to user
340 341 log.debug(
341 342 "Repository was not found on filesystem, check if it exists or is not damaged"
342 343 )
343 344 h.flash(
344 345 _(
345 346 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
346 347 "Please check if it exist, or is not damaged."
347 348 )
348 349 % {"repo_name": c.repo_name},
349 350 category="error",
350 351 ignore_duplicate=True,
351 352 )
352 353 if c.repository_requirements_missing:
353 354 route = self.request.matched_route.name
354 355 if route.startswith(("edit_repo", "repo_summary")):
355 356 # allow summary and edit repo on missing requirements
356 357 return c
357 358
358 359 raise HTTPFound(
359 360 h.route_path("repo_summary", repo_name=self.db_repo_name)
360 361 )
361 362
362 363 else: # redirect if we don't show missing requirements
363 364 raise HTTPFound(h.route_path("home"))
364 365
365 366 c.has_origin_repo_read_perm = False
366 367 if self.db_repo.fork:
367 368 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
368 369 "repository.write", "repository.read", "repository.admin"
369 370 )(self.db_repo.fork.repo_name, "summary fork link")
370 371
371 372 return c
372 373
373 374 def _get_f_path_unchecked(self, matchdict, default=None):
374 375 """
375 376 Should only be used by redirects, everything else should call _get_f_path
376 377 """
377 378 f_path = matchdict.get("f_path")
378 379 if f_path:
379 380 # fix for multiple initial slashes that causes errors for GIT
380 381 return f_path.lstrip("/")
381 382
382 383 return default
383 384
384 385 def _get_f_path(self, matchdict, default=None):
385 386 f_path_match = self._get_f_path_unchecked(matchdict, default)
386 387 return self.path_filter.assert_path_permissions(f_path_match)
387 388
388 389 def _get_general_setting(self, target_repo, settings_key, default=False):
389 390 settings_model = VcsSettingsModel(repo=target_repo)
390 391 settings = settings_model.get_general_settings()
391 392 return settings.get(settings_key, default)
392 393
393 394 def _get_repo_setting(self, target_repo, settings_key, default=False):
394 395 settings_model = VcsSettingsModel(repo=target_repo)
395 396 settings = settings_model.get_repo_settings_inherited()
396 397 return settings.get(settings_key, default)
397 398
398 399 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"):
399 400 log.debug("Looking for README file at path %s", path)
400 401 if commit_id:
401 402 landing_commit_id = commit_id
402 403 else:
403 404 landing_commit = db_repo.get_landing_commit()
404 405 if isinstance(landing_commit, EmptyCommit):
405 406 return None, None
406 407 landing_commit_id = landing_commit.raw_id
407 408
408 409 cache_namespace_uid = f"repo.{db_repo.repo_id}"
409 410 region = rc_cache.get_or_create_region(
410 411 "cache_repo", cache_namespace_uid, use_async_runner=False
411 412 )
412 413 start = time.time()
413 414
414 415 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
415 416 def generate_repo_readme(
416 417 repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type
417 418 ):
418 419 readme_data = None
419 420 readme_filename = None
420 421
421 422 commit = db_repo.get_commit(_commit_id)
422 423 log.debug("Searching for a README file at commit %s.", _commit_id)
423 424 readme_node = ReadmeFinder(_renderer_type).search(
424 425 commit, path=_readme_search_path
425 426 )
426 427
427 428 if readme_node:
428 429 log.debug("Found README node: %s", readme_node)
429 430
430 431 relative_urls = {
431 432 "raw": h.route_path(
432 433 "repo_file_raw",
433 434 repo_name=_repo_name,
434 435 commit_id=commit.raw_id,
435 436 f_path=readme_node.path,
436 437 ),
437 438 "standard": h.route_path(
438 439 "repo_files",
439 440 repo_name=_repo_name,
440 441 commit_id=commit.raw_id,
441 442 f_path=readme_node.path,
442 443 ),
443 444 }
444 445
445 446 readme_data = self._render_readme_or_none(
446 447 commit, readme_node, relative_urls
447 448 )
448 449 readme_filename = readme_node.str_path
449 450
450 451 return readme_data, readme_filename
451 452
452 453 readme_data, readme_filename = generate_repo_readme(
453 454 db_repo.repo_id,
454 455 landing_commit_id,
455 456 db_repo.repo_name,
456 457 path,
457 458 renderer_type,
458 459 )
459 460
460 461 compute_time = time.time() - start
461 462 log.debug(
462 463 "Repo README for path %s generated and computed in %.4fs",
463 464 path,
464 465 compute_time,
465 466 )
466 467 return readme_data, readme_filename
467 468
468 469 def _render_readme_or_none(self, commit, readme_node, relative_urls):
469 470 log.debug("Found README file `%s` rendering...", readme_node.path)
470 471 renderer = MarkupRenderer()
471 472 try:
472 473 html_source = renderer.render(
473 474 readme_node.str_content, filename=readme_node.path
474 475 )
475 476 if relative_urls:
476 477 return relative_links(html_source, relative_urls)
477 478 return html_source
478 479 except Exception:
479 480 log.exception("Exception while trying to render the README")
480 481
481 482 def get_recache_flag(self):
482 483 for flag_name in ["force_recache", "force-recache", "no-cache"]:
483 484 flag_val = self.request.GET.get(flag_name)
484 485 if str2bool(flag_val):
485 486 return True
486 487 return False
487 488
488 489 def get_commit_preload_attrs(cls):
489 490 pre_load = [
490 491 "author",
491 492 "branch",
492 493 "date",
493 494 "message",
494 495 "parents",
495 496 "obsolete",
496 497 "phase",
497 498 "hidden",
498 499 ]
499 500 return pre_load
500 501
501 502
502 503 class PathFilter(object):
503 504 # Expects and instance of BasePathPermissionChecker or None
504 505 def __init__(self, permission_checker):
505 506 self.permission_checker = permission_checker
506 507
507 508 def assert_path_permissions(self, path):
508 509 if self.path_access_allowed(path):
509 510 return path
510 511 raise HTTPForbidden()
511 512
512 513 def path_access_allowed(self, path):
513 514 log.debug("Checking ACL permissions for PathFilter for `%s`", path)
514 515 if self.permission_checker:
515 516 has_access = path and self.permission_checker.has_access(path)
516 517 log.debug(
517 518 "ACL Permissions checker enabled, ACL Check has_access: %s", has_access
518 519 )
519 520 return has_access
520 521
521 522 log.debug("ACL permissions checker not enabled, skipping...")
522 523 return True
523 524
524 525 def filter_patchset(self, patchset):
525 526 if not self.permission_checker or not patchset:
526 527 return patchset, False
527 528 had_filtered = False
528 529 filtered_patchset = []
529 530 for patch in patchset:
530 531 filename = patch.get("filename", None)
531 532 if not filename or self.permission_checker.has_access(filename):
532 533 filtered_patchset.append(patch)
533 534 else:
534 535 had_filtered = True
535 536 if had_filtered:
536 537 if isinstance(patchset, diffs.LimitedDiffContainer):
537 538 filtered_patchset = diffs.LimitedDiffContainer(
538 539 patchset.diff_limit, patchset.cur_diff_size, filtered_patchset
539 540 )
540 541 return filtered_patchset, True
541 542 else:
542 543 return patchset, False
543 544
544 545 def render_patchset_filtered(
545 546 self, diffset, patchset, source_ref=None, target_ref=None
546 547 ):
547 548 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
548 549 result = diffset.render_patchset(
549 550 filtered_patchset, source_ref=source_ref, target_ref=target_ref
550 551 )
551 552 result.has_hidden_changes = has_hidden_changes
552 553 return result
553 554
554 555 def get_raw_patch(self, diff_processor):
555 556 if self.permission_checker is None:
556 557 return diff_processor.as_raw()
557 558 elif self.permission_checker.has_full_access:
558 559 return diff_processor.as_raw()
559 560 else:
560 561 return "# Repository has user-specific filters, raw patch generation is disabled."
561 562
562 563 @property
563 564 def is_enabled(self):
564 565 return self.permission_checker is not None
565 566
566 567
567 568 class RepoGroupAppView(BaseAppView):
568 569 def __init__(self, context, request):
569 570 super().__init__(context, request)
570 571 self.db_repo_group = request.db_repo_group
571 572 self.db_repo_group_name = self.db_repo_group.group_name
572 573
573 574 def _get_local_tmpl_context(self, include_app_defaults=True):
574 575 _ = self.request.translate
575 576 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
576 577 c.repo_group = self.db_repo_group
577 578 return c
578 579
579 580 def _revoke_perms_on_yourself(self, form_result):
580 581 _updates = [
581 582 u
582 583 for u in form_result["perm_updates"]
583 584 if self._rhodecode_user.user_id == int(u[0])
584 585 ]
585 586 _additions = [
586 587 u
587 588 for u in form_result["perm_additions"]
588 589 if self._rhodecode_user.user_id == int(u[0])
589 590 ]
590 591 _deletions = [
591 592 u
592 593 for u in form_result["perm_deletions"]
593 594 if self._rhodecode_user.user_id == int(u[0])
594 595 ]
595 596 admin_perm = "group.admin"
596 597 if (
597 598 _updates
598 599 and _updates[0][1] != admin_perm
599 600 or _additions
600 601 and _additions[0][1] != admin_perm
601 602 or _deletions
602 603 and _deletions[0][1] != admin_perm
603 604 ):
604 605 return True
605 606 return False
606 607
607 608
608 609 class UserGroupAppView(BaseAppView):
609 610 def __init__(self, context, request):
610 611 super().__init__(context, request)
611 612 self.db_user_group = request.db_user_group
612 613 self.db_user_group_name = self.db_user_group.users_group_name
613 614
614 615
615 616 class UserAppView(BaseAppView):
616 617 def __init__(self, context, request):
617 618 super().__init__(context, request)
618 619 self.db_user = request.db_user
619 620 self.db_user_id = self.db_user.user_id
620 621
621 622 _ = self.request.translate
622 623 if not request.db_user_supports_default:
623 624 if self.db_user.username == User.DEFAULT_USER:
624 625 h.flash(
625 626 _("Editing user `{}` is disabled.".format(User.DEFAULT_USER)),
626 627 category="warning",
627 628 )
628 629 raise HTTPFound(h.route_path("users"))
629 630
630 631
631 632 class DataGridAppView(object):
632 633 """
633 634 Common class to have re-usable grid rendering components
634 635 """
635 636
636 637 def _extract_ordering(self, request, column_map=None):
637 638 column_map = column_map or {}
638 639 column_index = safe_int(request.GET.get("order[0][column]"))
639 640 order_dir = request.GET.get("order[0][dir]", "desc")
640 641 order_by = request.GET.get("columns[%s][data][sort]" % column_index, "name_raw")
641 642
642 643 # translate datatable to DB columns
643 644 order_by = column_map.get(order_by) or order_by
644 645
645 646 search_q = request.GET.get("search[value]")
646 647 return search_q, order_by, order_dir
647 648
648 649 def _extract_chunk(self, request):
649 650 start = safe_int(request.GET.get("start"), 0)
650 651 length = safe_int(request.GET.get("length"), 25)
651 652 draw = safe_int(request.GET.get("draw"))
652 653 return draw, start, length
653 654
654 655 def _get_order_col(self, order_by, model):
655 656 if isinstance(order_by, str):
656 657 try:
657 658 return operator.attrgetter(order_by)(model)
658 659 except AttributeError:
659 660 return None
660 661 else:
661 662 return order_by
662 663
663 664
664 665 class BaseReferencesView(RepoAppView):
665 666 """
666 667 Base for reference view for branches, tags and bookmarks.
667 668 """
668 669
669 670 def load_default_context(self):
670 671 c = self._get_local_tmpl_context()
671 672 return c
672 673
673 674 def load_refs_context(self, ref_items, partials_template):
674 675 _render = self.request.get_partial_renderer(partials_template)
675 676 pre_load = ["author", "date", "message", "parents"]
676 677
677 678 is_svn = h.is_svn(self.rhodecode_vcs_repo)
678 679 is_hg = h.is_hg(self.rhodecode_vcs_repo)
679 680
680 681 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
681 682
682 683 closed_refs = {}
683 684 if is_hg:
684 685 closed_refs = self.rhodecode_vcs_repo.branches_closed
685 686
686 687 data = []
687 688 for ref_name, commit_id in ref_items:
688 689 commit = self.rhodecode_vcs_repo.get_commit(
689 690 commit_id=commit_id, pre_load=pre_load
690 691 )
691 692 closed = ref_name in closed_refs
692 693
693 694 # TODO: johbo: Unify generation of reference links
694 695 use_commit_id = "/" in ref_name or is_svn
695 696
696 697 if use_commit_id:
697 698 files_url = h.route_path(
698 699 "repo_files",
699 700 repo_name=self.db_repo_name,
700 701 f_path=ref_name if is_svn else "",
701 702 commit_id=commit_id,
702 703 _query=dict(at=ref_name),
703 704 )
704 705
705 706 else:
706 707 files_url = h.route_path(
707 708 "repo_files",
708 709 repo_name=self.db_repo_name,
709 710 f_path=ref_name if is_svn else "",
710 711 commit_id=ref_name,
711 712 _query=dict(at=ref_name),
712 713 )
713 714
714 715 data.append(
715 716 {
716 717 "name": _render("name", ref_name, files_url, closed),
717 718 "name_raw": ref_name,
718 719 "date": _render("date", commit.date),
719 720 "date_raw": datetime_to_time(commit.date),
720 721 "author": _render("author", commit.author),
721 722 "commit": _render(
722 723 "commit", commit.message, commit.raw_id, commit.idx
723 724 ),
724 725 "commit_raw": commit.idx,
725 726 "compare": _render(
726 727 "compare", format_ref_id(ref_name, commit.raw_id)
727 728 ),
728 729 }
729 730 )
730 731
731 732 return data
732 733
733 734
734 735 class RepoRoutePredicate(object):
735 736 def __init__(self, val, config):
736 737 self.val = val
737 738
738 739 def text(self):
739 740 return f"repo_route = {self.val}"
740 741
741 742 phash = text
742 743
743 744 def __call__(self, info, request):
744 745 if hasattr(request, "vcs_call"):
745 746 # skip vcs calls
746 747 return
747 748
748 749 repo_name = info["match"]["repo_name"]
749 750
750 751 repo_name_parts = repo_name.split("/")
751 752 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
752 753
753 754 if repo_name_parts != repo_slugs:
754 755 # short-skip if the repo-name doesn't follow slug rule
755 756 log.warning(
756 757 "repo_name: %s is different than slug %s", repo_name_parts, repo_slugs
757 758 )
758 759 return False
759 760
760 761 repo_model = repo.RepoModel()
761 762
762 763 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
763 764
764 765 def redirect_if_creating(route_info, db_repo):
765 766 skip_views = ["edit_repo_advanced_delete"]
766 767 route = route_info["route"]
767 768 # we should skip delete view so we can actually "remove" repositories
768 769 # if they get stuck in creating state.
769 770 if route.name in skip_views:
770 771 return
771 772
772 773 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
773 774 repo_creating_url = request.route_path(
774 775 "repo_creating", repo_name=db_repo.repo_name
775 776 )
776 777 raise HTTPFound(repo_creating_url)
777 778
778 779 if by_name_match:
779 780 # register this as request object we can re-use later
780 781 request.db_repo = by_name_match
781 782 request.db_repo_name = request.db_repo.repo_name
782 783
783 784 redirect_if_creating(info, by_name_match)
784 785 return True
785 786
786 787 by_id_match = repo_model.get_repo_by_id(repo_name)
787 788 if by_id_match:
788 789 request.db_repo = by_id_match
789 790 request.db_repo_name = request.db_repo.repo_name
790 791 redirect_if_creating(info, by_id_match)
791 792 return True
792 793
793 794 return False
794 795
795 796
796 797 class RepoForbidArchivedRoutePredicate(object):
797 798 def __init__(self, val, config):
798 799 self.val = val
799 800
800 801 def text(self):
801 802 return f"repo_forbid_archived = {self.val}"
802 803
803 804 phash = text
804 805
805 806 def __call__(self, info, request):
806 807 _ = request.translate
807 808 rhodecode_db_repo = request.db_repo
808 809
809 810 log.debug(
810 811 "%s checking if archived flag for repo for %s",
811 812 self.__class__.__name__,
812 813 rhodecode_db_repo.repo_name,
813 814 )
814 815
815 816 if rhodecode_db_repo.archived:
816 817 log.warning(
817 818 "Current view is not supported for archived repo:%s",
818 819 rhodecode_db_repo.repo_name,
819 820 )
820 821
821 822 h.flash(
822 823 h.literal(_("Action not supported for archived repository.")),
823 824 category="warning",
824 825 )
825 826 summary_url = request.route_path(
826 827 "repo_summary", repo_name=rhodecode_db_repo.repo_name
827 828 )
828 829 raise HTTPFound(summary_url)
829 830 return True
830 831
831 832
832 833 class RepoTypeRoutePredicate(object):
833 834 def __init__(self, val, config):
834 835 self.val = val or ["hg", "git", "svn"]
835 836
836 837 def text(self):
837 838 return f"repo_accepted_type = {self.val}"
838 839
839 840 phash = text
840 841
841 842 def __call__(self, info, request):
842 843 if hasattr(request, "vcs_call"):
843 844 # skip vcs calls
844 845 return
845 846
846 847 rhodecode_db_repo = request.db_repo
847 848
848 849 log.debug(
849 850 "%s checking repo type for %s in %s",
850 851 self.__class__.__name__,
851 852 rhodecode_db_repo.repo_type,
852 853 self.val,
853 854 )
854 855
855 856 if rhodecode_db_repo.repo_type in self.val:
856 857 return True
857 858 else:
858 859 log.warning(
859 860 "Current view is not supported for repo type:%s",
860 861 rhodecode_db_repo.repo_type,
861 862 )
862 863 return False
863 864
864 865
865 866 class RepoGroupRoutePredicate(object):
866 867 def __init__(self, val, config):
867 868 self.val = val
868 869
869 870 def text(self):
870 871 return f"repo_group_route = {self.val}"
871 872
872 873 phash = text
873 874
874 875 def __call__(self, info, request):
875 876 if hasattr(request, "vcs_call"):
876 877 # skip vcs calls
877 878 return
878 879
879 880 repo_group_name = info["match"]["repo_group_name"]
880 881
881 882 repo_group_name_parts = repo_group_name.split("/")
882 883 repo_group_slugs = [
883 884 x for x in [repo_name_slug(x) for x in repo_group_name_parts]
884 885 ]
885 886 if repo_group_name_parts != repo_group_slugs:
886 887 # short-skip if the repo-name doesn't follow slug rule
887 888 log.warning(
888 889 "repo_group_name: %s is different than slug %s",
889 890 repo_group_name_parts,
890 891 repo_group_slugs,
891 892 )
892 893 return False
893 894
894 895 repo_group_model = repo_group.RepoGroupModel()
895 896 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
896 897
897 898 if by_name_match:
898 899 # register this as request object we can re-use later
899 900 request.db_repo_group = by_name_match
900 901 request.db_repo_group_name = request.db_repo_group.group_name
901 902 return True
902 903
903 904 return False
904 905
905 906
906 907 class UserGroupRoutePredicate(object):
907 908 def __init__(self, val, config):
908 909 self.val = val
909 910
910 911 def text(self):
911 912 return f"user_group_route = {self.val}"
912 913
913 914 phash = text
914 915
915 916 def __call__(self, info, request):
916 917 if hasattr(request, "vcs_call"):
917 918 # skip vcs calls
918 919 return
919 920
920 921 user_group_id = info["match"]["user_group_id"]
921 922 user_group_model = user_group.UserGroup()
922 923 by_id_match = user_group_model.get(user_group_id, cache=False)
923 924
924 925 if by_id_match:
925 926 # register this as request object we can re-use later
926 927 request.db_user_group = by_id_match
927 928 return True
928 929
929 930 return False
930 931
931 932
932 933 class UserRoutePredicateBase(object):
933 934 supports_default = None
934 935
935 936 def __init__(self, val, config):
936 937 self.val = val
937 938
938 939 def text(self):
939 940 raise NotImplementedError()
940 941
941 942 def __call__(self, info, request):
942 943 if hasattr(request, "vcs_call"):
943 944 # skip vcs calls
944 945 return
945 946
946 947 user_id = info["match"]["user_id"]
947 948 user_model = user.User()
948 949 by_id_match = user_model.get(user_id, cache=False)
949 950
950 951 if by_id_match:
951 952 # register this as request object we can re-use later
952 953 request.db_user = by_id_match
953 954 request.db_user_supports_default = self.supports_default
954 955 return True
955 956
956 957 return False
957 958
958 959
959 960 class UserRoutePredicate(UserRoutePredicateBase):
960 961 supports_default = False
961 962
962 963 def text(self):
963 964 return f"user_route = {self.val}"
964 965
965 966 phash = text
966 967
967 968
968 969 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
969 970 supports_default = True
970 971
971 972 def text(self):
972 973 return f"user_with_default_route = {self.val}"
973 974
974 975 phash = text
975 976
976 977
977 978 def includeme(config):
978 979 config.add_route_predicate("repo_route", RepoRoutePredicate)
979 980 config.add_route_predicate("repo_accepted_types", RepoTypeRoutePredicate)
980 981 config.add_route_predicate(
981 982 "repo_forbid_when_archived", RepoForbidArchivedRoutePredicate
982 983 )
983 984 config.add_route_predicate("repo_group_route", RepoGroupRoutePredicate)
984 985 config.add_route_predicate("user_group_route", UserGroupRoutePredicate)
985 986 config.add_route_predicate("user_route_with_default", UserRouteWithDefaultPredicate)
986 987 config.add_route_predicate("user_route", UserRoutePredicate)
@@ -1,303 +1,308 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import gzip
21 21 import shutil
22 22 import logging
23 23 import tempfile
24 24 import urllib.parse
25 25
26 26 from webob.exc import HTTPNotFound
27 27
28 28 import rhodecode
29 from rhodecode.apps._base import ADMIN_PREFIX
29 30 from rhodecode.lib.middleware.utils import get_path_info
30 31 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
31 32 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
32 33 from rhodecode.lib.middleware.simplehg import SimpleHg
33 34 from rhodecode.lib.middleware.simplesvn import SimpleSvn
34 35 from rhodecode.lib.str_utils import safe_str
35 36 from rhodecode.model.settings import VcsSettingsModel
36 37
37 38
38 39 log = logging.getLogger(__name__)
39 40
40 41 VCS_TYPE_KEY = '_rc_vcs_type'
41 42 VCS_TYPE_SKIP = '_rc_vcs_skip'
42 43
43 44
44 45 def is_git(environ):
45 46 """
46 47 Returns True if requests should be handled by GIT wsgi middleware
47 48 """
48 49 path_info = get_path_info(environ)
49 50 is_git_path = GIT_PROTO_PAT.match(path_info)
50 51 log.debug(
51 52 'request path: `%s` detected as GIT PROTOCOL %s', path_info,
52 53 is_git_path is not None)
53 54
54 55 return is_git_path
55 56
56 57
57 58 def is_hg(environ):
58 59 """
59 60 Returns True if requests target is mercurial server - header
60 61 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
61 62 """
62 63 is_hg_path = False
63 64
64 65 http_accept = environ.get('HTTP_ACCEPT')
65 66
66 67 if http_accept and http_accept.startswith('application/mercurial'):
67 68 query = urllib.parse.parse_qs(environ['QUERY_STRING'])
68 69 if 'cmd' in query:
69 70 is_hg_path = True
70 71
71 72 path_info = get_path_info(environ)
72 73 log.debug(
73 74 'request path: `%s` detected as HG PROTOCOL %s', path_info,
74 75 is_hg_path)
75 76
76 77 return is_hg_path
77 78
78 79
79 80 def is_svn(environ):
80 81 """
81 82 Returns True if requests target is Subversion server
82 83 """
83 84
84 85 http_dav = environ.get('HTTP_DAV', '')
85 86 magic_path_segment = rhodecode.CONFIG.get(
86 87 'rhodecode_subversion_magic_path', '/!svn')
87 88 path_info = get_path_info(environ)
88 89 req_method = environ['REQUEST_METHOD']
89 90
90 91 is_svn_path = (
91 92 'subversion' in http_dav or
92 93 magic_path_segment in path_info
93 94 or req_method in ['PROPFIND', 'PROPPATCH', 'HEAD']
94 95 )
95 96 log.debug(
96 97 'request path: `%s` detected as SVN PROTOCOL %s', path_info,
97 98 is_svn_path)
98 99
99 100 return is_svn_path
100 101
101 102
102 103 class GunzipMiddleware(object):
103 104 """
104 105 WSGI middleware that unzips gzip-encoded requests before
105 106 passing on to the underlying application.
106 107 """
107 108
108 109 def __init__(self, application):
109 110 self.app = application
110 111
111 112 def __call__(self, environ, start_response):
112 113 accepts_encoding_header = safe_str(environ.get('HTTP_CONTENT_ENCODING', ''))
113 114
114 115 if 'gzip' in accepts_encoding_header:
115 116 log.debug('gzip detected, now running gunzip wrapper')
116 117 wsgi_input = environ['wsgi.input']
117 118
118 119 if not hasattr(environ['wsgi.input'], 'seek'):
119 120 # The gzip implementation in the standard library of Python 2.x
120 121 # requires the '.seek()' and '.tell()' methods to be available
121 122 # on the input stream. Read the data into a temporary file to
122 123 # work around this limitation.
123 124
124 125 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
125 126 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
126 127 wsgi_input.seek(0)
127 128
128 129 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
129 130 # since we "Ungzipped" the content we say now it's no longer gzip
130 131 # content encoding
131 132 del environ['HTTP_CONTENT_ENCODING']
132 133
133 134 # content length has changes ? or i'm not sure
134 135 if 'CONTENT_LENGTH' in environ:
135 136 del environ['CONTENT_LENGTH']
136 137 else:
137 138 log.debug('content not gzipped, gzipMiddleware passing '
138 139 'request further')
139 140 return self.app(environ, start_response)
140 141
141 142
142 143 def is_vcs_call(environ):
143 144 if VCS_TYPE_KEY in environ:
144 145 raw_type = environ[VCS_TYPE_KEY]
145 146 return raw_type and raw_type != VCS_TYPE_SKIP
146 147 return False
147 148
148 149
149 150 def detect_vcs_request(environ, backends):
150 151 checks = {
151 152 'hg': (is_hg, SimpleHg),
152 153 'git': (is_git, SimpleGit),
153 154 'svn': (is_svn, SimpleSvn),
154 155 }
155 156 handler = None
156 157 # List of path views first chunk we don't do any checks
157 158 white_list = [
158 159 # favicon often requested by browsers
159 160 'favicon.ico',
160 161
161 162 # e.g /_file_store/download
162 163 '_file_store++',
163 164
164 165 # login
165 166 "_admin/login",
166 167
168 # 2fa
169 f"{ADMIN_PREFIX}/check_2fa",
170 f"{ADMIN_PREFIX}/setup_2fa",
171
167 172 # _admin/api is safe too
168 '_admin/api',
173 f'{ADMIN_PREFIX}/api',
169 174
170 175 # _admin/gist is safe too
171 '_admin/gists++',
176 f'{ADMIN_PREFIX}/gists++',
172 177
173 178 # _admin/my_account is safe too
174 '_admin/my_account++',
179 f'{ADMIN_PREFIX}/my_account++',
175 180
176 181 # static files no detection
177 182 '_static++',
178 183
179 184 # debug-toolbar
180 185 '_debug_toolbar++',
181 186
182 187 # skip ops ping, status
183 '_admin/ops/ping',
184 '_admin/ops/status',
188 f'{ADMIN_PREFIX}/ops/ping',
189 f'{ADMIN_PREFIX}/ops/status',
185 190
186 191 # full channelstream connect should be VCS skipped
187 '_admin/channelstream/connect',
192 f'{ADMIN_PREFIX}/channelstream/connect',
188 193
189 194 '++/repo_creating_check'
190 195 ]
191 196 path_info = get_path_info(environ)
192 197 path_url = path_info.lstrip('/')
193 198 req_method = environ.get('REQUEST_METHOD')
194 199
195 200 for item in white_list:
196 201 if item.endswith('++') and path_url.startswith(item[:-2]):
197 202 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
198 203 return handler
199 204 if item.startswith('++') and path_url.endswith(item[2:]):
200 205 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
201 206 return handler
202 207 if item == path_url:
203 208 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
204 209 return handler
205 210
206 211 if VCS_TYPE_KEY in environ:
207 212 raw_type = environ[VCS_TYPE_KEY]
208 213 if raw_type == VCS_TYPE_SKIP:
209 214 log.debug('got `skip` marker for vcs detection, skipping...')
210 215 return handler
211 216
212 217 _check, handler = checks.get(raw_type) or [None, None]
213 218 if handler:
214 219 log.debug('got handler:%s from environ', handler)
215 220
216 221 if not handler:
217 222 log.debug('request start: checking if request for `%s:%s` is of VCS type in order: %s',
218 223 req_method, path_url, backends)
219 224 for vcs_type in backends:
220 225 vcs_check, _handler = checks[vcs_type]
221 226 if vcs_check(environ):
222 227 log.debug('vcs handler found %s', _handler)
223 228 handler = _handler
224 229 break
225 230
226 231 return handler
227 232
228 233
229 234 class VCSMiddleware(object):
230 235
231 236 def __init__(self, app, registry, config, appenlight_client):
232 237 self.application = app
233 238 self.registry = registry
234 239 self.config = config
235 240 self.appenlight_client = appenlight_client
236 241 self.use_gzip = True
237 242 # order in which we check the middlewares, based on vcs.backends config
238 243 self.check_middlewares = config['vcs.backends']
239 244
240 245 def vcs_config(self, repo_name=None):
241 246 """
242 247 returns serialized VcsSettings
243 248 """
244 249 try:
245 250 return VcsSettingsModel(
246 251 repo=repo_name).get_ui_settings_as_config_obj()
247 252 except Exception:
248 253 pass
249 254
250 255 def wrap_in_gzip_if_enabled(self, app, config):
251 256 if self.use_gzip:
252 257 app = GunzipMiddleware(app)
253 258 return app
254 259
255 260 def _get_handler_app(self, environ):
256 261 app = None
257 262 log.debug('VCSMiddleware: detecting vcs type.')
258 263 handler = detect_vcs_request(environ, self.check_middlewares)
259 264 if handler:
260 265 app = handler(self.config, self.registry)
261 266
262 267 return app
263 268
264 269 def __call__(self, environ, start_response):
265 270 # check if we handle one of interesting protocols, optionally extract
266 271 # specific vcsSettings and allow changes of how things are wrapped
267 272 vcs_handler = self._get_handler_app(environ)
268 273 if vcs_handler:
269 274 # translate the _REPO_ID into real repo NAME for usage
270 275 # in middleware
271 276
272 277 path_info = get_path_info(environ)
273 278 environ['PATH_INFO'] = vcs_handler._get_by_id(path_info)
274 279
275 280 # Set acl, url and vcs repo names.
276 281 vcs_handler.set_repo_names(environ)
277 282
278 283 # register repo config back to the handler
279 284 vcs_conf = self.vcs_config(vcs_handler.acl_repo_name)
280 285 # maybe damaged/non existent settings. We still want to
281 286 # pass that point to validate on is_valid_and_existing_repo
282 287 # and return proper HTTP Code back to client
283 288 if vcs_conf:
284 289 vcs_handler.repo_vcs_config = vcs_conf
285 290
286 291 # check for type, presence in database and on filesystem
287 292 if not vcs_handler.is_valid_and_existing_repo(
288 293 vcs_handler.acl_repo_name,
289 294 vcs_handler.base_path,
290 295 vcs_handler.SCM):
291 296 return HTTPNotFound()(environ, start_response)
292 297
293 298 environ['REPO_NAME'] = vcs_handler.url_repo_name
294 299
295 300 # Wrap handler in middlewares if they are enabled.
296 301 vcs_handler = self.wrap_in_gzip_if_enabled(
297 302 vcs_handler, self.config)
298 303 vcs_handler, _ = wrap_in_appenlight_if_enabled(
299 304 vcs_handler, self.config, self.appenlight_client)
300 305
301 306 return vcs_handler(environ, start_response)
302 307
303 308 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now