##// END OF EJS Templates
chore(2fa): refactor some attributes for users
super-admin -
r5374:ced3d33b default
parent child Browse files
Show More
@@ -1,987 +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 147 "check_2fa",
148 148 "my_account_password",
149 149 "my_account_password_update",
150 150 ]
151 151
152 152 if not user_obj:
153 153 return
154 154
155 155 if user_obj.username == User.DEFAULT_USER:
156 156 return
157 157
158 158 now = time.time()
159 159 should_change = self.user_data.get("force_password_change")
160 160 change_after = safe_int(should_change) or 0
161 161 if should_change and now > change_after:
162 162 log.debug("User %s requires password change", user_obj)
163 163 h.flash(
164 164 "You are required to change your password",
165 165 "warning",
166 166 ignore_duplicate=True,
167 167 )
168 168
169 169 if view_name not in skip_user_views:
170 170 raise HTTPFound(self.request.route_path("my_account_password"))
171 171
172 172 def _maybe_needs_2fa_configuration(self, view_name, user_obj):
173 173 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
174 174 return
175 175
176 176 if not user_obj:
177 177 return
178 178
179 179 if user_obj.has_forced_2fa and user_obj.extern_type != 'rhodecode':
180 180 return
181 181
182 182 if user_obj.needs_2fa_configure and view_name != self.SETUP_2FA_VIEW:
183 183 h.flash(
184 184 "You are required to configure 2FA",
185 185 "warning",
186 186 ignore_duplicate=False,
187 187 )
188 188 raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW))
189 189
190 190 def _maybe_needs_2fa_check(self, view_name, user_obj):
191 191 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
192 192 return
193 193
194 194 if not user_obj:
195 195 return
196 196
197 if user_obj.has_check_2fa_flag and view_name != self.VERIFY_2FA_VIEW:
197 if user_obj.check_2fa_required and view_name != self.VERIFY_2FA_VIEW:
198 198 raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW))
199 199
200 200 def _log_creation_exception(self, e, repo_name):
201 201 _ = self.request.translate
202 202 reason = None
203 203 if len(e.args) == 2:
204 204 reason = e.args[1]
205 205
206 206 if reason == "INVALID_CERTIFICATE":
207 207 log.exception("Exception creating a repository: invalid certificate")
208 208 msg = _("Error creating repository %s: invalid certificate") % repo_name
209 209 else:
210 210 log.exception("Exception creating a repository")
211 211 msg = _("Error creating repository %s") % repo_name
212 212 return msg
213 213
214 214 def _get_local_tmpl_context(self, include_app_defaults=True):
215 215 c = TemplateArgs()
216 216 c.auth_user = self.request.user
217 217 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
218 218 c.rhodecode_user = self.request.user
219 219
220 220 if include_app_defaults:
221 221 from rhodecode.lib.base import attach_context_attributes
222 222
223 223 attach_context_attributes(c, self.request, self.request.user.user_id)
224 224
225 225 c.is_super_admin = c.auth_user.is_admin
226 226
227 227 c.can_create_repo = c.is_super_admin
228 228 c.can_create_repo_group = c.is_super_admin
229 229 c.can_create_user_group = c.is_super_admin
230 230
231 231 c.is_delegated_admin = False
232 232
233 233 if not c.auth_user.is_default and not c.is_super_admin:
234 234 c.can_create_repo = h.HasPermissionAny("hg.create.repository")(
235 235 user=self.request.user
236 236 )
237 237 repositories = c.auth_user.repositories_admin or c.can_create_repo
238 238
239 239 c.can_create_repo_group = h.HasPermissionAny("hg.repogroup.create.true")(
240 240 user=self.request.user
241 241 )
242 242 repository_groups = (
243 243 c.auth_user.repository_groups_admin or c.can_create_repo_group
244 244 )
245 245
246 246 c.can_create_user_group = h.HasPermissionAny("hg.usergroup.create.true")(
247 247 user=self.request.user
248 248 )
249 249 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
250 250 # delegated admin can create, or manage some objects
251 251 c.is_delegated_admin = repositories or repository_groups or user_groups
252 252 return c
253 253
254 254 def _get_template_context(self, tmpl_args, **kwargs):
255 255 local_tmpl_args = {"defaults": {}, "errors": {}, "c": tmpl_args}
256 256 local_tmpl_args.update(kwargs)
257 257 return local_tmpl_args
258 258
259 259 def load_default_context(self):
260 260 """
261 261 example:
262 262
263 263 def load_default_context(self):
264 264 c = self._get_local_tmpl_context()
265 265 c.custom_var = 'foobar'
266 266
267 267 return c
268 268 """
269 269 raise NotImplementedError("Needs implementation in view class")
270 270
271 271
272 272 class RepoAppView(BaseAppView):
273 273 def __init__(self, context, request):
274 274 super().__init__(context, request)
275 275 self.db_repo = request.db_repo
276 276 self.db_repo_name = self.db_repo.repo_name
277 277 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
278 278 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
279 279 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
280 280
281 281 def _handle_missing_requirements(self, error):
282 282 log.error(
283 283 "Requirements are missing for repository %s: %s",
284 284 self.db_repo_name,
285 285 safe_str(error),
286 286 )
287 287
288 288 def _prepare_and_set_clone_url(self, c):
289 289 username = ""
290 290 if self._rhodecode_user.username != User.DEFAULT_USER:
291 291 username = self._rhodecode_user.username
292 292
293 293 _def_clone_uri = c.clone_uri_tmpl
294 294 _def_clone_uri_id = c.clone_uri_id_tmpl
295 295 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
296 296
297 297 c.clone_repo_url = self.db_repo.clone_url(
298 298 user=username, uri_tmpl=_def_clone_uri
299 299 )
300 300 c.clone_repo_url_id = self.db_repo.clone_url(
301 301 user=username, uri_tmpl=_def_clone_uri_id
302 302 )
303 303 c.clone_repo_url_ssh = self.db_repo.clone_url(
304 304 uri_tmpl=_def_clone_uri_ssh, ssh=True
305 305 )
306 306
307 307 def _get_local_tmpl_context(self, include_app_defaults=True):
308 308 _ = self.request.translate
309 309 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
310 310
311 311 # register common vars for this type of view
312 312 c.rhodecode_db_repo = self.db_repo
313 313 c.repo_name = self.db_repo_name
314 314 c.repository_pull_requests = self.db_repo_pull_requests
315 315 c.repository_artifacts = self.db_repo_artifacts
316 316 c.repository_is_user_following = ScmModel().is_following_repo(
317 317 self.db_repo_name, self._rhodecode_user.user_id
318 318 )
319 319 self.path_filter = PathFilter(None)
320 320
321 321 c.repository_requirements_missing = {}
322 322 try:
323 323 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
324 324 # NOTE(marcink):
325 325 # comparison to None since if it's an object __bool__ is expensive to
326 326 # calculate
327 327 if self.rhodecode_vcs_repo is not None:
328 328 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
329 329 c.auth_user.username
330 330 )
331 331 self.path_filter = PathFilter(path_perms)
332 332 except RepositoryRequirementError as e:
333 333 c.repository_requirements_missing = {"error": str(e)}
334 334 self._handle_missing_requirements(e)
335 335 self.rhodecode_vcs_repo = None
336 336
337 337 c.path_filter = self.path_filter # used by atom_feed_entry.mako
338 338
339 339 if self.rhodecode_vcs_repo is None:
340 340 # unable to fetch this repo as vcs instance, report back to user
341 341 log.debug(
342 342 "Repository was not found on filesystem, check if it exists or is not damaged"
343 343 )
344 344 h.flash(
345 345 _(
346 346 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
347 347 "Please check if it exist, or is not damaged."
348 348 )
349 349 % {"repo_name": c.repo_name},
350 350 category="error",
351 351 ignore_duplicate=True,
352 352 )
353 353 if c.repository_requirements_missing:
354 354 route = self.request.matched_route.name
355 355 if route.startswith(("edit_repo", "repo_summary")):
356 356 # allow summary and edit repo on missing requirements
357 357 return c
358 358
359 359 raise HTTPFound(
360 360 h.route_path("repo_summary", repo_name=self.db_repo_name)
361 361 )
362 362
363 363 else: # redirect if we don't show missing requirements
364 364 raise HTTPFound(h.route_path("home"))
365 365
366 366 c.has_origin_repo_read_perm = False
367 367 if self.db_repo.fork:
368 368 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
369 369 "repository.write", "repository.read", "repository.admin"
370 370 )(self.db_repo.fork.repo_name, "summary fork link")
371 371
372 372 return c
373 373
374 374 def _get_f_path_unchecked(self, matchdict, default=None):
375 375 """
376 376 Should only be used by redirects, everything else should call _get_f_path
377 377 """
378 378 f_path = matchdict.get("f_path")
379 379 if f_path:
380 380 # fix for multiple initial slashes that causes errors for GIT
381 381 return f_path.lstrip("/")
382 382
383 383 return default
384 384
385 385 def _get_f_path(self, matchdict, default=None):
386 386 f_path_match = self._get_f_path_unchecked(matchdict, default)
387 387 return self.path_filter.assert_path_permissions(f_path_match)
388 388
389 389 def _get_general_setting(self, target_repo, settings_key, default=False):
390 390 settings_model = VcsSettingsModel(repo=target_repo)
391 391 settings = settings_model.get_general_settings()
392 392 return settings.get(settings_key, default)
393 393
394 394 def _get_repo_setting(self, target_repo, settings_key, default=False):
395 395 settings_model = VcsSettingsModel(repo=target_repo)
396 396 settings = settings_model.get_repo_settings_inherited()
397 397 return settings.get(settings_key, default)
398 398
399 399 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"):
400 400 log.debug("Looking for README file at path %s", path)
401 401 if commit_id:
402 402 landing_commit_id = commit_id
403 403 else:
404 404 landing_commit = db_repo.get_landing_commit()
405 405 if isinstance(landing_commit, EmptyCommit):
406 406 return None, None
407 407 landing_commit_id = landing_commit.raw_id
408 408
409 409 cache_namespace_uid = f"repo.{db_repo.repo_id}"
410 410 region = rc_cache.get_or_create_region(
411 411 "cache_repo", cache_namespace_uid, use_async_runner=False
412 412 )
413 413 start = time.time()
414 414
415 415 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
416 416 def generate_repo_readme(
417 417 repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type
418 418 ):
419 419 readme_data = None
420 420 readme_filename = None
421 421
422 422 commit = db_repo.get_commit(_commit_id)
423 423 log.debug("Searching for a README file at commit %s.", _commit_id)
424 424 readme_node = ReadmeFinder(_renderer_type).search(
425 425 commit, path=_readme_search_path
426 426 )
427 427
428 428 if readme_node:
429 429 log.debug("Found README node: %s", readme_node)
430 430
431 431 relative_urls = {
432 432 "raw": h.route_path(
433 433 "repo_file_raw",
434 434 repo_name=_repo_name,
435 435 commit_id=commit.raw_id,
436 436 f_path=readme_node.path,
437 437 ),
438 438 "standard": h.route_path(
439 439 "repo_files",
440 440 repo_name=_repo_name,
441 441 commit_id=commit.raw_id,
442 442 f_path=readme_node.path,
443 443 ),
444 444 }
445 445
446 446 readme_data = self._render_readme_or_none(
447 447 commit, readme_node, relative_urls
448 448 )
449 449 readme_filename = readme_node.str_path
450 450
451 451 return readme_data, readme_filename
452 452
453 453 readme_data, readme_filename = generate_repo_readme(
454 454 db_repo.repo_id,
455 455 landing_commit_id,
456 456 db_repo.repo_name,
457 457 path,
458 458 renderer_type,
459 459 )
460 460
461 461 compute_time = time.time() - start
462 462 log.debug(
463 463 "Repo README for path %s generated and computed in %.4fs",
464 464 path,
465 465 compute_time,
466 466 )
467 467 return readme_data, readme_filename
468 468
469 469 def _render_readme_or_none(self, commit, readme_node, relative_urls):
470 470 log.debug("Found README file `%s` rendering...", readme_node.path)
471 471 renderer = MarkupRenderer()
472 472 try:
473 473 html_source = renderer.render(
474 474 readme_node.str_content, filename=readme_node.path
475 475 )
476 476 if relative_urls:
477 477 return relative_links(html_source, relative_urls)
478 478 return html_source
479 479 except Exception:
480 480 log.exception("Exception while trying to render the README")
481 481
482 482 def get_recache_flag(self):
483 483 for flag_name in ["force_recache", "force-recache", "no-cache"]:
484 484 flag_val = self.request.GET.get(flag_name)
485 485 if str2bool(flag_val):
486 486 return True
487 487 return False
488 488
489 489 def get_commit_preload_attrs(cls):
490 490 pre_load = [
491 491 "author",
492 492 "branch",
493 493 "date",
494 494 "message",
495 495 "parents",
496 496 "obsolete",
497 497 "phase",
498 498 "hidden",
499 499 ]
500 500 return pre_load
501 501
502 502
503 503 class PathFilter(object):
504 504 # Expects and instance of BasePathPermissionChecker or None
505 505 def __init__(self, permission_checker):
506 506 self.permission_checker = permission_checker
507 507
508 508 def assert_path_permissions(self, path):
509 509 if self.path_access_allowed(path):
510 510 return path
511 511 raise HTTPForbidden()
512 512
513 513 def path_access_allowed(self, path):
514 514 log.debug("Checking ACL permissions for PathFilter for `%s`", path)
515 515 if self.permission_checker:
516 516 has_access = path and self.permission_checker.has_access(path)
517 517 log.debug(
518 518 "ACL Permissions checker enabled, ACL Check has_access: %s", has_access
519 519 )
520 520 return has_access
521 521
522 522 log.debug("ACL permissions checker not enabled, skipping...")
523 523 return True
524 524
525 525 def filter_patchset(self, patchset):
526 526 if not self.permission_checker or not patchset:
527 527 return patchset, False
528 528 had_filtered = False
529 529 filtered_patchset = []
530 530 for patch in patchset:
531 531 filename = patch.get("filename", None)
532 532 if not filename or self.permission_checker.has_access(filename):
533 533 filtered_patchset.append(patch)
534 534 else:
535 535 had_filtered = True
536 536 if had_filtered:
537 537 if isinstance(patchset, diffs.LimitedDiffContainer):
538 538 filtered_patchset = diffs.LimitedDiffContainer(
539 539 patchset.diff_limit, patchset.cur_diff_size, filtered_patchset
540 540 )
541 541 return filtered_patchset, True
542 542 else:
543 543 return patchset, False
544 544
545 545 def render_patchset_filtered(
546 546 self, diffset, patchset, source_ref=None, target_ref=None
547 547 ):
548 548 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
549 549 result = diffset.render_patchset(
550 550 filtered_patchset, source_ref=source_ref, target_ref=target_ref
551 551 )
552 552 result.has_hidden_changes = has_hidden_changes
553 553 return result
554 554
555 555 def get_raw_patch(self, diff_processor):
556 556 if self.permission_checker is None:
557 557 return diff_processor.as_raw()
558 558 elif self.permission_checker.has_full_access:
559 559 return diff_processor.as_raw()
560 560 else:
561 561 return "# Repository has user-specific filters, raw patch generation is disabled."
562 562
563 563 @property
564 564 def is_enabled(self):
565 565 return self.permission_checker is not None
566 566
567 567
568 568 class RepoGroupAppView(BaseAppView):
569 569 def __init__(self, context, request):
570 570 super().__init__(context, request)
571 571 self.db_repo_group = request.db_repo_group
572 572 self.db_repo_group_name = self.db_repo_group.group_name
573 573
574 574 def _get_local_tmpl_context(self, include_app_defaults=True):
575 575 _ = self.request.translate
576 576 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
577 577 c.repo_group = self.db_repo_group
578 578 return c
579 579
580 580 def _revoke_perms_on_yourself(self, form_result):
581 581 _updates = [
582 582 u
583 583 for u in form_result["perm_updates"]
584 584 if self._rhodecode_user.user_id == int(u[0])
585 585 ]
586 586 _additions = [
587 587 u
588 588 for u in form_result["perm_additions"]
589 589 if self._rhodecode_user.user_id == int(u[0])
590 590 ]
591 591 _deletions = [
592 592 u
593 593 for u in form_result["perm_deletions"]
594 594 if self._rhodecode_user.user_id == int(u[0])
595 595 ]
596 596 admin_perm = "group.admin"
597 597 if (
598 598 _updates
599 599 and _updates[0][1] != admin_perm
600 600 or _additions
601 601 and _additions[0][1] != admin_perm
602 602 or _deletions
603 603 and _deletions[0][1] != admin_perm
604 604 ):
605 605 return True
606 606 return False
607 607
608 608
609 609 class UserGroupAppView(BaseAppView):
610 610 def __init__(self, context, request):
611 611 super().__init__(context, request)
612 612 self.db_user_group = request.db_user_group
613 613 self.db_user_group_name = self.db_user_group.users_group_name
614 614
615 615
616 616 class UserAppView(BaseAppView):
617 617 def __init__(self, context, request):
618 618 super().__init__(context, request)
619 619 self.db_user = request.db_user
620 620 self.db_user_id = self.db_user.user_id
621 621
622 622 _ = self.request.translate
623 623 if not request.db_user_supports_default:
624 624 if self.db_user.username == User.DEFAULT_USER:
625 625 h.flash(
626 626 _("Editing user `{}` is disabled.".format(User.DEFAULT_USER)),
627 627 category="warning",
628 628 )
629 629 raise HTTPFound(h.route_path("users"))
630 630
631 631
632 632 class DataGridAppView(object):
633 633 """
634 634 Common class to have re-usable grid rendering components
635 635 """
636 636
637 637 def _extract_ordering(self, request, column_map=None):
638 638 column_map = column_map or {}
639 639 column_index = safe_int(request.GET.get("order[0][column]"))
640 640 order_dir = request.GET.get("order[0][dir]", "desc")
641 641 order_by = request.GET.get("columns[%s][data][sort]" % column_index, "name_raw")
642 642
643 643 # translate datatable to DB columns
644 644 order_by = column_map.get(order_by) or order_by
645 645
646 646 search_q = request.GET.get("search[value]")
647 647 return search_q, order_by, order_dir
648 648
649 649 def _extract_chunk(self, request):
650 650 start = safe_int(request.GET.get("start"), 0)
651 651 length = safe_int(request.GET.get("length"), 25)
652 652 draw = safe_int(request.GET.get("draw"))
653 653 return draw, start, length
654 654
655 655 def _get_order_col(self, order_by, model):
656 656 if isinstance(order_by, str):
657 657 try:
658 658 return operator.attrgetter(order_by)(model)
659 659 except AttributeError:
660 660 return None
661 661 else:
662 662 return order_by
663 663
664 664
665 665 class BaseReferencesView(RepoAppView):
666 666 """
667 667 Base for reference view for branches, tags and bookmarks.
668 668 """
669 669
670 670 def load_default_context(self):
671 671 c = self._get_local_tmpl_context()
672 672 return c
673 673
674 674 def load_refs_context(self, ref_items, partials_template):
675 675 _render = self.request.get_partial_renderer(partials_template)
676 676 pre_load = ["author", "date", "message", "parents"]
677 677
678 678 is_svn = h.is_svn(self.rhodecode_vcs_repo)
679 679 is_hg = h.is_hg(self.rhodecode_vcs_repo)
680 680
681 681 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
682 682
683 683 closed_refs = {}
684 684 if is_hg:
685 685 closed_refs = self.rhodecode_vcs_repo.branches_closed
686 686
687 687 data = []
688 688 for ref_name, commit_id in ref_items:
689 689 commit = self.rhodecode_vcs_repo.get_commit(
690 690 commit_id=commit_id, pre_load=pre_load
691 691 )
692 692 closed = ref_name in closed_refs
693 693
694 694 # TODO: johbo: Unify generation of reference links
695 695 use_commit_id = "/" in ref_name or is_svn
696 696
697 697 if use_commit_id:
698 698 files_url = h.route_path(
699 699 "repo_files",
700 700 repo_name=self.db_repo_name,
701 701 f_path=ref_name if is_svn else "",
702 702 commit_id=commit_id,
703 703 _query=dict(at=ref_name),
704 704 )
705 705
706 706 else:
707 707 files_url = h.route_path(
708 708 "repo_files",
709 709 repo_name=self.db_repo_name,
710 710 f_path=ref_name if is_svn else "",
711 711 commit_id=ref_name,
712 712 _query=dict(at=ref_name),
713 713 )
714 714
715 715 data.append(
716 716 {
717 717 "name": _render("name", ref_name, files_url, closed),
718 718 "name_raw": ref_name,
719 719 "date": _render("date", commit.date),
720 720 "date_raw": datetime_to_time(commit.date),
721 721 "author": _render("author", commit.author),
722 722 "commit": _render(
723 723 "commit", commit.message, commit.raw_id, commit.idx
724 724 ),
725 725 "commit_raw": commit.idx,
726 726 "compare": _render(
727 727 "compare", format_ref_id(ref_name, commit.raw_id)
728 728 ),
729 729 }
730 730 )
731 731
732 732 return data
733 733
734 734
735 735 class RepoRoutePredicate(object):
736 736 def __init__(self, val, config):
737 737 self.val = val
738 738
739 739 def text(self):
740 740 return f"repo_route = {self.val}"
741 741
742 742 phash = text
743 743
744 744 def __call__(self, info, request):
745 745 if hasattr(request, "vcs_call"):
746 746 # skip vcs calls
747 747 return
748 748
749 749 repo_name = info["match"]["repo_name"]
750 750
751 751 repo_name_parts = repo_name.split("/")
752 752 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
753 753
754 754 if repo_name_parts != repo_slugs:
755 755 # short-skip if the repo-name doesn't follow slug rule
756 756 log.warning(
757 757 "repo_name: %s is different than slug %s", repo_name_parts, repo_slugs
758 758 )
759 759 return False
760 760
761 761 repo_model = repo.RepoModel()
762 762
763 763 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
764 764
765 765 def redirect_if_creating(route_info, db_repo):
766 766 skip_views = ["edit_repo_advanced_delete"]
767 767 route = route_info["route"]
768 768 # we should skip delete view so we can actually "remove" repositories
769 769 # if they get stuck in creating state.
770 770 if route.name in skip_views:
771 771 return
772 772
773 773 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
774 774 repo_creating_url = request.route_path(
775 775 "repo_creating", repo_name=db_repo.repo_name
776 776 )
777 777 raise HTTPFound(repo_creating_url)
778 778
779 779 if by_name_match:
780 780 # register this as request object we can re-use later
781 781 request.db_repo = by_name_match
782 782 request.db_repo_name = request.db_repo.repo_name
783 783
784 784 redirect_if_creating(info, by_name_match)
785 785 return True
786 786
787 787 by_id_match = repo_model.get_repo_by_id(repo_name)
788 788 if by_id_match:
789 789 request.db_repo = by_id_match
790 790 request.db_repo_name = request.db_repo.repo_name
791 791 redirect_if_creating(info, by_id_match)
792 792 return True
793 793
794 794 return False
795 795
796 796
797 797 class RepoForbidArchivedRoutePredicate(object):
798 798 def __init__(self, val, config):
799 799 self.val = val
800 800
801 801 def text(self):
802 802 return f"repo_forbid_archived = {self.val}"
803 803
804 804 phash = text
805 805
806 806 def __call__(self, info, request):
807 807 _ = request.translate
808 808 rhodecode_db_repo = request.db_repo
809 809
810 810 log.debug(
811 811 "%s checking if archived flag for repo for %s",
812 812 self.__class__.__name__,
813 813 rhodecode_db_repo.repo_name,
814 814 )
815 815
816 816 if rhodecode_db_repo.archived:
817 817 log.warning(
818 818 "Current view is not supported for archived repo:%s",
819 819 rhodecode_db_repo.repo_name,
820 820 )
821 821
822 822 h.flash(
823 823 h.literal(_("Action not supported for archived repository.")),
824 824 category="warning",
825 825 )
826 826 summary_url = request.route_path(
827 827 "repo_summary", repo_name=rhodecode_db_repo.repo_name
828 828 )
829 829 raise HTTPFound(summary_url)
830 830 return True
831 831
832 832
833 833 class RepoTypeRoutePredicate(object):
834 834 def __init__(self, val, config):
835 835 self.val = val or ["hg", "git", "svn"]
836 836
837 837 def text(self):
838 838 return f"repo_accepted_type = {self.val}"
839 839
840 840 phash = text
841 841
842 842 def __call__(self, info, request):
843 843 if hasattr(request, "vcs_call"):
844 844 # skip vcs calls
845 845 return
846 846
847 847 rhodecode_db_repo = request.db_repo
848 848
849 849 log.debug(
850 850 "%s checking repo type for %s in %s",
851 851 self.__class__.__name__,
852 852 rhodecode_db_repo.repo_type,
853 853 self.val,
854 854 )
855 855
856 856 if rhodecode_db_repo.repo_type in self.val:
857 857 return True
858 858 else:
859 859 log.warning(
860 860 "Current view is not supported for repo type:%s",
861 861 rhodecode_db_repo.repo_type,
862 862 )
863 863 return False
864 864
865 865
866 866 class RepoGroupRoutePredicate(object):
867 867 def __init__(self, val, config):
868 868 self.val = val
869 869
870 870 def text(self):
871 871 return f"repo_group_route = {self.val}"
872 872
873 873 phash = text
874 874
875 875 def __call__(self, info, request):
876 876 if hasattr(request, "vcs_call"):
877 877 # skip vcs calls
878 878 return
879 879
880 880 repo_group_name = info["match"]["repo_group_name"]
881 881
882 882 repo_group_name_parts = repo_group_name.split("/")
883 883 repo_group_slugs = [
884 884 x for x in [repo_name_slug(x) for x in repo_group_name_parts]
885 885 ]
886 886 if repo_group_name_parts != repo_group_slugs:
887 887 # short-skip if the repo-name doesn't follow slug rule
888 888 log.warning(
889 889 "repo_group_name: %s is different than slug %s",
890 890 repo_group_name_parts,
891 891 repo_group_slugs,
892 892 )
893 893 return False
894 894
895 895 repo_group_model = repo_group.RepoGroupModel()
896 896 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
897 897
898 898 if by_name_match:
899 899 # register this as request object we can re-use later
900 900 request.db_repo_group = by_name_match
901 901 request.db_repo_group_name = request.db_repo_group.group_name
902 902 return True
903 903
904 904 return False
905 905
906 906
907 907 class UserGroupRoutePredicate(object):
908 908 def __init__(self, val, config):
909 909 self.val = val
910 910
911 911 def text(self):
912 912 return f"user_group_route = {self.val}"
913 913
914 914 phash = text
915 915
916 916 def __call__(self, info, request):
917 917 if hasattr(request, "vcs_call"):
918 918 # skip vcs calls
919 919 return
920 920
921 921 user_group_id = info["match"]["user_group_id"]
922 922 user_group_model = user_group.UserGroup()
923 923 by_id_match = user_group_model.get(user_group_id, cache=False)
924 924
925 925 if by_id_match:
926 926 # register this as request object we can re-use later
927 927 request.db_user_group = by_id_match
928 928 return True
929 929
930 930 return False
931 931
932 932
933 933 class UserRoutePredicateBase(object):
934 934 supports_default = None
935 935
936 936 def __init__(self, val, config):
937 937 self.val = val
938 938
939 939 def text(self):
940 940 raise NotImplementedError()
941 941
942 942 def __call__(self, info, request):
943 943 if hasattr(request, "vcs_call"):
944 944 # skip vcs calls
945 945 return
946 946
947 947 user_id = info["match"]["user_id"]
948 948 user_model = user.User()
949 949 by_id_match = user_model.get(user_id, cache=False)
950 950
951 951 if by_id_match:
952 952 # register this as request object we can re-use later
953 953 request.db_user = by_id_match
954 954 request.db_user_supports_default = self.supports_default
955 955 return True
956 956
957 957 return False
958 958
959 959
960 960 class UserRoutePredicate(UserRoutePredicateBase):
961 961 supports_default = False
962 962
963 963 def text(self):
964 964 return f"user_route = {self.val}"
965 965
966 966 phash = text
967 967
968 968
969 969 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
970 970 supports_default = True
971 971
972 972 def text(self):
973 973 return f"user_with_default_route = {self.val}"
974 974
975 975 phash = text
976 976
977 977
978 978 def includeme(config):
979 979 config.add_route_predicate("repo_route", RepoRoutePredicate)
980 980 config.add_route_predicate("repo_accepted_types", RepoTypeRoutePredicate)
981 981 config.add_route_predicate(
982 982 "repo_forbid_when_archived", RepoForbidArchivedRoutePredicate
983 983 )
984 984 config.add_route_predicate("repo_group_route", RepoGroupRoutePredicate)
985 985 config.add_route_predicate("user_group_route", UserGroupRoutePredicate)
986 986 config.add_route_predicate("user_route_with_default", UserRouteWithDefaultPredicate)
987 987 config.add_route_predicate("user_route", UserRoutePredicate)
@@ -1,553 +1,553 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 json
21 21 import pyotp
22 22 import qrcode
23 23 import collections
24 24 import datetime
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import logging
28 28 import urllib.parse
29 29 import requests
30 30 from io import BytesIO
31 31 from base64 import b64encode
32 32
33 33 from pyramid.renderers import render
34 34 from pyramid.response import Response
35 35 from pyramid.httpexceptions import HTTPFound
36 36
37 37 import rhodecode
38 38 from rhodecode.apps._base import BaseAppView
39 39 from rhodecode.authentication.base import authenticate, HTTP_TYPE
40 40 from rhodecode.authentication.plugins import auth_rhodecode
41 41 from rhodecode.events import UserRegistered, trigger
42 42 from rhodecode.lib import helpers as h
43 43 from rhodecode.lib import audit_logger
44 44 from rhodecode.lib.auth import (
45 45 AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous)
46 46 from rhodecode.lib.base import get_ip_addr
47 47 from rhodecode.lib.exceptions import UserCreationError
48 48 from rhodecode.lib.utils2 import safe_str
49 49 from rhodecode.model.db import User, UserApiKeys
50 50 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.auth_token import AuthTokenModel
53 53 from rhodecode.model.settings import SettingsModel
54 54 from rhodecode.model.user import UserModel
55 55 from rhodecode.translation import _
56 56
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60 CaptchaData = collections.namedtuple(
61 61 'CaptchaData', 'active, private_key, public_key')
62 62
63 63
64 64 def store_user_in_session(session, user_identifier, remember=False):
65 65 user = User.get_by_username_or_primary_email(user_identifier)
66 66 auth_user = AuthUser(user.user_id)
67 67 auth_user.set_authenticated()
68 68 cs = auth_user.get_cookie_store()
69 69 session['rhodecode_user'] = cs
70 70 user.update_lastlogin()
71 71 Session().commit()
72 72
73 73 # If they want to be remembered, update the cookie
74 74 if remember:
75 75 _year = (datetime.datetime.now() +
76 76 datetime.timedelta(seconds=60 * 60 * 24 * 365))
77 77 session._set_cookie_expires(_year)
78 78
79 79 session.save()
80 80
81 81 safe_cs = cs.copy()
82 82 safe_cs['password'] = '****'
83 83 log.info('user %s is now authenticated and stored in '
84 84 'session, session attrs %s', user_identifier, safe_cs)
85 85
86 86 # dumps session attrs back to cookie
87 87 session._update_cookie_out()
88 88 # we set new cookie
89 89 headers = None
90 90 if session.request['set_cookie']:
91 91 # send set-cookie headers back to response to update cookie
92 92 headers = [('Set-Cookie', session.request['cookie_out'])]
93 93 return headers
94 94
95 95
96 96 def get_came_from(request):
97 97 came_from = safe_str(request.GET.get('came_from', ''))
98 98 parsed = urllib.parse.urlparse(came_from)
99 99
100 100 allowed_schemes = ['http', 'https']
101 101 default_came_from = h.route_path('home')
102 102 if parsed.scheme and parsed.scheme not in allowed_schemes:
103 103 log.error('Suspicious URL scheme detected %s for url %s',
104 104 parsed.scheme, parsed)
105 105 came_from = default_came_from
106 106 elif parsed.netloc and request.host != parsed.netloc:
107 107 log.error('Suspicious NETLOC detected %s for url %s server url '
108 108 'is: %s', parsed.netloc, parsed, request.host)
109 109 came_from = default_came_from
110 110 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
111 111 log.error('Header injection detected `%s` for url %s server url ',
112 112 parsed.path, parsed)
113 113 came_from = default_came_from
114 114
115 115 return came_from or default_came_from
116 116
117 117
118 118 class LoginView(BaseAppView):
119 119
120 120 def load_default_context(self):
121 121 c = self._get_local_tmpl_context()
122 122 c.came_from = get_came_from(self.request)
123 123 return c
124 124
125 125 def _get_captcha_data(self):
126 126 settings = SettingsModel().get_all_settings()
127 127 private_key = settings.get('rhodecode_captcha_private_key')
128 128 public_key = settings.get('rhodecode_captcha_public_key')
129 129 active = bool(private_key)
130 130 return CaptchaData(
131 131 active=active, private_key=private_key, public_key=public_key)
132 132
133 133 def validate_captcha(self, private_key):
134 134
135 135 captcha_rs = self.request.POST.get('g-recaptcha-response')
136 136 url = "https://www.google.com/recaptcha/api/siteverify"
137 137 params = {
138 138 'secret': private_key,
139 139 'response': captcha_rs,
140 140 'remoteip': get_ip_addr(self.request.environ)
141 141 }
142 142 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
143 143 verify_rs = verify_rs.json()
144 144 captcha_status = verify_rs.get('success', False)
145 145 captcha_errors = verify_rs.get('error-codes', [])
146 146 if not isinstance(captcha_errors, list):
147 147 captcha_errors = [captcha_errors]
148 148 captcha_errors = ', '.join(captcha_errors)
149 149 captcha_message = ''
150 150 if captcha_status is False:
151 151 captcha_message = "Bad captcha. Errors: {}".format(
152 152 captcha_errors)
153 153
154 154 return captcha_status, captcha_message
155 155
156 156 def login(self):
157 157 c = self.load_default_context()
158 158 auth_user = self._rhodecode_user
159 159
160 160 # redirect if already logged in
161 161 if (auth_user.is_authenticated and
162 162 not auth_user.is_default and auth_user.ip_allowed):
163 163 raise HTTPFound(c.came_from)
164 164
165 165 # check if we use headers plugin, and try to login using it.
166 166 try:
167 167 log.debug('Running PRE-AUTH for headers based authentication')
168 168 auth_info = authenticate(
169 169 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
170 170 if auth_info:
171 171 headers = store_user_in_session(
172 172 self.session, auth_info.get('username'))
173 173 raise HTTPFound(c.came_from, headers=headers)
174 174 except UserCreationError as e:
175 175 log.error(e)
176 176 h.flash(e, category='error')
177 177
178 178 return self._get_template_context(c)
179 179
180 180 def login_post(self):
181 181 c = self.load_default_context()
182 182
183 183 login_form = LoginForm(self.request.translate)()
184 184
185 185 try:
186 186 self.session.invalidate()
187 187 form_result = login_form.to_python(self.request.POST)
188 188 # form checks for username/password, now we're authenticated
189 189 username = form_result['username']
190 190 if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa:
191 user.has_check_2fa_flag = True
191 user.check_2fa_required = True
192 192
193 193 headers = store_user_in_session(
194 194 self.session,
195 195 user_identifier=username,
196 196 remember=form_result['remember'])
197 197 log.debug('Redirecting to "%s" after login.', c.came_from)
198 198
199 199 audit_user = audit_logger.UserWrap(
200 200 username=self.request.POST.get('username'),
201 201 ip_addr=self.request.remote_addr)
202 202 action_data = {'user_agent': self.request.user_agent}
203 203 audit_logger.store_web(
204 204 'user.login.success', action_data=action_data,
205 205 user=audit_user, commit=True)
206 206
207 207 raise HTTPFound(c.came_from, headers=headers)
208 208 except formencode.Invalid as errors:
209 209 defaults = errors.value
210 210 # remove password from filling in form again
211 211 defaults.pop('password', None)
212 212 render_ctx = {
213 213 'errors': errors.error_dict,
214 214 'defaults': defaults,
215 215 }
216 216
217 217 audit_user = audit_logger.UserWrap(
218 218 username=self.request.POST.get('username'),
219 219 ip_addr=self.request.remote_addr)
220 220 action_data = {'user_agent': self.request.user_agent}
221 221 audit_logger.store_web(
222 222 'user.login.failure', action_data=action_data,
223 223 user=audit_user, commit=True)
224 224 return self._get_template_context(c, **render_ctx)
225 225
226 226 except UserCreationError as e:
227 227 # headers auth or other auth functions that create users on
228 228 # the fly can throw this exception signaling that there's issue
229 229 # with user creation, explanation should be provided in
230 230 # Exception itself
231 231 h.flash(e, category='error')
232 232 return self._get_template_context(c)
233 233
234 234 @CSRFRequired()
235 235 def logout(self):
236 236 auth_user = self._rhodecode_user
237 237 log.info('Deleting session for user: `%s`', auth_user)
238 238
239 239 action_data = {'user_agent': self.request.user_agent}
240 240 audit_logger.store_web(
241 241 'user.logout', action_data=action_data,
242 242 user=auth_user, commit=True)
243 243 self.session.delete()
244 244 return HTTPFound(h.route_path('home'))
245 245
246 246 @HasPermissionAnyDecorator(
247 247 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
248 248 def register(self, defaults=None, errors=None):
249 249 c = self.load_default_context()
250 250 defaults = defaults or {}
251 251 errors = errors or {}
252 252
253 253 settings = SettingsModel().get_all_settings()
254 254 register_message = settings.get('rhodecode_register_message') or ''
255 255 captcha = self._get_captcha_data()
256 256 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
257 257 .AuthUser().permissions['global']
258 258
259 259 render_ctx = self._get_template_context(c)
260 260 render_ctx.update({
261 261 'defaults': defaults,
262 262 'errors': errors,
263 263 'auto_active': auto_active,
264 264 'captcha_active': captcha.active,
265 265 'captcha_public_key': captcha.public_key,
266 266 'register_message': register_message,
267 267 })
268 268 return render_ctx
269 269
270 270 @HasPermissionAnyDecorator(
271 271 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
272 272 def register_post(self):
273 273 from rhodecode.authentication.plugins import auth_rhodecode
274 274
275 275 self.load_default_context()
276 276 captcha = self._get_captcha_data()
277 277 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
278 278 .AuthUser().permissions['global']
279 279
280 280 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
281 281 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
282 282
283 283 register_form = RegisterForm(self.request.translate)()
284 284 try:
285 285
286 286 form_result = register_form.to_python(self.request.POST)
287 287 form_result['active'] = auto_active
288 288 external_identity = self.request.POST.get('external_identity')
289 289
290 290 if external_identity:
291 291 extern_name = external_identity
292 292 extern_type = external_identity
293 293
294 294 if captcha.active:
295 295 captcha_status, captcha_message = self.validate_captcha(
296 296 captcha.private_key)
297 297
298 298 if not captcha_status:
299 299 _value = form_result
300 300 _msg = _('Bad captcha')
301 301 error_dict = {'recaptcha_field': captcha_message}
302 302 raise formencode.Invalid(
303 303 _msg, _value, None, error_dict=error_dict)
304 304
305 305 new_user = UserModel().create_registration(
306 306 form_result, extern_name=extern_name, extern_type=extern_type)
307 307
308 308 action_data = {'data': new_user.get_api_data(),
309 309 'user_agent': self.request.user_agent}
310 310
311 311 if external_identity:
312 312 action_data['external_identity'] = external_identity
313 313
314 314 audit_user = audit_logger.UserWrap(
315 315 username=new_user.username,
316 316 user_id=new_user.user_id,
317 317 ip_addr=self.request.remote_addr)
318 318
319 319 audit_logger.store_web(
320 320 'user.register', action_data=action_data,
321 321 user=audit_user)
322 322
323 323 event = UserRegistered(user=new_user, session=self.session)
324 324 trigger(event)
325 325 h.flash(
326 326 _('You have successfully registered with RhodeCode. You can log-in now.'),
327 327 category='success')
328 328 if external_identity:
329 329 h.flash(
330 330 _('Please use the {identity} button to log-in').format(
331 331 identity=external_identity),
332 332 category='success')
333 333 Session().commit()
334 334
335 335 redirect_ro = self.request.route_path('login')
336 336 raise HTTPFound(redirect_ro)
337 337
338 338 except formencode.Invalid as errors:
339 339 errors.value.pop('password', None)
340 340 errors.value.pop('password_confirmation', None)
341 341 return self.register(
342 342 defaults=errors.value, errors=errors.error_dict)
343 343
344 344 except UserCreationError as e:
345 345 # container auth or other auth functions that create users on
346 346 # the fly can throw this exception signaling that there's issue
347 347 # with user creation, explanation should be provided in
348 348 # Exception itself
349 349 h.flash(e, category='error')
350 350 return self.register()
351 351
352 352 def password_reset(self):
353 353 c = self.load_default_context()
354 354 captcha = self._get_captcha_data()
355 355
356 356 template_context = {
357 357 'captcha_active': captcha.active,
358 358 'captcha_public_key': captcha.public_key,
359 359 'defaults': {},
360 360 'errors': {},
361 361 }
362 362
363 363 # always send implicit message to prevent from discovery of
364 364 # matching emails
365 365 msg = _('If such email exists, a password reset link was sent to it.')
366 366
367 367 def default_response():
368 368 log.debug('faking response on invalid password reset')
369 369 # make this take 2s, to prevent brute forcing.
370 370 time.sleep(2)
371 371 h.flash(msg, category='success')
372 372 return HTTPFound(self.request.route_path('reset_password'))
373 373
374 374 if self.request.POST:
375 375 if h.HasPermissionAny('hg.password_reset.disabled')():
376 376 _email = self.request.POST.get('email', '')
377 377 log.error('Failed attempt to reset password for `%s`.', _email)
378 378 h.flash(_('Password reset has been disabled.'), category='error')
379 379 return HTTPFound(self.request.route_path('reset_password'))
380 380
381 381 password_reset_form = PasswordResetForm(self.request.translate)()
382 382 description = 'Generated token for password reset from {}'.format(
383 383 datetime.datetime.now().isoformat())
384 384
385 385 try:
386 386 form_result = password_reset_form.to_python(
387 387 self.request.POST)
388 388 user_email = form_result['email']
389 389
390 390 if captcha.active:
391 391 captcha_status, captcha_message = self.validate_captcha(
392 392 captcha.private_key)
393 393
394 394 if not captcha_status:
395 395 _value = form_result
396 396 _msg = _('Bad captcha')
397 397 error_dict = {'recaptcha_field': captcha_message}
398 398 raise formencode.Invalid(
399 399 _msg, _value, None, error_dict=error_dict)
400 400
401 401 # Generate reset URL and send mail.
402 402 user = User.get_by_email(user_email)
403 403
404 404 # only allow rhodecode based users to reset their password
405 405 # external auth shouldn't allow password reset
406 406 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
407 407 log.warning('User %s with external type `%s` tried a password reset. '
408 408 'This try was rejected', user, user.extern_type)
409 409 return default_response()
410 410
411 411 # generate password reset token that expires in 10 minutes
412 412 reset_token = UserModel().add_auth_token(
413 413 user=user, lifetime_minutes=10,
414 414 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
415 415 description=description)
416 416 Session().commit()
417 417
418 418 log.debug('Successfully created password recovery token')
419 419 password_reset_url = self.request.route_url(
420 420 'reset_password_confirmation',
421 421 _query={'key': reset_token.api_key})
422 422 UserModel().reset_password_link(
423 423 form_result, password_reset_url)
424 424
425 425 action_data = {'email': user_email,
426 426 'user_agent': self.request.user_agent}
427 427 audit_logger.store_web(
428 428 'user.password.reset_request', action_data=action_data,
429 429 user=self._rhodecode_user, commit=True)
430 430
431 431 return default_response()
432 432
433 433 except formencode.Invalid as errors:
434 434 template_context.update({
435 435 'defaults': errors.value,
436 436 'errors': errors.error_dict,
437 437 })
438 438 if not self.request.POST.get('email'):
439 439 # case of empty email, we want to report that
440 440 return self._get_template_context(c, **template_context)
441 441
442 442 if 'recaptcha_field' in errors.error_dict:
443 443 # case of failed captcha
444 444 return self._get_template_context(c, **template_context)
445 445
446 446 return default_response()
447 447
448 448 return self._get_template_context(c, **template_context)
449 449
450 450 def password_reset_confirmation(self):
451 451 self.load_default_context()
452 452
453 453 if key := self.request.GET.get('key'):
454 454 # make this take 2s, to prevent brute forcing.
455 455 time.sleep(2)
456 456
457 457 token = AuthTokenModel().get_auth_token(key)
458 458
459 459 # verify token is the correct role
460 460 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
461 461 log.debug('Got token with role:%s expected is %s',
462 462 getattr(token, 'role', 'EMPTY_TOKEN'),
463 463 UserApiKeys.ROLE_PASSWORD_RESET)
464 464 h.flash(
465 465 _('Given reset token is invalid'), category='error')
466 466 return HTTPFound(self.request.route_path('reset_password'))
467 467
468 468 try:
469 469 owner = token.user
470 470 data = {'email': owner.email, 'token': token.api_key}
471 471 UserModel().reset_password(data)
472 472 h.flash(
473 473 _('Your password reset was successful, '
474 474 'a new password has been sent to your email'),
475 475 category='success')
476 476 except Exception as e:
477 477 log.error(e)
478 478 return HTTPFound(self.request.route_path('reset_password'))
479 479
480 480 return HTTPFound(self.request.route_path('login'))
481 481
482 482 @LoginRequired()
483 483 @NotAnonymous()
484 484 def setup_2fa(self):
485 485 _ = self.request.translate
486 486 c = self.load_default_context()
487 487 user_instance = self._rhodecode_db_user
488 488 form = TOTPForm(_, user_instance)()
489 489 render_ctx = {}
490 490 if self.request.method == 'POST':
491 491 post_items = dict(self.request.POST)
492 492
493 493 try:
494 494 form_details = form.to_python(post_items)
495 495 secret = form_details['secret_totp']
496 496
497 497 user_instance.init_2fa_recovery_codes(persist=True, force=True)
498 user_instance.set_2fa_secret(secret)
498 user_instance.2fa_secret = secret
499 499
500 500 Session().commit()
501 501 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
502 502 except formencode.Invalid as errors:
503 503 defaults = errors.value
504 504 render_ctx = {
505 505 'errors': errors.error_dict,
506 506 'defaults': defaults,
507 507 }
508 508
509 509 # NOTE: here we DO NOT persist the secret 2FA, since this is only for setup, once a setup is completed
510 510 # only then we should persist it
511 511 secret = user_instance.init_secret_2fa(persist=False)
512 512
513 513 instance_name = rhodecode.ConfigGet().get_str('app.base_url', 'rhodecode')
514 514 totp_name = f'{instance_name}:{self.request.user.username}'
515 515
516 516 qr = qrcode.QRCode(version=1, box_size=5, border=4)
517 517 qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name))
518 518 qr.make(fit=True)
519 519 img = qr.make_image(fill_color='black', back_color='white')
520 520 buffered = BytesIO()
521 521 img.save(buffered)
522 522 return self._get_template_context(
523 523 c,
524 524 qr=b64encode(buffered.getvalue()).decode("utf-8"),
525 525 key=secret,
526 526 totp_name=totp_name,
527 527 ** render_ctx
528 528 )
529 529
530 530 @LoginRequired()
531 531 @NotAnonymous()
532 532 def verify_2fa(self):
533 533 _ = self.request.translate
534 534 c = self.load_default_context()
535 535 render_ctx = {}
536 536 user_instance = self._rhodecode_db_user
537 537 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
538 538 if self.request.method == 'POST':
539 539 post_items = dict(self.request.POST)
540 540 # NOTE: inject secret, as it's a post configured saved item.
541 post_items['secret_totp'] = user_instance.get_secret_2fa()
541 post_items['secret_totp'] = user_instance.secret_2fa
542 542 try:
543 543 totp_form.to_python(post_items)
544 user_instance.has_check_2fa_flag = False
544 user_instance.check_2fa_required = False
545 545 Session().commit()
546 546 raise HTTPFound(c.came_from)
547 547 except formencode.Invalid as errors:
548 548 defaults = errors.value
549 549 render_ctx = {
550 550 'errors': errors.error_dict,
551 551 'defaults': defaults,
552 552 }
553 553 return self._get_template_context(c, **render_ctx)
@@ -1,861 +1,861 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 datetime
22 22 import string
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
28 28
29 29 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 30 from rhodecode import forms
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib import ext_json
34 34 from rhodecode.lib.auth import (
35 35 LoginRequired, NotAnonymous, CSRFRequired,
36 36 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
37 37 from rhodecode.lib.channelstream import (
38 38 channelstream_request, ChannelstreamException)
39 39 from rhodecode.lib.hash_utils import md5_safe
40 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 IntegrityError, or_, in_filter_generator, select,
45 45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 47 from rhodecode.model.forms import TOTPForm
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.user_group import UserGroupModel
52 52 from rhodecode.model.validation_schema.schemas import user_schema
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class MyAccountView(BaseAppView, DataGridAppView):
58 58 ALLOW_SCOPED_TOKENS = False
59 59 """
60 60 This view has alternative version inside EE, if modified please take a look
61 61 in there as well.
62 62 """
63 63
64 64 def load_default_context(self):
65 65 c = self._get_local_tmpl_context()
66 66 c.user = c.auth_user.get_instance()
67 67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68 68 return c
69 69
70 70 @LoginRequired()
71 71 @NotAnonymous()
72 72 def my_account_profile(self):
73 73 c = self.load_default_context()
74 74 c.active = 'profile'
75 75 c.extern_type = c.user.extern_type
76 76 return self._get_template_context(c)
77 77
78 78 @LoginRequired()
79 79 @NotAnonymous()
80 80 def my_account_edit(self):
81 81 c = self.load_default_context()
82 82 c.active = 'profile_edit'
83 83 c.extern_type = c.user.extern_type
84 84 c.extern_name = c.user.extern_name
85 85
86 86 schema = user_schema.UserProfileSchema().bind(
87 87 username=c.user.username, user_emails=c.user.emails)
88 88 appstruct = {
89 89 'username': c.user.username,
90 90 'email': c.user.email,
91 91 'firstname': c.user.firstname,
92 92 'lastname': c.user.lastname,
93 93 'description': c.user.description,
94 94 }
95 95 c.form = forms.RcForm(
96 96 schema, appstruct=appstruct,
97 97 action=h.route_path('my_account_update'),
98 98 buttons=(forms.buttons.save, forms.buttons.reset))
99 99
100 100 return self._get_template_context(c)
101 101
102 102 @LoginRequired()
103 103 @NotAnonymous()
104 104 @CSRFRequired()
105 105 def my_account_update(self):
106 106 _ = self.request.translate
107 107 c = self.load_default_context()
108 108 c.active = 'profile_edit'
109 109 c.perm_user = c.auth_user
110 110 c.extern_type = c.user.extern_type
111 111 c.extern_name = c.user.extern_name
112 112
113 113 schema = user_schema.UserProfileSchema().bind(
114 114 username=c.user.username, user_emails=c.user.emails)
115 115 form = forms.RcForm(
116 116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117 117
118 118 controls = list(self.request.POST.items())
119 119 try:
120 120 valid_data = form.validate(controls)
121 121 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
122 122 'new_password', 'password_confirmation']
123 123 if c.extern_type != "rhodecode":
124 124 # forbid updating username for external accounts
125 125 skip_attrs.append('username')
126 126 old_email = c.user.email
127 127 UserModel().update_user(
128 128 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
129 129 **valid_data)
130 130 if old_email != valid_data['email']:
131 131 old = UserEmailMap.query() \
132 132 .filter(UserEmailMap.user == c.user)\
133 133 .filter(UserEmailMap.email == valid_data['email'])\
134 134 .first()
135 135 old.email = old_email
136 136 h.flash(_('Your account was updated successfully'), category='success')
137 137 Session().commit()
138 138 except forms.ValidationFailure as e:
139 139 c.form = e
140 140 return self._get_template_context(c)
141 141
142 142 except Exception:
143 143 log.exception("Exception updating user")
144 144 h.flash(_('Error occurred during update of user'),
145 145 category='error')
146 146 raise HTTPFound(h.route_path('my_account_profile'))
147 147
148 148 @LoginRequired()
149 149 @NotAnonymous()
150 150 def my_account_password(self):
151 151 c = self.load_default_context()
152 152 c.active = 'password'
153 153 c.extern_type = c.user.extern_type
154 154
155 155 schema = user_schema.ChangePasswordSchema().bind(
156 156 username=c.user.username)
157 157
158 158 form = forms.Form(
159 159 schema,
160 160 action=h.route_path('my_account_password_update'),
161 161 buttons=(forms.buttons.save, forms.buttons.reset))
162 162
163 163 c.form = form
164 164 return self._get_template_context(c)
165 165
166 166 @LoginRequired()
167 167 @NotAnonymous()
168 168 @CSRFRequired()
169 169 def my_account_password_update(self):
170 170 _ = self.request.translate
171 171 c = self.load_default_context()
172 172 c.active = 'password'
173 173 c.extern_type = c.user.extern_type
174 174
175 175 schema = user_schema.ChangePasswordSchema().bind(
176 176 username=c.user.username)
177 177
178 178 form = forms.Form(
179 179 schema, buttons=(forms.buttons.save, forms.buttons.reset))
180 180
181 181 if c.extern_type != 'rhodecode':
182 182 raise HTTPFound(self.request.route_path('my_account_password'))
183 183
184 184 controls = list(self.request.POST.items())
185 185 try:
186 186 valid_data = form.validate(controls)
187 187 UserModel().update_user(c.user.user_id, **valid_data)
188 188 c.user.update_userdata(force_password_change=False)
189 189 Session().commit()
190 190 except forms.ValidationFailure as e:
191 191 c.form = e
192 192 return self._get_template_context(c)
193 193
194 194 except Exception:
195 195 log.exception("Exception updating password")
196 196 h.flash(_('Error occurred during update of user password'),
197 197 category='error')
198 198 else:
199 199 instance = c.auth_user.get_instance()
200 200 self.session.setdefault('rhodecode_user', {}).update(
201 201 {'password': md5_safe(instance.password)})
202 202 self.session.save()
203 203 h.flash(_("Successfully updated password"), category='success')
204 204
205 205 raise HTTPFound(self.request.route_path('my_account_password'))
206 206
207 207 @LoginRequired()
208 208 @NotAnonymous()
209 209 def my_account_2fa(self):
210 210 _ = self.request.translate
211 211 c = self.load_default_context()
212 212 c.active = '2fa'
213 213 user_instance = c.auth_user.get_instance()
214 214 locked_by_admin = user_instance.has_forced_2fa
215 215 c.state_of_2fa = user_instance.has_enabled_2fa
216 216 c.user_seen_2fa_recovery_codes = user_instance.has_seen_2fa_codes
217 217 c.locked_2fa = str2bool(locked_by_admin)
218 218 return self._get_template_context(c)
219 219
220 220 @LoginRequired()
221 221 @NotAnonymous()
222 222 @CSRFRequired()
223 223 def my_account_2fa_update(self):
224 224 _ = self.request.translate
225 225 c = self.load_default_context()
226 226 c.active = '2fa'
227 227 user_instance = c.auth_user.get_instance()
228 228
229 229 state = self.request.POST.get('2fa_status') == '1'
230 230 user_instance.has_enabled_2fa = state
231 231 user_instance.update_userdata(update_2fa=time.time())
232 232 Session().commit()
233 233 if state:
234 234 h.flash(_("2FA has been successfully enabled"), category='success')
235 235 else:
236 236 h.flash(_("2FA has been successfully disabled"), category='success')
237 237 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
238 238
239 239 @LoginRequired()
240 240 @NotAnonymous()
241 241 @CSRFRequired()
242 242 def my_account_2fa_show_recovery_codes(self):
243 243 c = self.load_default_context()
244 244 user_instance = c.auth_user.get_instance()
245 245 user_instance.has_seen_2fa_codes = True
246 246 Session().commit()
247 247 return {'recovery_codes': user_instance.get_2fa_recovery_codes()}
248 248
249 249 @LoginRequired()
250 250 @NotAnonymous()
251 251 @CSRFRequired()
252 252 def my_account_2fa_regenerate_recovery_codes(self):
253 253 _ = self.request.translate
254 254 c = self.load_default_context()
255 255 user_instance = c.auth_user.get_instance()
256 256
257 257 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
258 258
259 259 post_items = dict(self.request.POST)
260 260 # NOTE: inject secret, as it's a post configured saved item.
261 post_items['secret_totp'] = user_instance.get_secret_2fa()
261 post_items['secret_totp'] = user_instance.secret_2fa
262 262 try:
263 263 totp_form.to_python(post_items)
264 264 user_instance.regenerate_2fa_recovery_codes()
265 265 Session().commit()
266 266 except formencode.Invalid as errors:
267 267 h.flash(_("Failed to generate new recovery codes: {}").format(errors), category='error')
268 268 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
269 269 except Exception as e:
270 270 h.flash(_("Failed to generate new recovery codes: {}").format(e), category='error')
271 271 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
272 272
273 273 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
274 274
275 275 @LoginRequired()
276 276 @NotAnonymous()
277 277 def my_account_auth_tokens(self):
278 278 _ = self.request.translate
279 279
280 280 c = self.load_default_context()
281 281 c.active = 'auth_tokens'
282 282 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
283 283 c.role_values = [
284 284 (x, AuthTokenModel.cls._get_role_name(x))
285 285 for x in AuthTokenModel.cls.ROLES]
286 286 c.role_options = [(c.role_values, _("Role"))]
287 287 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
288 288 c.user.user_id, show_expired=True)
289 289 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
290 290 return self._get_template_context(c)
291 291
292 292 @LoginRequired()
293 293 @NotAnonymous()
294 294 @CSRFRequired()
295 295 def my_account_auth_tokens_view(self):
296 296 _ = self.request.translate
297 297 c = self.load_default_context()
298 298
299 299 auth_token_id = self.request.POST.get('auth_token_id')
300 300
301 301 if auth_token_id:
302 302 token = UserApiKeys.get_or_404(auth_token_id)
303 303 if token.user.user_id != c.user.user_id:
304 304 raise HTTPNotFound()
305 305
306 306 return {
307 307 'auth_token': token.api_key
308 308 }
309 309
310 310 def maybe_attach_token_scope(self, token):
311 311 # implemented in EE edition
312 312 pass
313 313
314 314 @LoginRequired()
315 315 @NotAnonymous()
316 316 @CSRFRequired()
317 317 def my_account_auth_tokens_add(self):
318 318 _ = self.request.translate
319 319 c = self.load_default_context()
320 320
321 321 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
322 322 description = self.request.POST.get('description')
323 323 role = self.request.POST.get('role')
324 324
325 325 token = UserModel().add_auth_token(
326 326 user=c.user.user_id,
327 327 lifetime_minutes=lifetime, role=role, description=description,
328 328 scope_callback=self.maybe_attach_token_scope)
329 329 token_data = token.get_api_data()
330 330
331 331 audit_logger.store_web(
332 332 'user.edit.token.add', action_data={
333 333 'data': {'token': token_data, 'user': 'self'}},
334 334 user=self._rhodecode_user, )
335 335 Session().commit()
336 336
337 337 h.flash(_("Auth token successfully created"), category='success')
338 338 return HTTPFound(h.route_path('my_account_auth_tokens'))
339 339
340 340 @LoginRequired()
341 341 @NotAnonymous()
342 342 @CSRFRequired()
343 343 def my_account_auth_tokens_delete(self):
344 344 _ = self.request.translate
345 345 c = self.load_default_context()
346 346
347 347 del_auth_token = self.request.POST.get('del_auth_token')
348 348
349 349 if del_auth_token:
350 350 token = UserApiKeys.get_or_404(del_auth_token)
351 351 token_data = token.get_api_data()
352 352
353 353 AuthTokenModel().delete(del_auth_token, c.user.user_id)
354 354 audit_logger.store_web(
355 355 'user.edit.token.delete', action_data={
356 356 'data': {'token': token_data, 'user': 'self'}},
357 357 user=self._rhodecode_user,)
358 358 Session().commit()
359 359 h.flash(_("Auth token successfully deleted"), category='success')
360 360
361 361 return HTTPFound(h.route_path('my_account_auth_tokens'))
362 362
363 363 @LoginRequired()
364 364 @NotAnonymous()
365 365 def my_account_emails(self):
366 366 _ = self.request.translate
367 367
368 368 c = self.load_default_context()
369 369 c.active = 'emails'
370 370
371 371 c.user_email_map = UserEmailMap.query()\
372 372 .filter(UserEmailMap.user == c.user).all()
373 373
374 374 schema = user_schema.AddEmailSchema().bind(
375 375 username=c.user.username, user_emails=c.user.emails)
376 376
377 377 form = forms.RcForm(schema,
378 378 action=h.route_path('my_account_emails_add'),
379 379 buttons=(forms.buttons.save, forms.buttons.reset))
380 380
381 381 c.form = form
382 382 return self._get_template_context(c)
383 383
384 384 @LoginRequired()
385 385 @NotAnonymous()
386 386 @CSRFRequired()
387 387 def my_account_emails_add(self):
388 388 _ = self.request.translate
389 389 c = self.load_default_context()
390 390 c.active = 'emails'
391 391
392 392 schema = user_schema.AddEmailSchema().bind(
393 393 username=c.user.username, user_emails=c.user.emails)
394 394
395 395 form = forms.RcForm(
396 396 schema, action=h.route_path('my_account_emails_add'),
397 397 buttons=(forms.buttons.save, forms.buttons.reset))
398 398
399 399 controls = list(self.request.POST.items())
400 400 try:
401 401 valid_data = form.validate(controls)
402 402 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
403 403 audit_logger.store_web(
404 404 'user.edit.email.add', action_data={
405 405 'data': {'email': valid_data['email'], 'user': 'self'}},
406 406 user=self._rhodecode_user,)
407 407 Session().commit()
408 408 except formencode.Invalid as error:
409 409 h.flash(h.escape(error.error_dict['email']), category='error')
410 410 except forms.ValidationFailure as e:
411 411 c.user_email_map = UserEmailMap.query() \
412 412 .filter(UserEmailMap.user == c.user).all()
413 413 c.form = e
414 414 return self._get_template_context(c)
415 415 except Exception:
416 416 log.exception("Exception adding email")
417 417 h.flash(_('Error occurred during adding email'),
418 418 category='error')
419 419 else:
420 420 h.flash(_("Successfully added email"), category='success')
421 421
422 422 raise HTTPFound(self.request.route_path('my_account_emails'))
423 423
424 424 @LoginRequired()
425 425 @NotAnonymous()
426 426 @CSRFRequired()
427 427 def my_account_emails_delete(self):
428 428 _ = self.request.translate
429 429 c = self.load_default_context()
430 430
431 431 del_email_id = self.request.POST.get('del_email_id')
432 432 if del_email_id:
433 433 email = UserEmailMap.get_or_404(del_email_id).email
434 434 UserModel().delete_extra_email(c.user.user_id, del_email_id)
435 435 audit_logger.store_web(
436 436 'user.edit.email.delete', action_data={
437 437 'data': {'email': email, 'user': 'self'}},
438 438 user=self._rhodecode_user,)
439 439 Session().commit()
440 440 h.flash(_("Email successfully deleted"),
441 441 category='success')
442 442 return HTTPFound(h.route_path('my_account_emails'))
443 443
444 444 @LoginRequired()
445 445 @NotAnonymous()
446 446 @CSRFRequired()
447 447 def my_account_notifications_test_channelstream(self):
448 448 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
449 449 self._rhodecode_user.username, datetime.datetime.now())
450 450 payload = {
451 451 # 'channel': 'broadcast',
452 452 'type': 'message',
453 453 'timestamp': datetime.datetime.utcnow(),
454 454 'user': 'system',
455 455 'pm_users': [self._rhodecode_user.username],
456 456 'message': {
457 457 'message': message,
458 458 'level': 'info',
459 459 'topic': '/notifications'
460 460 }
461 461 }
462 462
463 463 registry = self.request.registry
464 464 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
465 465 channelstream_config = rhodecode_plugins.get('channelstream', {})
466 466
467 467 try:
468 468 channelstream_request(channelstream_config, [payload], '/message')
469 469 except ChannelstreamException as e:
470 470 log.exception('Failed to send channelstream data')
471 471 return {"response": f'ERROR: {e.__class__.__name__}'}
472 472 return {"response": 'Channelstream data sent. '
473 473 'You should see a new live message now.'}
474 474
475 475 def _load_my_repos_data(self, watched=False):
476 476
477 477 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
478 478
479 479 if watched:
480 480 # repos user watch
481 481 repo_list = Session().query(
482 482 Repository
483 483 ) \
484 484 .join(
485 485 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
486 486 ) \
487 487 .filter(
488 488 UserFollowing.user_id == self._rhodecode_user.user_id
489 489 ) \
490 490 .filter(or_(
491 491 # generate multiple IN to fix limitation problems
492 492 *in_filter_generator(Repository.repo_id, allowed_ids))
493 493 ) \
494 494 .order_by(Repository.repo_name) \
495 495 .all()
496 496
497 497 else:
498 498 # repos user is owner of
499 499 repo_list = Session().query(
500 500 Repository
501 501 ) \
502 502 .filter(
503 503 Repository.user_id == self._rhodecode_user.user_id
504 504 ) \
505 505 .filter(or_(
506 506 # generate multiple IN to fix limitation problems
507 507 *in_filter_generator(Repository.repo_id, allowed_ids))
508 508 ) \
509 509 .order_by(Repository.repo_name) \
510 510 .all()
511 511
512 512 _render = self.request.get_partial_renderer(
513 513 'rhodecode:templates/data_table/_dt_elements.mako')
514 514
515 515 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
516 516 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
517 517 short_name=False, admin=False)
518 518
519 519 repos_data = []
520 520 for repo in repo_list:
521 521 row = {
522 522 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
523 523 repo.private, repo.archived, repo.fork),
524 524 "name_raw": repo.repo_name.lower(),
525 525 }
526 526
527 527 repos_data.append(row)
528 528
529 529 # json used to render the grid
530 530 return ext_json.str_json(repos_data)
531 531
532 532 @LoginRequired()
533 533 @NotAnonymous()
534 534 def my_account_repos(self):
535 535 c = self.load_default_context()
536 536 c.active = 'repos'
537 537
538 538 # json used to render the grid
539 539 c.data = self._load_my_repos_data()
540 540 return self._get_template_context(c)
541 541
542 542 @LoginRequired()
543 543 @NotAnonymous()
544 544 def my_account_watched(self):
545 545 c = self.load_default_context()
546 546 c.active = 'watched'
547 547
548 548 # json used to render the grid
549 549 c.data = self._load_my_repos_data(watched=True)
550 550 return self._get_template_context(c)
551 551
552 552 @LoginRequired()
553 553 @NotAnonymous()
554 554 def my_account_bookmarks(self):
555 555 c = self.load_default_context()
556 556 c.active = 'bookmarks'
557 557
558 558 user_bookmarks = \
559 559 select(UserBookmark, Repository, RepoGroup) \
560 560 .where(UserBookmark.user_id == self._rhodecode_user.user_id) \
561 561 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
562 562 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
563 563 .order_by(UserBookmark.position.asc())
564 564
565 565 c.user_bookmark_items = Session().execute(user_bookmarks).all()
566 566 return self._get_template_context(c)
567 567
568 568 def _process_bookmark_entry(self, entry, user_id):
569 569 position = safe_int(entry.get('position'))
570 570 cur_position = safe_int(entry.get('cur_position'))
571 571 if position is None:
572 572 return
573 573
574 574 # check if this is an existing entry
575 575 is_new = False
576 576 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
577 577
578 578 if db_entry and str2bool(entry.get('remove')):
579 579 log.debug('Marked bookmark %s for deletion', db_entry)
580 580 Session().delete(db_entry)
581 581 return
582 582
583 583 if not db_entry:
584 584 # new
585 585 db_entry = UserBookmark()
586 586 is_new = True
587 587
588 588 should_save = False
589 589 default_redirect_url = ''
590 590
591 591 # save repo
592 592 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
593 593 repo = Repository.get(entry['bookmark_repo'])
594 594 perm_check = HasRepoPermissionAny(
595 595 'repository.read', 'repository.write', 'repository.admin')
596 596 if repo and perm_check(repo_name=repo.repo_name):
597 597 db_entry.repository = repo
598 598 should_save = True
599 599 default_redirect_url = '${repo_url}'
600 600 # save repo group
601 601 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
602 602 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
603 603 perm_check = HasRepoGroupPermissionAny(
604 604 'group.read', 'group.write', 'group.admin')
605 605
606 606 if repo_group and perm_check(group_name=repo_group.group_name):
607 607 db_entry.repository_group = repo_group
608 608 should_save = True
609 609 default_redirect_url = '${repo_group_url}'
610 610 # save generic info
611 611 elif entry.get('title') and entry.get('redirect_url'):
612 612 should_save = True
613 613
614 614 if should_save:
615 615 # mark user and position
616 616 db_entry.user_id = user_id
617 617 db_entry.position = position
618 618 db_entry.title = entry.get('title')
619 619 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
620 620 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
621 621
622 622 Session().add(db_entry)
623 623
624 624 @LoginRequired()
625 625 @NotAnonymous()
626 626 @CSRFRequired()
627 627 def my_account_bookmarks_update(self):
628 628 _ = self.request.translate
629 629 c = self.load_default_context()
630 630 c.active = 'bookmarks'
631 631
632 632 controls = peppercorn.parse(self.request.POST.items())
633 633 user_id = c.user.user_id
634 634
635 635 # validate positions
636 636 positions = {}
637 637 for entry in controls.get('bookmarks', []):
638 638 position = safe_int(entry['position'])
639 639 if position is None:
640 640 continue
641 641
642 642 if position in positions:
643 643 h.flash(_("Position {} is defined twice. "
644 644 "Please correct this error.").format(position), category='error')
645 645 return HTTPFound(h.route_path('my_account_bookmarks'))
646 646
647 647 entry['position'] = position
648 648 entry['cur_position'] = safe_int(entry.get('cur_position'))
649 649 positions[position] = entry
650 650
651 651 try:
652 652 for entry in positions.values():
653 653 self._process_bookmark_entry(entry, user_id)
654 654
655 655 Session().commit()
656 656 h.flash(_("Update Bookmarks"), category='success')
657 657 except IntegrityError:
658 658 h.flash(_("Failed to update bookmarks. "
659 659 "Make sure an unique position is used."), category='error')
660 660
661 661 return HTTPFound(h.route_path('my_account_bookmarks'))
662 662
663 663 @LoginRequired()
664 664 @NotAnonymous()
665 665 def my_account_goto_bookmark(self):
666 666
667 667 bookmark_id = self.request.matchdict['bookmark_id']
668 668 user_bookmark = UserBookmark().query()\
669 669 .filter(UserBookmark.user_id == self.request.user.user_id) \
670 670 .filter(UserBookmark.position == bookmark_id).scalar()
671 671
672 672 redirect_url = h.route_path('my_account_bookmarks')
673 673 if not user_bookmark:
674 674 raise HTTPFound(redirect_url)
675 675
676 676 # repository set
677 677 if user_bookmark.repository:
678 678 repo_name = user_bookmark.repository.repo_name
679 679 base_redirect_url = h.route_path(
680 680 'repo_summary', repo_name=repo_name)
681 681 if user_bookmark.redirect_url and \
682 682 '${repo_url}' in user_bookmark.redirect_url:
683 683 redirect_url = string.Template(user_bookmark.redirect_url)\
684 684 .safe_substitute({'repo_url': base_redirect_url})
685 685 else:
686 686 redirect_url = base_redirect_url
687 687 # repository group set
688 688 elif user_bookmark.repository_group:
689 689 repo_group_name = user_bookmark.repository_group.group_name
690 690 base_redirect_url = h.route_path(
691 691 'repo_group_home', repo_group_name=repo_group_name)
692 692 if user_bookmark.redirect_url and \
693 693 '${repo_group_url}' in user_bookmark.redirect_url:
694 694 redirect_url = string.Template(user_bookmark.redirect_url)\
695 695 .safe_substitute({'repo_group_url': base_redirect_url})
696 696 else:
697 697 redirect_url = base_redirect_url
698 698 # custom URL set
699 699 elif user_bookmark.redirect_url:
700 700 server_url = h.route_url('home').rstrip('/')
701 701 redirect_url = string.Template(user_bookmark.redirect_url) \
702 702 .safe_substitute({'server_url': server_url})
703 703
704 704 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
705 705 raise HTTPFound(redirect_url)
706 706
707 707 @LoginRequired()
708 708 @NotAnonymous()
709 709 def my_account_perms(self):
710 710 c = self.load_default_context()
711 711 c.active = 'perms'
712 712
713 713 c.perm_user = c.auth_user
714 714 return self._get_template_context(c)
715 715
716 716 @LoginRequired()
717 717 @NotAnonymous()
718 718 def my_notifications(self):
719 719 c = self.load_default_context()
720 720 c.active = 'notifications'
721 721
722 722 return self._get_template_context(c)
723 723
724 724 @LoginRequired()
725 725 @NotAnonymous()
726 726 @CSRFRequired()
727 727 def my_notifications_toggle_visibility(self):
728 728 user = self._rhodecode_db_user
729 729 new_status = not user.user_data.get('notification_status', True)
730 730 user.update_userdata(notification_status=new_status)
731 731 Session().commit()
732 732 return user.user_data['notification_status']
733 733
734 734 def _get_pull_requests_list(self, statuses, filter_type=None):
735 735 draw, start, limit = self._extract_chunk(self.request)
736 736 search_q, order_by, order_dir = self._extract_ordering(self.request)
737 737
738 738 _render = self.request.get_partial_renderer(
739 739 'rhodecode:templates/data_table/_dt_elements.mako')
740 740
741 741 if filter_type == 'awaiting_my_review':
742 742 pull_requests = PullRequestModel().get_im_participating_in_for_review(
743 743 user_id=self._rhodecode_user.user_id,
744 744 statuses=statuses, query=search_q,
745 745 offset=start, length=limit, order_by=order_by,
746 746 order_dir=order_dir)
747 747
748 748 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
749 749 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
750 750 else:
751 751 pull_requests = PullRequestModel().get_im_participating_in(
752 752 user_id=self._rhodecode_user.user_id,
753 753 statuses=statuses, query=search_q,
754 754 offset=start, length=limit, order_by=order_by,
755 755 order_dir=order_dir)
756 756
757 757 pull_requests_total_count = PullRequestModel().count_im_participating_in(
758 758 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
759 759
760 760 data = []
761 761 comments_model = CommentsModel()
762 762 for pr in pull_requests:
763 763 repo_id = pr.target_repo_id
764 764 comments_count = comments_model.get_all_comments(
765 765 repo_id, pull_request=pr, include_drafts=False, count_only=True)
766 766 owned = pr.user_id == self._rhodecode_user.user_id
767 767
768 768 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
769 769 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
770 770 if review_statuses and review_statuses[4]:
771 771 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
772 772 my_review_status = statuses[0][1].status
773 773
774 774 data.append({
775 775 'target_repo': _render('pullrequest_target_repo',
776 776 pr.target_repo.repo_name),
777 777 'name': _render('pullrequest_name',
778 778 pr.pull_request_id, pr.pull_request_state,
779 779 pr.work_in_progress, pr.target_repo.repo_name,
780 780 short=True),
781 781 'name_raw': pr.pull_request_id,
782 782 'status': _render('pullrequest_status',
783 783 pr.calculated_review_status()),
784 784 'my_status': _render('pullrequest_status',
785 785 my_review_status),
786 786 'title': _render('pullrequest_title', pr.title, pr.description),
787 787 'pr_flow': _render('pullrequest_commit_flow', pr),
788 788 'description': h.escape(pr.description),
789 789 'updated_on': _render('pullrequest_updated_on',
790 790 h.datetime_to_time(pr.updated_on),
791 791 pr.versions_count),
792 792 'updated_on_raw': h.datetime_to_time(pr.updated_on),
793 793 'created_on': _render('pullrequest_updated_on',
794 794 h.datetime_to_time(pr.created_on)),
795 795 'created_on_raw': h.datetime_to_time(pr.created_on),
796 796 'state': pr.pull_request_state,
797 797 'author': _render('pullrequest_author',
798 798 pr.author.full_contact, ),
799 799 'author_raw': pr.author.full_name,
800 800 'comments': _render('pullrequest_comments', comments_count),
801 801 'comments_raw': comments_count,
802 802 'closed': pr.is_closed(),
803 803 'owned': owned
804 804 })
805 805
806 806 # json used to render the grid
807 807 data = ({
808 808 'draw': draw,
809 809 'data': data,
810 810 'recordsTotal': pull_requests_total_count,
811 811 'recordsFiltered': pull_requests_total_count,
812 812 })
813 813 return data
814 814
815 815 @LoginRequired()
816 816 @NotAnonymous()
817 817 def my_account_pullrequests(self):
818 818 c = self.load_default_context()
819 819 c.active = 'pullrequests'
820 820 req_get = self.request.GET
821 821
822 822 c.closed = str2bool(req_get.get('closed'))
823 823 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
824 824
825 825 c.selected_filter = 'all'
826 826 if c.closed:
827 827 c.selected_filter = 'all_closed'
828 828 if c.awaiting_my_review:
829 829 c.selected_filter = 'awaiting_my_review'
830 830
831 831 return self._get_template_context(c)
832 832
833 833 @LoginRequired()
834 834 @NotAnonymous()
835 835 def my_account_pullrequests_data(self):
836 836 self.load_default_context()
837 837 req_get = self.request.GET
838 838
839 839 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
840 840 closed = str2bool(req_get.get('closed'))
841 841
842 842 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
843 843 if closed:
844 844 statuses += [PullRequest.STATUS_CLOSED]
845 845
846 846 filter_type = \
847 847 'awaiting_my_review' if awaiting_my_review \
848 848 else None
849 849
850 850 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
851 851 return data
852 852
853 853 @LoginRequired()
854 854 @NotAnonymous()
855 855 def my_account_user_group_membership(self):
856 856 c = self.load_default_context()
857 857 c.active = 'user_group_membership'
858 858 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
859 859 for group in self._rhodecode_db_user.group_member]
860 860 c.user_groups = ext_json.str_json(groups)
861 861 return self._get_template_context(c)
@@ -1,6038 +1,6043 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Database Models for RhodeCode Enterprise
21 21 """
22 22
23 23 import re
24 24 import os
25 25 import time
26 26 import string
27 27 import logging
28 28 import datetime
29 29 import uuid
30 30 import warnings
31 31 import ipaddress
32 32 import functools
33 33 import traceback
34 34 import collections
35 35
36 36 import pyotp
37 37 from sqlalchemy import (
38 38 or_, and_, not_, func, cast, TypeDecorator, event, select,
39 39 true, false, null, union_all,
40 40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 42 Text, Float, PickleType, BigInteger)
43 43 from sqlalchemy.sql.expression import case
44 44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 45 from sqlalchemy.orm import (
46 46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
47 47 from sqlalchemy.ext.declarative import declared_attr
48 48 from sqlalchemy.ext.hybrid import hybrid_property
49 49 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 50 from sqlalchemy.dialects.mysql import LONGTEXT
51 51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 52 from pyramid.threadlocal import get_current_request
53 53 from webhelpers2.text import remove_formatting
54 54
55 55 from rhodecode import ConfigGet
56 56 from rhodecode.lib.str_utils import safe_bytes
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 59 from rhodecode.lib.vcs.backends.base import (
60 60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 61 from rhodecode.lib.utils2 import (
62 62 str2bool, safe_str, get_commit_safe, sha1_safe,
63 63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
65 65 from rhodecode.lib.jsonalchemy import (
66 66 MutationObj, MutationList, JsonType, JsonRaw)
67 67 from rhodecode.lib.hash_utils import sha1
68 68 from rhodecode.lib import ext_json
69 69 from rhodecode.lib import enc_utils
70 70 from rhodecode.lib.ext_json import json, str_json
71 71 from rhodecode.lib.caching_query import FromCache
72 72 from rhodecode.lib.exceptions import (
73 73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
74 74 from rhodecode.model.meta import Base, Session
75 75
76 76 URL_SEP = '/'
77 77 log = logging.getLogger(__name__)
78 78
79 79 # =============================================================================
80 80 # BASE CLASSES
81 81 # =============================================================================
82 82
83 83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
84 84 # beaker.session.secret if first is not set.
85 85 # and initialized at environment.py
86 86 ENCRYPTION_KEY: bytes = b''
87 87
88 88 # used to sort permissions by types, '#' used here is not allowed to be in
89 89 # usernames, and it's very early in sorted string.printable table.
90 90 PERMISSION_TYPE_SORT = {
91 91 'admin': '####',
92 92 'write': '###',
93 93 'read': '##',
94 94 'none': '#',
95 95 }
96 96
97 97
98 98 def display_user_sort(obj):
99 99 """
100 100 Sort function used to sort permissions in .permissions() function of
101 101 Repository, RepoGroup, UserGroup. Also it put the default user in front
102 102 of all other resources
103 103 """
104 104
105 105 if obj.username == User.DEFAULT_USER:
106 106 return '#####'
107 107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
108 108 extra_sort_num = '1' # default
109 109
110 110 # NOTE(dan): inactive duplicates goes last
111 111 if getattr(obj, 'duplicate_perm', None):
112 112 extra_sort_num = '9'
113 113 return prefix + extra_sort_num + obj.username
114 114
115 115
116 116 def display_user_group_sort(obj):
117 117 """
118 118 Sort function used to sort permissions in .permissions() function of
119 119 Repository, RepoGroup, UserGroup. Also it put the default user in front
120 120 of all other resources
121 121 """
122 122
123 123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
124 124 return prefix + obj.users_group_name
125 125
126 126
127 127 def _hash_key(k):
128 128 return sha1_safe(k)
129 129
130 130
131 131 def in_filter_generator(qry, items, limit=500):
132 132 """
133 133 Splits IN() into multiple with OR
134 134 e.g.::
135 135 cnt = Repository.query().filter(
136 136 or_(
137 137 *in_filter_generator(Repository.repo_id, range(100000))
138 138 )).count()
139 139 """
140 140 if not items:
141 141 # empty list will cause empty query which might cause security issues
142 142 # this can lead to hidden unpleasant results
143 143 items = [-1]
144 144
145 145 parts = []
146 146 for chunk in range(0, len(items), limit):
147 147 parts.append(
148 148 qry.in_(items[chunk: chunk + limit])
149 149 )
150 150
151 151 return parts
152 152
153 153
154 154 base_table_args = {
155 155 'extend_existing': True,
156 156 'mysql_engine': 'InnoDB',
157 157 'mysql_charset': 'utf8',
158 158 'sqlite_autoincrement': True
159 159 }
160 160
161 161
162 162 class EncryptedTextValue(TypeDecorator):
163 163 """
164 164 Special column for encrypted long text data, use like::
165 165
166 166 value = Column("encrypted_value", EncryptedValue(), nullable=False)
167 167
168 168 This column is intelligent so if value is in unencrypted form it return
169 169 unencrypted form, but on save it always encrypts
170 170 """
171 171 cache_ok = True
172 172 impl = Text
173 173
174 174 def process_bind_param(self, value, dialect):
175 175 """
176 176 Setter for storing value
177 177 """
178 178 import rhodecode
179 179 if not value:
180 180 return value
181 181
182 182 # protect against double encrypting if values is already encrypted
183 183 if value.startswith('enc$aes$') \
184 184 or value.startswith('enc$aes_hmac$') \
185 185 or value.startswith('enc2$'):
186 186 raise ValueError('value needs to be in unencrypted format, '
187 187 'ie. not starting with enc$ or enc2$')
188 188
189 189 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
190 190 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
191 191 return safe_str(bytes_val)
192 192
193 193 def process_result_value(self, value, dialect):
194 194 """
195 195 Getter for retrieving value
196 196 """
197 197
198 198 import rhodecode
199 199 if not value:
200 200 return value
201 201
202 202 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
203 203
204 204 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
205 205
206 206 return safe_str(bytes_val)
207 207
208 208
209 209 class BaseModel(object):
210 210 """
211 211 Base Model for all classes
212 212 """
213 213
214 214 @classmethod
215 215 def _get_keys(cls):
216 216 """return column names for this model """
217 217 return class_mapper(cls).c.keys()
218 218
219 219 def get_dict(self):
220 220 """
221 221 return dict with keys and values corresponding
222 222 to this model data """
223 223
224 224 d = {}
225 225 for k in self._get_keys():
226 226 d[k] = getattr(self, k)
227 227
228 228 # also use __json__() if present to get additional fields
229 229 _json_attr = getattr(self, '__json__', None)
230 230 if _json_attr:
231 231 # update with attributes from __json__
232 232 if callable(_json_attr):
233 233 _json_attr = _json_attr()
234 234 for k, val in _json_attr.items():
235 235 d[k] = val
236 236 return d
237 237
238 238 def get_appstruct(self):
239 239 """return list with keys and values tuples corresponding
240 240 to this model data """
241 241
242 242 lst = []
243 243 for k in self._get_keys():
244 244 lst.append((k, getattr(self, k),))
245 245 return lst
246 246
247 247 def populate_obj(self, populate_dict):
248 248 """populate model with data from given populate_dict"""
249 249
250 250 for k in self._get_keys():
251 251 if k in populate_dict:
252 252 setattr(self, k, populate_dict[k])
253 253
254 254 @classmethod
255 255 def query(cls):
256 256 return Session().query(cls)
257 257
258 258 @classmethod
259 259 def select(cls, custom_cls=None):
260 260 """
261 261 stmt = cls.select().where(cls.user_id==1)
262 262 # optionally
263 263 stmt = cls.select(User.user_id).where(cls.user_id==1)
264 264 result = cls.execute(stmt) | cls.scalars(stmt)
265 265 """
266 266
267 267 if custom_cls:
268 268 stmt = select(custom_cls)
269 269 else:
270 270 stmt = select(cls)
271 271 return stmt
272 272
273 273 @classmethod
274 274 def execute(cls, stmt):
275 275 return Session().execute(stmt)
276 276
277 277 @classmethod
278 278 def scalars(cls, stmt):
279 279 return Session().scalars(stmt)
280 280
281 281 @classmethod
282 282 def get(cls, id_):
283 283 if id_:
284 284 return cls.query().get(id_)
285 285
286 286 @classmethod
287 287 def get_or_404(cls, id_):
288 288 from pyramid.httpexceptions import HTTPNotFound
289 289
290 290 try:
291 291 id_ = int(id_)
292 292 except (TypeError, ValueError):
293 293 raise HTTPNotFound()
294 294
295 295 res = cls.query().get(id_)
296 296 if not res:
297 297 raise HTTPNotFound()
298 298 return res
299 299
300 300 @classmethod
301 301 def getAll(cls):
302 302 # deprecated and left for backward compatibility
303 303 return cls.get_all()
304 304
305 305 @classmethod
306 306 def get_all(cls):
307 307 return cls.query().all()
308 308
309 309 @classmethod
310 310 def delete(cls, id_):
311 311 obj = cls.query().get(id_)
312 312 Session().delete(obj)
313 313
314 314 @classmethod
315 315 def identity_cache(cls, session, attr_name, value):
316 316 exist_in_session = []
317 317 for (item_cls, pkey), instance in session.identity_map.items():
318 318 if cls == item_cls and getattr(instance, attr_name) == value:
319 319 exist_in_session.append(instance)
320 320 if exist_in_session:
321 321 if len(exist_in_session) == 1:
322 322 return exist_in_session[0]
323 323 log.exception(
324 324 'multiple objects with attr %s and '
325 325 'value %s found with same name: %r',
326 326 attr_name, value, exist_in_session)
327 327
328 328 @property
329 329 def cls_name(self):
330 330 return self.__class__.__name__
331 331
332 332 def __repr__(self):
333 333 return f'<DB:{self.cls_name}>'
334 334
335 335
336 336 class RhodeCodeSetting(Base, BaseModel):
337 337 __tablename__ = 'rhodecode_settings'
338 338 __table_args__ = (
339 339 UniqueConstraint('app_settings_name'),
340 340 base_table_args
341 341 )
342 342
343 343 SETTINGS_TYPES = {
344 344 'str': safe_str,
345 345 'int': safe_int,
346 346 'unicode': safe_str,
347 347 'bool': str2bool,
348 348 'list': functools.partial(aslist, sep=',')
349 349 }
350 350 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
351 351 GLOBAL_CONF_KEY = 'app_settings'
352 352
353 353 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
354 354 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
355 355 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
356 356 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
357 357
358 358 def __init__(self, key='', val='', type='unicode'):
359 359 self.app_settings_name = key
360 360 self.app_settings_type = type
361 361 self.app_settings_value = val
362 362
363 363 @validates('_app_settings_value')
364 364 def validate_settings_value(self, key, val):
365 365 assert type(val) == str
366 366 return val
367 367
368 368 @hybrid_property
369 369 def app_settings_value(self):
370 370 v = self._app_settings_value
371 371 _type = self.app_settings_type
372 372 if _type:
373 373 _type = self.app_settings_type.split('.')[0]
374 374 # decode the encrypted value
375 375 if 'encrypted' in self.app_settings_type:
376 376 cipher = EncryptedTextValue()
377 377 v = safe_str(cipher.process_result_value(v, None))
378 378
379 379 converter = self.SETTINGS_TYPES.get(_type) or \
380 380 self.SETTINGS_TYPES['unicode']
381 381 return converter(v)
382 382
383 383 @app_settings_value.setter
384 384 def app_settings_value(self, val):
385 385 """
386 386 Setter that will always make sure we use unicode in app_settings_value
387 387
388 388 :param val:
389 389 """
390 390 val = safe_str(val)
391 391 # encode the encrypted value
392 392 if 'encrypted' in self.app_settings_type:
393 393 cipher = EncryptedTextValue()
394 394 val = safe_str(cipher.process_bind_param(val, None))
395 395 self._app_settings_value = val
396 396
397 397 @hybrid_property
398 398 def app_settings_type(self):
399 399 return self._app_settings_type
400 400
401 401 @app_settings_type.setter
402 402 def app_settings_type(self, val):
403 403 if val.split('.')[0] not in self.SETTINGS_TYPES:
404 404 raise Exception('type must be one of %s got %s'
405 405 % (self.SETTINGS_TYPES.keys(), val))
406 406 self._app_settings_type = val
407 407
408 408 @classmethod
409 409 def get_by_prefix(cls, prefix):
410 410 return RhodeCodeSetting.query()\
411 411 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
412 412 .all()
413 413
414 414 def __repr__(self):
415 415 return "<%s('%s:%s[%s]')>" % (
416 416 self.cls_name,
417 417 self.app_settings_name, self.app_settings_value,
418 418 self.app_settings_type
419 419 )
420 420
421 421
422 422 class RhodeCodeUi(Base, BaseModel):
423 423 __tablename__ = 'rhodecode_ui'
424 424 __table_args__ = (
425 425 UniqueConstraint('ui_key'),
426 426 base_table_args
427 427 )
428 428 # Sync those values with vcsserver.config.hooks
429 429
430 430 HOOK_REPO_SIZE = 'changegroup.repo_size'
431 431 # HG
432 432 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
433 433 HOOK_PULL = 'outgoing.pull_logger'
434 434 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
435 435 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
436 436 HOOK_PUSH = 'changegroup.push_logger'
437 437 HOOK_PUSH_KEY = 'pushkey.key_push'
438 438
439 439 HOOKS_BUILTIN = [
440 440 HOOK_PRE_PULL,
441 441 HOOK_PULL,
442 442 HOOK_PRE_PUSH,
443 443 HOOK_PRETX_PUSH,
444 444 HOOK_PUSH,
445 445 HOOK_PUSH_KEY,
446 446 ]
447 447
448 448 # TODO: johbo: Unify way how hooks are configured for git and hg,
449 449 # git part is currently hardcoded.
450 450
451 451 # SVN PATTERNS
452 452 SVN_BRANCH_ID = 'vcs_svn_branch'
453 453 SVN_TAG_ID = 'vcs_svn_tag'
454 454
455 455 ui_id = Column(
456 456 "ui_id", Integer(), nullable=False, unique=True, default=None,
457 457 primary_key=True)
458 458 ui_section = Column(
459 459 "ui_section", String(255), nullable=True, unique=None, default=None)
460 460 ui_key = Column(
461 461 "ui_key", String(255), nullable=True, unique=None, default=None)
462 462 ui_value = Column(
463 463 "ui_value", String(255), nullable=True, unique=None, default=None)
464 464 ui_active = Column(
465 465 "ui_active", Boolean(), nullable=True, unique=None, default=True)
466 466
467 467 def __repr__(self):
468 468 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
469 469 self.ui_key, self.ui_value)
470 470
471 471
472 472 class RepoRhodeCodeSetting(Base, BaseModel):
473 473 __tablename__ = 'repo_rhodecode_settings'
474 474 __table_args__ = (
475 475 UniqueConstraint(
476 476 'app_settings_name', 'repository_id',
477 477 name='uq_repo_rhodecode_setting_name_repo_id'),
478 478 base_table_args
479 479 )
480 480
481 481 repository_id = Column(
482 482 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
483 483 nullable=False)
484 484 app_settings_id = Column(
485 485 "app_settings_id", Integer(), nullable=False, unique=True,
486 486 default=None, primary_key=True)
487 487 app_settings_name = Column(
488 488 "app_settings_name", String(255), nullable=True, unique=None,
489 489 default=None)
490 490 _app_settings_value = Column(
491 491 "app_settings_value", String(4096), nullable=True, unique=None,
492 492 default=None)
493 493 _app_settings_type = Column(
494 494 "app_settings_type", String(255), nullable=True, unique=None,
495 495 default=None)
496 496
497 497 repository = relationship('Repository', viewonly=True)
498 498
499 499 def __init__(self, repository_id, key='', val='', type='unicode'):
500 500 self.repository_id = repository_id
501 501 self.app_settings_name = key
502 502 self.app_settings_type = type
503 503 self.app_settings_value = val
504 504
505 505 @validates('_app_settings_value')
506 506 def validate_settings_value(self, key, val):
507 507 assert type(val) == str
508 508 return val
509 509
510 510 @hybrid_property
511 511 def app_settings_value(self):
512 512 v = self._app_settings_value
513 513 type_ = self.app_settings_type
514 514 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
515 515 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
516 516 return converter(v)
517 517
518 518 @app_settings_value.setter
519 519 def app_settings_value(self, val):
520 520 """
521 521 Setter that will always make sure we use unicode in app_settings_value
522 522
523 523 :param val:
524 524 """
525 525 self._app_settings_value = safe_str(val)
526 526
527 527 @hybrid_property
528 528 def app_settings_type(self):
529 529 return self._app_settings_type
530 530
531 531 @app_settings_type.setter
532 532 def app_settings_type(self, val):
533 533 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
534 534 if val not in SETTINGS_TYPES:
535 535 raise Exception('type must be one of %s got %s'
536 536 % (SETTINGS_TYPES.keys(), val))
537 537 self._app_settings_type = val
538 538
539 539 def __repr__(self):
540 540 return "<%s('%s:%s:%s[%s]')>" % (
541 541 self.cls_name, self.repository.repo_name,
542 542 self.app_settings_name, self.app_settings_value,
543 543 self.app_settings_type
544 544 )
545 545
546 546
547 547 class RepoRhodeCodeUi(Base, BaseModel):
548 548 __tablename__ = 'repo_rhodecode_ui'
549 549 __table_args__ = (
550 550 UniqueConstraint(
551 551 'repository_id', 'ui_section', 'ui_key',
552 552 name='uq_repo_rhodecode_ui_repository_id_section_key'),
553 553 base_table_args
554 554 )
555 555
556 556 repository_id = Column(
557 557 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
558 558 nullable=False)
559 559 ui_id = Column(
560 560 "ui_id", Integer(), nullable=False, unique=True, default=None,
561 561 primary_key=True)
562 562 ui_section = Column(
563 563 "ui_section", String(255), nullable=True, unique=None, default=None)
564 564 ui_key = Column(
565 565 "ui_key", String(255), nullable=True, unique=None, default=None)
566 566 ui_value = Column(
567 567 "ui_value", String(255), nullable=True, unique=None, default=None)
568 568 ui_active = Column(
569 569 "ui_active", Boolean(), nullable=True, unique=None, default=True)
570 570
571 571 repository = relationship('Repository', viewonly=True)
572 572
573 573 def __repr__(self):
574 574 return '<%s[%s:%s]%s=>%s]>' % (
575 575 self.cls_name, self.repository.repo_name,
576 576 self.ui_section, self.ui_key, self.ui_value)
577 577
578 578
579 579 class User(Base, BaseModel):
580 580 __tablename__ = 'users'
581 581 __table_args__ = (
582 582 UniqueConstraint('username'), UniqueConstraint('email'),
583 583 Index('u_username_idx', 'username'),
584 584 Index('u_email_idx', 'email'),
585 585 base_table_args
586 586 )
587 587
588 588 DEFAULT_USER = 'default'
589 589 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
590 590 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
591 591 RECOVERY_CODES_COUNT = 10
592 592
593 593 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
594 594 username = Column("username", String(255), nullable=True, unique=None, default=None)
595 595 password = Column("password", String(255), nullable=True, unique=None, default=None)
596 596 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
597 597 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
598 598 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
599 599 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
600 600 _email = Column("email", String(255), nullable=True, unique=None, default=None)
601 601 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
602 602 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
603 603 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
604 604
605 605 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
606 606 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
607 607 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
608 608 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
609 609 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
610 610 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
611 611
612 612 user_log = relationship('UserLog', back_populates='user')
613 613 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
614 614
615 615 repositories = relationship('Repository', back_populates='user')
616 616 repository_groups = relationship('RepoGroup', back_populates='user')
617 617 user_groups = relationship('UserGroup', back_populates='user')
618 618
619 619 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
620 620 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
621 621
622 622 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
623 623 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
624 624 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
625 625
626 626 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
627 627
628 628 notifications = relationship('UserNotification', cascade='all', back_populates='user')
629 629 # notifications assigned to this user
630 630 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
631 631 # comments created by this user
632 632 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
633 633 # user profile extra info
634 634 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
635 635 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
636 636 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
637 637 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
638 638
639 639 # gists
640 640 user_gists = relationship('Gist', cascade='all', back_populates='owner')
641 641 # user pull requests
642 642 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
643 643
644 644 # external identities
645 645 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
646 646 # review rules
647 647 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
648 648
649 649 # artifacts owned
650 650 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
651 651
652 652 # no cascade, set NULL
653 653 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
654 654
655 655 def __repr__(self):
656 656 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
657 657
658 658 @hybrid_property
659 659 def email(self):
660 660 return self._email
661 661
662 662 @email.setter
663 663 def email(self, val):
664 664 self._email = val.lower() if val else None
665 665
666 666 @hybrid_property
667 667 def first_name(self):
668 668 from rhodecode.lib import helpers as h
669 669 if self.name:
670 670 return h.escape(self.name)
671 671 return self.name
672 672
673 673 @hybrid_property
674 674 def last_name(self):
675 675 from rhodecode.lib import helpers as h
676 676 if self.lastname:
677 677 return h.escape(self.lastname)
678 678 return self.lastname
679 679
680 680 @hybrid_property
681 681 def api_key(self):
682 682 """
683 683 Fetch if exist an auth-token with role ALL connected to this user
684 684 """
685 685 user_auth_token = UserApiKeys.query()\
686 686 .filter(UserApiKeys.user_id == self.user_id)\
687 687 .filter(or_(UserApiKeys.expires == -1,
688 688 UserApiKeys.expires >= time.time()))\
689 689 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
690 690 if user_auth_token:
691 691 user_auth_token = user_auth_token.api_key
692 692
693 693 return user_auth_token
694 694
695 695 @api_key.setter
696 696 def api_key(self, val):
697 697 # don't allow to set API key this is deprecated for now
698 698 self._api_key = None
699 699
700 700 @property
701 701 def reviewer_pull_requests(self):
702 702 return PullRequestReviewers.query() \
703 703 .options(joinedload(PullRequestReviewers.pull_request)) \
704 704 .filter(PullRequestReviewers.user_id == self.user_id) \
705 705 .all()
706 706
707 707 @property
708 708 def firstname(self):
709 709 # alias for future
710 710 return self.name
711 711
712 712 @property
713 713 def emails(self):
714 714 other = UserEmailMap.query()\
715 715 .filter(UserEmailMap.user == self) \
716 716 .order_by(UserEmailMap.email_id.asc()) \
717 717 .all()
718 718 return [self.email] + [x.email for x in other]
719 719
720 720 def emails_cached(self):
721 721 emails = []
722 722 if self.user_id != self.get_default_user_id():
723 723 emails = UserEmailMap.query()\
724 724 .filter(UserEmailMap.user == self) \
725 725 .order_by(UserEmailMap.email_id.asc())
726 726
727 727 emails = emails.options(
728 728 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
729 729 )
730 730
731 731 return [self.email] + [x.email for x in emails]
732 732
733 733 @property
734 734 def auth_tokens(self):
735 735 auth_tokens = self.get_auth_tokens()
736 736 return [x.api_key for x in auth_tokens]
737 737
738 738 def get_auth_tokens(self):
739 739 return UserApiKeys.query()\
740 740 .filter(UserApiKeys.user == self)\
741 741 .order_by(UserApiKeys.user_api_key_id.asc())\
742 742 .all()
743 743
744 744 @LazyProperty
745 745 def feed_token(self):
746 746 return self.get_feed_token()
747 747
748 748 def get_feed_token(self, cache=True):
749 749 feed_tokens = UserApiKeys.query()\
750 750 .filter(UserApiKeys.user == self)\
751 751 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
752 752 if cache:
753 753 feed_tokens = feed_tokens.options(
754 754 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
755 755
756 756 feed_tokens = feed_tokens.all()
757 757 if feed_tokens:
758 758 return feed_tokens[0].api_key
759 759 return 'NO_FEED_TOKEN_AVAILABLE'
760 760
761 761 @LazyProperty
762 762 def artifact_token(self):
763 763 return self.get_artifact_token()
764 764
765 765 def get_artifact_token(self, cache=True):
766 766 artifacts_tokens = UserApiKeys.query()\
767 767 .filter(UserApiKeys.user == self) \
768 768 .filter(or_(UserApiKeys.expires == -1,
769 769 UserApiKeys.expires >= time.time())) \
770 770 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
771 771
772 772 if cache:
773 773 artifacts_tokens = artifacts_tokens.options(
774 774 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
775 775
776 776 artifacts_tokens = artifacts_tokens.all()
777 777 if artifacts_tokens:
778 778 return artifacts_tokens[0].api_key
779 779 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
780 780
781 781 def get_or_create_artifact_token(self):
782 782 artifacts_tokens = UserApiKeys.query()\
783 783 .filter(UserApiKeys.user == self) \
784 784 .filter(or_(UserApiKeys.expires == -1,
785 785 UserApiKeys.expires >= time.time())) \
786 786 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
787 787
788 788 artifacts_tokens = artifacts_tokens.all()
789 789 if artifacts_tokens:
790 790 return artifacts_tokens[0].api_key
791 791 else:
792 792 from rhodecode.model.auth_token import AuthTokenModel
793 793 artifact_token = AuthTokenModel().create(
794 794 self, 'auto-generated-artifact-token',
795 795 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
796 796 Session.commit()
797 797 return artifact_token.api_key
798 798
799 799 def is_totp_valid(self, received_code, secret):
800 800 totp = pyotp.TOTP(secret)
801 801 return totp.verify(received_code)
802 802
803 803 def is_2fa_recovery_code_valid(self, received_code, secret):
804 804 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
805 805 recovery_codes = self.get_2fa_recovery_codes()
806 806 if received_code in recovery_codes:
807 807 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
808 808 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
809 809 return True
810 810 return False
811 811
812 812 @hybrid_property
813 813 def has_forced_2fa(self):
814 814 """
815 815 Checks if 2fa was forced for ALL users (including current one)
816 816 """
817 817 from rhodecode.model.settings import SettingsModel
818 818 # So now we're supporting only auth_rhodecode_global_2f
819 819 if value := SettingsModel().get_setting_by_name('auth_rhodecode_global_2fa'):
820 820 return value.app_settings_value
821 821 return False
822 822
823 823 @hybrid_property
824 824 def has_enabled_2fa(self):
825 825 """
826 826 Checks if user enabled 2fa
827 827 """
828 828 if value := self.has_forced_2fa:
829 829 return value
830 830 return self.user_data.get('enabled_2fa', False)
831 831
832 832 @has_enabled_2fa.setter
833 833 def has_enabled_2fa(self, val):
834 834 val = str2bool(val)
835 835 self.update_userdata(enabled_2fa=val)
836 836 if not val:
837 837 # NOTE: setting to false we clear the user_data to not store any 2fa artifacts
838 838 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False)
839 839 Session().commit()
840 840
841 841 @hybrid_property
842 def has_check_2fa_flag(self):
842 def check_2fa_required(self):
843 843 """
844 844 Check if check 2fa flag is set for this user
845 845 """
846 846 value = self.user_data.get('check_2fa', False)
847 847 return value
848 848
849 @has_check_2fa_flag.setter
850 def has_check_2fa_flag(self, val):
849 @check_2fa_required.setter
850 def check_2fa_required(self, val):
851 851 val = str2bool(val)
852 852 self.update_userdata(check_2fa=val)
853 853 Session().commit()
854 854
855 855 @hybrid_property
856 856 def has_seen_2fa_codes(self):
857 857 """
858 858 get the flag about if user has seen 2fa recovery codes
859 859 """
860 860 value = self.user_data.get('recovery_codes_2fa_seen', False)
861 861 return value
862 862
863 863 @has_seen_2fa_codes.setter
864 864 def has_seen_2fa_codes(self, val):
865 865 val = str2bool(val)
866 866 self.update_userdata(recovery_codes_2fa_seen=val)
867 867 Session().commit()
868 868
869 869 @hybrid_property
870 870 def needs_2fa_configure(self):
871 871 """
872 872 Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work.
873 873
874 874 Currently this is 2fa enabled and secret exists
875 875 """
876 876 if self.has_enabled_2fa:
877 877 return not self.user_data.get('secret_2fa')
878 878 return False
879 879
880 880 def init_2fa_recovery_codes(self, persist=True, force=False):
881 881 """
882 882 Creates 2fa recovery codes
883 883 """
884 884 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
885 885 encrypted_codes = []
886 886 if not recovery_codes or force:
887 887 for _ in range(self.RECOVERY_CODES_COUNT):
888 888 recovery_code = pyotp.random_base32()
889 889 recovery_codes.append(recovery_code)
890 890 encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY)
891 891 encrypted_codes.append(safe_str(encrypted_code))
892 892 if persist:
893 893 self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False)
894 894 return recovery_codes
895 895 # User should not check the same recovery codes more than once
896 896 return []
897 897
898 898 def get_2fa_recovery_codes(self):
899 899 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
900 900 strict_mode = ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
901 901
902 902 recovery_codes = list(map(
903 903 lambda val: safe_str(
904 904 enc_utils.decrypt_value(
905 905 val,
906 906 enc_key=ENCRYPTION_KEY,
907 907 strict_mode=strict_mode
908 908 )),
909 909 encrypted_recovery_codes))
910 910 return recovery_codes
911 911
912 912 def init_secret_2fa(self, persist=True, force=False):
913 913 secret_2fa = self.user_data.get('secret_2fa')
914 914 if not secret_2fa or force:
915 915 secret = pyotp.random_base32()
916 916 if persist:
917 917 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY)))
918 918 return secret
919 919 return ''
920 920
921 def get_secret_2fa(self) -> str:
921 @hybrid_property
922 def secret_2fa(self) -> str:
923 """
924 get stored secret for 2fa
925 """
922 926 secret_2fa = self.user_data.get('secret_2fa')
923 927 if secret_2fa:
924 928 strict_mode = ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
925 929 return safe_str(
926 930 enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY, strict_mode=strict_mode))
927 931 return ''
928 932
929 def set_2fa_secret(self, value):
933 @secret_2fa.setter
934 def secret_2fa(self, value: str) -> None:
930 935 encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY)
931 936 self.update_userdata(secret_2fa=safe_str(encrypted_value))
932 937
933 938 def regenerate_2fa_recovery_codes(self):
934 939 """
935 940 Regenerates 2fa recovery codes upon request
936 941 """
937 942 new_recovery_codes = self.init_2fa_recovery_codes(force=True)
938 943 Session().commit()
939 944 return new_recovery_codes
940 945
941 946 @classmethod
942 947 def extra_valid_auth_tokens(cls, user, role=None):
943 948 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
944 949 .filter(or_(UserApiKeys.expires == -1,
945 950 UserApiKeys.expires >= time.time()))
946 951 if role:
947 952 tokens = tokens.filter(or_(UserApiKeys.role == role,
948 953 UserApiKeys.role == UserApiKeys.ROLE_ALL))
949 954 return tokens.all()
950 955
951 956 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
952 957 from rhodecode.lib import auth
953 958
954 959 log.debug('Trying to authenticate user: %s via auth-token, '
955 960 'and roles: %s', self, roles)
956 961
957 962 if not auth_token:
958 963 return False
959 964
960 965 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
961 966 tokens_q = UserApiKeys.query()\
962 967 .filter(UserApiKeys.user_id == self.user_id)\
963 968 .filter(or_(UserApiKeys.expires == -1,
964 969 UserApiKeys.expires >= time.time()))
965 970
966 971 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
967 972
968 973 crypto_backend = auth.crypto_backend()
969 974 enc_token_map = {}
970 975 plain_token_map = {}
971 976 for token in tokens_q:
972 977 if token.api_key.startswith(crypto_backend.ENC_PREF):
973 978 enc_token_map[token.api_key] = token
974 979 else:
975 980 plain_token_map[token.api_key] = token
976 981 log.debug(
977 982 'Found %s plain and %s encrypted tokens to check for authentication for this user',
978 983 len(plain_token_map), len(enc_token_map))
979 984
980 985 # plain token match comes first
981 986 match = plain_token_map.get(auth_token)
982 987
983 988 # check encrypted tokens now
984 989 if not match:
985 990 for token_hash, token in enc_token_map.items():
986 991 # NOTE(marcink): this is expensive to calculate, but most secure
987 992 if crypto_backend.hash_check(auth_token, token_hash):
988 993 match = token
989 994 break
990 995
991 996 if match:
992 997 log.debug('Found matching token %s', match)
993 998 if match.repo_id:
994 999 log.debug('Found scope, checking for scope match of token %s', match)
995 1000 if match.repo_id == scope_repo_id:
996 1001 return True
997 1002 else:
998 1003 log.debug(
999 1004 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
1000 1005 'and calling scope is:%s, skipping further checks',
1001 1006 match.repo, scope_repo_id)
1002 1007 return False
1003 1008 else:
1004 1009 return True
1005 1010
1006 1011 return False
1007 1012
1008 1013 @property
1009 1014 def ip_addresses(self):
1010 1015 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
1011 1016 return [x.ip_addr for x in ret]
1012 1017
1013 1018 @property
1014 1019 def username_and_name(self):
1015 1020 return f'{self.username} ({self.first_name} {self.last_name})'
1016 1021
1017 1022 @property
1018 1023 def username_or_name_or_email(self):
1019 1024 full_name = self.full_name if self.full_name != ' ' else None
1020 1025 return self.username or full_name or self.email
1021 1026
1022 1027 @property
1023 1028 def full_name(self):
1024 1029 return f'{self.first_name} {self.last_name}'
1025 1030
1026 1031 @property
1027 1032 def full_name_or_username(self):
1028 1033 return (f'{self.first_name} {self.last_name}'
1029 1034 if (self.first_name and self.last_name) else self.username)
1030 1035
1031 1036 @property
1032 1037 def full_contact(self):
1033 1038 return f'{self.first_name} {self.last_name} <{self.email}>'
1034 1039
1035 1040 @property
1036 1041 def short_contact(self):
1037 1042 return f'{self.first_name} {self.last_name}'
1038 1043
1039 1044 @property
1040 1045 def is_admin(self):
1041 1046 return self.admin
1042 1047
1043 1048 @property
1044 1049 def language(self):
1045 1050 return self.user_data.get('language')
1046 1051
1047 1052 def AuthUser(self, **kwargs):
1048 1053 """
1049 1054 Returns instance of AuthUser for this user
1050 1055 """
1051 1056 from rhodecode.lib.auth import AuthUser
1052 1057 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1053 1058
1054 1059 @hybrid_property
1055 1060 def user_data(self):
1056 1061 if not self._user_data:
1057 1062 return {}
1058 1063
1059 1064 try:
1060 1065 return json.loads(self._user_data) or {}
1061 1066 except TypeError:
1062 1067 return {}
1063 1068
1064 1069 @user_data.setter
1065 1070 def user_data(self, val):
1066 1071 if not isinstance(val, dict):
1067 1072 raise Exception(f'user_data must be dict, got {type(val)}')
1068 1073 try:
1069 1074 self._user_data = safe_bytes(json.dumps(val))
1070 1075 except Exception:
1071 1076 log.error(traceback.format_exc())
1072 1077
1073 1078 @classmethod
1074 1079 def get(cls, user_id, cache=False):
1075 1080 if not user_id:
1076 1081 return
1077 1082
1078 1083 user = cls.query()
1079 1084 if cache:
1080 1085 user = user.options(
1081 1086 FromCache("sql_cache_short", f"get_users_{user_id}"))
1082 1087 return user.get(user_id)
1083 1088
1084 1089 @classmethod
1085 1090 def get_by_username(cls, username, case_insensitive=False,
1086 1091 cache=False):
1087 1092
1088 1093 if case_insensitive:
1089 1094 q = cls.select().where(
1090 1095 func.lower(cls.username) == func.lower(username))
1091 1096 else:
1092 1097 q = cls.select().where(cls.username == username)
1093 1098
1094 1099 if cache:
1095 1100 hash_key = _hash_key(username)
1096 1101 q = q.options(
1097 1102 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1098 1103
1099 1104 return cls.execute(q).scalar_one_or_none()
1100 1105
1101 1106 @classmethod
1102 1107 def get_by_username_or_primary_email(cls, user_identifier):
1103 1108 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1104 1109 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1105 1110 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1106 1111
1107 1112 @classmethod
1108 1113 def get_by_auth_token(cls, auth_token, cache=False):
1109 1114
1110 1115 q = cls.select(User)\
1111 1116 .join(UserApiKeys)\
1112 1117 .where(UserApiKeys.api_key == auth_token)\
1113 1118 .where(or_(UserApiKeys.expires == -1,
1114 1119 UserApiKeys.expires >= time.time()))
1115 1120
1116 1121 if cache:
1117 1122 q = q.options(
1118 1123 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1119 1124
1120 1125 matched_user = cls.execute(q).scalar_one_or_none()
1121 1126
1122 1127 return matched_user
1123 1128
1124 1129 @classmethod
1125 1130 def get_by_email(cls, email, case_insensitive=False, cache=False):
1126 1131
1127 1132 if case_insensitive:
1128 1133 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1129 1134 else:
1130 1135 q = cls.select().where(cls.email == email)
1131 1136
1132 1137 if cache:
1133 1138 email_key = _hash_key(email)
1134 1139 q = q.options(
1135 1140 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1136 1141
1137 1142 ret = cls.execute(q).scalar_one_or_none()
1138 1143
1139 1144 if ret is None:
1140 1145 q = cls.select(UserEmailMap)
1141 1146 # try fetching in alternate email map
1142 1147 if case_insensitive:
1143 1148 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1144 1149 else:
1145 1150 q = q.where(UserEmailMap.email == email)
1146 1151 q = q.options(joinedload(UserEmailMap.user))
1147 1152 if cache:
1148 1153 q = q.options(
1149 1154 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1150 1155
1151 1156 result = cls.execute(q).scalar_one_or_none()
1152 1157 ret = getattr(result, 'user', None)
1153 1158
1154 1159 return ret
1155 1160
1156 1161 @classmethod
1157 1162 def get_from_cs_author(cls, author):
1158 1163 """
1159 1164 Tries to get User objects out of commit author string
1160 1165
1161 1166 :param author:
1162 1167 """
1163 1168 from rhodecode.lib.helpers import email, author_name
1164 1169 # Valid email in the attribute passed, see if they're in the system
1165 1170 _email = email(author)
1166 1171 if _email:
1167 1172 user = cls.get_by_email(_email, case_insensitive=True)
1168 1173 if user:
1169 1174 return user
1170 1175 # Maybe we can match by username?
1171 1176 _author = author_name(author)
1172 1177 user = cls.get_by_username(_author, case_insensitive=True)
1173 1178 if user:
1174 1179 return user
1175 1180
1176 1181 def update_userdata(self, **kwargs):
1177 1182 usr = self
1178 1183 old = usr.user_data
1179 1184 old.update(**kwargs)
1180 1185 usr.user_data = old
1181 1186 Session().add(usr)
1182 1187 log.debug('updated userdata with %s', kwargs)
1183 1188
1184 1189 def update_lastlogin(self):
1185 1190 """Update user lastlogin"""
1186 1191 self.last_login = datetime.datetime.now()
1187 1192 Session().add(self)
1188 1193 log.debug('updated user %s lastlogin', self.username)
1189 1194
1190 1195 def update_password(self, new_password):
1191 1196 from rhodecode.lib.auth import get_crypt_password
1192 1197
1193 1198 self.password = get_crypt_password(new_password)
1194 1199 Session().add(self)
1195 1200
1196 1201 @classmethod
1197 1202 def get_first_super_admin(cls):
1198 1203 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1199 1204 user = cls.scalars(stmt).first()
1200 1205
1201 1206 if user is None:
1202 1207 raise Exception('FATAL: Missing administrative account!')
1203 1208 return user
1204 1209
1205 1210 @classmethod
1206 1211 def get_all_super_admins(cls, only_active=False):
1207 1212 """
1208 1213 Returns all admin accounts sorted by username
1209 1214 """
1210 1215 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1211 1216 if only_active:
1212 1217 qry = qry.filter(User.active == true())
1213 1218 return qry.all()
1214 1219
1215 1220 @classmethod
1216 1221 def get_all_user_ids(cls, only_active=True):
1217 1222 """
1218 1223 Returns all users IDs
1219 1224 """
1220 1225 qry = Session().query(User.user_id)
1221 1226
1222 1227 if only_active:
1223 1228 qry = qry.filter(User.active == true())
1224 1229 return [x.user_id for x in qry]
1225 1230
1226 1231 @classmethod
1227 1232 def get_default_user(cls, cache=False, refresh=False):
1228 1233 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1229 1234 if user is None:
1230 1235 raise Exception('FATAL: Missing default account!')
1231 1236 if refresh:
1232 1237 # The default user might be based on outdated state which
1233 1238 # has been loaded from the cache.
1234 1239 # A call to refresh() ensures that the
1235 1240 # latest state from the database is used.
1236 1241 Session().refresh(user)
1237 1242
1238 1243 return user
1239 1244
1240 1245 @classmethod
1241 1246 def get_default_user_id(cls):
1242 1247 import rhodecode
1243 1248 return rhodecode.CONFIG['default_user_id']
1244 1249
1245 1250 def _get_default_perms(self, user, suffix=''):
1246 1251 from rhodecode.model.permission import PermissionModel
1247 1252 return PermissionModel().get_default_perms(user.user_perms, suffix)
1248 1253
1249 1254 def get_default_perms(self, suffix=''):
1250 1255 return self._get_default_perms(self, suffix)
1251 1256
1252 1257 def get_api_data(self, include_secrets=False, details='full'):
1253 1258 """
1254 1259 Common function for generating user related data for API
1255 1260
1256 1261 :param include_secrets: By default secrets in the API data will be replaced
1257 1262 by a placeholder value to prevent exposing this data by accident. In case
1258 1263 this data shall be exposed, set this flag to ``True``.
1259 1264
1260 1265 :param details: details can be 'basic|full' basic gives only a subset of
1261 1266 the available user information that includes user_id, name and emails.
1262 1267 """
1263 1268 user = self
1264 1269 user_data = self.user_data
1265 1270 data = {
1266 1271 'user_id': user.user_id,
1267 1272 'username': user.username,
1268 1273 'firstname': user.name,
1269 1274 'lastname': user.lastname,
1270 1275 'description': user.description,
1271 1276 'email': user.email,
1272 1277 'emails': user.emails,
1273 1278 }
1274 1279 if details == 'basic':
1275 1280 return data
1276 1281
1277 1282 auth_token_length = 40
1278 1283 auth_token_replacement = '*' * auth_token_length
1279 1284
1280 1285 extras = {
1281 1286 'auth_tokens': [auth_token_replacement],
1282 1287 'active': user.active,
1283 1288 'admin': user.admin,
1284 1289 'extern_type': user.extern_type,
1285 1290 'extern_name': user.extern_name,
1286 1291 'last_login': user.last_login,
1287 1292 'last_activity': user.last_activity,
1288 1293 'ip_addresses': user.ip_addresses,
1289 1294 'language': user_data.get('language')
1290 1295 }
1291 1296 data.update(extras)
1292 1297
1293 1298 if include_secrets:
1294 1299 data['auth_tokens'] = user.auth_tokens
1295 1300 return data
1296 1301
1297 1302 def __json__(self):
1298 1303 data = {
1299 1304 'full_name': self.full_name,
1300 1305 'full_name_or_username': self.full_name_or_username,
1301 1306 'short_contact': self.short_contact,
1302 1307 'full_contact': self.full_contact,
1303 1308 }
1304 1309 data.update(self.get_api_data())
1305 1310 return data
1306 1311
1307 1312
1308 1313 class UserApiKeys(Base, BaseModel):
1309 1314 __tablename__ = 'user_api_keys'
1310 1315 __table_args__ = (
1311 1316 Index('uak_api_key_idx', 'api_key'),
1312 1317 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1313 1318 base_table_args
1314 1319 )
1315 1320
1316 1321 # ApiKey role
1317 1322 ROLE_ALL = 'token_role_all'
1318 1323 ROLE_VCS = 'token_role_vcs'
1319 1324 ROLE_API = 'token_role_api'
1320 1325 ROLE_HTTP = 'token_role_http'
1321 1326 ROLE_FEED = 'token_role_feed'
1322 1327 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1323 1328 # The last one is ignored in the list as we only
1324 1329 # use it for one action, and cannot be created by users
1325 1330 ROLE_PASSWORD_RESET = 'token_password_reset'
1326 1331
1327 1332 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1328 1333
1329 1334 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1330 1335 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1331 1336 api_key = Column("api_key", String(255), nullable=False, unique=True)
1332 1337 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1333 1338 expires = Column('expires', Float(53), nullable=False)
1334 1339 role = Column('role', String(255), nullable=True)
1335 1340 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1336 1341
1337 1342 # scope columns
1338 1343 repo_id = Column(
1339 1344 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1340 1345 nullable=True, unique=None, default=None)
1341 1346 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1342 1347
1343 1348 repo_group_id = Column(
1344 1349 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1345 1350 nullable=True, unique=None, default=None)
1346 1351 repo_group = relationship('RepoGroup', lazy='joined')
1347 1352
1348 1353 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1349 1354
1350 1355 def __repr__(self):
1351 1356 return f"<{self.cls_name}('{self.role}')>"
1352 1357
1353 1358 def __json__(self):
1354 1359 data = {
1355 1360 'auth_token': self.api_key,
1356 1361 'role': self.role,
1357 1362 'scope': self.scope_humanized,
1358 1363 'expired': self.expired
1359 1364 }
1360 1365 return data
1361 1366
1362 1367 def get_api_data(self, include_secrets=False):
1363 1368 data = self.__json__()
1364 1369 if include_secrets:
1365 1370 return data
1366 1371 else:
1367 1372 data['auth_token'] = self.token_obfuscated
1368 1373 return data
1369 1374
1370 1375 @hybrid_property
1371 1376 def description_safe(self):
1372 1377 from rhodecode.lib import helpers as h
1373 1378 return h.escape(self.description)
1374 1379
1375 1380 @property
1376 1381 def expired(self):
1377 1382 if self.expires == -1:
1378 1383 return False
1379 1384 return time.time() > self.expires
1380 1385
1381 1386 @classmethod
1382 1387 def _get_role_name(cls, role):
1383 1388 return {
1384 1389 cls.ROLE_ALL: _('all'),
1385 1390 cls.ROLE_HTTP: _('http/web interface'),
1386 1391 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1387 1392 cls.ROLE_API: _('api calls'),
1388 1393 cls.ROLE_FEED: _('feed access'),
1389 1394 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1390 1395 }.get(role, role)
1391 1396
1392 1397 @classmethod
1393 1398 def _get_role_description(cls, role):
1394 1399 return {
1395 1400 cls.ROLE_ALL: _('Token for all actions.'),
1396 1401 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1397 1402 'login using `api_access_controllers_whitelist` functionality.'),
1398 1403 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1399 1404 'Requires auth_token authentication plugin to be active. <br/>'
1400 1405 'Such Token should be used then instead of a password to '
1401 1406 'interact with a repository, and additionally can be '
1402 1407 'limited to single repository using repo scope.'),
1403 1408 cls.ROLE_API: _('Token limited to api calls.'),
1404 1409 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1405 1410 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1406 1411 }.get(role, role)
1407 1412
1408 1413 @property
1409 1414 def role_humanized(self):
1410 1415 return self._get_role_name(self.role)
1411 1416
1412 1417 def _get_scope(self):
1413 1418 if self.repo:
1414 1419 return 'Repository: {}'.format(self.repo.repo_name)
1415 1420 if self.repo_group:
1416 1421 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1417 1422 return 'Global'
1418 1423
1419 1424 @property
1420 1425 def scope_humanized(self):
1421 1426 return self._get_scope()
1422 1427
1423 1428 @property
1424 1429 def token_obfuscated(self):
1425 1430 if self.api_key:
1426 1431 return self.api_key[:4] + "****"
1427 1432
1428 1433
1429 1434 class UserEmailMap(Base, BaseModel):
1430 1435 __tablename__ = 'user_email_map'
1431 1436 __table_args__ = (
1432 1437 Index('uem_email_idx', 'email'),
1433 1438 Index('uem_user_id_idx', 'user_id'),
1434 1439 UniqueConstraint('email'),
1435 1440 base_table_args
1436 1441 )
1437 1442
1438 1443 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1439 1444 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1440 1445 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1441 1446 user = relationship('User', lazy='joined', back_populates='user_emails')
1442 1447
1443 1448 @validates('_email')
1444 1449 def validate_email(self, key, email):
1445 1450 # check if this email is not main one
1446 1451 main_email = Session().query(User).filter(User.email == email).scalar()
1447 1452 if main_email is not None:
1448 1453 raise AttributeError('email %s is present is user table' % email)
1449 1454 return email
1450 1455
1451 1456 @hybrid_property
1452 1457 def email(self):
1453 1458 return self._email
1454 1459
1455 1460 @email.setter
1456 1461 def email(self, val):
1457 1462 self._email = val.lower() if val else None
1458 1463
1459 1464
1460 1465 class UserIpMap(Base, BaseModel):
1461 1466 __tablename__ = 'user_ip_map'
1462 1467 __table_args__ = (
1463 1468 UniqueConstraint('user_id', 'ip_addr'),
1464 1469 base_table_args
1465 1470 )
1466 1471
1467 1472 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1468 1473 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1469 1474 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1470 1475 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1471 1476 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1472 1477 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1473 1478
1474 1479 @hybrid_property
1475 1480 def description_safe(self):
1476 1481 from rhodecode.lib import helpers as h
1477 1482 return h.escape(self.description)
1478 1483
1479 1484 @classmethod
1480 1485 def _get_ip_range(cls, ip_addr):
1481 1486 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1482 1487 return [str(net.network_address), str(net.broadcast_address)]
1483 1488
1484 1489 def __json__(self):
1485 1490 return {
1486 1491 'ip_addr': self.ip_addr,
1487 1492 'ip_range': self._get_ip_range(self.ip_addr),
1488 1493 }
1489 1494
1490 1495 def __repr__(self):
1491 1496 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1492 1497
1493 1498
1494 1499 class UserSshKeys(Base, BaseModel):
1495 1500 __tablename__ = 'user_ssh_keys'
1496 1501 __table_args__ = (
1497 1502 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1498 1503
1499 1504 UniqueConstraint('ssh_key_fingerprint'),
1500 1505
1501 1506 base_table_args
1502 1507 )
1503 1508
1504 1509 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1505 1510 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1506 1511 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1507 1512
1508 1513 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1509 1514
1510 1515 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1511 1516 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1512 1517 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1513 1518
1514 1519 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1515 1520
1516 1521 def __json__(self):
1517 1522 data = {
1518 1523 'ssh_fingerprint': self.ssh_key_fingerprint,
1519 1524 'description': self.description,
1520 1525 'created_on': self.created_on
1521 1526 }
1522 1527 return data
1523 1528
1524 1529 def get_api_data(self):
1525 1530 data = self.__json__()
1526 1531 return data
1527 1532
1528 1533
1529 1534 class UserLog(Base, BaseModel):
1530 1535 __tablename__ = 'user_logs'
1531 1536 __table_args__ = (
1532 1537 base_table_args,
1533 1538 )
1534 1539
1535 1540 VERSION_1 = 'v1'
1536 1541 VERSION_2 = 'v2'
1537 1542 VERSIONS = [VERSION_1, VERSION_2]
1538 1543
1539 1544 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1540 1545 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1541 1546 username = Column("username", String(255), nullable=True, unique=None, default=None)
1542 1547 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1543 1548 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1544 1549 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1545 1550 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1546 1551 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1547 1552
1548 1553 version = Column("version", String(255), nullable=True, default=VERSION_1)
1549 1554 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1550 1555 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1551 1556 user = relationship('User', cascade='', back_populates='user_log')
1552 1557 repository = relationship('Repository', cascade='', back_populates='logs')
1553 1558
1554 1559 def __repr__(self):
1555 1560 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1556 1561
1557 1562 def __json__(self):
1558 1563 return {
1559 1564 'user_id': self.user_id,
1560 1565 'username': self.username,
1561 1566 'repository_id': self.repository_id,
1562 1567 'repository_name': self.repository_name,
1563 1568 'user_ip': self.user_ip,
1564 1569 'action_date': self.action_date,
1565 1570 'action': self.action,
1566 1571 }
1567 1572
1568 1573 @hybrid_property
1569 1574 def entry_id(self):
1570 1575 return self.user_log_id
1571 1576
1572 1577 @property
1573 1578 def action_as_day(self):
1574 1579 return datetime.date(*self.action_date.timetuple()[:3])
1575 1580
1576 1581
1577 1582 class UserGroup(Base, BaseModel):
1578 1583 __tablename__ = 'users_groups'
1579 1584 __table_args__ = (
1580 1585 base_table_args,
1581 1586 )
1582 1587
1583 1588 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1584 1589 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1585 1590 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1586 1591 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1587 1592 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1588 1593 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1589 1594 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1590 1595 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1591 1596
1592 1597 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1593 1598 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1594 1599 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1595 1600 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1596 1601 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1597 1602
1598 1603 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1599 1604
1600 1605 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1601 1606 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1602 1607
1603 1608 @classmethod
1604 1609 def _load_group_data(cls, column):
1605 1610 if not column:
1606 1611 return {}
1607 1612
1608 1613 try:
1609 1614 return json.loads(column) or {}
1610 1615 except TypeError:
1611 1616 return {}
1612 1617
1613 1618 @hybrid_property
1614 1619 def description_safe(self):
1615 1620 from rhodecode.lib import helpers as h
1616 1621 return h.escape(self.user_group_description)
1617 1622
1618 1623 @hybrid_property
1619 1624 def group_data(self):
1620 1625 return self._load_group_data(self._group_data)
1621 1626
1622 1627 @group_data.expression
1623 1628 def group_data(self, **kwargs):
1624 1629 return self._group_data
1625 1630
1626 1631 @group_data.setter
1627 1632 def group_data(self, val):
1628 1633 try:
1629 1634 self._group_data = json.dumps(val)
1630 1635 except Exception:
1631 1636 log.error(traceback.format_exc())
1632 1637
1633 1638 @classmethod
1634 1639 def _load_sync(cls, group_data):
1635 1640 if group_data:
1636 1641 return group_data.get('extern_type')
1637 1642
1638 1643 @property
1639 1644 def sync(self):
1640 1645 return self._load_sync(self.group_data)
1641 1646
1642 1647 def __repr__(self):
1643 1648 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1644 1649
1645 1650 @classmethod
1646 1651 def get_by_group_name(cls, group_name, cache=False,
1647 1652 case_insensitive=False):
1648 1653 if case_insensitive:
1649 1654 q = cls.query().filter(func.lower(cls.users_group_name) ==
1650 1655 func.lower(group_name))
1651 1656
1652 1657 else:
1653 1658 q = cls.query().filter(cls.users_group_name == group_name)
1654 1659 if cache:
1655 1660 name_key = _hash_key(group_name)
1656 1661 q = q.options(
1657 1662 FromCache("sql_cache_short", f"get_group_{name_key}"))
1658 1663 return q.scalar()
1659 1664
1660 1665 @classmethod
1661 1666 def get(cls, user_group_id, cache=False):
1662 1667 if not user_group_id:
1663 1668 return
1664 1669
1665 1670 user_group = cls.query()
1666 1671 if cache:
1667 1672 user_group = user_group.options(
1668 1673 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1669 1674 return user_group.get(user_group_id)
1670 1675
1671 1676 def permissions(self, with_admins=True, with_owner=True,
1672 1677 expand_from_user_groups=False):
1673 1678 """
1674 1679 Permissions for user groups
1675 1680 """
1676 1681 _admin_perm = 'usergroup.admin'
1677 1682
1678 1683 owner_row = []
1679 1684 if with_owner:
1680 1685 usr = AttributeDict(self.user.get_dict())
1681 1686 usr.owner_row = True
1682 1687 usr.permission = _admin_perm
1683 1688 owner_row.append(usr)
1684 1689
1685 1690 super_admin_ids = []
1686 1691 super_admin_rows = []
1687 1692 if with_admins:
1688 1693 for usr in User.get_all_super_admins():
1689 1694 super_admin_ids.append(usr.user_id)
1690 1695 # if this admin is also owner, don't double the record
1691 1696 if usr.user_id == owner_row[0].user_id:
1692 1697 owner_row[0].admin_row = True
1693 1698 else:
1694 1699 usr = AttributeDict(usr.get_dict())
1695 1700 usr.admin_row = True
1696 1701 usr.permission = _admin_perm
1697 1702 super_admin_rows.append(usr)
1698 1703
1699 1704 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1700 1705 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1701 1706 joinedload(UserUserGroupToPerm.user),
1702 1707 joinedload(UserUserGroupToPerm.permission),)
1703 1708
1704 1709 # get owners and admins and permissions. We do a trick of re-writing
1705 1710 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1706 1711 # has a global reference and changing one object propagates to all
1707 1712 # others. This means if admin is also an owner admin_row that change
1708 1713 # would propagate to both objects
1709 1714 perm_rows = []
1710 1715 for _usr in q.all():
1711 1716 usr = AttributeDict(_usr.user.get_dict())
1712 1717 # if this user is also owner/admin, mark as duplicate record
1713 1718 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1714 1719 usr.duplicate_perm = True
1715 1720 usr.permission = _usr.permission.permission_name
1716 1721 perm_rows.append(usr)
1717 1722
1718 1723 # filter the perm rows by 'default' first and then sort them by
1719 1724 # admin,write,read,none permissions sorted again alphabetically in
1720 1725 # each group
1721 1726 perm_rows = sorted(perm_rows, key=display_user_sort)
1722 1727
1723 1728 user_groups_rows = []
1724 1729 if expand_from_user_groups:
1725 1730 for ug in self.permission_user_groups(with_members=True):
1726 1731 for user_data in ug.members:
1727 1732 user_groups_rows.append(user_data)
1728 1733
1729 1734 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1730 1735
1731 1736 def permission_user_groups(self, with_members=False):
1732 1737 q = UserGroupUserGroupToPerm.query()\
1733 1738 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1734 1739 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1735 1740 joinedload(UserGroupUserGroupToPerm.target_user_group),
1736 1741 joinedload(UserGroupUserGroupToPerm.permission),)
1737 1742
1738 1743 perm_rows = []
1739 1744 for _user_group in q.all():
1740 1745 entry = AttributeDict(_user_group.user_group.get_dict())
1741 1746 entry.permission = _user_group.permission.permission_name
1742 1747 if with_members:
1743 1748 entry.members = [x.user.get_dict()
1744 1749 for x in _user_group.user_group.members]
1745 1750 perm_rows.append(entry)
1746 1751
1747 1752 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1748 1753 return perm_rows
1749 1754
1750 1755 def _get_default_perms(self, user_group, suffix=''):
1751 1756 from rhodecode.model.permission import PermissionModel
1752 1757 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1753 1758
1754 1759 def get_default_perms(self, suffix=''):
1755 1760 return self._get_default_perms(self, suffix)
1756 1761
1757 1762 def get_api_data(self, with_group_members=True, include_secrets=False):
1758 1763 """
1759 1764 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1760 1765 basically forwarded.
1761 1766
1762 1767 """
1763 1768 user_group = self
1764 1769 data = {
1765 1770 'users_group_id': user_group.users_group_id,
1766 1771 'group_name': user_group.users_group_name,
1767 1772 'group_description': user_group.user_group_description,
1768 1773 'active': user_group.users_group_active,
1769 1774 'owner': user_group.user.username,
1770 1775 'sync': user_group.sync,
1771 1776 'owner_email': user_group.user.email,
1772 1777 }
1773 1778
1774 1779 if with_group_members:
1775 1780 users = []
1776 1781 for user in user_group.members:
1777 1782 user = user.user
1778 1783 users.append(user.get_api_data(include_secrets=include_secrets))
1779 1784 data['users'] = users
1780 1785
1781 1786 return data
1782 1787
1783 1788
1784 1789 class UserGroupMember(Base, BaseModel):
1785 1790 __tablename__ = 'users_groups_members'
1786 1791 __table_args__ = (
1787 1792 base_table_args,
1788 1793 )
1789 1794
1790 1795 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1791 1796 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1792 1797 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1793 1798
1794 1799 user = relationship('User', lazy='joined', back_populates='group_member')
1795 1800 users_group = relationship('UserGroup', back_populates='members')
1796 1801
1797 1802 def __init__(self, gr_id='', u_id=''):
1798 1803 self.users_group_id = gr_id
1799 1804 self.user_id = u_id
1800 1805
1801 1806
1802 1807 class RepositoryField(Base, BaseModel):
1803 1808 __tablename__ = 'repositories_fields'
1804 1809 __table_args__ = (
1805 1810 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1806 1811 base_table_args,
1807 1812 )
1808 1813
1809 1814 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1810 1815
1811 1816 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1812 1817 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1813 1818 field_key = Column("field_key", String(250))
1814 1819 field_label = Column("field_label", String(1024), nullable=False)
1815 1820 field_value = Column("field_value", String(10000), nullable=False)
1816 1821 field_desc = Column("field_desc", String(1024), nullable=False)
1817 1822 field_type = Column("field_type", String(255), nullable=False, unique=None)
1818 1823 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1819 1824
1820 1825 repository = relationship('Repository', back_populates='extra_fields')
1821 1826
1822 1827 @property
1823 1828 def field_key_prefixed(self):
1824 1829 return 'ex_%s' % self.field_key
1825 1830
1826 1831 @classmethod
1827 1832 def un_prefix_key(cls, key):
1828 1833 if key.startswith(cls.PREFIX):
1829 1834 return key[len(cls.PREFIX):]
1830 1835 return key
1831 1836
1832 1837 @classmethod
1833 1838 def get_by_key_name(cls, key, repo):
1834 1839 row = cls.query()\
1835 1840 .filter(cls.repository == repo)\
1836 1841 .filter(cls.field_key == key).scalar()
1837 1842 return row
1838 1843
1839 1844
1840 1845 class Repository(Base, BaseModel):
1841 1846 __tablename__ = 'repositories'
1842 1847 __table_args__ = (
1843 1848 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1844 1849 base_table_args,
1845 1850 )
1846 1851 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1847 1852 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1848 1853 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1849 1854
1850 1855 STATE_CREATED = 'repo_state_created'
1851 1856 STATE_PENDING = 'repo_state_pending'
1852 1857 STATE_ERROR = 'repo_state_error'
1853 1858
1854 1859 LOCK_AUTOMATIC = 'lock_auto'
1855 1860 LOCK_API = 'lock_api'
1856 1861 LOCK_WEB = 'lock_web'
1857 1862 LOCK_PULL = 'lock_pull'
1858 1863
1859 1864 NAME_SEP = URL_SEP
1860 1865
1861 1866 repo_id = Column(
1862 1867 "repo_id", Integer(), nullable=False, unique=True, default=None,
1863 1868 primary_key=True)
1864 1869 _repo_name = Column(
1865 1870 "repo_name", Text(), nullable=False, default=None)
1866 1871 repo_name_hash = Column(
1867 1872 "repo_name_hash", String(255), nullable=False, unique=True)
1868 1873 repo_state = Column("repo_state", String(255), nullable=True)
1869 1874
1870 1875 clone_uri = Column(
1871 1876 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1872 1877 default=None)
1873 1878 push_uri = Column(
1874 1879 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1875 1880 default=None)
1876 1881 repo_type = Column(
1877 1882 "repo_type", String(255), nullable=False, unique=False, default=None)
1878 1883 user_id = Column(
1879 1884 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1880 1885 unique=False, default=None)
1881 1886 private = Column(
1882 1887 "private", Boolean(), nullable=True, unique=None, default=None)
1883 1888 archived = Column(
1884 1889 "archived", Boolean(), nullable=True, unique=None, default=None)
1885 1890 enable_statistics = Column(
1886 1891 "statistics", Boolean(), nullable=True, unique=None, default=True)
1887 1892 enable_downloads = Column(
1888 1893 "downloads", Boolean(), nullable=True, unique=None, default=True)
1889 1894 description = Column(
1890 1895 "description", String(10000), nullable=True, unique=None, default=None)
1891 1896 created_on = Column(
1892 1897 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1893 1898 default=datetime.datetime.now)
1894 1899 updated_on = Column(
1895 1900 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1896 1901 default=datetime.datetime.now)
1897 1902 _landing_revision = Column(
1898 1903 "landing_revision", String(255), nullable=False, unique=False,
1899 1904 default=None)
1900 1905 enable_locking = Column(
1901 1906 "enable_locking", Boolean(), nullable=False, unique=None,
1902 1907 default=False)
1903 1908 _locked = Column(
1904 1909 "locked", String(255), nullable=True, unique=False, default=None)
1905 1910 _changeset_cache = Column(
1906 1911 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1907 1912
1908 1913 fork_id = Column(
1909 1914 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1910 1915 nullable=True, unique=False, default=None)
1911 1916 group_id = Column(
1912 1917 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1913 1918 unique=False, default=None)
1914 1919
1915 1920 user = relationship('User', lazy='joined', back_populates='repositories')
1916 1921 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1917 1922 group = relationship('RepoGroup', lazy='joined')
1918 1923 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1919 1924 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1920 1925 stats = relationship('Statistics', cascade='all', uselist=False)
1921 1926
1922 1927 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1923 1928 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1924 1929
1925 1930 logs = relationship('UserLog', back_populates='repository')
1926 1931
1927 1932 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1928 1933
1929 1934 pull_requests_source = relationship(
1930 1935 'PullRequest',
1931 1936 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1932 1937 cascade="all, delete-orphan",
1933 1938 overlaps="source_repo"
1934 1939 )
1935 1940 pull_requests_target = relationship(
1936 1941 'PullRequest',
1937 1942 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1938 1943 cascade="all, delete-orphan",
1939 1944 overlaps="target_repo"
1940 1945 )
1941 1946
1942 1947 ui = relationship('RepoRhodeCodeUi', cascade="all")
1943 1948 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1944 1949 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1945 1950
1946 1951 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1947 1952
1948 1953 # no cascade, set NULL
1949 1954 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1950 1955
1951 1956 review_rules = relationship('RepoReviewRule')
1952 1957 user_branch_perms = relationship('UserToRepoBranchPermission')
1953 1958 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1954 1959
1955 1960 def __repr__(self):
1956 1961 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1957 1962
1958 1963 @hybrid_property
1959 1964 def description_safe(self):
1960 1965 from rhodecode.lib import helpers as h
1961 1966 return h.escape(self.description)
1962 1967
1963 1968 @hybrid_property
1964 1969 def landing_rev(self):
1965 1970 # always should return [rev_type, rev], e.g ['branch', 'master']
1966 1971 if self._landing_revision:
1967 1972 _rev_info = self._landing_revision.split(':')
1968 1973 if len(_rev_info) < 2:
1969 1974 _rev_info.insert(0, 'rev')
1970 1975 return [_rev_info[0], _rev_info[1]]
1971 1976 return [None, None]
1972 1977
1973 1978 @property
1974 1979 def landing_ref_type(self):
1975 1980 return self.landing_rev[0]
1976 1981
1977 1982 @property
1978 1983 def landing_ref_name(self):
1979 1984 return self.landing_rev[1]
1980 1985
1981 1986 @landing_rev.setter
1982 1987 def landing_rev(self, val):
1983 1988 if ':' not in val:
1984 1989 raise ValueError('value must be delimited with `:` and consist '
1985 1990 'of <rev_type>:<rev>, got %s instead' % val)
1986 1991 self._landing_revision = val
1987 1992
1988 1993 @hybrid_property
1989 1994 def locked(self):
1990 1995 if self._locked:
1991 1996 user_id, timelocked, reason = self._locked.split(':')
1992 1997 lock_values = int(user_id), timelocked, reason
1993 1998 else:
1994 1999 lock_values = [None, None, None]
1995 2000 return lock_values
1996 2001
1997 2002 @locked.setter
1998 2003 def locked(self, val):
1999 2004 if val and isinstance(val, (list, tuple)):
2000 2005 self._locked = ':'.join(map(str, val))
2001 2006 else:
2002 2007 self._locked = None
2003 2008
2004 2009 @classmethod
2005 2010 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2006 2011 from rhodecode.lib.vcs.backends.base import EmptyCommit
2007 2012 dummy = EmptyCommit().__json__()
2008 2013 if not changeset_cache_raw:
2009 2014 dummy['source_repo_id'] = repo_id
2010 2015 return json.loads(json.dumps(dummy))
2011 2016
2012 2017 try:
2013 2018 return json.loads(changeset_cache_raw)
2014 2019 except TypeError:
2015 2020 return dummy
2016 2021 except Exception:
2017 2022 log.error(traceback.format_exc())
2018 2023 return dummy
2019 2024
2020 2025 @hybrid_property
2021 2026 def changeset_cache(self):
2022 2027 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
2023 2028
2024 2029 @changeset_cache.setter
2025 2030 def changeset_cache(self, val):
2026 2031 try:
2027 2032 self._changeset_cache = json.dumps(val)
2028 2033 except Exception:
2029 2034 log.error(traceback.format_exc())
2030 2035
2031 2036 @hybrid_property
2032 2037 def repo_name(self):
2033 2038 return self._repo_name
2034 2039
2035 2040 @repo_name.setter
2036 2041 def repo_name(self, value):
2037 2042 self._repo_name = value
2038 2043 self.repo_name_hash = sha1(safe_bytes(value))
2039 2044
2040 2045 @classmethod
2041 2046 def normalize_repo_name(cls, repo_name):
2042 2047 """
2043 2048 Normalizes os specific repo_name to the format internally stored inside
2044 2049 database using URL_SEP
2045 2050
2046 2051 :param cls:
2047 2052 :param repo_name:
2048 2053 """
2049 2054 return cls.NAME_SEP.join(repo_name.split(os.sep))
2050 2055
2051 2056 @classmethod
2052 2057 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
2053 2058 session = Session()
2054 2059 q = session.query(cls).filter(cls.repo_name == repo_name)
2055 2060
2056 2061 if cache:
2057 2062 if identity_cache:
2058 2063 val = cls.identity_cache(session, 'repo_name', repo_name)
2059 2064 if val:
2060 2065 return val
2061 2066 else:
2062 2067 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2063 2068 q = q.options(
2064 2069 FromCache("sql_cache_short", cache_key))
2065 2070
2066 2071 return q.scalar()
2067 2072
2068 2073 @classmethod
2069 2074 def get_by_id_or_repo_name(cls, repoid):
2070 2075 if isinstance(repoid, int):
2071 2076 try:
2072 2077 repo = cls.get(repoid)
2073 2078 except ValueError:
2074 2079 repo = None
2075 2080 else:
2076 2081 repo = cls.get_by_repo_name(repoid)
2077 2082 return repo
2078 2083
2079 2084 @classmethod
2080 2085 def get_by_full_path(cls, repo_full_path):
2081 2086 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2082 2087 repo_name = cls.normalize_repo_name(repo_name)
2083 2088 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2084 2089
2085 2090 @classmethod
2086 2091 def get_repo_forks(cls, repo_id):
2087 2092 return cls.query().filter(Repository.fork_id == repo_id)
2088 2093
2089 2094 @classmethod
2090 2095 def base_path(cls):
2091 2096 """
2092 2097 Returns base path when all repos are stored
2093 2098
2094 2099 :param cls:
2095 2100 """
2096 2101 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2097 2102 return get_rhodecode_repo_store_path()
2098 2103
2099 2104 @classmethod
2100 2105 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2101 2106 case_insensitive=True, archived=False):
2102 2107 q = Repository.query()
2103 2108
2104 2109 if not archived:
2105 2110 q = q.filter(Repository.archived.isnot(true()))
2106 2111
2107 2112 if not isinstance(user_id, Optional):
2108 2113 q = q.filter(Repository.user_id == user_id)
2109 2114
2110 2115 if not isinstance(group_id, Optional):
2111 2116 q = q.filter(Repository.group_id == group_id)
2112 2117
2113 2118 if case_insensitive:
2114 2119 q = q.order_by(func.lower(Repository.repo_name))
2115 2120 else:
2116 2121 q = q.order_by(Repository.repo_name)
2117 2122
2118 2123 return q.all()
2119 2124
2120 2125 @property
2121 2126 def repo_uid(self):
2122 2127 return '_{}'.format(self.repo_id)
2123 2128
2124 2129 @property
2125 2130 def forks(self):
2126 2131 """
2127 2132 Return forks of this repo
2128 2133 """
2129 2134 return Repository.get_repo_forks(self.repo_id)
2130 2135
2131 2136 @property
2132 2137 def parent(self):
2133 2138 """
2134 2139 Returns fork parent
2135 2140 """
2136 2141 return self.fork
2137 2142
2138 2143 @property
2139 2144 def just_name(self):
2140 2145 return self.repo_name.split(self.NAME_SEP)[-1]
2141 2146
2142 2147 @property
2143 2148 def groups_with_parents(self):
2144 2149 groups = []
2145 2150 if self.group is None:
2146 2151 return groups
2147 2152
2148 2153 cur_gr = self.group
2149 2154 groups.insert(0, cur_gr)
2150 2155 while 1:
2151 2156 gr = getattr(cur_gr, 'parent_group', None)
2152 2157 cur_gr = cur_gr.parent_group
2153 2158 if gr is None:
2154 2159 break
2155 2160 groups.insert(0, gr)
2156 2161
2157 2162 return groups
2158 2163
2159 2164 @property
2160 2165 def groups_and_repo(self):
2161 2166 return self.groups_with_parents, self
2162 2167
2163 2168 @property
2164 2169 def repo_path(self):
2165 2170 """
2166 2171 Returns base full path for that repository means where it actually
2167 2172 exists on a filesystem
2168 2173 """
2169 2174 return self.base_path()
2170 2175
2171 2176 @property
2172 2177 def repo_full_path(self):
2173 2178 p = [self.repo_path]
2174 2179 # we need to split the name by / since this is how we store the
2175 2180 # names in the database, but that eventually needs to be converted
2176 2181 # into a valid system path
2177 2182 p += self.repo_name.split(self.NAME_SEP)
2178 2183 return os.path.join(*map(safe_str, p))
2179 2184
2180 2185 @property
2181 2186 def cache_keys(self):
2182 2187 """
2183 2188 Returns associated cache keys for that repo
2184 2189 """
2185 2190 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2186 2191 return CacheKey.query()\
2187 2192 .filter(CacheKey.cache_key == repo_namespace_key)\
2188 2193 .order_by(CacheKey.cache_key)\
2189 2194 .all()
2190 2195
2191 2196 @property
2192 2197 def cached_diffs_relative_dir(self):
2193 2198 """
2194 2199 Return a relative to the repository store path of cached diffs
2195 2200 used for safe display for users, who shouldn't know the absolute store
2196 2201 path
2197 2202 """
2198 2203 return os.path.join(
2199 2204 os.path.dirname(self.repo_name),
2200 2205 self.cached_diffs_dir.split(os.path.sep)[-1])
2201 2206
2202 2207 @property
2203 2208 def cached_diffs_dir(self):
2204 2209 path = self.repo_full_path
2205 2210 return os.path.join(
2206 2211 os.path.dirname(path),
2207 2212 f'.__shadow_diff_cache_repo_{self.repo_id}')
2208 2213
2209 2214 def cached_diffs(self):
2210 2215 diff_cache_dir = self.cached_diffs_dir
2211 2216 if os.path.isdir(diff_cache_dir):
2212 2217 return os.listdir(diff_cache_dir)
2213 2218 return []
2214 2219
2215 2220 def shadow_repos(self):
2216 2221 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2217 2222 return [
2218 2223 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2219 2224 if x.startswith(shadow_repos_pattern)
2220 2225 ]
2221 2226
2222 2227 def get_new_name(self, repo_name):
2223 2228 """
2224 2229 returns new full repository name based on assigned group and new new
2225 2230
2226 2231 :param repo_name:
2227 2232 """
2228 2233 path_prefix = self.group.full_path_splitted if self.group else []
2229 2234 return self.NAME_SEP.join(path_prefix + [repo_name])
2230 2235
2231 2236 @property
2232 2237 def _config(self):
2233 2238 """
2234 2239 Returns db based config object.
2235 2240 """
2236 2241 from rhodecode.lib.utils import make_db_config
2237 2242 return make_db_config(clear_session=False, repo=self)
2238 2243
2239 2244 def permissions(self, with_admins=True, with_owner=True,
2240 2245 expand_from_user_groups=False):
2241 2246 """
2242 2247 Permissions for repositories
2243 2248 """
2244 2249 _admin_perm = 'repository.admin'
2245 2250
2246 2251 owner_row = []
2247 2252 if with_owner:
2248 2253 usr = AttributeDict(self.user.get_dict())
2249 2254 usr.owner_row = True
2250 2255 usr.permission = _admin_perm
2251 2256 usr.permission_id = None
2252 2257 owner_row.append(usr)
2253 2258
2254 2259 super_admin_ids = []
2255 2260 super_admin_rows = []
2256 2261 if with_admins:
2257 2262 for usr in User.get_all_super_admins():
2258 2263 super_admin_ids.append(usr.user_id)
2259 2264 # if this admin is also owner, don't double the record
2260 2265 if usr.user_id == owner_row[0].user_id:
2261 2266 owner_row[0].admin_row = True
2262 2267 else:
2263 2268 usr = AttributeDict(usr.get_dict())
2264 2269 usr.admin_row = True
2265 2270 usr.permission = _admin_perm
2266 2271 usr.permission_id = None
2267 2272 super_admin_rows.append(usr)
2268 2273
2269 2274 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2270 2275 q = q.options(joinedload(UserRepoToPerm.repository),
2271 2276 joinedload(UserRepoToPerm.user),
2272 2277 joinedload(UserRepoToPerm.permission),)
2273 2278
2274 2279 # get owners and admins and permissions. We do a trick of re-writing
2275 2280 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2276 2281 # has a global reference and changing one object propagates to all
2277 2282 # others. This means if admin is also an owner admin_row that change
2278 2283 # would propagate to both objects
2279 2284 perm_rows = []
2280 2285 for _usr in q.all():
2281 2286 usr = AttributeDict(_usr.user.get_dict())
2282 2287 # if this user is also owner/admin, mark as duplicate record
2283 2288 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2284 2289 usr.duplicate_perm = True
2285 2290 # also check if this permission is maybe used by branch_permissions
2286 2291 if _usr.branch_perm_entry:
2287 2292 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2288 2293
2289 2294 usr.permission = _usr.permission.permission_name
2290 2295 usr.permission_id = _usr.repo_to_perm_id
2291 2296 perm_rows.append(usr)
2292 2297
2293 2298 # filter the perm rows by 'default' first and then sort them by
2294 2299 # admin,write,read,none permissions sorted again alphabetically in
2295 2300 # each group
2296 2301 perm_rows = sorted(perm_rows, key=display_user_sort)
2297 2302
2298 2303 user_groups_rows = []
2299 2304 if expand_from_user_groups:
2300 2305 for ug in self.permission_user_groups(with_members=True):
2301 2306 for user_data in ug.members:
2302 2307 user_groups_rows.append(user_data)
2303 2308
2304 2309 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2305 2310
2306 2311 def permission_user_groups(self, with_members=True):
2307 2312 q = UserGroupRepoToPerm.query()\
2308 2313 .filter(UserGroupRepoToPerm.repository == self)
2309 2314 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2310 2315 joinedload(UserGroupRepoToPerm.users_group),
2311 2316 joinedload(UserGroupRepoToPerm.permission),)
2312 2317
2313 2318 perm_rows = []
2314 2319 for _user_group in q.all():
2315 2320 entry = AttributeDict(_user_group.users_group.get_dict())
2316 2321 entry.permission = _user_group.permission.permission_name
2317 2322 if with_members:
2318 2323 entry.members = [x.user.get_dict()
2319 2324 for x in _user_group.users_group.members]
2320 2325 perm_rows.append(entry)
2321 2326
2322 2327 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2323 2328 return perm_rows
2324 2329
2325 2330 def get_api_data(self, include_secrets=False):
2326 2331 """
2327 2332 Common function for generating repo api data
2328 2333
2329 2334 :param include_secrets: See :meth:`User.get_api_data`.
2330 2335
2331 2336 """
2332 2337 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2333 2338 # move this methods on models level.
2334 2339 from rhodecode.model.settings import SettingsModel
2335 2340 from rhodecode.model.repo import RepoModel
2336 2341
2337 2342 repo = self
2338 2343 _user_id, _time, _reason = self.locked
2339 2344
2340 2345 data = {
2341 2346 'repo_id': repo.repo_id,
2342 2347 'repo_name': repo.repo_name,
2343 2348 'repo_type': repo.repo_type,
2344 2349 'clone_uri': repo.clone_uri or '',
2345 2350 'push_uri': repo.push_uri or '',
2346 2351 'url': RepoModel().get_url(self),
2347 2352 'private': repo.private,
2348 2353 'created_on': repo.created_on,
2349 2354 'description': repo.description_safe,
2350 2355 'landing_rev': repo.landing_rev,
2351 2356 'owner': repo.user.username,
2352 2357 'fork_of': repo.fork.repo_name if repo.fork else None,
2353 2358 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2354 2359 'enable_statistics': repo.enable_statistics,
2355 2360 'enable_locking': repo.enable_locking,
2356 2361 'enable_downloads': repo.enable_downloads,
2357 2362 'last_changeset': repo.changeset_cache,
2358 2363 'locked_by': User.get(_user_id).get_api_data(
2359 2364 include_secrets=include_secrets) if _user_id else None,
2360 2365 'locked_date': time_to_datetime(_time) if _time else None,
2361 2366 'lock_reason': _reason if _reason else None,
2362 2367 }
2363 2368
2364 2369 # TODO: mikhail: should be per-repo settings here
2365 2370 rc_config = SettingsModel().get_all_settings()
2366 2371 repository_fields = str2bool(
2367 2372 rc_config.get('rhodecode_repository_fields'))
2368 2373 if repository_fields:
2369 2374 for f in self.extra_fields:
2370 2375 data[f.field_key_prefixed] = f.field_value
2371 2376
2372 2377 return data
2373 2378
2374 2379 @classmethod
2375 2380 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2376 2381 if not lock_time:
2377 2382 lock_time = time.time()
2378 2383 if not lock_reason:
2379 2384 lock_reason = cls.LOCK_AUTOMATIC
2380 2385 repo.locked = [user_id, lock_time, lock_reason]
2381 2386 Session().add(repo)
2382 2387 Session().commit()
2383 2388
2384 2389 @classmethod
2385 2390 def unlock(cls, repo):
2386 2391 repo.locked = None
2387 2392 Session().add(repo)
2388 2393 Session().commit()
2389 2394
2390 2395 @classmethod
2391 2396 def getlock(cls, repo):
2392 2397 return repo.locked
2393 2398
2394 2399 def get_locking_state(self, action, user_id, only_when_enabled=True):
2395 2400 """
2396 2401 Checks locking on this repository, if locking is enabled and lock is
2397 2402 present returns a tuple of make_lock, locked, locked_by.
2398 2403 make_lock can have 3 states None (do nothing) True, make lock
2399 2404 False release lock, This value is later propagated to hooks, which
2400 2405 do the locking. Think about this as signals passed to hooks what to do.
2401 2406
2402 2407 """
2403 2408 # TODO: johbo: This is part of the business logic and should be moved
2404 2409 # into the RepositoryModel.
2405 2410
2406 2411 if action not in ('push', 'pull'):
2407 2412 raise ValueError("Invalid action value: %s" % repr(action))
2408 2413
2409 2414 # defines if locked error should be thrown to user
2410 2415 currently_locked = False
2411 2416 # defines if new lock should be made, tri-state
2412 2417 make_lock = None
2413 2418 repo = self
2414 2419 user = User.get(user_id)
2415 2420
2416 2421 lock_info = repo.locked
2417 2422
2418 2423 if repo and (repo.enable_locking or not only_when_enabled):
2419 2424 if action == 'push':
2420 2425 # check if it's already locked !, if it is compare users
2421 2426 locked_by_user_id = lock_info[0]
2422 2427 if user.user_id == locked_by_user_id:
2423 2428 log.debug(
2424 2429 'Got `push` action from user %s, now unlocking', user)
2425 2430 # unlock if we have push from user who locked
2426 2431 make_lock = False
2427 2432 else:
2428 2433 # we're not the same user who locked, ban with
2429 2434 # code defined in settings (default is 423 HTTP Locked) !
2430 2435 log.debug('Repo %s is currently locked by %s', repo, user)
2431 2436 currently_locked = True
2432 2437 elif action == 'pull':
2433 2438 # [0] user [1] date
2434 2439 if lock_info[0] and lock_info[1]:
2435 2440 log.debug('Repo %s is currently locked by %s', repo, user)
2436 2441 currently_locked = True
2437 2442 else:
2438 2443 log.debug('Setting lock on repo %s by %s', repo, user)
2439 2444 make_lock = True
2440 2445
2441 2446 else:
2442 2447 log.debug('Repository %s do not have locking enabled', repo)
2443 2448
2444 2449 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2445 2450 make_lock, currently_locked, lock_info)
2446 2451
2447 2452 from rhodecode.lib.auth import HasRepoPermissionAny
2448 2453 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2449 2454 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2450 2455 # if we don't have at least write permission we cannot make a lock
2451 2456 log.debug('lock state reset back to FALSE due to lack '
2452 2457 'of at least read permission')
2453 2458 make_lock = False
2454 2459
2455 2460 return make_lock, currently_locked, lock_info
2456 2461
2457 2462 @property
2458 2463 def last_commit_cache_update_diff(self):
2459 2464 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2460 2465
2461 2466 @classmethod
2462 2467 def _load_commit_change(cls, last_commit_cache):
2463 2468 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2464 2469 empty_date = datetime.datetime.fromtimestamp(0)
2465 2470 date_latest = last_commit_cache.get('date', empty_date)
2466 2471 try:
2467 2472 return parse_datetime(date_latest)
2468 2473 except Exception:
2469 2474 return empty_date
2470 2475
2471 2476 @property
2472 2477 def last_commit_change(self):
2473 2478 return self._load_commit_change(self.changeset_cache)
2474 2479
2475 2480 @property
2476 2481 def last_db_change(self):
2477 2482 return self.updated_on
2478 2483
2479 2484 @property
2480 2485 def clone_uri_hidden(self):
2481 2486 clone_uri = self.clone_uri
2482 2487 if clone_uri:
2483 2488 import urlobject
2484 2489 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2485 2490 if url_obj.password:
2486 2491 clone_uri = url_obj.with_password('*****')
2487 2492 return clone_uri
2488 2493
2489 2494 @property
2490 2495 def push_uri_hidden(self):
2491 2496 push_uri = self.push_uri
2492 2497 if push_uri:
2493 2498 import urlobject
2494 2499 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2495 2500 if url_obj.password:
2496 2501 push_uri = url_obj.with_password('*****')
2497 2502 return push_uri
2498 2503
2499 2504 def clone_url(self, **override):
2500 2505 from rhodecode.model.settings import SettingsModel
2501 2506
2502 2507 uri_tmpl = None
2503 2508 if 'with_id' in override:
2504 2509 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2505 2510 del override['with_id']
2506 2511
2507 2512 if 'uri_tmpl' in override:
2508 2513 uri_tmpl = override['uri_tmpl']
2509 2514 del override['uri_tmpl']
2510 2515
2511 2516 ssh = False
2512 2517 if 'ssh' in override:
2513 2518 ssh = True
2514 2519 del override['ssh']
2515 2520
2516 2521 # we didn't override our tmpl from **overrides
2517 2522 request = get_current_request()
2518 2523 if not uri_tmpl:
2519 2524 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2520 2525 rc_config = request.call_context.rc_config
2521 2526 else:
2522 2527 rc_config = SettingsModel().get_all_settings(cache=True)
2523 2528
2524 2529 if ssh:
2525 2530 uri_tmpl = rc_config.get(
2526 2531 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2527 2532
2528 2533 else:
2529 2534 uri_tmpl = rc_config.get(
2530 2535 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2531 2536
2532 2537 return get_clone_url(request=request,
2533 2538 uri_tmpl=uri_tmpl,
2534 2539 repo_name=self.repo_name,
2535 2540 repo_id=self.repo_id,
2536 2541 repo_type=self.repo_type,
2537 2542 **override)
2538 2543
2539 2544 def set_state(self, state):
2540 2545 self.repo_state = state
2541 2546 Session().add(self)
2542 2547 #==========================================================================
2543 2548 # SCM PROPERTIES
2544 2549 #==========================================================================
2545 2550
2546 2551 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2547 2552 return get_commit_safe(
2548 2553 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2549 2554 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2550 2555
2551 2556 def get_changeset(self, rev=None, pre_load=None):
2552 2557 warnings.warn("Use get_commit", DeprecationWarning)
2553 2558 commit_id = None
2554 2559 commit_idx = None
2555 2560 if isinstance(rev, str):
2556 2561 commit_id = rev
2557 2562 else:
2558 2563 commit_idx = rev
2559 2564 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2560 2565 pre_load=pre_load)
2561 2566
2562 2567 def get_landing_commit(self):
2563 2568 """
2564 2569 Returns landing commit, or if that doesn't exist returns the tip
2565 2570 """
2566 2571 _rev_type, _rev = self.landing_rev
2567 2572 commit = self.get_commit(_rev)
2568 2573 if isinstance(commit, EmptyCommit):
2569 2574 return self.get_commit()
2570 2575 return commit
2571 2576
2572 2577 def flush_commit_cache(self):
2573 2578 self.update_commit_cache(cs_cache={'raw_id':'0'})
2574 2579 self.update_commit_cache()
2575 2580
2576 2581 def update_commit_cache(self, cs_cache=None, config=None):
2577 2582 """
2578 2583 Update cache of last commit for repository
2579 2584 cache_keys should be::
2580 2585
2581 2586 source_repo_id
2582 2587 short_id
2583 2588 raw_id
2584 2589 revision
2585 2590 parents
2586 2591 message
2587 2592 date
2588 2593 author
2589 2594 updated_on
2590 2595
2591 2596 """
2592 2597 from rhodecode.lib.vcs.backends.base import BaseCommit
2593 2598 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2594 2599 empty_date = datetime.datetime.fromtimestamp(0)
2595 2600 repo_commit_count = 0
2596 2601
2597 2602 if cs_cache is None:
2598 2603 # use no-cache version here
2599 2604 try:
2600 2605 scm_repo = self.scm_instance(cache=False, config=config)
2601 2606 except VCSError:
2602 2607 scm_repo = None
2603 2608 empty = scm_repo is None or scm_repo.is_empty()
2604 2609
2605 2610 if not empty:
2606 2611 cs_cache = scm_repo.get_commit(
2607 2612 pre_load=["author", "date", "message", "parents", "branch"])
2608 2613 repo_commit_count = scm_repo.count()
2609 2614 else:
2610 2615 cs_cache = EmptyCommit()
2611 2616
2612 2617 if isinstance(cs_cache, BaseCommit):
2613 2618 cs_cache = cs_cache.__json__()
2614 2619
2615 2620 def is_outdated(new_cs_cache):
2616 2621 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2617 2622 new_cs_cache['revision'] != self.changeset_cache['revision']):
2618 2623 return True
2619 2624 return False
2620 2625
2621 2626 # check if we have maybe already latest cached revision
2622 2627 if is_outdated(cs_cache) or not self.changeset_cache:
2623 2628 _current_datetime = datetime.datetime.utcnow()
2624 2629 last_change = cs_cache.get('date') or _current_datetime
2625 2630 # we check if last update is newer than the new value
2626 2631 # if yes, we use the current timestamp instead. Imagine you get
2627 2632 # old commit pushed 1y ago, we'd set last update 1y to ago.
2628 2633 last_change_timestamp = datetime_to_time(last_change)
2629 2634 current_timestamp = datetime_to_time(last_change)
2630 2635 if last_change_timestamp > current_timestamp and not empty:
2631 2636 cs_cache['date'] = _current_datetime
2632 2637
2633 2638 # also store size of repo
2634 2639 cs_cache['repo_commit_count'] = repo_commit_count
2635 2640
2636 2641 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2637 2642 cs_cache['updated_on'] = time.time()
2638 2643 self.changeset_cache = cs_cache
2639 2644 self.updated_on = last_change
2640 2645 Session().add(self)
2641 2646 Session().commit()
2642 2647
2643 2648 else:
2644 2649 if empty:
2645 2650 cs_cache = EmptyCommit().__json__()
2646 2651 else:
2647 2652 cs_cache = self.changeset_cache
2648 2653
2649 2654 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2650 2655
2651 2656 cs_cache['updated_on'] = time.time()
2652 2657 self.changeset_cache = cs_cache
2653 2658 self.updated_on = _date_latest
2654 2659 Session().add(self)
2655 2660 Session().commit()
2656 2661
2657 2662 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2658 2663 self.repo_name, cs_cache, _date_latest)
2659 2664
2660 2665 @property
2661 2666 def tip(self):
2662 2667 return self.get_commit('tip')
2663 2668
2664 2669 @property
2665 2670 def author(self):
2666 2671 return self.tip.author
2667 2672
2668 2673 @property
2669 2674 def last_change(self):
2670 2675 return self.scm_instance().last_change
2671 2676
2672 2677 def get_comments(self, revisions=None):
2673 2678 """
2674 2679 Returns comments for this repository grouped by revisions
2675 2680
2676 2681 :param revisions: filter query by revisions only
2677 2682 """
2678 2683 cmts = ChangesetComment.query()\
2679 2684 .filter(ChangesetComment.repo == self)
2680 2685 if revisions:
2681 2686 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2682 2687 grouped = collections.defaultdict(list)
2683 2688 for cmt in cmts.all():
2684 2689 grouped[cmt.revision].append(cmt)
2685 2690 return grouped
2686 2691
2687 2692 def statuses(self, revisions=None):
2688 2693 """
2689 2694 Returns statuses for this repository
2690 2695
2691 2696 :param revisions: list of revisions to get statuses for
2692 2697 """
2693 2698 statuses = ChangesetStatus.query()\
2694 2699 .filter(ChangesetStatus.repo == self)\
2695 2700 .filter(ChangesetStatus.version == 0)
2696 2701
2697 2702 if revisions:
2698 2703 # Try doing the filtering in chunks to avoid hitting limits
2699 2704 size = 500
2700 2705 status_results = []
2701 2706 for chunk in range(0, len(revisions), size):
2702 2707 status_results += statuses.filter(
2703 2708 ChangesetStatus.revision.in_(
2704 2709 revisions[chunk: chunk+size])
2705 2710 ).all()
2706 2711 else:
2707 2712 status_results = statuses.all()
2708 2713
2709 2714 grouped = {}
2710 2715
2711 2716 # maybe we have open new pullrequest without a status?
2712 2717 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2713 2718 status_lbl = ChangesetStatus.get_status_lbl(stat)
2714 2719 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2715 2720 for rev in pr.revisions:
2716 2721 pr_id = pr.pull_request_id
2717 2722 pr_repo = pr.target_repo.repo_name
2718 2723 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2719 2724
2720 2725 for stat in status_results:
2721 2726 pr_id = pr_repo = None
2722 2727 if stat.pull_request:
2723 2728 pr_id = stat.pull_request.pull_request_id
2724 2729 pr_repo = stat.pull_request.target_repo.repo_name
2725 2730 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2726 2731 pr_id, pr_repo]
2727 2732 return grouped
2728 2733
2729 2734 # ==========================================================================
2730 2735 # SCM CACHE INSTANCE
2731 2736 # ==========================================================================
2732 2737
2733 2738 def scm_instance(self, **kwargs):
2734 2739 import rhodecode
2735 2740
2736 2741 # Passing a config will not hit the cache currently only used
2737 2742 # for repo2dbmapper
2738 2743 config = kwargs.pop('config', None)
2739 2744 cache = kwargs.pop('cache', None)
2740 2745 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2741 2746 if vcs_full_cache is not None:
2742 2747 # allows override global config
2743 2748 full_cache = vcs_full_cache
2744 2749 else:
2745 2750 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2746 2751 # if cache is NOT defined use default global, else we have a full
2747 2752 # control over cache behaviour
2748 2753 if cache is None and full_cache and not config:
2749 2754 log.debug('Initializing pure cached instance for %s', self.repo_path)
2750 2755 return self._get_instance_cached()
2751 2756
2752 2757 # cache here is sent to the "vcs server"
2753 2758 return self._get_instance(cache=bool(cache), config=config)
2754 2759
2755 2760 def _get_instance_cached(self):
2756 2761 from rhodecode.lib import rc_cache
2757 2762
2758 2763 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2759 2764 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2760 2765
2761 2766 # we must use thread scoped cache here,
2762 2767 # because each thread of gevent needs it's own not shared connection and cache
2763 2768 # we also alter `args` so the cache key is individual for every green thread.
2764 2769 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2765 2770 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2766 2771
2767 2772 # our wrapped caching function that takes state_uid to save the previous state in
2768 2773 def cache_generator(_state_uid):
2769 2774
2770 2775 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2771 2776 def get_instance_cached(_repo_id, _process_context_id):
2772 2777 # we save in cached func the generation state so we can detect a change and invalidate caches
2773 2778 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2774 2779
2775 2780 return get_instance_cached
2776 2781
2777 2782 with inv_context_manager as invalidation_context:
2778 2783 cache_state_uid = invalidation_context.state_uid
2779 2784 cache_func = cache_generator(cache_state_uid)
2780 2785
2781 2786 args = self.repo_id, inv_context_manager.proc_key
2782 2787
2783 2788 previous_state_uid, instance = cache_func(*args)
2784 2789
2785 2790 # now compare keys, the "cache" state vs expected state.
2786 2791 if previous_state_uid != cache_state_uid:
2787 2792 log.warning('Cached state uid %s is different than current state uid %s',
2788 2793 previous_state_uid, cache_state_uid)
2789 2794 _, instance = cache_func.refresh(*args)
2790 2795
2791 2796 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2792 2797 return instance
2793 2798
2794 2799 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2795 2800 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2796 2801 self.repo_type, self.repo_path, cache)
2797 2802 config = config or self._config
2798 2803 custom_wire = {
2799 2804 'cache': cache, # controls the vcs.remote cache
2800 2805 'repo_state_uid': repo_state_uid
2801 2806 }
2802 2807
2803 2808 repo = get_vcs_instance(
2804 2809 repo_path=safe_str(self.repo_full_path),
2805 2810 config=config,
2806 2811 with_wire=custom_wire,
2807 2812 create=False,
2808 2813 _vcs_alias=self.repo_type)
2809 2814 if repo is not None:
2810 2815 repo.count() # cache rebuild
2811 2816
2812 2817 return repo
2813 2818
2814 2819 def get_shadow_repository_path(self, workspace_id):
2815 2820 from rhodecode.lib.vcs.backends.base import BaseRepository
2816 2821 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2817 2822 self.repo_full_path, self.repo_id, workspace_id)
2818 2823 return shadow_repo_path
2819 2824
2820 2825 def __json__(self):
2821 2826 return {'landing_rev': self.landing_rev}
2822 2827
2823 2828 def get_dict(self):
2824 2829
2825 2830 # Since we transformed `repo_name` to a hybrid property, we need to
2826 2831 # keep compatibility with the code which uses `repo_name` field.
2827 2832
2828 2833 result = super(Repository, self).get_dict()
2829 2834 result['repo_name'] = result.pop('_repo_name', None)
2830 2835 result.pop('_changeset_cache', '')
2831 2836 return result
2832 2837
2833 2838
2834 2839 class RepoGroup(Base, BaseModel):
2835 2840 __tablename__ = 'groups'
2836 2841 __table_args__ = (
2837 2842 UniqueConstraint('group_name', 'group_parent_id'),
2838 2843 base_table_args,
2839 2844 )
2840 2845
2841 2846 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2842 2847
2843 2848 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2844 2849 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2845 2850 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2846 2851 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2847 2852 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2848 2853 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2849 2854 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2850 2855 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2851 2856 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2852 2857 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2853 2858 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2854 2859
2855 2860 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2856 2861 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2857 2862 parent_group = relationship('RepoGroup', remote_side=group_id)
2858 2863 user = relationship('User', back_populates='repository_groups')
2859 2864 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2860 2865
2861 2866 # no cascade, set NULL
2862 2867 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2863 2868
2864 2869 def __init__(self, group_name='', parent_group=None):
2865 2870 self.group_name = group_name
2866 2871 self.parent_group = parent_group
2867 2872
2868 2873 def __repr__(self):
2869 2874 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2870 2875
2871 2876 @hybrid_property
2872 2877 def group_name(self):
2873 2878 return self._group_name
2874 2879
2875 2880 @group_name.setter
2876 2881 def group_name(self, value):
2877 2882 self._group_name = value
2878 2883 self.group_name_hash = self.hash_repo_group_name(value)
2879 2884
2880 2885 @classmethod
2881 2886 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2882 2887 from rhodecode.lib.vcs.backends.base import EmptyCommit
2883 2888 dummy = EmptyCommit().__json__()
2884 2889 if not changeset_cache_raw:
2885 2890 dummy['source_repo_id'] = repo_id
2886 2891 return json.loads(json.dumps(dummy))
2887 2892
2888 2893 try:
2889 2894 return json.loads(changeset_cache_raw)
2890 2895 except TypeError:
2891 2896 return dummy
2892 2897 except Exception:
2893 2898 log.error(traceback.format_exc())
2894 2899 return dummy
2895 2900
2896 2901 @hybrid_property
2897 2902 def changeset_cache(self):
2898 2903 return self._load_changeset_cache('', self._changeset_cache)
2899 2904
2900 2905 @changeset_cache.setter
2901 2906 def changeset_cache(self, val):
2902 2907 try:
2903 2908 self._changeset_cache = json.dumps(val)
2904 2909 except Exception:
2905 2910 log.error(traceback.format_exc())
2906 2911
2907 2912 @validates('group_parent_id')
2908 2913 def validate_group_parent_id(self, key, val):
2909 2914 """
2910 2915 Check cycle references for a parent group to self
2911 2916 """
2912 2917 if self.group_id and val:
2913 2918 assert val != self.group_id
2914 2919
2915 2920 return val
2916 2921
2917 2922 @hybrid_property
2918 2923 def description_safe(self):
2919 2924 from rhodecode.lib import helpers as h
2920 2925 return h.escape(self.group_description)
2921 2926
2922 2927 @classmethod
2923 2928 def hash_repo_group_name(cls, repo_group_name):
2924 2929 val = remove_formatting(repo_group_name)
2925 2930 val = safe_str(val).lower()
2926 2931 chars = []
2927 2932 for c in val:
2928 2933 if c not in string.ascii_letters:
2929 2934 c = str(ord(c))
2930 2935 chars.append(c)
2931 2936
2932 2937 return ''.join(chars)
2933 2938
2934 2939 @classmethod
2935 2940 def _generate_choice(cls, repo_group):
2936 2941 from webhelpers2.html import literal as _literal
2937 2942
2938 2943 def _name(k):
2939 2944 return _literal(cls.CHOICES_SEPARATOR.join(k))
2940 2945
2941 2946 return repo_group.group_id, _name(repo_group.full_path_splitted)
2942 2947
2943 2948 @classmethod
2944 2949 def groups_choices(cls, groups=None, show_empty_group=True):
2945 2950 if not groups:
2946 2951 groups = cls.query().all()
2947 2952
2948 2953 repo_groups = []
2949 2954 if show_empty_group:
2950 2955 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2951 2956
2952 2957 repo_groups.extend([cls._generate_choice(x) for x in groups])
2953 2958
2954 2959 repo_groups = sorted(
2955 2960 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2956 2961 return repo_groups
2957 2962
2958 2963 @classmethod
2959 2964 def url_sep(cls):
2960 2965 return URL_SEP
2961 2966
2962 2967 @classmethod
2963 2968 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2964 2969 if case_insensitive:
2965 2970 gr = cls.query().filter(func.lower(cls.group_name)
2966 2971 == func.lower(group_name))
2967 2972 else:
2968 2973 gr = cls.query().filter(cls.group_name == group_name)
2969 2974 if cache:
2970 2975 name_key = _hash_key(group_name)
2971 2976 gr = gr.options(
2972 2977 FromCache("sql_cache_short", f"get_group_{name_key}"))
2973 2978 return gr.scalar()
2974 2979
2975 2980 @classmethod
2976 2981 def get_user_personal_repo_group(cls, user_id):
2977 2982 user = User.get(user_id)
2978 2983 if user.username == User.DEFAULT_USER:
2979 2984 return None
2980 2985
2981 2986 return cls.query()\
2982 2987 .filter(cls.personal == true()) \
2983 2988 .filter(cls.user == user) \
2984 2989 .order_by(cls.group_id.asc()) \
2985 2990 .first()
2986 2991
2987 2992 @classmethod
2988 2993 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2989 2994 case_insensitive=True):
2990 2995 q = RepoGroup.query()
2991 2996
2992 2997 if not isinstance(user_id, Optional):
2993 2998 q = q.filter(RepoGroup.user_id == user_id)
2994 2999
2995 3000 if not isinstance(group_id, Optional):
2996 3001 q = q.filter(RepoGroup.group_parent_id == group_id)
2997 3002
2998 3003 if case_insensitive:
2999 3004 q = q.order_by(func.lower(RepoGroup.group_name))
3000 3005 else:
3001 3006 q = q.order_by(RepoGroup.group_name)
3002 3007 return q.all()
3003 3008
3004 3009 @property
3005 3010 def parents(self, parents_recursion_limit=10):
3006 3011 groups = []
3007 3012 if self.parent_group is None:
3008 3013 return groups
3009 3014 cur_gr = self.parent_group
3010 3015 groups.insert(0, cur_gr)
3011 3016 cnt = 0
3012 3017 while 1:
3013 3018 cnt += 1
3014 3019 gr = getattr(cur_gr, 'parent_group', None)
3015 3020 cur_gr = cur_gr.parent_group
3016 3021 if gr is None:
3017 3022 break
3018 3023 if cnt == parents_recursion_limit:
3019 3024 # this will prevent accidental infinit loops
3020 3025 log.error('more than %s parents found for group %s, stopping '
3021 3026 'recursive parent fetching', parents_recursion_limit, self)
3022 3027 break
3023 3028
3024 3029 groups.insert(0, gr)
3025 3030 return groups
3026 3031
3027 3032 @property
3028 3033 def last_commit_cache_update_diff(self):
3029 3034 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
3030 3035
3031 3036 @classmethod
3032 3037 def _load_commit_change(cls, last_commit_cache):
3033 3038 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3034 3039 empty_date = datetime.datetime.fromtimestamp(0)
3035 3040 date_latest = last_commit_cache.get('date', empty_date)
3036 3041 try:
3037 3042 return parse_datetime(date_latest)
3038 3043 except Exception:
3039 3044 return empty_date
3040 3045
3041 3046 @property
3042 3047 def last_commit_change(self):
3043 3048 return self._load_commit_change(self.changeset_cache)
3044 3049
3045 3050 @property
3046 3051 def last_db_change(self):
3047 3052 return self.updated_on
3048 3053
3049 3054 @property
3050 3055 def children(self):
3051 3056 return RepoGroup.query().filter(RepoGroup.parent_group == self)
3052 3057
3053 3058 @property
3054 3059 def name(self):
3055 3060 return self.group_name.split(RepoGroup.url_sep())[-1]
3056 3061
3057 3062 @property
3058 3063 def full_path(self):
3059 3064 return self.group_name
3060 3065
3061 3066 @property
3062 3067 def full_path_splitted(self):
3063 3068 return self.group_name.split(RepoGroup.url_sep())
3064 3069
3065 3070 @property
3066 3071 def repositories(self):
3067 3072 return Repository.query()\
3068 3073 .filter(Repository.group == self)\
3069 3074 .order_by(Repository.repo_name)
3070 3075
3071 3076 @property
3072 3077 def repositories_recursive_count(self):
3073 3078 cnt = self.repositories.count()
3074 3079
3075 3080 def children_count(group):
3076 3081 cnt = 0
3077 3082 for child in group.children:
3078 3083 cnt += child.repositories.count()
3079 3084 cnt += children_count(child)
3080 3085 return cnt
3081 3086
3082 3087 return cnt + children_count(self)
3083 3088
3084 3089 def _recursive_objects(self, include_repos=True, include_groups=True):
3085 3090 all_ = []
3086 3091
3087 3092 def _get_members(root_gr):
3088 3093 if include_repos:
3089 3094 for r in root_gr.repositories:
3090 3095 all_.append(r)
3091 3096 childs = root_gr.children.all()
3092 3097 if childs:
3093 3098 for gr in childs:
3094 3099 if include_groups:
3095 3100 all_.append(gr)
3096 3101 _get_members(gr)
3097 3102
3098 3103 root_group = []
3099 3104 if include_groups:
3100 3105 root_group = [self]
3101 3106
3102 3107 _get_members(self)
3103 3108 return root_group + all_
3104 3109
3105 3110 def recursive_groups_and_repos(self):
3106 3111 """
3107 3112 Recursive return all groups, with repositories in those groups
3108 3113 """
3109 3114 return self._recursive_objects()
3110 3115
3111 3116 def recursive_groups(self):
3112 3117 """
3113 3118 Returns all children groups for this group including children of children
3114 3119 """
3115 3120 return self._recursive_objects(include_repos=False)
3116 3121
3117 3122 def recursive_repos(self):
3118 3123 """
3119 3124 Returns all children repositories for this group
3120 3125 """
3121 3126 return self._recursive_objects(include_groups=False)
3122 3127
3123 3128 def get_new_name(self, group_name):
3124 3129 """
3125 3130 returns new full group name based on parent and new name
3126 3131
3127 3132 :param group_name:
3128 3133 """
3129 3134 path_prefix = (self.parent_group.full_path_splitted if
3130 3135 self.parent_group else [])
3131 3136 return RepoGroup.url_sep().join(path_prefix + [group_name])
3132 3137
3133 3138 def update_commit_cache(self, config=None):
3134 3139 """
3135 3140 Update cache of last commit for newest repository inside this repository group.
3136 3141 cache_keys should be::
3137 3142
3138 3143 source_repo_id
3139 3144 short_id
3140 3145 raw_id
3141 3146 revision
3142 3147 parents
3143 3148 message
3144 3149 date
3145 3150 author
3146 3151
3147 3152 """
3148 3153 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3149 3154 empty_date = datetime.datetime.fromtimestamp(0)
3150 3155
3151 3156 def repo_groups_and_repos(root_gr):
3152 3157 for _repo in root_gr.repositories:
3153 3158 yield _repo
3154 3159 for child_group in root_gr.children.all():
3155 3160 yield child_group
3156 3161
3157 3162 latest_repo_cs_cache = {}
3158 3163 for obj in repo_groups_and_repos(self):
3159 3164 repo_cs_cache = obj.changeset_cache
3160 3165 date_latest = latest_repo_cs_cache.get('date', empty_date)
3161 3166 date_current = repo_cs_cache.get('date', empty_date)
3162 3167 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3163 3168 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3164 3169 latest_repo_cs_cache = repo_cs_cache
3165 3170 if hasattr(obj, 'repo_id'):
3166 3171 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3167 3172 else:
3168 3173 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3169 3174
3170 3175 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3171 3176
3172 3177 latest_repo_cs_cache['updated_on'] = time.time()
3173 3178 self.changeset_cache = latest_repo_cs_cache
3174 3179 self.updated_on = _date_latest
3175 3180 Session().add(self)
3176 3181 Session().commit()
3177 3182
3178 3183 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3179 3184 self.group_name, latest_repo_cs_cache, _date_latest)
3180 3185
3181 3186 def permissions(self, with_admins=True, with_owner=True,
3182 3187 expand_from_user_groups=False):
3183 3188 """
3184 3189 Permissions for repository groups
3185 3190 """
3186 3191 _admin_perm = 'group.admin'
3187 3192
3188 3193 owner_row = []
3189 3194 if with_owner:
3190 3195 usr = AttributeDict(self.user.get_dict())
3191 3196 usr.owner_row = True
3192 3197 usr.permission = _admin_perm
3193 3198 owner_row.append(usr)
3194 3199
3195 3200 super_admin_ids = []
3196 3201 super_admin_rows = []
3197 3202 if with_admins:
3198 3203 for usr in User.get_all_super_admins():
3199 3204 super_admin_ids.append(usr.user_id)
3200 3205 # if this admin is also owner, don't double the record
3201 3206 if usr.user_id == owner_row[0].user_id:
3202 3207 owner_row[0].admin_row = True
3203 3208 else:
3204 3209 usr = AttributeDict(usr.get_dict())
3205 3210 usr.admin_row = True
3206 3211 usr.permission = _admin_perm
3207 3212 super_admin_rows.append(usr)
3208 3213
3209 3214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3210 3215 q = q.options(joinedload(UserRepoGroupToPerm.group),
3211 3216 joinedload(UserRepoGroupToPerm.user),
3212 3217 joinedload(UserRepoGroupToPerm.permission),)
3213 3218
3214 3219 # get owners and admins and permissions. We do a trick of re-writing
3215 3220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3216 3221 # has a global reference and changing one object propagates to all
3217 3222 # others. This means if admin is also an owner admin_row that change
3218 3223 # would propagate to both objects
3219 3224 perm_rows = []
3220 3225 for _usr in q.all():
3221 3226 usr = AttributeDict(_usr.user.get_dict())
3222 3227 # if this user is also owner/admin, mark as duplicate record
3223 3228 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3224 3229 usr.duplicate_perm = True
3225 3230 usr.permission = _usr.permission.permission_name
3226 3231 perm_rows.append(usr)
3227 3232
3228 3233 # filter the perm rows by 'default' first and then sort them by
3229 3234 # admin,write,read,none permissions sorted again alphabetically in
3230 3235 # each group
3231 3236 perm_rows = sorted(perm_rows, key=display_user_sort)
3232 3237
3233 3238 user_groups_rows = []
3234 3239 if expand_from_user_groups:
3235 3240 for ug in self.permission_user_groups(with_members=True):
3236 3241 for user_data in ug.members:
3237 3242 user_groups_rows.append(user_data)
3238 3243
3239 3244 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3240 3245
3241 3246 def permission_user_groups(self, with_members=False):
3242 3247 q = UserGroupRepoGroupToPerm.query()\
3243 3248 .filter(UserGroupRepoGroupToPerm.group == self)
3244 3249 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3245 3250 joinedload(UserGroupRepoGroupToPerm.users_group),
3246 3251 joinedload(UserGroupRepoGroupToPerm.permission),)
3247 3252
3248 3253 perm_rows = []
3249 3254 for _user_group in q.all():
3250 3255 entry = AttributeDict(_user_group.users_group.get_dict())
3251 3256 entry.permission = _user_group.permission.permission_name
3252 3257 if with_members:
3253 3258 entry.members = [x.user.get_dict()
3254 3259 for x in _user_group.users_group.members]
3255 3260 perm_rows.append(entry)
3256 3261
3257 3262 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3258 3263 return perm_rows
3259 3264
3260 3265 def get_api_data(self):
3261 3266 """
3262 3267 Common function for generating api data
3263 3268
3264 3269 """
3265 3270 group = self
3266 3271 data = {
3267 3272 'group_id': group.group_id,
3268 3273 'group_name': group.group_name,
3269 3274 'group_description': group.description_safe,
3270 3275 'parent_group': group.parent_group.group_name if group.parent_group else None,
3271 3276 'repositories': [x.repo_name for x in group.repositories],
3272 3277 'owner': group.user.username,
3273 3278 }
3274 3279 return data
3275 3280
3276 3281 def get_dict(self):
3277 3282 # Since we transformed `group_name` to a hybrid property, we need to
3278 3283 # keep compatibility with the code which uses `group_name` field.
3279 3284 result = super(RepoGroup, self).get_dict()
3280 3285 result['group_name'] = result.pop('_group_name', None)
3281 3286 result.pop('_changeset_cache', '')
3282 3287 return result
3283 3288
3284 3289
3285 3290 class Permission(Base, BaseModel):
3286 3291 __tablename__ = 'permissions'
3287 3292 __table_args__ = (
3288 3293 Index('p_perm_name_idx', 'permission_name'),
3289 3294 base_table_args,
3290 3295 )
3291 3296
3292 3297 PERMS = [
3293 3298 ('hg.admin', _('RhodeCode Super Administrator')),
3294 3299
3295 3300 ('repository.none', _('Repository no access')),
3296 3301 ('repository.read', _('Repository read access')),
3297 3302 ('repository.write', _('Repository write access')),
3298 3303 ('repository.admin', _('Repository admin access')),
3299 3304
3300 3305 ('group.none', _('Repository group no access')),
3301 3306 ('group.read', _('Repository group read access')),
3302 3307 ('group.write', _('Repository group write access')),
3303 3308 ('group.admin', _('Repository group admin access')),
3304 3309
3305 3310 ('usergroup.none', _('User group no access')),
3306 3311 ('usergroup.read', _('User group read access')),
3307 3312 ('usergroup.write', _('User group write access')),
3308 3313 ('usergroup.admin', _('User group admin access')),
3309 3314
3310 3315 ('branch.none', _('Branch no permissions')),
3311 3316 ('branch.merge', _('Branch access by web merge')),
3312 3317 ('branch.push', _('Branch access by push')),
3313 3318 ('branch.push_force', _('Branch access by push with force')),
3314 3319
3315 3320 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3316 3321 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3317 3322
3318 3323 ('hg.usergroup.create.false', _('User Group creation disabled')),
3319 3324 ('hg.usergroup.create.true', _('User Group creation enabled')),
3320 3325
3321 3326 ('hg.create.none', _('Repository creation disabled')),
3322 3327 ('hg.create.repository', _('Repository creation enabled')),
3323 3328 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3324 3329 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3325 3330
3326 3331 ('hg.fork.none', _('Repository forking disabled')),
3327 3332 ('hg.fork.repository', _('Repository forking enabled')),
3328 3333
3329 3334 ('hg.register.none', _('Registration disabled')),
3330 3335 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3331 3336 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3332 3337
3333 3338 ('hg.password_reset.enabled', _('Password reset enabled')),
3334 3339 ('hg.password_reset.hidden', _('Password reset hidden')),
3335 3340 ('hg.password_reset.disabled', _('Password reset disabled')),
3336 3341
3337 3342 ('hg.extern_activate.manual', _('Manual activation of external account')),
3338 3343 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3339 3344
3340 3345 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3341 3346 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3342 3347 ]
3343 3348
3344 3349 # definition of system default permissions for DEFAULT user, created on
3345 3350 # system setup
3346 3351 DEFAULT_USER_PERMISSIONS = [
3347 3352 # object perms
3348 3353 'repository.read',
3349 3354 'group.read',
3350 3355 'usergroup.read',
3351 3356 # branch, for backward compat we need same value as before so forced pushed
3352 3357 'branch.push_force',
3353 3358 # global
3354 3359 'hg.create.repository',
3355 3360 'hg.repogroup.create.false',
3356 3361 'hg.usergroup.create.false',
3357 3362 'hg.create.write_on_repogroup.true',
3358 3363 'hg.fork.repository',
3359 3364 'hg.register.manual_activate',
3360 3365 'hg.password_reset.enabled',
3361 3366 'hg.extern_activate.auto',
3362 3367 'hg.inherit_default_perms.true',
3363 3368 ]
3364 3369
3365 3370 # defines which permissions are more important higher the more important
3366 3371 # Weight defines which permissions are more important.
3367 3372 # The higher number the more important.
3368 3373 PERM_WEIGHTS = {
3369 3374 'repository.none': 0,
3370 3375 'repository.read': 1,
3371 3376 'repository.write': 3,
3372 3377 'repository.admin': 4,
3373 3378
3374 3379 'group.none': 0,
3375 3380 'group.read': 1,
3376 3381 'group.write': 3,
3377 3382 'group.admin': 4,
3378 3383
3379 3384 'usergroup.none': 0,
3380 3385 'usergroup.read': 1,
3381 3386 'usergroup.write': 3,
3382 3387 'usergroup.admin': 4,
3383 3388
3384 3389 'branch.none': 0,
3385 3390 'branch.merge': 1,
3386 3391 'branch.push': 3,
3387 3392 'branch.push_force': 4,
3388 3393
3389 3394 'hg.repogroup.create.false': 0,
3390 3395 'hg.repogroup.create.true': 1,
3391 3396
3392 3397 'hg.usergroup.create.false': 0,
3393 3398 'hg.usergroup.create.true': 1,
3394 3399
3395 3400 'hg.fork.none': 0,
3396 3401 'hg.fork.repository': 1,
3397 3402 'hg.create.none': 0,
3398 3403 'hg.create.repository': 1
3399 3404 }
3400 3405
3401 3406 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3402 3407 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3403 3408 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3404 3409
3405 3410 def __repr__(self):
3406 3411 return "<%s('%s:%s')>" % (
3407 3412 self.cls_name, self.permission_id, self.permission_name
3408 3413 )
3409 3414
3410 3415 @classmethod
3411 3416 def get_by_key(cls, key):
3412 3417 return cls.query().filter(cls.permission_name == key).scalar()
3413 3418
3414 3419 @classmethod
3415 3420 def get_default_repo_perms(cls, user_id, repo_id=None):
3416 3421 q = Session().query(UserRepoToPerm, Repository, Permission)\
3417 3422 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3418 3423 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3419 3424 .filter(UserRepoToPerm.user_id == user_id)
3420 3425 if repo_id:
3421 3426 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3422 3427 return q.all()
3423 3428
3424 3429 @classmethod
3425 3430 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3426 3431 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3427 3432 .join(
3428 3433 Permission,
3429 3434 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3430 3435 .join(
3431 3436 UserRepoToPerm,
3432 3437 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3433 3438 .filter(UserRepoToPerm.user_id == user_id)
3434 3439
3435 3440 if repo_id:
3436 3441 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3437 3442 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3438 3443
3439 3444 @classmethod
3440 3445 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3441 3446 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3442 3447 .join(
3443 3448 Permission,
3444 3449 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3445 3450 .join(
3446 3451 Repository,
3447 3452 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3448 3453 .join(
3449 3454 UserGroup,
3450 3455 UserGroupRepoToPerm.users_group_id ==
3451 3456 UserGroup.users_group_id)\
3452 3457 .join(
3453 3458 UserGroupMember,
3454 3459 UserGroupRepoToPerm.users_group_id ==
3455 3460 UserGroupMember.users_group_id)\
3456 3461 .filter(
3457 3462 UserGroupMember.user_id == user_id,
3458 3463 UserGroup.users_group_active == true())
3459 3464 if repo_id:
3460 3465 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3461 3466 return q.all()
3462 3467
3463 3468 @classmethod
3464 3469 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3465 3470 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3466 3471 .join(
3467 3472 Permission,
3468 3473 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3469 3474 .join(
3470 3475 UserGroupRepoToPerm,
3471 3476 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3472 3477 .join(
3473 3478 UserGroup,
3474 3479 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3475 3480 .join(
3476 3481 UserGroupMember,
3477 3482 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3478 3483 .filter(
3479 3484 UserGroupMember.user_id == user_id,
3480 3485 UserGroup.users_group_active == true())
3481 3486
3482 3487 if repo_id:
3483 3488 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3484 3489 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3485 3490
3486 3491 @classmethod
3487 3492 def get_default_group_perms(cls, user_id, repo_group_id=None):
3488 3493 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3489 3494 .join(
3490 3495 Permission,
3491 3496 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3492 3497 .join(
3493 3498 RepoGroup,
3494 3499 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3495 3500 .filter(UserRepoGroupToPerm.user_id == user_id)
3496 3501 if repo_group_id:
3497 3502 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3498 3503 return q.all()
3499 3504
3500 3505 @classmethod
3501 3506 def get_default_group_perms_from_user_group(
3502 3507 cls, user_id, repo_group_id=None):
3503 3508 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3504 3509 .join(
3505 3510 Permission,
3506 3511 UserGroupRepoGroupToPerm.permission_id ==
3507 3512 Permission.permission_id)\
3508 3513 .join(
3509 3514 RepoGroup,
3510 3515 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3511 3516 .join(
3512 3517 UserGroup,
3513 3518 UserGroupRepoGroupToPerm.users_group_id ==
3514 3519 UserGroup.users_group_id)\
3515 3520 .join(
3516 3521 UserGroupMember,
3517 3522 UserGroupRepoGroupToPerm.users_group_id ==
3518 3523 UserGroupMember.users_group_id)\
3519 3524 .filter(
3520 3525 UserGroupMember.user_id == user_id,
3521 3526 UserGroup.users_group_active == true())
3522 3527 if repo_group_id:
3523 3528 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3524 3529 return q.all()
3525 3530
3526 3531 @classmethod
3527 3532 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3528 3533 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3529 3534 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3530 3535 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3531 3536 .filter(UserUserGroupToPerm.user_id == user_id)
3532 3537 if user_group_id:
3533 3538 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3534 3539 return q.all()
3535 3540
3536 3541 @classmethod
3537 3542 def get_default_user_group_perms_from_user_group(
3538 3543 cls, user_id, user_group_id=None):
3539 3544 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3540 3545 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3541 3546 .join(
3542 3547 Permission,
3543 3548 UserGroupUserGroupToPerm.permission_id ==
3544 3549 Permission.permission_id)\
3545 3550 .join(
3546 3551 TargetUserGroup,
3547 3552 UserGroupUserGroupToPerm.target_user_group_id ==
3548 3553 TargetUserGroup.users_group_id)\
3549 3554 .join(
3550 3555 UserGroup,
3551 3556 UserGroupUserGroupToPerm.user_group_id ==
3552 3557 UserGroup.users_group_id)\
3553 3558 .join(
3554 3559 UserGroupMember,
3555 3560 UserGroupUserGroupToPerm.user_group_id ==
3556 3561 UserGroupMember.users_group_id)\
3557 3562 .filter(
3558 3563 UserGroupMember.user_id == user_id,
3559 3564 UserGroup.users_group_active == true())
3560 3565 if user_group_id:
3561 3566 q = q.filter(
3562 3567 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3563 3568
3564 3569 return q.all()
3565 3570
3566 3571
3567 3572 class UserRepoToPerm(Base, BaseModel):
3568 3573 __tablename__ = 'repo_to_perm'
3569 3574 __table_args__ = (
3570 3575 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3571 3576 base_table_args
3572 3577 )
3573 3578
3574 3579 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3575 3580 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3576 3581 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3577 3582 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3578 3583
3579 3584 user = relationship('User', back_populates="repo_to_perm")
3580 3585 repository = relationship('Repository', back_populates="repo_to_perm")
3581 3586 permission = relationship('Permission')
3582 3587
3583 3588 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3584 3589
3585 3590 @classmethod
3586 3591 def create(cls, user, repository, permission):
3587 3592 n = cls()
3588 3593 n.user = user
3589 3594 n.repository = repository
3590 3595 n.permission = permission
3591 3596 Session().add(n)
3592 3597 return n
3593 3598
3594 3599 def __repr__(self):
3595 3600 return f'<{self.user} => {self.repository} >'
3596 3601
3597 3602
3598 3603 class UserUserGroupToPerm(Base, BaseModel):
3599 3604 __tablename__ = 'user_user_group_to_perm'
3600 3605 __table_args__ = (
3601 3606 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3602 3607 base_table_args
3603 3608 )
3604 3609
3605 3610 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3606 3611 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3607 3612 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3608 3613 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3609 3614
3610 3615 user = relationship('User', back_populates='user_group_to_perm')
3611 3616 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3612 3617 permission = relationship('Permission')
3613 3618
3614 3619 @classmethod
3615 3620 def create(cls, user, user_group, permission):
3616 3621 n = cls()
3617 3622 n.user = user
3618 3623 n.user_group = user_group
3619 3624 n.permission = permission
3620 3625 Session().add(n)
3621 3626 return n
3622 3627
3623 3628 def __repr__(self):
3624 3629 return f'<{self.user} => {self.user_group} >'
3625 3630
3626 3631
3627 3632 class UserToPerm(Base, BaseModel):
3628 3633 __tablename__ = 'user_to_perm'
3629 3634 __table_args__ = (
3630 3635 UniqueConstraint('user_id', 'permission_id'),
3631 3636 base_table_args
3632 3637 )
3633 3638
3634 3639 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3635 3640 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3636 3641 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3637 3642
3638 3643 user = relationship('User', back_populates='user_perms')
3639 3644 permission = relationship('Permission', lazy='joined')
3640 3645
3641 3646 def __repr__(self):
3642 3647 return f'<{self.user} => {self.permission} >'
3643 3648
3644 3649
3645 3650 class UserGroupRepoToPerm(Base, BaseModel):
3646 3651 __tablename__ = 'users_group_repo_to_perm'
3647 3652 __table_args__ = (
3648 3653 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3649 3654 base_table_args
3650 3655 )
3651 3656
3652 3657 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3653 3658 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3654 3659 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3655 3660 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3656 3661
3657 3662 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3658 3663 permission = relationship('Permission')
3659 3664 repository = relationship('Repository', back_populates='users_group_to_perm')
3660 3665 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3661 3666
3662 3667 @classmethod
3663 3668 def create(cls, users_group, repository, permission):
3664 3669 n = cls()
3665 3670 n.users_group = users_group
3666 3671 n.repository = repository
3667 3672 n.permission = permission
3668 3673 Session().add(n)
3669 3674 return n
3670 3675
3671 3676 def __repr__(self):
3672 3677 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3673 3678
3674 3679
3675 3680 class UserGroupUserGroupToPerm(Base, BaseModel):
3676 3681 __tablename__ = 'user_group_user_group_to_perm'
3677 3682 __table_args__ = (
3678 3683 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3679 3684 CheckConstraint('target_user_group_id != user_group_id'),
3680 3685 base_table_args
3681 3686 )
3682 3687
3683 3688 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3684 3689 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3685 3690 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3686 3691 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3687 3692
3688 3693 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3689 3694 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3690 3695 permission = relationship('Permission')
3691 3696
3692 3697 @classmethod
3693 3698 def create(cls, target_user_group, user_group, permission):
3694 3699 n = cls()
3695 3700 n.target_user_group = target_user_group
3696 3701 n.user_group = user_group
3697 3702 n.permission = permission
3698 3703 Session().add(n)
3699 3704 return n
3700 3705
3701 3706 def __repr__(self):
3702 3707 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3703 3708
3704 3709
3705 3710 class UserGroupToPerm(Base, BaseModel):
3706 3711 __tablename__ = 'users_group_to_perm'
3707 3712 __table_args__ = (
3708 3713 UniqueConstraint('users_group_id', 'permission_id',),
3709 3714 base_table_args
3710 3715 )
3711 3716
3712 3717 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3713 3718 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3714 3719 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3715 3720
3716 3721 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3717 3722 permission = relationship('Permission')
3718 3723
3719 3724
3720 3725 class UserRepoGroupToPerm(Base, BaseModel):
3721 3726 __tablename__ = 'user_repo_group_to_perm'
3722 3727 __table_args__ = (
3723 3728 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3724 3729 base_table_args
3725 3730 )
3726 3731
3727 3732 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3728 3733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3729 3734 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3730 3735 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3731 3736
3732 3737 user = relationship('User', back_populates='repo_group_to_perm')
3733 3738 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3734 3739 permission = relationship('Permission')
3735 3740
3736 3741 @classmethod
3737 3742 def create(cls, user, repository_group, permission):
3738 3743 n = cls()
3739 3744 n.user = user
3740 3745 n.group = repository_group
3741 3746 n.permission = permission
3742 3747 Session().add(n)
3743 3748 return n
3744 3749
3745 3750
3746 3751 class UserGroupRepoGroupToPerm(Base, BaseModel):
3747 3752 __tablename__ = 'users_group_repo_group_to_perm'
3748 3753 __table_args__ = (
3749 3754 UniqueConstraint('users_group_id', 'group_id'),
3750 3755 base_table_args
3751 3756 )
3752 3757
3753 3758 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3754 3759 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3755 3760 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3756 3761 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3757 3762
3758 3763 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3759 3764 permission = relationship('Permission')
3760 3765 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3761 3766
3762 3767 @classmethod
3763 3768 def create(cls, user_group, repository_group, permission):
3764 3769 n = cls()
3765 3770 n.users_group = user_group
3766 3771 n.group = repository_group
3767 3772 n.permission = permission
3768 3773 Session().add(n)
3769 3774 return n
3770 3775
3771 3776 def __repr__(self):
3772 3777 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3773 3778
3774 3779
3775 3780 class Statistics(Base, BaseModel):
3776 3781 __tablename__ = 'statistics'
3777 3782 __table_args__ = (
3778 3783 base_table_args
3779 3784 )
3780 3785
3781 3786 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3782 3787 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3783 3788 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3784 3789 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3785 3790 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3786 3791 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3787 3792
3788 3793 repository = relationship('Repository', single_parent=True, viewonly=True)
3789 3794
3790 3795
3791 3796 class UserFollowing(Base, BaseModel):
3792 3797 __tablename__ = 'user_followings'
3793 3798 __table_args__ = (
3794 3799 UniqueConstraint('user_id', 'follows_repository_id'),
3795 3800 UniqueConstraint('user_id', 'follows_user_id'),
3796 3801 base_table_args
3797 3802 )
3798 3803
3799 3804 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3800 3805 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3801 3806 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3802 3807 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3803 3808 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3804 3809
3805 3810 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3806 3811
3807 3812 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3808 3813 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3809 3814
3810 3815 @classmethod
3811 3816 def get_repo_followers(cls, repo_id):
3812 3817 return cls.query().filter(cls.follows_repo_id == repo_id)
3813 3818
3814 3819
3815 3820 class CacheKey(Base, BaseModel):
3816 3821 __tablename__ = 'cache_invalidation'
3817 3822 __table_args__ = (
3818 3823 UniqueConstraint('cache_key'),
3819 3824 Index('key_idx', 'cache_key'),
3820 3825 Index('cache_args_idx', 'cache_args'),
3821 3826 base_table_args,
3822 3827 )
3823 3828
3824 3829 CACHE_TYPE_FEED = 'FEED'
3825 3830
3826 3831 # namespaces used to register process/thread aware caches
3827 3832 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3828 3833
3829 3834 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3830 3835 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3831 3836 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3832 3837 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3833 3838 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3834 3839
3835 3840 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3836 3841 self.cache_key = cache_key
3837 3842 self.cache_args = cache_args
3838 3843 self.cache_active = cache_active
3839 3844 # first key should be same for all entries, since all workers should share it
3840 3845 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3841 3846
3842 3847 def __repr__(self):
3843 3848 return "<%s('%s:%s[%s]')>" % (
3844 3849 self.cls_name,
3845 3850 self.cache_id, self.cache_key, self.cache_active)
3846 3851
3847 3852 def _cache_key_partition(self):
3848 3853 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3849 3854 return prefix, repo_name, suffix
3850 3855
3851 3856 def get_prefix(self):
3852 3857 """
3853 3858 Try to extract prefix from existing cache key. The key could consist
3854 3859 of prefix, repo_name, suffix
3855 3860 """
3856 3861 # this returns prefix, repo_name, suffix
3857 3862 return self._cache_key_partition()[0]
3858 3863
3859 3864 def get_suffix(self):
3860 3865 """
3861 3866 get suffix that might have been used in _get_cache_key to
3862 3867 generate self.cache_key. Only used for informational purposes
3863 3868 in repo_edit.mako.
3864 3869 """
3865 3870 # prefix, repo_name, suffix
3866 3871 return self._cache_key_partition()[2]
3867 3872
3868 3873 @classmethod
3869 3874 def generate_new_state_uid(cls, based_on=None):
3870 3875 if based_on:
3871 3876 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3872 3877 else:
3873 3878 return str(uuid.uuid4())
3874 3879
3875 3880 @classmethod
3876 3881 def delete_all_cache(cls):
3877 3882 """
3878 3883 Delete all cache keys from database.
3879 3884 Should only be run when all instances are down and all entries
3880 3885 thus stale.
3881 3886 """
3882 3887 cls.query().delete()
3883 3888 Session().commit()
3884 3889
3885 3890 @classmethod
3886 3891 def set_invalidate(cls, cache_uid, delete=False):
3887 3892 """
3888 3893 Mark all caches of a repo as invalid in the database.
3889 3894 """
3890 3895 try:
3891 3896 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3892 3897 if delete:
3893 3898 qry.delete()
3894 3899 log.debug('cache objects deleted for cache args %s',
3895 3900 safe_str(cache_uid))
3896 3901 else:
3897 3902 new_uid = cls.generate_new_state_uid()
3898 3903 qry.update({"cache_state_uid": new_uid,
3899 3904 "cache_args": f"repo_state:{time.time()}"})
3900 3905 log.debug('cache object %s set new UID %s',
3901 3906 safe_str(cache_uid), new_uid)
3902 3907
3903 3908 Session().commit()
3904 3909 except Exception:
3905 3910 log.exception(
3906 3911 'Cache key invalidation failed for cache args %s',
3907 3912 safe_str(cache_uid))
3908 3913 Session().rollback()
3909 3914
3910 3915 @classmethod
3911 3916 def get_active_cache(cls, cache_key):
3912 3917 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3913 3918 if inv_obj:
3914 3919 return inv_obj
3915 3920 return None
3916 3921
3917 3922 @classmethod
3918 3923 def get_namespace_map(cls, namespace):
3919 3924 return {
3920 3925 x.cache_key: x
3921 3926 for x in cls.query().filter(cls.cache_args == namespace)}
3922 3927
3923 3928
3924 3929 class ChangesetComment(Base, BaseModel):
3925 3930 __tablename__ = 'changeset_comments'
3926 3931 __table_args__ = (
3927 3932 Index('cc_revision_idx', 'revision'),
3928 3933 base_table_args,
3929 3934 )
3930 3935
3931 3936 COMMENT_OUTDATED = 'comment_outdated'
3932 3937 COMMENT_TYPE_NOTE = 'note'
3933 3938 COMMENT_TYPE_TODO = 'todo'
3934 3939 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3935 3940
3936 3941 OP_IMMUTABLE = 'immutable'
3937 3942 OP_CHANGEABLE = 'changeable'
3938 3943
3939 3944 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3940 3945 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3941 3946 revision = Column('revision', String(40), nullable=True)
3942 3947 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3943 3948 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3944 3949 line_no = Column('line_no', Unicode(10), nullable=True)
3945 3950 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3946 3951 f_path = Column('f_path', Unicode(1000), nullable=True)
3947 3952 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3948 3953 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3949 3954 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3950 3955 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3951 3956 renderer = Column('renderer', Unicode(64), nullable=True)
3952 3957 display_state = Column('display_state', Unicode(128), nullable=True)
3953 3958 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3954 3959 draft = Column('draft', Boolean(), nullable=True, default=False)
3955 3960
3956 3961 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3957 3962 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3958 3963
3959 3964 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3960 3965 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3961 3966
3962 3967 author = relationship('User', lazy='select', back_populates='user_comments')
3963 3968 repo = relationship('Repository', back_populates='comments')
3964 3969 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3965 3970 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3966 3971 pull_request_version = relationship('PullRequestVersion', lazy='select')
3967 3972 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3968 3973
3969 3974 @classmethod
3970 3975 def get_users(cls, revision=None, pull_request_id=None):
3971 3976 """
3972 3977 Returns user associated with this ChangesetComment. ie those
3973 3978 who actually commented
3974 3979
3975 3980 :param cls:
3976 3981 :param revision:
3977 3982 """
3978 3983 q = Session().query(User).join(ChangesetComment.author)
3979 3984 if revision:
3980 3985 q = q.filter(cls.revision == revision)
3981 3986 elif pull_request_id:
3982 3987 q = q.filter(cls.pull_request_id == pull_request_id)
3983 3988 return q.all()
3984 3989
3985 3990 @classmethod
3986 3991 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3987 3992 if pr_version is None:
3988 3993 return 0
3989 3994
3990 3995 if versions is not None:
3991 3996 num_versions = [x.pull_request_version_id for x in versions]
3992 3997
3993 3998 num_versions = num_versions or []
3994 3999 try:
3995 4000 return num_versions.index(pr_version) + 1
3996 4001 except (IndexError, ValueError):
3997 4002 return 0
3998 4003
3999 4004 @property
4000 4005 def outdated(self):
4001 4006 return self.display_state == self.COMMENT_OUTDATED
4002 4007
4003 4008 @property
4004 4009 def outdated_js(self):
4005 4010 return str_json(self.display_state == self.COMMENT_OUTDATED)
4006 4011
4007 4012 @property
4008 4013 def immutable(self):
4009 4014 return self.immutable_state == self.OP_IMMUTABLE
4010 4015
4011 4016 def outdated_at_version(self, version: int) -> bool:
4012 4017 """
4013 4018 Checks if comment is outdated for given pull request version
4014 4019 """
4015 4020
4016 4021 def version_check():
4017 4022 return self.pull_request_version_id and self.pull_request_version_id != version
4018 4023
4019 4024 if self.is_inline:
4020 4025 return self.outdated and version_check()
4021 4026 else:
4022 4027 # general comments don't have .outdated set, also latest don't have a version
4023 4028 return version_check()
4024 4029
4025 4030 def outdated_at_version_js(self, version):
4026 4031 """
4027 4032 Checks if comment is outdated for given pull request version
4028 4033 """
4029 4034 return str_json(self.outdated_at_version(version))
4030 4035
4031 4036 def older_than_version(self, version: int) -> bool:
4032 4037 """
4033 4038 Checks if comment is made from a previous version than given.
4034 4039 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
4035 4040 """
4036 4041
4037 4042 # If version is None, return False as the current version cannot be less than None
4038 4043 if version is None:
4039 4044 return False
4040 4045
4041 4046 # Ensure that the version is an integer to prevent TypeError on comparison
4042 4047 if not isinstance(version, int):
4043 4048 raise ValueError("The provided version must be an integer.")
4044 4049
4045 4050 # Initialize current version to 0 or pull_request_version_id if it's available
4046 4051 cur_ver = 0
4047 4052 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
4048 4053 cur_ver = self.pull_request_version.pull_request_version_id
4049 4054
4050 4055 # Return True if the current version is less than the given version
4051 4056 return cur_ver < version
4052 4057
4053 4058 def older_than_version_js(self, version):
4054 4059 """
4055 4060 Checks if comment is made from previous version than given
4056 4061 """
4057 4062 return str_json(self.older_than_version(version))
4058 4063
4059 4064 @property
4060 4065 def commit_id(self):
4061 4066 """New style naming to stop using .revision"""
4062 4067 return self.revision
4063 4068
4064 4069 @property
4065 4070 def resolved(self):
4066 4071 return self.resolved_by[0] if self.resolved_by else None
4067 4072
4068 4073 @property
4069 4074 def is_todo(self):
4070 4075 return self.comment_type == self.COMMENT_TYPE_TODO
4071 4076
4072 4077 @property
4073 4078 def is_inline(self):
4074 4079 if self.line_no and self.f_path:
4075 4080 return True
4076 4081 return False
4077 4082
4078 4083 @property
4079 4084 def last_version(self):
4080 4085 version = 0
4081 4086 if self.history:
4082 4087 version = self.history[-1].version
4083 4088 return version
4084 4089
4085 4090 def get_index_version(self, versions):
4086 4091 return self.get_index_from_version(
4087 4092 self.pull_request_version_id, versions)
4088 4093
4089 4094 @property
4090 4095 def review_status(self):
4091 4096 if self.status_change:
4092 4097 return self.status_change[0].status
4093 4098
4094 4099 @property
4095 4100 def review_status_lbl(self):
4096 4101 if self.status_change:
4097 4102 return self.status_change[0].status_lbl
4098 4103
4099 4104 def __repr__(self):
4100 4105 if self.comment_id:
4101 4106 return f'<DB:Comment #{self.comment_id}>'
4102 4107 else:
4103 4108 return f'<DB:Comment at {id(self)!r}>'
4104 4109
4105 4110 def get_api_data(self):
4106 4111 comment = self
4107 4112
4108 4113 data = {
4109 4114 'comment_id': comment.comment_id,
4110 4115 'comment_type': comment.comment_type,
4111 4116 'comment_text': comment.text,
4112 4117 'comment_status': comment.status_change,
4113 4118 'comment_f_path': comment.f_path,
4114 4119 'comment_lineno': comment.line_no,
4115 4120 'comment_author': comment.author,
4116 4121 'comment_created_on': comment.created_on,
4117 4122 'comment_resolved_by': self.resolved,
4118 4123 'comment_commit_id': comment.revision,
4119 4124 'comment_pull_request_id': comment.pull_request_id,
4120 4125 'comment_last_version': self.last_version
4121 4126 }
4122 4127 return data
4123 4128
4124 4129 def __json__(self):
4125 4130 data = dict()
4126 4131 data.update(self.get_api_data())
4127 4132 return data
4128 4133
4129 4134
4130 4135 class ChangesetCommentHistory(Base, BaseModel):
4131 4136 __tablename__ = 'changeset_comments_history'
4132 4137 __table_args__ = (
4133 4138 Index('cch_comment_id_idx', 'comment_id'),
4134 4139 base_table_args,
4135 4140 )
4136 4141
4137 4142 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4138 4143 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4139 4144 version = Column("version", Integer(), nullable=False, default=0)
4140 4145 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4141 4146 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4142 4147 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4143 4148 deleted = Column('deleted', Boolean(), default=False)
4144 4149
4145 4150 author = relationship('User', lazy='joined')
4146 4151 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4147 4152
4148 4153 @classmethod
4149 4154 def get_version(cls, comment_id):
4150 4155 q = Session().query(ChangesetCommentHistory).filter(
4151 4156 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4152 4157 if q.count() == 0:
4153 4158 return 1
4154 4159 elif q.count() >= q[0].version:
4155 4160 return q.count() + 1
4156 4161 else:
4157 4162 return q[0].version + 1
4158 4163
4159 4164
4160 4165 class ChangesetStatus(Base, BaseModel):
4161 4166 __tablename__ = 'changeset_statuses'
4162 4167 __table_args__ = (
4163 4168 Index('cs_revision_idx', 'revision'),
4164 4169 Index('cs_version_idx', 'version'),
4165 4170 UniqueConstraint('repo_id', 'revision', 'version'),
4166 4171 base_table_args
4167 4172 )
4168 4173
4169 4174 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4170 4175 STATUS_APPROVED = 'approved'
4171 4176 STATUS_REJECTED = 'rejected'
4172 4177 STATUS_UNDER_REVIEW = 'under_review'
4173 4178
4174 4179 STATUSES = [
4175 4180 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4176 4181 (STATUS_APPROVED, _("Approved")),
4177 4182 (STATUS_REJECTED, _("Rejected")),
4178 4183 (STATUS_UNDER_REVIEW, _("Under Review")),
4179 4184 ]
4180 4185
4181 4186 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4182 4187 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4183 4188 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4184 4189 revision = Column('revision', String(40), nullable=False)
4185 4190 status = Column('status', String(128), nullable=False, default=DEFAULT)
4186 4191 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4187 4192 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4188 4193 version = Column('version', Integer(), nullable=False, default=0)
4189 4194 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4190 4195
4191 4196 author = relationship('User', lazy='select')
4192 4197 repo = relationship('Repository', lazy='select')
4193 4198 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4194 4199 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4195 4200
4196 4201 def __repr__(self):
4197 4202 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4198 4203
4199 4204 @classmethod
4200 4205 def get_status_lbl(cls, value):
4201 4206 return dict(cls.STATUSES).get(value)
4202 4207
4203 4208 @property
4204 4209 def status_lbl(self):
4205 4210 return ChangesetStatus.get_status_lbl(self.status)
4206 4211
4207 4212 def get_api_data(self):
4208 4213 status = self
4209 4214 data = {
4210 4215 'status_id': status.changeset_status_id,
4211 4216 'status': status.status,
4212 4217 }
4213 4218 return data
4214 4219
4215 4220 def __json__(self):
4216 4221 data = dict()
4217 4222 data.update(self.get_api_data())
4218 4223 return data
4219 4224
4220 4225
4221 4226 class _SetState(object):
4222 4227 """
4223 4228 Context processor allowing changing state for sensitive operation such as
4224 4229 pull request update or merge
4225 4230 """
4226 4231
4227 4232 def __init__(self, pull_request, pr_state, back_state=None):
4228 4233 self._pr = pull_request
4229 4234 self._org_state = back_state or pull_request.pull_request_state
4230 4235 self._pr_state = pr_state
4231 4236 self._current_state = None
4232 4237
4233 4238 def __enter__(self):
4234 4239 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4235 4240 self._pr, self._pr_state)
4236 4241 self.set_pr_state(self._pr_state)
4237 4242 return self
4238 4243
4239 4244 def __exit__(self, exc_type, exc_val, exc_tb):
4240 4245 if exc_val is not None or exc_type is not None:
4241 4246 log.error(traceback.format_tb(exc_tb))
4242 4247 return None
4243 4248
4244 4249 self.set_pr_state(self._org_state)
4245 4250 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4246 4251 self._pr, self._org_state)
4247 4252
4248 4253 @property
4249 4254 def state(self):
4250 4255 return self._current_state
4251 4256
4252 4257 def set_pr_state(self, pr_state):
4253 4258 try:
4254 4259 self._pr.pull_request_state = pr_state
4255 4260 Session().add(self._pr)
4256 4261 Session().commit()
4257 4262 self._current_state = pr_state
4258 4263 except Exception:
4259 4264 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4260 4265 raise
4261 4266
4262 4267
4263 4268 class _PullRequestBase(BaseModel):
4264 4269 """
4265 4270 Common attributes of pull request and version entries.
4266 4271 """
4267 4272
4268 4273 # .status values
4269 4274 STATUS_NEW = 'new'
4270 4275 STATUS_OPEN = 'open'
4271 4276 STATUS_CLOSED = 'closed'
4272 4277
4273 4278 # available states
4274 4279 STATE_CREATING = 'creating'
4275 4280 STATE_UPDATING = 'updating'
4276 4281 STATE_MERGING = 'merging'
4277 4282 STATE_CREATED = 'created'
4278 4283
4279 4284 title = Column('title', Unicode(255), nullable=True)
4280 4285 description = Column(
4281 4286 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4282 4287 nullable=True)
4283 4288 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4284 4289
4285 4290 # new/open/closed status of pull request (not approve/reject/etc)
4286 4291 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4287 4292 created_on = Column(
4288 4293 'created_on', DateTime(timezone=False), nullable=False,
4289 4294 default=datetime.datetime.now)
4290 4295 updated_on = Column(
4291 4296 'updated_on', DateTime(timezone=False), nullable=False,
4292 4297 default=datetime.datetime.now)
4293 4298
4294 4299 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4295 4300
4296 4301 @declared_attr
4297 4302 def user_id(cls):
4298 4303 return Column(
4299 4304 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4300 4305 unique=None)
4301 4306
4302 4307 # 500 revisions max
4303 4308 _revisions = Column(
4304 4309 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4305 4310
4306 4311 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4307 4312
4308 4313 @declared_attr
4309 4314 def source_repo_id(cls):
4310 4315 # TODO: dan: rename column to source_repo_id
4311 4316 return Column(
4312 4317 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4313 4318 nullable=False)
4314 4319
4315 4320 @declared_attr
4316 4321 def pr_source(cls):
4317 4322 return relationship(
4318 4323 'Repository',
4319 4324 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4320 4325 overlaps="pull_requests_source"
4321 4326 )
4322 4327
4323 4328 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4324 4329
4325 4330 @hybrid_property
4326 4331 def source_ref(self):
4327 4332 return self._source_ref
4328 4333
4329 4334 @source_ref.setter
4330 4335 def source_ref(self, val):
4331 4336 parts = (val or '').split(':')
4332 4337 if len(parts) != 3:
4333 4338 raise ValueError(
4334 4339 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4335 4340 self._source_ref = safe_str(val)
4336 4341
4337 4342 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4338 4343
4339 4344 @hybrid_property
4340 4345 def target_ref(self):
4341 4346 return self._target_ref
4342 4347
4343 4348 @target_ref.setter
4344 4349 def target_ref(self, val):
4345 4350 parts = (val or '').split(':')
4346 4351 if len(parts) != 3:
4347 4352 raise ValueError(
4348 4353 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4349 4354 self._target_ref = safe_str(val)
4350 4355
4351 4356 @declared_attr
4352 4357 def target_repo_id(cls):
4353 4358 # TODO: dan: rename column to target_repo_id
4354 4359 return Column(
4355 4360 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4356 4361 nullable=False)
4357 4362
4358 4363 @declared_attr
4359 4364 def pr_target(cls):
4360 4365 return relationship(
4361 4366 'Repository',
4362 4367 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4363 4368 overlaps="pull_requests_target"
4364 4369 )
4365 4370
4366 4371 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4367 4372
4368 4373 # TODO: dan: rename column to last_merge_source_rev
4369 4374 _last_merge_source_rev = Column(
4370 4375 'last_merge_org_rev', String(40), nullable=True)
4371 4376 # TODO: dan: rename column to last_merge_target_rev
4372 4377 _last_merge_target_rev = Column(
4373 4378 'last_merge_other_rev', String(40), nullable=True)
4374 4379 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4375 4380 last_merge_metadata = Column(
4376 4381 'last_merge_metadata', MutationObj.as_mutable(
4377 4382 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4378 4383
4379 4384 merge_rev = Column('merge_rev', String(40), nullable=True)
4380 4385
4381 4386 reviewer_data = Column(
4382 4387 'reviewer_data_json', MutationObj.as_mutable(
4383 4388 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4384 4389
4385 4390 @property
4386 4391 def reviewer_data_json(self):
4387 4392 return str_json(self.reviewer_data)
4388 4393
4389 4394 @property
4390 4395 def last_merge_metadata_parsed(self):
4391 4396 metadata = {}
4392 4397 if not self.last_merge_metadata:
4393 4398 return metadata
4394 4399
4395 4400 if hasattr(self.last_merge_metadata, 'de_coerce'):
4396 4401 for k, v in self.last_merge_metadata.de_coerce().items():
4397 4402 if k in ['target_ref', 'source_ref']:
4398 4403 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4399 4404 else:
4400 4405 if hasattr(v, 'de_coerce'):
4401 4406 metadata[k] = v.de_coerce()
4402 4407 else:
4403 4408 metadata[k] = v
4404 4409 return metadata
4405 4410
4406 4411 @property
4407 4412 def work_in_progress(self):
4408 4413 """checks if pull request is work in progress by checking the title"""
4409 4414 title = self.title.upper()
4410 4415 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4411 4416 return True
4412 4417 return False
4413 4418
4414 4419 @property
4415 4420 def title_safe(self):
4416 4421 return self.title\
4417 4422 .replace('{', '{{')\
4418 4423 .replace('}', '}}')
4419 4424
4420 4425 @hybrid_property
4421 4426 def description_safe(self):
4422 4427 from rhodecode.lib import helpers as h
4423 4428 return h.escape(self.description)
4424 4429
4425 4430 @hybrid_property
4426 4431 def revisions(self):
4427 4432 return self._revisions.split(':') if self._revisions else []
4428 4433
4429 4434 @revisions.setter
4430 4435 def revisions(self, val):
4431 4436 self._revisions = ':'.join(val)
4432 4437
4433 4438 @hybrid_property
4434 4439 def last_merge_status(self):
4435 4440 return safe_int(self._last_merge_status)
4436 4441
4437 4442 @last_merge_status.setter
4438 4443 def last_merge_status(self, val):
4439 4444 self._last_merge_status = val
4440 4445
4441 4446 @declared_attr
4442 4447 def author(cls):
4443 4448 return relationship(
4444 4449 'User', lazy='joined',
4445 4450 #TODO, problem that is somehow :?
4446 4451 #back_populates='user_pull_requests'
4447 4452 )
4448 4453
4449 4454 @declared_attr
4450 4455 def source_repo(cls):
4451 4456 return relationship(
4452 4457 'Repository',
4453 4458 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4454 4459 overlaps="pr_source"
4455 4460 )
4456 4461
4457 4462 @property
4458 4463 def source_ref_parts(self):
4459 4464 return self.unicode_to_reference(self.source_ref)
4460 4465
4461 4466 @declared_attr
4462 4467 def target_repo(cls):
4463 4468 return relationship(
4464 4469 'Repository',
4465 4470 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4466 4471 overlaps="pr_target"
4467 4472 )
4468 4473
4469 4474 @property
4470 4475 def target_ref_parts(self):
4471 4476 return self.unicode_to_reference(self.target_ref)
4472 4477
4473 4478 @property
4474 4479 def shadow_merge_ref(self):
4475 4480 return self.unicode_to_reference(self._shadow_merge_ref)
4476 4481
4477 4482 @shadow_merge_ref.setter
4478 4483 def shadow_merge_ref(self, ref):
4479 4484 self._shadow_merge_ref = self.reference_to_unicode(ref)
4480 4485
4481 4486 @staticmethod
4482 4487 def unicode_to_reference(raw):
4483 4488 return unicode_to_reference(raw)
4484 4489
4485 4490 @staticmethod
4486 4491 def reference_to_unicode(ref):
4487 4492 return reference_to_unicode(ref)
4488 4493
4489 4494 def get_api_data(self, with_merge_state=True):
4490 4495 from rhodecode.model.pull_request import PullRequestModel
4491 4496
4492 4497 pull_request = self
4493 4498 if with_merge_state:
4494 4499 merge_response, merge_status, msg = \
4495 4500 PullRequestModel().merge_status(pull_request)
4496 4501 merge_state = {
4497 4502 'status': merge_status,
4498 4503 'message': safe_str(msg),
4499 4504 }
4500 4505 else:
4501 4506 merge_state = {'status': 'not_available',
4502 4507 'message': 'not_available'}
4503 4508
4504 4509 merge_data = {
4505 4510 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4506 4511 'reference': (
4507 4512 pull_request.shadow_merge_ref.asdict()
4508 4513 if pull_request.shadow_merge_ref else None),
4509 4514 }
4510 4515
4511 4516 data = {
4512 4517 'pull_request_id': pull_request.pull_request_id,
4513 4518 'url': PullRequestModel().get_url(pull_request),
4514 4519 'title': pull_request.title,
4515 4520 'description': pull_request.description,
4516 4521 'status': pull_request.status,
4517 4522 'state': pull_request.pull_request_state,
4518 4523 'created_on': pull_request.created_on,
4519 4524 'updated_on': pull_request.updated_on,
4520 4525 'commit_ids': pull_request.revisions,
4521 4526 'review_status': pull_request.calculated_review_status(),
4522 4527 'mergeable': merge_state,
4523 4528 'source': {
4524 4529 'clone_url': pull_request.source_repo.clone_url(),
4525 4530 'repository': pull_request.source_repo.repo_name,
4526 4531 'reference': {
4527 4532 'name': pull_request.source_ref_parts.name,
4528 4533 'type': pull_request.source_ref_parts.type,
4529 4534 'commit_id': pull_request.source_ref_parts.commit_id,
4530 4535 },
4531 4536 },
4532 4537 'target': {
4533 4538 'clone_url': pull_request.target_repo.clone_url(),
4534 4539 'repository': pull_request.target_repo.repo_name,
4535 4540 'reference': {
4536 4541 'name': pull_request.target_ref_parts.name,
4537 4542 'type': pull_request.target_ref_parts.type,
4538 4543 'commit_id': pull_request.target_ref_parts.commit_id,
4539 4544 },
4540 4545 },
4541 4546 'merge': merge_data,
4542 4547 'author': pull_request.author.get_api_data(include_secrets=False,
4543 4548 details='basic'),
4544 4549 'reviewers': [
4545 4550 {
4546 4551 'user': reviewer.get_api_data(include_secrets=False,
4547 4552 details='basic'),
4548 4553 'reasons': reasons,
4549 4554 'review_status': st[0][1].status if st else 'not_reviewed',
4550 4555 }
4551 4556 for obj, reviewer, reasons, mandatory, st in
4552 4557 pull_request.reviewers_statuses()
4553 4558 ]
4554 4559 }
4555 4560
4556 4561 return data
4557 4562
4558 4563 def set_state(self, pull_request_state, final_state=None):
4559 4564 """
4560 4565 # goes from initial state to updating to initial state.
4561 4566 # initial state can be changed by specifying back_state=
4562 4567 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4563 4568 pull_request.merge()
4564 4569
4565 4570 :param pull_request_state:
4566 4571 :param final_state:
4567 4572
4568 4573 """
4569 4574
4570 4575 return _SetState(self, pull_request_state, back_state=final_state)
4571 4576
4572 4577
4573 4578 class PullRequest(Base, _PullRequestBase):
4574 4579 __tablename__ = 'pull_requests'
4575 4580 __table_args__ = (
4576 4581 base_table_args,
4577 4582 )
4578 4583 LATEST_VER = 'latest'
4579 4584
4580 4585 pull_request_id = Column(
4581 4586 'pull_request_id', Integer(), nullable=False, primary_key=True)
4582 4587
4583 4588 def __repr__(self):
4584 4589 if self.pull_request_id:
4585 4590 return f'<DB:PullRequest #{self.pull_request_id}>'
4586 4591 else:
4587 4592 return f'<DB:PullRequest at {id(self)!r}>'
4588 4593
4589 4594 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4590 4595 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4591 4596 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4592 4597 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4593 4598
4594 4599 @classmethod
4595 4600 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4596 4601 internal_methods=None):
4597 4602
4598 4603 class PullRequestDisplay(object):
4599 4604 """
4600 4605 Special object wrapper for showing PullRequest data via Versions
4601 4606 It mimics PR object as close as possible. This is read only object
4602 4607 just for display
4603 4608 """
4604 4609
4605 4610 def __init__(self, attrs, internal=None):
4606 4611 self.attrs = attrs
4607 4612 # internal have priority over the given ones via attrs
4608 4613 self.internal = internal or ['versions']
4609 4614
4610 4615 def __getattr__(self, item):
4611 4616 if item in self.internal:
4612 4617 return getattr(self, item)
4613 4618 try:
4614 4619 return self.attrs[item]
4615 4620 except KeyError:
4616 4621 raise AttributeError(
4617 4622 '%s object has no attribute %s' % (self, item))
4618 4623
4619 4624 def __repr__(self):
4620 4625 pr_id = self.attrs.get('pull_request_id')
4621 4626 return f'<DB:PullRequestDisplay #{pr_id}>'
4622 4627
4623 4628 def versions(self):
4624 4629 return pull_request_obj.versions.order_by(
4625 4630 PullRequestVersion.pull_request_version_id).all()
4626 4631
4627 4632 def is_closed(self):
4628 4633 return pull_request_obj.is_closed()
4629 4634
4630 4635 def is_state_changing(self):
4631 4636 return pull_request_obj.is_state_changing()
4632 4637
4633 4638 @property
4634 4639 def pull_request_version_id(self):
4635 4640 return getattr(pull_request_obj, 'pull_request_version_id', None)
4636 4641
4637 4642 @property
4638 4643 def pull_request_last_version(self):
4639 4644 return pull_request_obj.pull_request_last_version
4640 4645
4641 4646 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4642 4647
4643 4648 attrs.author = StrictAttributeDict(
4644 4649 pull_request_obj.author.get_api_data())
4645 4650 if pull_request_obj.target_repo:
4646 4651 attrs.target_repo = StrictAttributeDict(
4647 4652 pull_request_obj.target_repo.get_api_data())
4648 4653 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4649 4654
4650 4655 if pull_request_obj.source_repo:
4651 4656 attrs.source_repo = StrictAttributeDict(
4652 4657 pull_request_obj.source_repo.get_api_data())
4653 4658 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4654 4659
4655 4660 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4656 4661 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4657 4662 attrs.revisions = pull_request_obj.revisions
4658 4663 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4659 4664 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4660 4665 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4661 4666 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4662 4667
4663 4668 return PullRequestDisplay(attrs, internal=internal_methods)
4664 4669
4665 4670 def is_closed(self):
4666 4671 return self.status == self.STATUS_CLOSED
4667 4672
4668 4673 def is_state_changing(self):
4669 4674 return self.pull_request_state != PullRequest.STATE_CREATED
4670 4675
4671 4676 def __json__(self):
4672 4677 return {
4673 4678 'revisions': self.revisions,
4674 4679 'versions': self.versions_count
4675 4680 }
4676 4681
4677 4682 def calculated_review_status(self):
4678 4683 from rhodecode.model.changeset_status import ChangesetStatusModel
4679 4684 return ChangesetStatusModel().calculated_review_status(self)
4680 4685
4681 4686 def reviewers_statuses(self, user=None):
4682 4687 from rhodecode.model.changeset_status import ChangesetStatusModel
4683 4688 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4684 4689
4685 4690 def get_pull_request_reviewers(self, role=None):
4686 4691 qry = PullRequestReviewers.query()\
4687 4692 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4688 4693 if role:
4689 4694 qry = qry.filter(PullRequestReviewers.role == role)
4690 4695
4691 4696 return qry.all()
4692 4697
4693 4698 @property
4694 4699 def reviewers_count(self):
4695 4700 qry = PullRequestReviewers.query()\
4696 4701 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4697 4702 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4698 4703 return qry.count()
4699 4704
4700 4705 @property
4701 4706 def observers_count(self):
4702 4707 qry = PullRequestReviewers.query()\
4703 4708 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4704 4709 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4705 4710 return qry.count()
4706 4711
4707 4712 def observers(self):
4708 4713 qry = PullRequestReviewers.query()\
4709 4714 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4710 4715 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4711 4716 .all()
4712 4717
4713 4718 for entry in qry:
4714 4719 yield entry, entry.user
4715 4720
4716 4721 @property
4717 4722 def workspace_id(self):
4718 4723 from rhodecode.model.pull_request import PullRequestModel
4719 4724 return PullRequestModel()._workspace_id(self)
4720 4725
4721 4726 def get_shadow_repo(self):
4722 4727 workspace_id = self.workspace_id
4723 4728 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4724 4729 if os.path.isdir(shadow_repository_path):
4725 4730 vcs_obj = self.target_repo.scm_instance()
4726 4731 return vcs_obj.get_shadow_instance(shadow_repository_path)
4727 4732
4728 4733 @property
4729 4734 def versions_count(self):
4730 4735 """
4731 4736 return number of versions this PR have, e.g a PR that once been
4732 4737 updated will have 2 versions
4733 4738 """
4734 4739 return self.versions.count() + 1
4735 4740
4736 4741 @property
4737 4742 def pull_request_last_version(self):
4738 4743 return self.versions_count
4739 4744
4740 4745
4741 4746 class PullRequestVersion(Base, _PullRequestBase):
4742 4747 __tablename__ = 'pull_request_versions'
4743 4748 __table_args__ = (
4744 4749 base_table_args,
4745 4750 )
4746 4751
4747 4752 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4748 4753 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4749 4754 pull_request = relationship('PullRequest', back_populates='versions')
4750 4755
4751 4756 def __repr__(self):
4752 4757 if self.pull_request_version_id:
4753 4758 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4754 4759 else:
4755 4760 return f'<DB:PullRequestVersion at {id(self)!r}>'
4756 4761
4757 4762 @property
4758 4763 def reviewers(self):
4759 4764 return self.pull_request.reviewers
4760 4765
4761 4766 @property
4762 4767 def versions(self):
4763 4768 return self.pull_request.versions
4764 4769
4765 4770 def is_closed(self):
4766 4771 # calculate from original
4767 4772 return self.pull_request.status == self.STATUS_CLOSED
4768 4773
4769 4774 def is_state_changing(self):
4770 4775 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4771 4776
4772 4777 def calculated_review_status(self):
4773 4778 return self.pull_request.calculated_review_status()
4774 4779
4775 4780 def reviewers_statuses(self):
4776 4781 return self.pull_request.reviewers_statuses()
4777 4782
4778 4783 def observers(self):
4779 4784 return self.pull_request.observers()
4780 4785
4781 4786
4782 4787 class PullRequestReviewers(Base, BaseModel):
4783 4788 __tablename__ = 'pull_request_reviewers'
4784 4789 __table_args__ = (
4785 4790 base_table_args,
4786 4791 )
4787 4792 ROLE_REVIEWER = 'reviewer'
4788 4793 ROLE_OBSERVER = 'observer'
4789 4794 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4790 4795
4791 4796 @hybrid_property
4792 4797 def reasons(self):
4793 4798 if not self._reasons:
4794 4799 return []
4795 4800 return self._reasons
4796 4801
4797 4802 @reasons.setter
4798 4803 def reasons(self, val):
4799 4804 val = val or []
4800 4805 if any(not isinstance(x, str) for x in val):
4801 4806 raise Exception('invalid reasons type, must be list of strings')
4802 4807 self._reasons = val
4803 4808
4804 4809 pull_requests_reviewers_id = Column(
4805 4810 'pull_requests_reviewers_id', Integer(), nullable=False,
4806 4811 primary_key=True)
4807 4812 pull_request_id = Column(
4808 4813 "pull_request_id", Integer(),
4809 4814 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4810 4815 user_id = Column(
4811 4816 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4812 4817 _reasons = Column(
4813 4818 'reason', MutationList.as_mutable(
4814 4819 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4815 4820
4816 4821 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4817 4822 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4818 4823
4819 4824 user = relationship('User')
4820 4825 pull_request = relationship('PullRequest', back_populates='reviewers')
4821 4826
4822 4827 rule_data = Column(
4823 4828 'rule_data_json',
4824 4829 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4825 4830
4826 4831 def rule_user_group_data(self):
4827 4832 """
4828 4833 Returns the voting user group rule data for this reviewer
4829 4834 """
4830 4835
4831 4836 if self.rule_data and 'vote_rule' in self.rule_data:
4832 4837 user_group_data = {}
4833 4838 if 'rule_user_group_entry_id' in self.rule_data:
4834 4839 # means a group with voting rules !
4835 4840 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4836 4841 user_group_data['name'] = self.rule_data['rule_name']
4837 4842 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4838 4843
4839 4844 return user_group_data
4840 4845
4841 4846 @classmethod
4842 4847 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4843 4848 qry = PullRequestReviewers.query()\
4844 4849 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4845 4850 if role:
4846 4851 qry = qry.filter(PullRequestReviewers.role == role)
4847 4852
4848 4853 return qry.all()
4849 4854
4850 4855 def __repr__(self):
4851 4856 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4852 4857
4853 4858
4854 4859 class Notification(Base, BaseModel):
4855 4860 __tablename__ = 'notifications'
4856 4861 __table_args__ = (
4857 4862 Index('notification_type_idx', 'type'),
4858 4863 base_table_args,
4859 4864 )
4860 4865
4861 4866 TYPE_CHANGESET_COMMENT = 'cs_comment'
4862 4867 TYPE_MESSAGE = 'message'
4863 4868 TYPE_MENTION = 'mention'
4864 4869 TYPE_REGISTRATION = 'registration'
4865 4870 TYPE_PULL_REQUEST = 'pull_request'
4866 4871 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4867 4872 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4868 4873
4869 4874 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4870 4875 subject = Column('subject', Unicode(512), nullable=True)
4871 4876 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4872 4877 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4873 4878 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4874 4879 type_ = Column('type', Unicode(255))
4875 4880
4876 4881 created_by_user = relationship('User', back_populates='user_created_notifications')
4877 4882 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4878 4883
4879 4884 @property
4880 4885 def recipients(self):
4881 4886 return [x.user for x in UserNotification.query()\
4882 4887 .filter(UserNotification.notification == self)\
4883 4888 .order_by(UserNotification.user_id.asc()).all()]
4884 4889
4885 4890 @classmethod
4886 4891 def create(cls, created_by, subject, body, recipients, type_=None):
4887 4892 if type_ is None:
4888 4893 type_ = Notification.TYPE_MESSAGE
4889 4894
4890 4895 notification = cls()
4891 4896 notification.created_by_user = created_by
4892 4897 notification.subject = subject
4893 4898 notification.body = body
4894 4899 notification.type_ = type_
4895 4900 notification.created_on = datetime.datetime.now()
4896 4901
4897 4902 # For each recipient link the created notification to his account
4898 4903 for u in recipients:
4899 4904 assoc = UserNotification()
4900 4905 assoc.user_id = u.user_id
4901 4906 assoc.notification = notification
4902 4907
4903 4908 # if created_by is inside recipients mark his notification
4904 4909 # as read
4905 4910 if u.user_id == created_by.user_id:
4906 4911 assoc.read = True
4907 4912 Session().add(assoc)
4908 4913
4909 4914 Session().add(notification)
4910 4915
4911 4916 return notification
4912 4917
4913 4918
4914 4919 class UserNotification(Base, BaseModel):
4915 4920 __tablename__ = 'user_to_notification'
4916 4921 __table_args__ = (
4917 4922 UniqueConstraint('user_id', 'notification_id'),
4918 4923 base_table_args
4919 4924 )
4920 4925
4921 4926 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4922 4927 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4923 4928 read = Column('read', Boolean, default=False)
4924 4929 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4925 4930
4926 4931 user = relationship('User', lazy="joined", back_populates='notifications')
4927 4932 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4928 4933
4929 4934 def mark_as_read(self):
4930 4935 self.read = True
4931 4936 Session().add(self)
4932 4937
4933 4938
4934 4939 class UserNotice(Base, BaseModel):
4935 4940 __tablename__ = 'user_notices'
4936 4941 __table_args__ = (
4937 4942 base_table_args
4938 4943 )
4939 4944
4940 4945 NOTIFICATION_TYPE_MESSAGE = 'message'
4941 4946 NOTIFICATION_TYPE_NOTICE = 'notice'
4942 4947
4943 4948 NOTIFICATION_LEVEL_INFO = 'info'
4944 4949 NOTIFICATION_LEVEL_WARNING = 'warning'
4945 4950 NOTIFICATION_LEVEL_ERROR = 'error'
4946 4951
4947 4952 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4948 4953
4949 4954 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4950 4955 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4951 4956
4952 4957 notice_read = Column('notice_read', Boolean, default=False)
4953 4958
4954 4959 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4955 4960 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4956 4961
4957 4962 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4958 4963 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4959 4964
4960 4965 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4961 4966 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4962 4967
4963 4968 @classmethod
4964 4969 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4965 4970
4966 4971 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4967 4972 cls.NOTIFICATION_LEVEL_WARNING,
4968 4973 cls.NOTIFICATION_LEVEL_INFO]:
4969 4974 return
4970 4975
4971 4976 from rhodecode.model.user import UserModel
4972 4977 user = UserModel().get_user(user)
4973 4978
4974 4979 new_notice = UserNotice()
4975 4980 if not allow_duplicate:
4976 4981 existing_msg = UserNotice().query() \
4977 4982 .filter(UserNotice.user == user) \
4978 4983 .filter(UserNotice.notice_body == body) \
4979 4984 .filter(UserNotice.notice_read == false()) \
4980 4985 .scalar()
4981 4986 if existing_msg:
4982 4987 log.warning('Ignoring duplicate notice for user %s', user)
4983 4988 return
4984 4989
4985 4990 new_notice.user = user
4986 4991 new_notice.notice_subject = subject
4987 4992 new_notice.notice_body = body
4988 4993 new_notice.notification_level = notice_level
4989 4994 Session().add(new_notice)
4990 4995 Session().commit()
4991 4996
4992 4997
4993 4998 class Gist(Base, BaseModel):
4994 4999 __tablename__ = 'gists'
4995 5000 __table_args__ = (
4996 5001 Index('g_gist_access_id_idx', 'gist_access_id'),
4997 5002 Index('g_created_on_idx', 'created_on'),
4998 5003 base_table_args
4999 5004 )
5000 5005
5001 5006 GIST_PUBLIC = 'public'
5002 5007 GIST_PRIVATE = 'private'
5003 5008 DEFAULT_FILENAME = 'gistfile1.txt'
5004 5009
5005 5010 ACL_LEVEL_PUBLIC = 'acl_public'
5006 5011 ACL_LEVEL_PRIVATE = 'acl_private'
5007 5012
5008 5013 gist_id = Column('gist_id', Integer(), primary_key=True)
5009 5014 gist_access_id = Column('gist_access_id', Unicode(250))
5010 5015 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
5011 5016 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
5012 5017 gist_expires = Column('gist_expires', Float(53), nullable=False)
5013 5018 gist_type = Column('gist_type', Unicode(128), nullable=False)
5014 5019 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5015 5020 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5016 5021 acl_level = Column('acl_level', Unicode(128), nullable=True)
5017 5022
5018 5023 owner = relationship('User', back_populates='user_gists')
5019 5024
5020 5025 def __repr__(self):
5021 5026 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
5022 5027
5023 5028 @hybrid_property
5024 5029 def description_safe(self):
5025 5030 from rhodecode.lib import helpers as h
5026 5031 return h.escape(self.gist_description)
5027 5032
5028 5033 @classmethod
5029 5034 def get_or_404(cls, id_):
5030 5035 from pyramid.httpexceptions import HTTPNotFound
5031 5036
5032 5037 res = cls.query().filter(cls.gist_access_id == id_).scalar()
5033 5038 if not res:
5034 5039 log.debug('WARN: No DB entry with id %s', id_)
5035 5040 raise HTTPNotFound()
5036 5041 return res
5037 5042
5038 5043 @classmethod
5039 5044 def get_by_access_id(cls, gist_access_id):
5040 5045 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
5041 5046
5042 5047 def gist_url(self):
5043 5048 from rhodecode.model.gist import GistModel
5044 5049 return GistModel().get_url(self)
5045 5050
5046 5051 @classmethod
5047 5052 def base_path(cls):
5048 5053 """
5049 5054 Returns base path when all gists are stored
5050 5055
5051 5056 :param cls:
5052 5057 """
5053 5058 from rhodecode.model.gist import GIST_STORE_LOC
5054 5059 from rhodecode.lib.utils import get_rhodecode_repo_store_path
5055 5060 repo_store_path = get_rhodecode_repo_store_path()
5056 5061 return os.path.join(repo_store_path, GIST_STORE_LOC)
5057 5062
5058 5063 def get_api_data(self):
5059 5064 """
5060 5065 Common function for generating gist related data for API
5061 5066 """
5062 5067 gist = self
5063 5068 data = {
5064 5069 'gist_id': gist.gist_id,
5065 5070 'type': gist.gist_type,
5066 5071 'access_id': gist.gist_access_id,
5067 5072 'description': gist.gist_description,
5068 5073 'url': gist.gist_url(),
5069 5074 'expires': gist.gist_expires,
5070 5075 'created_on': gist.created_on,
5071 5076 'modified_at': gist.modified_at,
5072 5077 'content': None,
5073 5078 'acl_level': gist.acl_level,
5074 5079 }
5075 5080 return data
5076 5081
5077 5082 def __json__(self):
5078 5083 data = dict()
5079 5084 data.update(self.get_api_data())
5080 5085 return data
5081 5086 # SCM functions
5082 5087
5083 5088 def scm_instance(self, **kwargs):
5084 5089 """
5085 5090 Get an instance of VCS Repository
5086 5091
5087 5092 :param kwargs:
5088 5093 """
5089 5094 from rhodecode.model.gist import GistModel
5090 5095 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5091 5096 return get_vcs_instance(
5092 5097 repo_path=safe_str(full_repo_path), create=False,
5093 5098 _vcs_alias=GistModel.vcs_backend)
5094 5099
5095 5100
5096 5101 class ExternalIdentity(Base, BaseModel):
5097 5102 __tablename__ = 'external_identities'
5098 5103 __table_args__ = (
5099 5104 Index('local_user_id_idx', 'local_user_id'),
5100 5105 Index('external_id_idx', 'external_id'),
5101 5106 base_table_args
5102 5107 )
5103 5108
5104 5109 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5105 5110 external_username = Column('external_username', Unicode(1024), default='')
5106 5111 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5107 5112 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5108 5113 access_token = Column('access_token', String(1024), default='')
5109 5114 alt_token = Column('alt_token', String(1024), default='')
5110 5115 token_secret = Column('token_secret', String(1024), default='')
5111 5116
5112 5117 @classmethod
5113 5118 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5114 5119 """
5115 5120 Returns ExternalIdentity instance based on search params
5116 5121
5117 5122 :param external_id:
5118 5123 :param provider_name:
5119 5124 :return: ExternalIdentity
5120 5125 """
5121 5126 query = cls.query()
5122 5127 query = query.filter(cls.external_id == external_id)
5123 5128 query = query.filter(cls.provider_name == provider_name)
5124 5129 if local_user_id:
5125 5130 query = query.filter(cls.local_user_id == local_user_id)
5126 5131 return query.first()
5127 5132
5128 5133 @classmethod
5129 5134 def user_by_external_id_and_provider(cls, external_id, provider_name):
5130 5135 """
5131 5136 Returns User instance based on search params
5132 5137
5133 5138 :param external_id:
5134 5139 :param provider_name:
5135 5140 :return: User
5136 5141 """
5137 5142 query = User.query()
5138 5143 query = query.filter(cls.external_id == external_id)
5139 5144 query = query.filter(cls.provider_name == provider_name)
5140 5145 query = query.filter(User.user_id == cls.local_user_id)
5141 5146 return query.first()
5142 5147
5143 5148 @classmethod
5144 5149 def by_local_user_id(cls, local_user_id):
5145 5150 """
5146 5151 Returns all tokens for user
5147 5152
5148 5153 :param local_user_id:
5149 5154 :return: ExternalIdentity
5150 5155 """
5151 5156 query = cls.query()
5152 5157 query = query.filter(cls.local_user_id == local_user_id)
5153 5158 return query
5154 5159
5155 5160 @classmethod
5156 5161 def load_provider_plugin(cls, plugin_id):
5157 5162 from rhodecode.authentication.base import loadplugin
5158 5163 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5159 5164 auth_plugin = loadplugin(_plugin_id)
5160 5165 return auth_plugin
5161 5166
5162 5167
5163 5168 class Integration(Base, BaseModel):
5164 5169 __tablename__ = 'integrations'
5165 5170 __table_args__ = (
5166 5171 base_table_args
5167 5172 )
5168 5173
5169 5174 integration_id = Column('integration_id', Integer(), primary_key=True)
5170 5175 integration_type = Column('integration_type', String(255))
5171 5176 enabled = Column('enabled', Boolean(), nullable=False)
5172 5177 name = Column('name', String(255), nullable=False)
5173 5178 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5174 5179
5175 5180 settings = Column(
5176 5181 'settings_json', MutationObj.as_mutable(
5177 5182 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5178 5183 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5179 5184 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5180 5185
5181 5186 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5182 5187 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5183 5188
5184 5189 @property
5185 5190 def scope(self):
5186 5191 if self.repo:
5187 5192 return repr(self.repo)
5188 5193 if self.repo_group:
5189 5194 if self.child_repos_only:
5190 5195 return repr(self.repo_group) + ' (child repos only)'
5191 5196 else:
5192 5197 return repr(self.repo_group) + ' (recursive)'
5193 5198 if self.child_repos_only:
5194 5199 return 'root_repos'
5195 5200 return 'global'
5196 5201
5197 5202 def __repr__(self):
5198 5203 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5199 5204
5200 5205
5201 5206 class RepoReviewRuleUser(Base, BaseModel):
5202 5207 __tablename__ = 'repo_review_rules_users'
5203 5208 __table_args__ = (
5204 5209 base_table_args
5205 5210 )
5206 5211 ROLE_REVIEWER = 'reviewer'
5207 5212 ROLE_OBSERVER = 'observer'
5208 5213 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5209 5214
5210 5215 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5211 5216 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5212 5217 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5213 5218 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5214 5219 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5215 5220 user = relationship('User', back_populates='user_review_rules')
5216 5221
5217 5222 def rule_data(self):
5218 5223 return {
5219 5224 'mandatory': self.mandatory,
5220 5225 'role': self.role,
5221 5226 }
5222 5227
5223 5228
5224 5229 class RepoReviewRuleUserGroup(Base, BaseModel):
5225 5230 __tablename__ = 'repo_review_rules_users_groups'
5226 5231 __table_args__ = (
5227 5232 base_table_args
5228 5233 )
5229 5234
5230 5235 VOTE_RULE_ALL = -1
5231 5236 ROLE_REVIEWER = 'reviewer'
5232 5237 ROLE_OBSERVER = 'observer'
5233 5238 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5234 5239
5235 5240 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5236 5241 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5237 5242 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5238 5243 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5239 5244 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5240 5245 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5241 5246 users_group = relationship('UserGroup')
5242 5247
5243 5248 def rule_data(self):
5244 5249 return {
5245 5250 'mandatory': self.mandatory,
5246 5251 'role': self.role,
5247 5252 'vote_rule': self.vote_rule
5248 5253 }
5249 5254
5250 5255 @property
5251 5256 def vote_rule_label(self):
5252 5257 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5253 5258 return 'all must vote'
5254 5259 else:
5255 5260 return 'min. vote {}'.format(self.vote_rule)
5256 5261
5257 5262
5258 5263 class RepoReviewRule(Base, BaseModel):
5259 5264 __tablename__ = 'repo_review_rules'
5260 5265 __table_args__ = (
5261 5266 base_table_args
5262 5267 )
5263 5268
5264 5269 repo_review_rule_id = Column(
5265 5270 'repo_review_rule_id', Integer(), primary_key=True)
5266 5271 repo_id = Column(
5267 5272 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5268 5273 repo = relationship('Repository', back_populates='review_rules')
5269 5274
5270 5275 review_rule_name = Column('review_rule_name', String(255))
5271 5276 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5272 5277 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5273 5278 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5274 5279
5275 5280 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5276 5281
5277 5282 # Legacy fields, just for backward compat
5278 5283 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5279 5284 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5280 5285
5281 5286 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5282 5287 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5283 5288
5284 5289 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5285 5290
5286 5291 rule_users = relationship('RepoReviewRuleUser')
5287 5292 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5288 5293
5289 5294 def _validate_pattern(self, value):
5290 5295 re.compile('^' + glob2re(value) + '$')
5291 5296
5292 5297 @hybrid_property
5293 5298 def source_branch_pattern(self):
5294 5299 return self._branch_pattern or '*'
5295 5300
5296 5301 @source_branch_pattern.setter
5297 5302 def source_branch_pattern(self, value):
5298 5303 self._validate_pattern(value)
5299 5304 self._branch_pattern = value or '*'
5300 5305
5301 5306 @hybrid_property
5302 5307 def target_branch_pattern(self):
5303 5308 return self._target_branch_pattern or '*'
5304 5309
5305 5310 @target_branch_pattern.setter
5306 5311 def target_branch_pattern(self, value):
5307 5312 self._validate_pattern(value)
5308 5313 self._target_branch_pattern = value or '*'
5309 5314
5310 5315 @hybrid_property
5311 5316 def file_pattern(self):
5312 5317 return self._file_pattern or '*'
5313 5318
5314 5319 @file_pattern.setter
5315 5320 def file_pattern(self, value):
5316 5321 self._validate_pattern(value)
5317 5322 self._file_pattern = value or '*'
5318 5323
5319 5324 @hybrid_property
5320 5325 def forbid_pr_author_to_review(self):
5321 5326 return self.pr_author == 'forbid_pr_author'
5322 5327
5323 5328 @hybrid_property
5324 5329 def include_pr_author_to_review(self):
5325 5330 return self.pr_author == 'include_pr_author'
5326 5331
5327 5332 @hybrid_property
5328 5333 def forbid_commit_author_to_review(self):
5329 5334 return self.commit_author == 'forbid_commit_author'
5330 5335
5331 5336 @hybrid_property
5332 5337 def include_commit_author_to_review(self):
5333 5338 return self.commit_author == 'include_commit_author'
5334 5339
5335 5340 def matches(self, source_branch, target_branch, files_changed):
5336 5341 """
5337 5342 Check if this review rule matches a branch/files in a pull request
5338 5343
5339 5344 :param source_branch: source branch name for the commit
5340 5345 :param target_branch: target branch name for the commit
5341 5346 :param files_changed: list of file paths changed in the pull request
5342 5347 """
5343 5348
5344 5349 source_branch = source_branch or ''
5345 5350 target_branch = target_branch or ''
5346 5351 files_changed = files_changed or []
5347 5352
5348 5353 branch_matches = True
5349 5354 if source_branch or target_branch:
5350 5355 if self.source_branch_pattern == '*':
5351 5356 source_branch_match = True
5352 5357 else:
5353 5358 if self.source_branch_pattern.startswith('re:'):
5354 5359 source_pattern = self.source_branch_pattern[3:]
5355 5360 else:
5356 5361 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5357 5362 source_branch_regex = re.compile(source_pattern)
5358 5363 source_branch_match = bool(source_branch_regex.search(source_branch))
5359 5364 if self.target_branch_pattern == '*':
5360 5365 target_branch_match = True
5361 5366 else:
5362 5367 if self.target_branch_pattern.startswith('re:'):
5363 5368 target_pattern = self.target_branch_pattern[3:]
5364 5369 else:
5365 5370 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5366 5371 target_branch_regex = re.compile(target_pattern)
5367 5372 target_branch_match = bool(target_branch_regex.search(target_branch))
5368 5373
5369 5374 branch_matches = source_branch_match and target_branch_match
5370 5375
5371 5376 files_matches = True
5372 5377 if self.file_pattern != '*':
5373 5378 files_matches = False
5374 5379 if self.file_pattern.startswith('re:'):
5375 5380 file_pattern = self.file_pattern[3:]
5376 5381 else:
5377 5382 file_pattern = glob2re(self.file_pattern)
5378 5383 file_regex = re.compile(file_pattern)
5379 5384 for file_data in files_changed:
5380 5385 filename = file_data.get('filename')
5381 5386
5382 5387 if file_regex.search(filename):
5383 5388 files_matches = True
5384 5389 break
5385 5390
5386 5391 return branch_matches and files_matches
5387 5392
5388 5393 @property
5389 5394 def review_users(self):
5390 5395 """ Returns the users which this rule applies to """
5391 5396
5392 5397 users = collections.OrderedDict()
5393 5398
5394 5399 for rule_user in self.rule_users:
5395 5400 if rule_user.user.active:
5396 5401 if rule_user.user not in users:
5397 5402 users[rule_user.user.username] = {
5398 5403 'user': rule_user.user,
5399 5404 'source': 'user',
5400 5405 'source_data': {},
5401 5406 'data': rule_user.rule_data()
5402 5407 }
5403 5408
5404 5409 for rule_user_group in self.rule_user_groups:
5405 5410 source_data = {
5406 5411 'user_group_id': rule_user_group.users_group.users_group_id,
5407 5412 'name': rule_user_group.users_group.users_group_name,
5408 5413 'members': len(rule_user_group.users_group.members)
5409 5414 }
5410 5415 for member in rule_user_group.users_group.members:
5411 5416 if member.user.active:
5412 5417 key = member.user.username
5413 5418 if key in users:
5414 5419 # skip this member as we have him already
5415 5420 # this prevents from override the "first" matched
5416 5421 # users with duplicates in multiple groups
5417 5422 continue
5418 5423
5419 5424 users[key] = {
5420 5425 'user': member.user,
5421 5426 'source': 'user_group',
5422 5427 'source_data': source_data,
5423 5428 'data': rule_user_group.rule_data()
5424 5429 }
5425 5430
5426 5431 return users
5427 5432
5428 5433 def user_group_vote_rule(self, user_id):
5429 5434
5430 5435 rules = []
5431 5436 if not self.rule_user_groups:
5432 5437 return rules
5433 5438
5434 5439 for user_group in self.rule_user_groups:
5435 5440 user_group_members = [x.user_id for x in user_group.users_group.members]
5436 5441 if user_id in user_group_members:
5437 5442 rules.append(user_group)
5438 5443 return rules
5439 5444
5440 5445 def __repr__(self):
5441 5446 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5442 5447
5443 5448
5444 5449 class ScheduleEntry(Base, BaseModel):
5445 5450 __tablename__ = 'schedule_entries'
5446 5451 __table_args__ = (
5447 5452 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5448 5453 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5449 5454 base_table_args,
5450 5455 )
5451 5456 SCHEDULE_TYPE_INTEGER = "integer"
5452 5457 SCHEDULE_TYPE_CRONTAB = "crontab"
5453 5458
5454 5459 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5455 5460 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5456 5461
5457 5462 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5458 5463 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5459 5464 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5460 5465
5461 5466 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5462 5467 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5463 5468
5464 5469 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5465 5470 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5466 5471
5467 5472 # task
5468 5473 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5469 5474 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5470 5475 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5471 5476 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5472 5477
5473 5478 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5474 5479 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5475 5480
5476 5481 @hybrid_property
5477 5482 def schedule_type(self):
5478 5483 return self._schedule_type
5479 5484
5480 5485 @schedule_type.setter
5481 5486 def schedule_type(self, val):
5482 5487 if val not in self.schedule_types:
5483 5488 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5484 5489 val, self.schedule_type))
5485 5490
5486 5491 self._schedule_type = val
5487 5492
5488 5493 @classmethod
5489 5494 def get_uid(cls, obj):
5490 5495 args = obj.task_args
5491 5496 kwargs = obj.task_kwargs
5492 5497 if isinstance(args, JsonRaw):
5493 5498 try:
5494 5499 args = json.loads(args)
5495 5500 except ValueError:
5496 5501 args = tuple()
5497 5502
5498 5503 if isinstance(kwargs, JsonRaw):
5499 5504 try:
5500 5505 kwargs = json.loads(kwargs)
5501 5506 except ValueError:
5502 5507 kwargs = dict()
5503 5508
5504 5509 dot_notation = obj.task_dot_notation
5505 5510 val = '.'.join(map(safe_str, [
5506 5511 sorted(dot_notation), args, sorted(kwargs.items())]))
5507 5512 return sha1(safe_bytes(val))
5508 5513
5509 5514 @classmethod
5510 5515 def get_by_schedule_name(cls, schedule_name):
5511 5516 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5512 5517
5513 5518 @classmethod
5514 5519 def get_by_schedule_id(cls, schedule_id):
5515 5520 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5516 5521
5517 5522 @property
5518 5523 def task(self):
5519 5524 return self.task_dot_notation
5520 5525
5521 5526 @property
5522 5527 def schedule(self):
5523 5528 from rhodecode.lib.celerylib.utils import raw_2_schedule
5524 5529 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5525 5530 return schedule
5526 5531
5527 5532 @property
5528 5533 def args(self):
5529 5534 try:
5530 5535 return list(self.task_args or [])
5531 5536 except ValueError:
5532 5537 return list()
5533 5538
5534 5539 @property
5535 5540 def kwargs(self):
5536 5541 try:
5537 5542 return dict(self.task_kwargs or {})
5538 5543 except ValueError:
5539 5544 return dict()
5540 5545
5541 5546 def _as_raw(self, val, indent=False):
5542 5547 if hasattr(val, 'de_coerce'):
5543 5548 val = val.de_coerce()
5544 5549 if val:
5545 5550 if indent:
5546 5551 val = ext_json.formatted_str_json(val)
5547 5552 else:
5548 5553 val = ext_json.str_json(val)
5549 5554
5550 5555 return val
5551 5556
5552 5557 @property
5553 5558 def schedule_definition_raw(self):
5554 5559 return self._as_raw(self.schedule_definition)
5555 5560
5556 5561 def args_raw(self, indent=False):
5557 5562 return self._as_raw(self.task_args, indent)
5558 5563
5559 5564 def kwargs_raw(self, indent=False):
5560 5565 return self._as_raw(self.task_kwargs, indent)
5561 5566
5562 5567 def __repr__(self):
5563 5568 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5564 5569
5565 5570
5566 5571 @event.listens_for(ScheduleEntry, 'before_update')
5567 5572 def update_task_uid(mapper, connection, target):
5568 5573 target.task_uid = ScheduleEntry.get_uid(target)
5569 5574
5570 5575
5571 5576 @event.listens_for(ScheduleEntry, 'before_insert')
5572 5577 def set_task_uid(mapper, connection, target):
5573 5578 target.task_uid = ScheduleEntry.get_uid(target)
5574 5579
5575 5580
5576 5581 class _BaseBranchPerms(BaseModel):
5577 5582 @classmethod
5578 5583 def compute_hash(cls, value):
5579 5584 return sha1_safe(value)
5580 5585
5581 5586 @hybrid_property
5582 5587 def branch_pattern(self):
5583 5588 return self._branch_pattern or '*'
5584 5589
5585 5590 @hybrid_property
5586 5591 def branch_hash(self):
5587 5592 return self._branch_hash
5588 5593
5589 5594 def _validate_glob(self, value):
5590 5595 re.compile('^' + glob2re(value) + '$')
5591 5596
5592 5597 @branch_pattern.setter
5593 5598 def branch_pattern(self, value):
5594 5599 self._validate_glob(value)
5595 5600 self._branch_pattern = value or '*'
5596 5601 # set the Hash when setting the branch pattern
5597 5602 self._branch_hash = self.compute_hash(self._branch_pattern)
5598 5603
5599 5604 def matches(self, branch):
5600 5605 """
5601 5606 Check if this the branch matches entry
5602 5607
5603 5608 :param branch: branch name for the commit
5604 5609 """
5605 5610
5606 5611 branch = branch or ''
5607 5612
5608 5613 branch_matches = True
5609 5614 if branch:
5610 5615 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5611 5616 branch_matches = bool(branch_regex.search(branch))
5612 5617
5613 5618 return branch_matches
5614 5619
5615 5620
5616 5621 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5617 5622 __tablename__ = 'user_to_repo_branch_permissions'
5618 5623 __table_args__ = (
5619 5624 base_table_args
5620 5625 )
5621 5626
5622 5627 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5623 5628
5624 5629 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5625 5630 repo = relationship('Repository', back_populates='user_branch_perms')
5626 5631
5627 5632 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5628 5633 permission = relationship('Permission')
5629 5634
5630 5635 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5631 5636 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5632 5637
5633 5638 rule_order = Column('rule_order', Integer(), nullable=False)
5634 5639 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5635 5640 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5636 5641
5637 5642 def __repr__(self):
5638 5643 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5639 5644
5640 5645
5641 5646 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5642 5647 __tablename__ = 'user_group_to_repo_branch_permissions'
5643 5648 __table_args__ = (
5644 5649 base_table_args
5645 5650 )
5646 5651
5647 5652 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5648 5653
5649 5654 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5650 5655 repo = relationship('Repository', back_populates='user_group_branch_perms')
5651 5656
5652 5657 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5653 5658 permission = relationship('Permission')
5654 5659
5655 5660 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5656 5661 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5657 5662
5658 5663 rule_order = Column('rule_order', Integer(), nullable=False)
5659 5664 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5660 5665 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5661 5666
5662 5667 def __repr__(self):
5663 5668 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5664 5669
5665 5670
5666 5671 class UserBookmark(Base, BaseModel):
5667 5672 __tablename__ = 'user_bookmarks'
5668 5673 __table_args__ = (
5669 5674 UniqueConstraint('user_id', 'bookmark_repo_id'),
5670 5675 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5671 5676 UniqueConstraint('user_id', 'bookmark_position'),
5672 5677 base_table_args
5673 5678 )
5674 5679
5675 5680 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5676 5681 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5677 5682 position = Column("bookmark_position", Integer(), nullable=False)
5678 5683 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5679 5684 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5680 5685 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5681 5686
5682 5687 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5683 5688 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5684 5689
5685 5690 user = relationship("User")
5686 5691
5687 5692 repository = relationship("Repository")
5688 5693 repository_group = relationship("RepoGroup")
5689 5694
5690 5695 @classmethod
5691 5696 def get_by_position_for_user(cls, position, user_id):
5692 5697 return cls.query() \
5693 5698 .filter(UserBookmark.user_id == user_id) \
5694 5699 .filter(UserBookmark.position == position).scalar()
5695 5700
5696 5701 @classmethod
5697 5702 def get_bookmarks_for_user(cls, user_id, cache=True):
5698 5703 bookmarks = select(
5699 5704 UserBookmark.title,
5700 5705 UserBookmark.position,
5701 5706 ) \
5702 5707 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5703 5708 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5704 5709 .where(UserBookmark.user_id == user_id) \
5705 5710 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5706 5711 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5707 5712 .order_by(UserBookmark.position.asc())
5708 5713
5709 5714 if cache:
5710 5715 bookmarks = bookmarks.options(
5711 5716 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5712 5717 )
5713 5718
5714 5719 return Session().execute(bookmarks).all()
5715 5720
5716 5721 def __repr__(self):
5717 5722 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5718 5723
5719 5724
5720 5725 class FileStore(Base, BaseModel):
5721 5726 __tablename__ = 'file_store'
5722 5727 __table_args__ = (
5723 5728 base_table_args
5724 5729 )
5725 5730
5726 5731 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5727 5732 file_uid = Column('file_uid', String(1024), nullable=False)
5728 5733 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5729 5734 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5730 5735 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5731 5736
5732 5737 # sha256 hash
5733 5738 file_hash = Column('file_hash', String(512), nullable=False)
5734 5739 file_size = Column('file_size', BigInteger(), nullable=False)
5735 5740
5736 5741 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5737 5742 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5738 5743 accessed_count = Column('accessed_count', Integer(), default=0)
5739 5744
5740 5745 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5741 5746
5742 5747 # if repo/repo_group reference is set, check for permissions
5743 5748 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5744 5749
5745 5750 # hidden defines an attachment that should be hidden from showing in artifact listing
5746 5751 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5747 5752
5748 5753 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5749 5754 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5750 5755
5751 5756 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5752 5757
5753 5758 # scope limited to user, which requester have access to
5754 5759 scope_user_id = Column(
5755 5760 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5756 5761 nullable=True, unique=None, default=None)
5757 5762 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5758 5763
5759 5764 # scope limited to user group, which requester have access to
5760 5765 scope_user_group_id = Column(
5761 5766 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5762 5767 nullable=True, unique=None, default=None)
5763 5768 user_group = relationship('UserGroup', lazy='joined')
5764 5769
5765 5770 # scope limited to repo, which requester have access to
5766 5771 scope_repo_id = Column(
5767 5772 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5768 5773 nullable=True, unique=None, default=None)
5769 5774 repo = relationship('Repository', lazy='joined')
5770 5775
5771 5776 # scope limited to repo group, which requester have access to
5772 5777 scope_repo_group_id = Column(
5773 5778 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5774 5779 nullable=True, unique=None, default=None)
5775 5780 repo_group = relationship('RepoGroup', lazy='joined')
5776 5781
5777 5782 @classmethod
5778 5783 def get_scope(cls, scope_type, scope_id):
5779 5784 if scope_type == 'repo':
5780 5785 return f'repo:{scope_id}'
5781 5786 elif scope_type == 'repo-group':
5782 5787 return f'repo-group:{scope_id}'
5783 5788 elif scope_type == 'user':
5784 5789 return f'user:{scope_id}'
5785 5790 elif scope_type == 'user-group':
5786 5791 return f'user-group:{scope_id}'
5787 5792 else:
5788 5793 return scope_type
5789 5794
5790 5795 @classmethod
5791 5796 def get_by_store_uid(cls, file_store_uid, safe=False):
5792 5797 if safe:
5793 5798 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5794 5799 else:
5795 5800 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5796 5801
5797 5802 @classmethod
5798 5803 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5799 5804 file_description='', enabled=True, hidden=False, check_acl=True,
5800 5805 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5801 5806
5802 5807 store_entry = FileStore()
5803 5808 store_entry.file_uid = file_uid
5804 5809 store_entry.file_display_name = file_display_name
5805 5810 store_entry.file_org_name = filename
5806 5811 store_entry.file_size = file_size
5807 5812 store_entry.file_hash = file_hash
5808 5813 store_entry.file_description = file_description
5809 5814
5810 5815 store_entry.check_acl = check_acl
5811 5816 store_entry.enabled = enabled
5812 5817 store_entry.hidden = hidden
5813 5818
5814 5819 store_entry.user_id = user_id
5815 5820 store_entry.scope_user_id = scope_user_id
5816 5821 store_entry.scope_repo_id = scope_repo_id
5817 5822 store_entry.scope_repo_group_id = scope_repo_group_id
5818 5823
5819 5824 return store_entry
5820 5825
5821 5826 @classmethod
5822 5827 def store_metadata(cls, file_store_id, args, commit=True):
5823 5828 file_store = FileStore.get(file_store_id)
5824 5829 if file_store is None:
5825 5830 return
5826 5831
5827 5832 for section, key, value, value_type in args:
5828 5833 has_key = FileStoreMetadata().query() \
5829 5834 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5830 5835 .filter(FileStoreMetadata.file_store_meta_section == section) \
5831 5836 .filter(FileStoreMetadata.file_store_meta_key == key) \
5832 5837 .scalar()
5833 5838 if has_key:
5834 5839 msg = 'key `{}` already defined under section `{}` for this file.'\
5835 5840 .format(key, section)
5836 5841 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5837 5842
5838 5843 # NOTE(marcink): raises ArtifactMetadataBadValueType
5839 5844 FileStoreMetadata.valid_value_type(value_type)
5840 5845
5841 5846 meta_entry = FileStoreMetadata()
5842 5847 meta_entry.file_store = file_store
5843 5848 meta_entry.file_store_meta_section = section
5844 5849 meta_entry.file_store_meta_key = key
5845 5850 meta_entry.file_store_meta_value_type = value_type
5846 5851 meta_entry.file_store_meta_value = value
5847 5852
5848 5853 Session().add(meta_entry)
5849 5854
5850 5855 try:
5851 5856 if commit:
5852 5857 Session().commit()
5853 5858 except IntegrityError:
5854 5859 Session().rollback()
5855 5860 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5856 5861
5857 5862 @classmethod
5858 5863 def bump_access_counter(cls, file_uid, commit=True):
5859 5864 FileStore().query()\
5860 5865 .filter(FileStore.file_uid == file_uid)\
5861 5866 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5862 5867 FileStore.accessed_on: datetime.datetime.now()})
5863 5868 if commit:
5864 5869 Session().commit()
5865 5870
5866 5871 def __json__(self):
5867 5872 data = {
5868 5873 'filename': self.file_display_name,
5869 5874 'filename_org': self.file_org_name,
5870 5875 'file_uid': self.file_uid,
5871 5876 'description': self.file_description,
5872 5877 'hidden': self.hidden,
5873 5878 'size': self.file_size,
5874 5879 'created_on': self.created_on,
5875 5880 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5876 5881 'downloaded_times': self.accessed_count,
5877 5882 'sha256': self.file_hash,
5878 5883 'metadata': self.file_metadata,
5879 5884 }
5880 5885
5881 5886 return data
5882 5887
5883 5888 def __repr__(self):
5884 5889 return f'<FileStore({self.file_store_id})>'
5885 5890
5886 5891
5887 5892 class FileStoreMetadata(Base, BaseModel):
5888 5893 __tablename__ = 'file_store_metadata'
5889 5894 __table_args__ = (
5890 5895 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5891 5896 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5892 5897 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5893 5898 base_table_args
5894 5899 )
5895 5900 SETTINGS_TYPES = {
5896 5901 'str': safe_str,
5897 5902 'int': safe_int,
5898 5903 'unicode': safe_str,
5899 5904 'bool': str2bool,
5900 5905 'list': functools.partial(aslist, sep=',')
5901 5906 }
5902 5907
5903 5908 file_store_meta_id = Column(
5904 5909 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5905 5910 primary_key=True)
5906 5911 _file_store_meta_section = Column(
5907 5912 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5908 5913 nullable=True, unique=None, default=None)
5909 5914 _file_store_meta_section_hash = Column(
5910 5915 "file_store_meta_section_hash", String(255),
5911 5916 nullable=True, unique=None, default=None)
5912 5917 _file_store_meta_key = Column(
5913 5918 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5914 5919 nullable=True, unique=None, default=None)
5915 5920 _file_store_meta_key_hash = Column(
5916 5921 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5917 5922 _file_store_meta_value = Column(
5918 5923 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5919 5924 nullable=True, unique=None, default=None)
5920 5925 _file_store_meta_value_type = Column(
5921 5926 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5922 5927 default='unicode')
5923 5928
5924 5929 file_store_id = Column(
5925 5930 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5926 5931 nullable=True, unique=None, default=None)
5927 5932
5928 5933 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5929 5934
5930 5935 @classmethod
5931 5936 def valid_value_type(cls, value):
5932 5937 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5933 5938 raise ArtifactMetadataBadValueType(
5934 5939 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5935 5940
5936 5941 @hybrid_property
5937 5942 def file_store_meta_section(self):
5938 5943 return self._file_store_meta_section
5939 5944
5940 5945 @file_store_meta_section.setter
5941 5946 def file_store_meta_section(self, value):
5942 5947 self._file_store_meta_section = value
5943 5948 self._file_store_meta_section_hash = _hash_key(value)
5944 5949
5945 5950 @hybrid_property
5946 5951 def file_store_meta_key(self):
5947 5952 return self._file_store_meta_key
5948 5953
5949 5954 @file_store_meta_key.setter
5950 5955 def file_store_meta_key(self, value):
5951 5956 self._file_store_meta_key = value
5952 5957 self._file_store_meta_key_hash = _hash_key(value)
5953 5958
5954 5959 @hybrid_property
5955 5960 def file_store_meta_value(self):
5956 5961 val = self._file_store_meta_value
5957 5962
5958 5963 if self._file_store_meta_value_type:
5959 5964 # e.g unicode.encrypted == unicode
5960 5965 _type = self._file_store_meta_value_type.split('.')[0]
5961 5966 # decode the encrypted value if it's encrypted field type
5962 5967 if '.encrypted' in self._file_store_meta_value_type:
5963 5968 cipher = EncryptedTextValue()
5964 5969 val = safe_str(cipher.process_result_value(val, None))
5965 5970 # do final type conversion
5966 5971 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5967 5972 val = converter(val)
5968 5973
5969 5974 return val
5970 5975
5971 5976 @file_store_meta_value.setter
5972 5977 def file_store_meta_value(self, val):
5973 5978 val = safe_str(val)
5974 5979 # encode the encrypted value
5975 5980 if '.encrypted' in self.file_store_meta_value_type:
5976 5981 cipher = EncryptedTextValue()
5977 5982 val = safe_str(cipher.process_bind_param(val, None))
5978 5983 self._file_store_meta_value = val
5979 5984
5980 5985 @hybrid_property
5981 5986 def file_store_meta_value_type(self):
5982 5987 return self._file_store_meta_value_type
5983 5988
5984 5989 @file_store_meta_value_type.setter
5985 5990 def file_store_meta_value_type(self, val):
5986 5991 # e.g unicode.encrypted
5987 5992 self.valid_value_type(val)
5988 5993 self._file_store_meta_value_type = val
5989 5994
5990 5995 def __json__(self):
5991 5996 data = {
5992 5997 'artifact': self.file_store.file_uid,
5993 5998 'section': self.file_store_meta_section,
5994 5999 'key': self.file_store_meta_key,
5995 6000 'value': self.file_store_meta_value,
5996 6001 }
5997 6002
5998 6003 return data
5999 6004
6000 6005 def __repr__(self):
6001 6006 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
6002 6007 self.file_store_meta_key, self.file_store_meta_value)
6003 6008
6004 6009
6005 6010 class DbMigrateVersion(Base, BaseModel):
6006 6011 __tablename__ = 'db_migrate_version'
6007 6012 __table_args__ = (
6008 6013 base_table_args,
6009 6014 )
6010 6015
6011 6016 repository_id = Column('repository_id', String(250), primary_key=True)
6012 6017 repository_path = Column('repository_path', Text)
6013 6018 version = Column('version', Integer)
6014 6019
6015 6020 @classmethod
6016 6021 def set_version(cls, version):
6017 6022 """
6018 6023 Helper for forcing a different version, usually for debugging purposes via ishell.
6019 6024 """
6020 6025 ver = DbMigrateVersion.query().first()
6021 6026 ver.version = version
6022 6027 Session().commit()
6023 6028
6024 6029
6025 6030 class DbSession(Base, BaseModel):
6026 6031 __tablename__ = 'db_session'
6027 6032 __table_args__ = (
6028 6033 base_table_args,
6029 6034 )
6030 6035
6031 6036 def __repr__(self):
6032 6037 return f'<DB:DbSession({self.id})>'
6033 6038
6034 6039 id = Column('id', Integer())
6035 6040 namespace = Column('namespace', String(255), primary_key=True)
6036 6041 accessed = Column('accessed', DateTime, nullable=False)
6037 6042 created = Column('created', DateTime, nullable=False)
6038 6043 data = Column('data', PickleType, nullable=False)
General Comments 0
You need to be logged in to leave comments. Login now