##// 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 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import time
19 import time
20 import logging
20 import logging
21 import operator
21 import operator
22
22
23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
24
24
25 from rhodecode.lib import helpers as h, diffs, rc_cache
25 from rhodecode.lib import helpers as h, diffs, rc_cache
26 from rhodecode.lib.str_utils import safe_str
26 from rhodecode.lib.str_utils import safe_str
27 from rhodecode.lib.utils import repo_name_slug
27 from rhodecode.lib.utils import repo_name_slug
28 from rhodecode.lib.utils2 import (
28 from rhodecode.lib.utils2 import (
29 StrictAttributeDict,
29 StrictAttributeDict,
30 str2bool,
30 str2bool,
31 safe_int,
31 safe_int,
32 datetime_to_time,
32 datetime_to_time,
33 )
33 )
34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
35 from rhodecode.lib.vcs.backends.base import EmptyCommit
35 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
36 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
37 from rhodecode.model import repo
37 from rhodecode.model import repo
38 from rhodecode.model import repo_group
38 from rhodecode.model import repo_group
39 from rhodecode.model import user_group
39 from rhodecode.model import user_group
40 from rhodecode.model import user
40 from rhodecode.model import user
41 from rhodecode.model.db import User
41 from rhodecode.model.db import User
42 from rhodecode.model.scm import ScmModel
42 from rhodecode.model.scm import ScmModel
43 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
43 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
44 from rhodecode.model.repo import ReadmeFinder
44 from rhodecode.model.repo import ReadmeFinder
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 ADMIN_PREFIX: str = "/_admin"
49 ADMIN_PREFIX: str = "/_admin"
50 STATIC_FILE_PREFIX: str = "/_static"
50 STATIC_FILE_PREFIX: str = "/_static"
51
51
52 URL_NAME_REQUIREMENTS = {
52 URL_NAME_REQUIREMENTS = {
53 # group name can have a slash in them, but they must not end with a slash
53 # group name can have a slash in them, but they must not end with a slash
54 "group_name": r".*?[^/]",
54 "group_name": r".*?[^/]",
55 "repo_group_name": r".*?[^/]",
55 "repo_group_name": r".*?[^/]",
56 # repo names can have a slash in them, but they must not end with a slash
56 # repo names can have a slash in them, but they must not end with a slash
57 "repo_name": r".*?[^/]",
57 "repo_name": r".*?[^/]",
58 # file path eats up everything at the end
58 # file path eats up everything at the end
59 "f_path": r".*",
59 "f_path": r".*",
60 # reference types
60 # reference types
61 "source_ref_type": r"(branch|book|tag|rev|\%\(source_ref_type\)s)",
61 "source_ref_type": r"(branch|book|tag|rev|\%\(source_ref_type\)s)",
62 "target_ref_type": r"(branch|book|tag|rev|\%\(target_ref_type\)s)",
62 "target_ref_type": r"(branch|book|tag|rev|\%\(target_ref_type\)s)",
63 }
63 }
64
64
65
65
66 def add_route_with_slash(config, name, pattern, **kw):
66 def add_route_with_slash(config, name, pattern, **kw):
67 config.add_route(name, pattern, **kw)
67 config.add_route(name, pattern, **kw)
68 if not pattern.endswith("/"):
68 if not pattern.endswith("/"):
69 config.add_route(name + "_slash", pattern + "/", **kw)
69 config.add_route(name + "_slash", pattern + "/", **kw)
70
70
71
71
72 def add_route_requirements(route_path, requirements=None):
72 def add_route_requirements(route_path, requirements=None):
73 """
73 """
74 Adds regex requirements to pyramid routes using a mapping dict
74 Adds regex requirements to pyramid routes using a mapping dict
75 e.g::
75 e.g::
76 add_route_requirements('{repo_name}/settings')
76 add_route_requirements('{repo_name}/settings')
77 """
77 """
78 requirements = requirements or URL_NAME_REQUIREMENTS
78 requirements = requirements or URL_NAME_REQUIREMENTS
79 for key, regex in list(requirements.items()):
79 for key, regex in list(requirements.items()):
80 route_path = route_path.replace("{%s}" % key, "{%s:%s}" % (key, regex))
80 route_path = route_path.replace("{%s}" % key, "{%s:%s}" % (key, regex))
81 return route_path
81 return route_path
82
82
83
83
84 def get_format_ref_id(repo):
84 def get_format_ref_id(repo):
85 """Returns a `repo` specific reference formatter function"""
85 """Returns a `repo` specific reference formatter function"""
86 if h.is_svn(repo):
86 if h.is_svn(repo):
87 return _format_ref_id_svn
87 return _format_ref_id_svn
88 else:
88 else:
89 return _format_ref_id
89 return _format_ref_id
90
90
91
91
92 def _format_ref_id(name, raw_id):
92 def _format_ref_id(name, raw_id):
93 """Default formatting of a given reference `name`"""
93 """Default formatting of a given reference `name`"""
94 return name
94 return name
95
95
96
96
97 def _format_ref_id_svn(name, raw_id):
97 def _format_ref_id_svn(name, raw_id):
98 """Special way of formatting a reference for Subversion including path"""
98 """Special way of formatting a reference for Subversion including path"""
99 return f"{name}@{raw_id}"
99 return f"{name}@{raw_id}"
100
100
101
101
102 class TemplateArgs(StrictAttributeDict):
102 class TemplateArgs(StrictAttributeDict):
103 pass
103 pass
104
104
105
105
106 class BaseAppView(object):
106 class BaseAppView(object):
107 DONT_CHECKOUT_VIEWS = ["channelstream_connect", "ops_ping"]
107 DONT_CHECKOUT_VIEWS = ["channelstream_connect", "ops_ping"]
108 EXTRA_VIEWS_TO_IGNORE = ['login', 'register', 'logout']
108 EXTRA_VIEWS_TO_IGNORE = ['login', 'register', 'logout']
109 SETUP_2FA_VIEW = 'setup_2fa'
109 SETUP_2FA_VIEW = 'setup_2fa'
110 VERIFY_2FA_VIEW = 'check_2fa'
110 VERIFY_2FA_VIEW = 'check_2fa'
111
111
112 def __init__(self, context, request):
112 def __init__(self, context, request):
113 self.request = request
113 self.request = request
114 self.context = context
114 self.context = context
115 self.session = request.session
115 self.session = request.session
116 if not hasattr(request, "user"):
116 if not hasattr(request, "user"):
117 # NOTE(marcink): edge case, we ended up in matched route
117 # NOTE(marcink): edge case, we ended up in matched route
118 # but probably of web-app context, e.g API CALL/VCS CALL
118 # but probably of web-app context, e.g API CALL/VCS CALL
119 if hasattr(request, "vcs_call") or hasattr(request, "rpc_method"):
119 if hasattr(request, "vcs_call") or hasattr(request, "rpc_method"):
120 log.warning("Unable to process request `%s` in this scope", request)
120 log.warning("Unable to process request `%s` in this scope", request)
121 raise HTTPBadRequest()
121 raise HTTPBadRequest()
122
122
123 self._rhodecode_user = request.user # auth user
123 self._rhodecode_user = request.user # auth user
124 self._rhodecode_db_user = self._rhodecode_user.get_instance()
124 self._rhodecode_db_user = self._rhodecode_user.get_instance()
125 self.user_data = self._rhodecode_db_user.user_data if self._rhodecode_db_user else {}
125 self.user_data = self._rhodecode_db_user.user_data if self._rhodecode_db_user else {}
126 self._maybe_needs_password_change(
126 self._maybe_needs_password_change(
127 request.matched_route.name, self._rhodecode_db_user
127 request.matched_route.name, self._rhodecode_db_user
128 )
128 )
129 self._maybe_needs_2fa_configuration(
129 self._maybe_needs_2fa_configuration(
130 request.matched_route.name, self._rhodecode_db_user
130 request.matched_route.name, self._rhodecode_db_user
131 )
131 )
132 self._maybe_needs_2fa_check(
132 self._maybe_needs_2fa_check(
133 request.matched_route.name, self._rhodecode_db_user
133 request.matched_route.name, self._rhodecode_db_user
134 )
134 )
135
135
136 def _maybe_needs_password_change(self, view_name, user_obj):
136 def _maybe_needs_password_change(self, view_name, user_obj):
137 if view_name in self.DONT_CHECKOUT_VIEWS:
137 if view_name in self.DONT_CHECKOUT_VIEWS:
138 return
138 return
139
139
140 log.debug(
140 log.debug(
141 "Checking if user %s needs password change on view %s", user_obj, view_name
141 "Checking if user %s needs password change on view %s", user_obj, view_name
142 )
142 )
143
143
144 skip_user_views = [
144 skip_user_views = [
145 "logout",
145 "logout",
146 "login",
146 "login",
147 "check_2fa",
147 "check_2fa",
148 "my_account_password",
148 "my_account_password",
149 "my_account_password_update",
149 "my_account_password_update",
150 ]
150 ]
151
151
152 if not user_obj:
152 if not user_obj:
153 return
153 return
154
154
155 if user_obj.username == User.DEFAULT_USER:
155 if user_obj.username == User.DEFAULT_USER:
156 return
156 return
157
157
158 now = time.time()
158 now = time.time()
159 should_change = self.user_data.get("force_password_change")
159 should_change = self.user_data.get("force_password_change")
160 change_after = safe_int(should_change) or 0
160 change_after = safe_int(should_change) or 0
161 if should_change and now > change_after:
161 if should_change and now > change_after:
162 log.debug("User %s requires password change", user_obj)
162 log.debug("User %s requires password change", user_obj)
163 h.flash(
163 h.flash(
164 "You are required to change your password",
164 "You are required to change your password",
165 "warning",
165 "warning",
166 ignore_duplicate=True,
166 ignore_duplicate=True,
167 )
167 )
168
168
169 if view_name not in skip_user_views:
169 if view_name not in skip_user_views:
170 raise HTTPFound(self.request.route_path("my_account_password"))
170 raise HTTPFound(self.request.route_path("my_account_password"))
171
171
172 def _maybe_needs_2fa_configuration(self, view_name, user_obj):
172 def _maybe_needs_2fa_configuration(self, view_name, user_obj):
173 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
173 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
174 return
174 return
175
175
176 if not user_obj:
176 if not user_obj:
177 return
177 return
178
178
179 if user_obj.has_forced_2fa and user_obj.extern_type != 'rhodecode':
179 if user_obj.has_forced_2fa and user_obj.extern_type != 'rhodecode':
180 return
180 return
181
181
182 if user_obj.needs_2fa_configure and view_name != self.SETUP_2FA_VIEW:
182 if user_obj.needs_2fa_configure and view_name != self.SETUP_2FA_VIEW:
183 h.flash(
183 h.flash(
184 "You are required to configure 2FA",
184 "You are required to configure 2FA",
185 "warning",
185 "warning",
186 ignore_duplicate=False,
186 ignore_duplicate=False,
187 )
187 )
188 raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW))
188 raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW))
189
189
190 def _maybe_needs_2fa_check(self, view_name, user_obj):
190 def _maybe_needs_2fa_check(self, view_name, user_obj):
191 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
191 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
192 return
192 return
193
193
194 if not user_obj:
194 if not user_obj:
195 return
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 raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW))
198 raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW))
199
199
200 def _log_creation_exception(self, e, repo_name):
200 def _log_creation_exception(self, e, repo_name):
201 _ = self.request.translate
201 _ = self.request.translate
202 reason = None
202 reason = None
203 if len(e.args) == 2:
203 if len(e.args) == 2:
204 reason = e.args[1]
204 reason = e.args[1]
205
205
206 if reason == "INVALID_CERTIFICATE":
206 if reason == "INVALID_CERTIFICATE":
207 log.exception("Exception creating a repository: invalid certificate")
207 log.exception("Exception creating a repository: invalid certificate")
208 msg = _("Error creating repository %s: invalid certificate") % repo_name
208 msg = _("Error creating repository %s: invalid certificate") % repo_name
209 else:
209 else:
210 log.exception("Exception creating a repository")
210 log.exception("Exception creating a repository")
211 msg = _("Error creating repository %s") % repo_name
211 msg = _("Error creating repository %s") % repo_name
212 return msg
212 return msg
213
213
214 def _get_local_tmpl_context(self, include_app_defaults=True):
214 def _get_local_tmpl_context(self, include_app_defaults=True):
215 c = TemplateArgs()
215 c = TemplateArgs()
216 c.auth_user = self.request.user
216 c.auth_user = self.request.user
217 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
217 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
218 c.rhodecode_user = self.request.user
218 c.rhodecode_user = self.request.user
219
219
220 if include_app_defaults:
220 if include_app_defaults:
221 from rhodecode.lib.base import attach_context_attributes
221 from rhodecode.lib.base import attach_context_attributes
222
222
223 attach_context_attributes(c, self.request, self.request.user.user_id)
223 attach_context_attributes(c, self.request, self.request.user.user_id)
224
224
225 c.is_super_admin = c.auth_user.is_admin
225 c.is_super_admin = c.auth_user.is_admin
226
226
227 c.can_create_repo = c.is_super_admin
227 c.can_create_repo = c.is_super_admin
228 c.can_create_repo_group = c.is_super_admin
228 c.can_create_repo_group = c.is_super_admin
229 c.can_create_user_group = c.is_super_admin
229 c.can_create_user_group = c.is_super_admin
230
230
231 c.is_delegated_admin = False
231 c.is_delegated_admin = False
232
232
233 if not c.auth_user.is_default and not c.is_super_admin:
233 if not c.auth_user.is_default and not c.is_super_admin:
234 c.can_create_repo = h.HasPermissionAny("hg.create.repository")(
234 c.can_create_repo = h.HasPermissionAny("hg.create.repository")(
235 user=self.request.user
235 user=self.request.user
236 )
236 )
237 repositories = c.auth_user.repositories_admin or c.can_create_repo
237 repositories = c.auth_user.repositories_admin or c.can_create_repo
238
238
239 c.can_create_repo_group = h.HasPermissionAny("hg.repogroup.create.true")(
239 c.can_create_repo_group = h.HasPermissionAny("hg.repogroup.create.true")(
240 user=self.request.user
240 user=self.request.user
241 )
241 )
242 repository_groups = (
242 repository_groups = (
243 c.auth_user.repository_groups_admin or c.can_create_repo_group
243 c.auth_user.repository_groups_admin or c.can_create_repo_group
244 )
244 )
245
245
246 c.can_create_user_group = h.HasPermissionAny("hg.usergroup.create.true")(
246 c.can_create_user_group = h.HasPermissionAny("hg.usergroup.create.true")(
247 user=self.request.user
247 user=self.request.user
248 )
248 )
249 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
249 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
250 # delegated admin can create, or manage some objects
250 # delegated admin can create, or manage some objects
251 c.is_delegated_admin = repositories or repository_groups or user_groups
251 c.is_delegated_admin = repositories or repository_groups or user_groups
252 return c
252 return c
253
253
254 def _get_template_context(self, tmpl_args, **kwargs):
254 def _get_template_context(self, tmpl_args, **kwargs):
255 local_tmpl_args = {"defaults": {}, "errors": {}, "c": tmpl_args}
255 local_tmpl_args = {"defaults": {}, "errors": {}, "c": tmpl_args}
256 local_tmpl_args.update(kwargs)
256 local_tmpl_args.update(kwargs)
257 return local_tmpl_args
257 return local_tmpl_args
258
258
259 def load_default_context(self):
259 def load_default_context(self):
260 """
260 """
261 example:
261 example:
262
262
263 def load_default_context(self):
263 def load_default_context(self):
264 c = self._get_local_tmpl_context()
264 c = self._get_local_tmpl_context()
265 c.custom_var = 'foobar'
265 c.custom_var = 'foobar'
266
266
267 return c
267 return c
268 """
268 """
269 raise NotImplementedError("Needs implementation in view class")
269 raise NotImplementedError("Needs implementation in view class")
270
270
271
271
272 class RepoAppView(BaseAppView):
272 class RepoAppView(BaseAppView):
273 def __init__(self, context, request):
273 def __init__(self, context, request):
274 super().__init__(context, request)
274 super().__init__(context, request)
275 self.db_repo = request.db_repo
275 self.db_repo = request.db_repo
276 self.db_repo_name = self.db_repo.repo_name
276 self.db_repo_name = self.db_repo.repo_name
277 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
277 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
278 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
278 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
279 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
279 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
280
280
281 def _handle_missing_requirements(self, error):
281 def _handle_missing_requirements(self, error):
282 log.error(
282 log.error(
283 "Requirements are missing for repository %s: %s",
283 "Requirements are missing for repository %s: %s",
284 self.db_repo_name,
284 self.db_repo_name,
285 safe_str(error),
285 safe_str(error),
286 )
286 )
287
287
288 def _prepare_and_set_clone_url(self, c):
288 def _prepare_and_set_clone_url(self, c):
289 username = ""
289 username = ""
290 if self._rhodecode_user.username != User.DEFAULT_USER:
290 if self._rhodecode_user.username != User.DEFAULT_USER:
291 username = self._rhodecode_user.username
291 username = self._rhodecode_user.username
292
292
293 _def_clone_uri = c.clone_uri_tmpl
293 _def_clone_uri = c.clone_uri_tmpl
294 _def_clone_uri_id = c.clone_uri_id_tmpl
294 _def_clone_uri_id = c.clone_uri_id_tmpl
295 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
295 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
296
296
297 c.clone_repo_url = self.db_repo.clone_url(
297 c.clone_repo_url = self.db_repo.clone_url(
298 user=username, uri_tmpl=_def_clone_uri
298 user=username, uri_tmpl=_def_clone_uri
299 )
299 )
300 c.clone_repo_url_id = self.db_repo.clone_url(
300 c.clone_repo_url_id = self.db_repo.clone_url(
301 user=username, uri_tmpl=_def_clone_uri_id
301 user=username, uri_tmpl=_def_clone_uri_id
302 )
302 )
303 c.clone_repo_url_ssh = self.db_repo.clone_url(
303 c.clone_repo_url_ssh = self.db_repo.clone_url(
304 uri_tmpl=_def_clone_uri_ssh, ssh=True
304 uri_tmpl=_def_clone_uri_ssh, ssh=True
305 )
305 )
306
306
307 def _get_local_tmpl_context(self, include_app_defaults=True):
307 def _get_local_tmpl_context(self, include_app_defaults=True):
308 _ = self.request.translate
308 _ = self.request.translate
309 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
309 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
310
310
311 # register common vars for this type of view
311 # register common vars for this type of view
312 c.rhodecode_db_repo = self.db_repo
312 c.rhodecode_db_repo = self.db_repo
313 c.repo_name = self.db_repo_name
313 c.repo_name = self.db_repo_name
314 c.repository_pull_requests = self.db_repo_pull_requests
314 c.repository_pull_requests = self.db_repo_pull_requests
315 c.repository_artifacts = self.db_repo_artifacts
315 c.repository_artifacts = self.db_repo_artifacts
316 c.repository_is_user_following = ScmModel().is_following_repo(
316 c.repository_is_user_following = ScmModel().is_following_repo(
317 self.db_repo_name, self._rhodecode_user.user_id
317 self.db_repo_name, self._rhodecode_user.user_id
318 )
318 )
319 self.path_filter = PathFilter(None)
319 self.path_filter = PathFilter(None)
320
320
321 c.repository_requirements_missing = {}
321 c.repository_requirements_missing = {}
322 try:
322 try:
323 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
323 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
324 # NOTE(marcink):
324 # NOTE(marcink):
325 # comparison to None since if it's an object __bool__ is expensive to
325 # comparison to None since if it's an object __bool__ is expensive to
326 # calculate
326 # calculate
327 if self.rhodecode_vcs_repo is not None:
327 if self.rhodecode_vcs_repo is not None:
328 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
328 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
329 c.auth_user.username
329 c.auth_user.username
330 )
330 )
331 self.path_filter = PathFilter(path_perms)
331 self.path_filter = PathFilter(path_perms)
332 except RepositoryRequirementError as e:
332 except RepositoryRequirementError as e:
333 c.repository_requirements_missing = {"error": str(e)}
333 c.repository_requirements_missing = {"error": str(e)}
334 self._handle_missing_requirements(e)
334 self._handle_missing_requirements(e)
335 self.rhodecode_vcs_repo = None
335 self.rhodecode_vcs_repo = None
336
336
337 c.path_filter = self.path_filter # used by atom_feed_entry.mako
337 c.path_filter = self.path_filter # used by atom_feed_entry.mako
338
338
339 if self.rhodecode_vcs_repo is None:
339 if self.rhodecode_vcs_repo is None:
340 # unable to fetch this repo as vcs instance, report back to user
340 # unable to fetch this repo as vcs instance, report back to user
341 log.debug(
341 log.debug(
342 "Repository was not found on filesystem, check if it exists or is not damaged"
342 "Repository was not found on filesystem, check if it exists or is not damaged"
343 )
343 )
344 h.flash(
344 h.flash(
345 _(
345 _(
346 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
346 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
347 "Please check if it exist, or is not damaged."
347 "Please check if it exist, or is not damaged."
348 )
348 )
349 % {"repo_name": c.repo_name},
349 % {"repo_name": c.repo_name},
350 category="error",
350 category="error",
351 ignore_duplicate=True,
351 ignore_duplicate=True,
352 )
352 )
353 if c.repository_requirements_missing:
353 if c.repository_requirements_missing:
354 route = self.request.matched_route.name
354 route = self.request.matched_route.name
355 if route.startswith(("edit_repo", "repo_summary")):
355 if route.startswith(("edit_repo", "repo_summary")):
356 # allow summary and edit repo on missing requirements
356 # allow summary and edit repo on missing requirements
357 return c
357 return c
358
358
359 raise HTTPFound(
359 raise HTTPFound(
360 h.route_path("repo_summary", repo_name=self.db_repo_name)
360 h.route_path("repo_summary", repo_name=self.db_repo_name)
361 )
361 )
362
362
363 else: # redirect if we don't show missing requirements
363 else: # redirect if we don't show missing requirements
364 raise HTTPFound(h.route_path("home"))
364 raise HTTPFound(h.route_path("home"))
365
365
366 c.has_origin_repo_read_perm = False
366 c.has_origin_repo_read_perm = False
367 if self.db_repo.fork:
367 if self.db_repo.fork:
368 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
368 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
369 "repository.write", "repository.read", "repository.admin"
369 "repository.write", "repository.read", "repository.admin"
370 )(self.db_repo.fork.repo_name, "summary fork link")
370 )(self.db_repo.fork.repo_name, "summary fork link")
371
371
372 return c
372 return c
373
373
374 def _get_f_path_unchecked(self, matchdict, default=None):
374 def _get_f_path_unchecked(self, matchdict, default=None):
375 """
375 """
376 Should only be used by redirects, everything else should call _get_f_path
376 Should only be used by redirects, everything else should call _get_f_path
377 """
377 """
378 f_path = matchdict.get("f_path")
378 f_path = matchdict.get("f_path")
379 if f_path:
379 if f_path:
380 # fix for multiple initial slashes that causes errors for GIT
380 # fix for multiple initial slashes that causes errors for GIT
381 return f_path.lstrip("/")
381 return f_path.lstrip("/")
382
382
383 return default
383 return default
384
384
385 def _get_f_path(self, matchdict, default=None):
385 def _get_f_path(self, matchdict, default=None):
386 f_path_match = self._get_f_path_unchecked(matchdict, default)
386 f_path_match = self._get_f_path_unchecked(matchdict, default)
387 return self.path_filter.assert_path_permissions(f_path_match)
387 return self.path_filter.assert_path_permissions(f_path_match)
388
388
389 def _get_general_setting(self, target_repo, settings_key, default=False):
389 def _get_general_setting(self, target_repo, settings_key, default=False):
390 settings_model = VcsSettingsModel(repo=target_repo)
390 settings_model = VcsSettingsModel(repo=target_repo)
391 settings = settings_model.get_general_settings()
391 settings = settings_model.get_general_settings()
392 return settings.get(settings_key, default)
392 return settings.get(settings_key, default)
393
393
394 def _get_repo_setting(self, target_repo, settings_key, default=False):
394 def _get_repo_setting(self, target_repo, settings_key, default=False):
395 settings_model = VcsSettingsModel(repo=target_repo)
395 settings_model = VcsSettingsModel(repo=target_repo)
396 settings = settings_model.get_repo_settings_inherited()
396 settings = settings_model.get_repo_settings_inherited()
397 return settings.get(settings_key, default)
397 return settings.get(settings_key, default)
398
398
399 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"):
399 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"):
400 log.debug("Looking for README file at path %s", path)
400 log.debug("Looking for README file at path %s", path)
401 if commit_id:
401 if commit_id:
402 landing_commit_id = commit_id
402 landing_commit_id = commit_id
403 else:
403 else:
404 landing_commit = db_repo.get_landing_commit()
404 landing_commit = db_repo.get_landing_commit()
405 if isinstance(landing_commit, EmptyCommit):
405 if isinstance(landing_commit, EmptyCommit):
406 return None, None
406 return None, None
407 landing_commit_id = landing_commit.raw_id
407 landing_commit_id = landing_commit.raw_id
408
408
409 cache_namespace_uid = f"repo.{db_repo.repo_id}"
409 cache_namespace_uid = f"repo.{db_repo.repo_id}"
410 region = rc_cache.get_or_create_region(
410 region = rc_cache.get_or_create_region(
411 "cache_repo", cache_namespace_uid, use_async_runner=False
411 "cache_repo", cache_namespace_uid, use_async_runner=False
412 )
412 )
413 start = time.time()
413 start = time.time()
414
414
415 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
415 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
416 def generate_repo_readme(
416 def generate_repo_readme(
417 repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type
417 repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type
418 ):
418 ):
419 readme_data = None
419 readme_data = None
420 readme_filename = None
420 readme_filename = None
421
421
422 commit = db_repo.get_commit(_commit_id)
422 commit = db_repo.get_commit(_commit_id)
423 log.debug("Searching for a README file at commit %s.", _commit_id)
423 log.debug("Searching for a README file at commit %s.", _commit_id)
424 readme_node = ReadmeFinder(_renderer_type).search(
424 readme_node = ReadmeFinder(_renderer_type).search(
425 commit, path=_readme_search_path
425 commit, path=_readme_search_path
426 )
426 )
427
427
428 if readme_node:
428 if readme_node:
429 log.debug("Found README node: %s", readme_node)
429 log.debug("Found README node: %s", readme_node)
430
430
431 relative_urls = {
431 relative_urls = {
432 "raw": h.route_path(
432 "raw": h.route_path(
433 "repo_file_raw",
433 "repo_file_raw",
434 repo_name=_repo_name,
434 repo_name=_repo_name,
435 commit_id=commit.raw_id,
435 commit_id=commit.raw_id,
436 f_path=readme_node.path,
436 f_path=readme_node.path,
437 ),
437 ),
438 "standard": h.route_path(
438 "standard": h.route_path(
439 "repo_files",
439 "repo_files",
440 repo_name=_repo_name,
440 repo_name=_repo_name,
441 commit_id=commit.raw_id,
441 commit_id=commit.raw_id,
442 f_path=readme_node.path,
442 f_path=readme_node.path,
443 ),
443 ),
444 }
444 }
445
445
446 readme_data = self._render_readme_or_none(
446 readme_data = self._render_readme_or_none(
447 commit, readme_node, relative_urls
447 commit, readme_node, relative_urls
448 )
448 )
449 readme_filename = readme_node.str_path
449 readme_filename = readme_node.str_path
450
450
451 return readme_data, readme_filename
451 return readme_data, readme_filename
452
452
453 readme_data, readme_filename = generate_repo_readme(
453 readme_data, readme_filename = generate_repo_readme(
454 db_repo.repo_id,
454 db_repo.repo_id,
455 landing_commit_id,
455 landing_commit_id,
456 db_repo.repo_name,
456 db_repo.repo_name,
457 path,
457 path,
458 renderer_type,
458 renderer_type,
459 )
459 )
460
460
461 compute_time = time.time() - start
461 compute_time = time.time() - start
462 log.debug(
462 log.debug(
463 "Repo README for path %s generated and computed in %.4fs",
463 "Repo README for path %s generated and computed in %.4fs",
464 path,
464 path,
465 compute_time,
465 compute_time,
466 )
466 )
467 return readme_data, readme_filename
467 return readme_data, readme_filename
468
468
469 def _render_readme_or_none(self, commit, readme_node, relative_urls):
469 def _render_readme_or_none(self, commit, readme_node, relative_urls):
470 log.debug("Found README file `%s` rendering...", readme_node.path)
470 log.debug("Found README file `%s` rendering...", readme_node.path)
471 renderer = MarkupRenderer()
471 renderer = MarkupRenderer()
472 try:
472 try:
473 html_source = renderer.render(
473 html_source = renderer.render(
474 readme_node.str_content, filename=readme_node.path
474 readme_node.str_content, filename=readme_node.path
475 )
475 )
476 if relative_urls:
476 if relative_urls:
477 return relative_links(html_source, relative_urls)
477 return relative_links(html_source, relative_urls)
478 return html_source
478 return html_source
479 except Exception:
479 except Exception:
480 log.exception("Exception while trying to render the README")
480 log.exception("Exception while trying to render the README")
481
481
482 def get_recache_flag(self):
482 def get_recache_flag(self):
483 for flag_name in ["force_recache", "force-recache", "no-cache"]:
483 for flag_name in ["force_recache", "force-recache", "no-cache"]:
484 flag_val = self.request.GET.get(flag_name)
484 flag_val = self.request.GET.get(flag_name)
485 if str2bool(flag_val):
485 if str2bool(flag_val):
486 return True
486 return True
487 return False
487 return False
488
488
489 def get_commit_preload_attrs(cls):
489 def get_commit_preload_attrs(cls):
490 pre_load = [
490 pre_load = [
491 "author",
491 "author",
492 "branch",
492 "branch",
493 "date",
493 "date",
494 "message",
494 "message",
495 "parents",
495 "parents",
496 "obsolete",
496 "obsolete",
497 "phase",
497 "phase",
498 "hidden",
498 "hidden",
499 ]
499 ]
500 return pre_load
500 return pre_load
501
501
502
502
503 class PathFilter(object):
503 class PathFilter(object):
504 # Expects and instance of BasePathPermissionChecker or None
504 # Expects and instance of BasePathPermissionChecker or None
505 def __init__(self, permission_checker):
505 def __init__(self, permission_checker):
506 self.permission_checker = permission_checker
506 self.permission_checker = permission_checker
507
507
508 def assert_path_permissions(self, path):
508 def assert_path_permissions(self, path):
509 if self.path_access_allowed(path):
509 if self.path_access_allowed(path):
510 return path
510 return path
511 raise HTTPForbidden()
511 raise HTTPForbidden()
512
512
513 def path_access_allowed(self, path):
513 def path_access_allowed(self, path):
514 log.debug("Checking ACL permissions for PathFilter for `%s`", path)
514 log.debug("Checking ACL permissions for PathFilter for `%s`", path)
515 if self.permission_checker:
515 if self.permission_checker:
516 has_access = path and self.permission_checker.has_access(path)
516 has_access = path and self.permission_checker.has_access(path)
517 log.debug(
517 log.debug(
518 "ACL Permissions checker enabled, ACL Check has_access: %s", has_access
518 "ACL Permissions checker enabled, ACL Check has_access: %s", has_access
519 )
519 )
520 return has_access
520 return has_access
521
521
522 log.debug("ACL permissions checker not enabled, skipping...")
522 log.debug("ACL permissions checker not enabled, skipping...")
523 return True
523 return True
524
524
525 def filter_patchset(self, patchset):
525 def filter_patchset(self, patchset):
526 if not self.permission_checker or not patchset:
526 if not self.permission_checker or not patchset:
527 return patchset, False
527 return patchset, False
528 had_filtered = False
528 had_filtered = False
529 filtered_patchset = []
529 filtered_patchset = []
530 for patch in patchset:
530 for patch in patchset:
531 filename = patch.get("filename", None)
531 filename = patch.get("filename", None)
532 if not filename or self.permission_checker.has_access(filename):
532 if not filename or self.permission_checker.has_access(filename):
533 filtered_patchset.append(patch)
533 filtered_patchset.append(patch)
534 else:
534 else:
535 had_filtered = True
535 had_filtered = True
536 if had_filtered:
536 if had_filtered:
537 if isinstance(patchset, diffs.LimitedDiffContainer):
537 if isinstance(patchset, diffs.LimitedDiffContainer):
538 filtered_patchset = diffs.LimitedDiffContainer(
538 filtered_patchset = diffs.LimitedDiffContainer(
539 patchset.diff_limit, patchset.cur_diff_size, filtered_patchset
539 patchset.diff_limit, patchset.cur_diff_size, filtered_patchset
540 )
540 )
541 return filtered_patchset, True
541 return filtered_patchset, True
542 else:
542 else:
543 return patchset, False
543 return patchset, False
544
544
545 def render_patchset_filtered(
545 def render_patchset_filtered(
546 self, diffset, patchset, source_ref=None, target_ref=None
546 self, diffset, patchset, source_ref=None, target_ref=None
547 ):
547 ):
548 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
548 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
549 result = diffset.render_patchset(
549 result = diffset.render_patchset(
550 filtered_patchset, source_ref=source_ref, target_ref=target_ref
550 filtered_patchset, source_ref=source_ref, target_ref=target_ref
551 )
551 )
552 result.has_hidden_changes = has_hidden_changes
552 result.has_hidden_changes = has_hidden_changes
553 return result
553 return result
554
554
555 def get_raw_patch(self, diff_processor):
555 def get_raw_patch(self, diff_processor):
556 if self.permission_checker is None:
556 if self.permission_checker is None:
557 return diff_processor.as_raw()
557 return diff_processor.as_raw()
558 elif self.permission_checker.has_full_access:
558 elif self.permission_checker.has_full_access:
559 return diff_processor.as_raw()
559 return diff_processor.as_raw()
560 else:
560 else:
561 return "# Repository has user-specific filters, raw patch generation is disabled."
561 return "# Repository has user-specific filters, raw patch generation is disabled."
562
562
563 @property
563 @property
564 def is_enabled(self):
564 def is_enabled(self):
565 return self.permission_checker is not None
565 return self.permission_checker is not None
566
566
567
567
568 class RepoGroupAppView(BaseAppView):
568 class RepoGroupAppView(BaseAppView):
569 def __init__(self, context, request):
569 def __init__(self, context, request):
570 super().__init__(context, request)
570 super().__init__(context, request)
571 self.db_repo_group = request.db_repo_group
571 self.db_repo_group = request.db_repo_group
572 self.db_repo_group_name = self.db_repo_group.group_name
572 self.db_repo_group_name = self.db_repo_group.group_name
573
573
574 def _get_local_tmpl_context(self, include_app_defaults=True):
574 def _get_local_tmpl_context(self, include_app_defaults=True):
575 _ = self.request.translate
575 _ = self.request.translate
576 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
576 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
577 c.repo_group = self.db_repo_group
577 c.repo_group = self.db_repo_group
578 return c
578 return c
579
579
580 def _revoke_perms_on_yourself(self, form_result):
580 def _revoke_perms_on_yourself(self, form_result):
581 _updates = [
581 _updates = [
582 u
582 u
583 for u in form_result["perm_updates"]
583 for u in form_result["perm_updates"]
584 if self._rhodecode_user.user_id == int(u[0])
584 if self._rhodecode_user.user_id == int(u[0])
585 ]
585 ]
586 _additions = [
586 _additions = [
587 u
587 u
588 for u in form_result["perm_additions"]
588 for u in form_result["perm_additions"]
589 if self._rhodecode_user.user_id == int(u[0])
589 if self._rhodecode_user.user_id == int(u[0])
590 ]
590 ]
591 _deletions = [
591 _deletions = [
592 u
592 u
593 for u in form_result["perm_deletions"]
593 for u in form_result["perm_deletions"]
594 if self._rhodecode_user.user_id == int(u[0])
594 if self._rhodecode_user.user_id == int(u[0])
595 ]
595 ]
596 admin_perm = "group.admin"
596 admin_perm = "group.admin"
597 if (
597 if (
598 _updates
598 _updates
599 and _updates[0][1] != admin_perm
599 and _updates[0][1] != admin_perm
600 or _additions
600 or _additions
601 and _additions[0][1] != admin_perm
601 and _additions[0][1] != admin_perm
602 or _deletions
602 or _deletions
603 and _deletions[0][1] != admin_perm
603 and _deletions[0][1] != admin_perm
604 ):
604 ):
605 return True
605 return True
606 return False
606 return False
607
607
608
608
609 class UserGroupAppView(BaseAppView):
609 class UserGroupAppView(BaseAppView):
610 def __init__(self, context, request):
610 def __init__(self, context, request):
611 super().__init__(context, request)
611 super().__init__(context, request)
612 self.db_user_group = request.db_user_group
612 self.db_user_group = request.db_user_group
613 self.db_user_group_name = self.db_user_group.users_group_name
613 self.db_user_group_name = self.db_user_group.users_group_name
614
614
615
615
616 class UserAppView(BaseAppView):
616 class UserAppView(BaseAppView):
617 def __init__(self, context, request):
617 def __init__(self, context, request):
618 super().__init__(context, request)
618 super().__init__(context, request)
619 self.db_user = request.db_user
619 self.db_user = request.db_user
620 self.db_user_id = self.db_user.user_id
620 self.db_user_id = self.db_user.user_id
621
621
622 _ = self.request.translate
622 _ = self.request.translate
623 if not request.db_user_supports_default:
623 if not request.db_user_supports_default:
624 if self.db_user.username == User.DEFAULT_USER:
624 if self.db_user.username == User.DEFAULT_USER:
625 h.flash(
625 h.flash(
626 _("Editing user `{}` is disabled.".format(User.DEFAULT_USER)),
626 _("Editing user `{}` is disabled.".format(User.DEFAULT_USER)),
627 category="warning",
627 category="warning",
628 )
628 )
629 raise HTTPFound(h.route_path("users"))
629 raise HTTPFound(h.route_path("users"))
630
630
631
631
632 class DataGridAppView(object):
632 class DataGridAppView(object):
633 """
633 """
634 Common class to have re-usable grid rendering components
634 Common class to have re-usable grid rendering components
635 """
635 """
636
636
637 def _extract_ordering(self, request, column_map=None):
637 def _extract_ordering(self, request, column_map=None):
638 column_map = column_map or {}
638 column_map = column_map or {}
639 column_index = safe_int(request.GET.get("order[0][column]"))
639 column_index = safe_int(request.GET.get("order[0][column]"))
640 order_dir = request.GET.get("order[0][dir]", "desc")
640 order_dir = request.GET.get("order[0][dir]", "desc")
641 order_by = request.GET.get("columns[%s][data][sort]" % column_index, "name_raw")
641 order_by = request.GET.get("columns[%s][data][sort]" % column_index, "name_raw")
642
642
643 # translate datatable to DB columns
643 # translate datatable to DB columns
644 order_by = column_map.get(order_by) or order_by
644 order_by = column_map.get(order_by) or order_by
645
645
646 search_q = request.GET.get("search[value]")
646 search_q = request.GET.get("search[value]")
647 return search_q, order_by, order_dir
647 return search_q, order_by, order_dir
648
648
649 def _extract_chunk(self, request):
649 def _extract_chunk(self, request):
650 start = safe_int(request.GET.get("start"), 0)
650 start = safe_int(request.GET.get("start"), 0)
651 length = safe_int(request.GET.get("length"), 25)
651 length = safe_int(request.GET.get("length"), 25)
652 draw = safe_int(request.GET.get("draw"))
652 draw = safe_int(request.GET.get("draw"))
653 return draw, start, length
653 return draw, start, length
654
654
655 def _get_order_col(self, order_by, model):
655 def _get_order_col(self, order_by, model):
656 if isinstance(order_by, str):
656 if isinstance(order_by, str):
657 try:
657 try:
658 return operator.attrgetter(order_by)(model)
658 return operator.attrgetter(order_by)(model)
659 except AttributeError:
659 except AttributeError:
660 return None
660 return None
661 else:
661 else:
662 return order_by
662 return order_by
663
663
664
664
665 class BaseReferencesView(RepoAppView):
665 class BaseReferencesView(RepoAppView):
666 """
666 """
667 Base for reference view for branches, tags and bookmarks.
667 Base for reference view for branches, tags and bookmarks.
668 """
668 """
669
669
670 def load_default_context(self):
670 def load_default_context(self):
671 c = self._get_local_tmpl_context()
671 c = self._get_local_tmpl_context()
672 return c
672 return c
673
673
674 def load_refs_context(self, ref_items, partials_template):
674 def load_refs_context(self, ref_items, partials_template):
675 _render = self.request.get_partial_renderer(partials_template)
675 _render = self.request.get_partial_renderer(partials_template)
676 pre_load = ["author", "date", "message", "parents"]
676 pre_load = ["author", "date", "message", "parents"]
677
677
678 is_svn = h.is_svn(self.rhodecode_vcs_repo)
678 is_svn = h.is_svn(self.rhodecode_vcs_repo)
679 is_hg = h.is_hg(self.rhodecode_vcs_repo)
679 is_hg = h.is_hg(self.rhodecode_vcs_repo)
680
680
681 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
681 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
682
682
683 closed_refs = {}
683 closed_refs = {}
684 if is_hg:
684 if is_hg:
685 closed_refs = self.rhodecode_vcs_repo.branches_closed
685 closed_refs = self.rhodecode_vcs_repo.branches_closed
686
686
687 data = []
687 data = []
688 for ref_name, commit_id in ref_items:
688 for ref_name, commit_id in ref_items:
689 commit = self.rhodecode_vcs_repo.get_commit(
689 commit = self.rhodecode_vcs_repo.get_commit(
690 commit_id=commit_id, pre_load=pre_load
690 commit_id=commit_id, pre_load=pre_load
691 )
691 )
692 closed = ref_name in closed_refs
692 closed = ref_name in closed_refs
693
693
694 # TODO: johbo: Unify generation of reference links
694 # TODO: johbo: Unify generation of reference links
695 use_commit_id = "/" in ref_name or is_svn
695 use_commit_id = "/" in ref_name or is_svn
696
696
697 if use_commit_id:
697 if use_commit_id:
698 files_url = h.route_path(
698 files_url = h.route_path(
699 "repo_files",
699 "repo_files",
700 repo_name=self.db_repo_name,
700 repo_name=self.db_repo_name,
701 f_path=ref_name if is_svn else "",
701 f_path=ref_name if is_svn else "",
702 commit_id=commit_id,
702 commit_id=commit_id,
703 _query=dict(at=ref_name),
703 _query=dict(at=ref_name),
704 )
704 )
705
705
706 else:
706 else:
707 files_url = h.route_path(
707 files_url = h.route_path(
708 "repo_files",
708 "repo_files",
709 repo_name=self.db_repo_name,
709 repo_name=self.db_repo_name,
710 f_path=ref_name if is_svn else "",
710 f_path=ref_name if is_svn else "",
711 commit_id=ref_name,
711 commit_id=ref_name,
712 _query=dict(at=ref_name),
712 _query=dict(at=ref_name),
713 )
713 )
714
714
715 data.append(
715 data.append(
716 {
716 {
717 "name": _render("name", ref_name, files_url, closed),
717 "name": _render("name", ref_name, files_url, closed),
718 "name_raw": ref_name,
718 "name_raw": ref_name,
719 "date": _render("date", commit.date),
719 "date": _render("date", commit.date),
720 "date_raw": datetime_to_time(commit.date),
720 "date_raw": datetime_to_time(commit.date),
721 "author": _render("author", commit.author),
721 "author": _render("author", commit.author),
722 "commit": _render(
722 "commit": _render(
723 "commit", commit.message, commit.raw_id, commit.idx
723 "commit", commit.message, commit.raw_id, commit.idx
724 ),
724 ),
725 "commit_raw": commit.idx,
725 "commit_raw": commit.idx,
726 "compare": _render(
726 "compare": _render(
727 "compare", format_ref_id(ref_name, commit.raw_id)
727 "compare", format_ref_id(ref_name, commit.raw_id)
728 ),
728 ),
729 }
729 }
730 )
730 )
731
731
732 return data
732 return data
733
733
734
734
735 class RepoRoutePredicate(object):
735 class RepoRoutePredicate(object):
736 def __init__(self, val, config):
736 def __init__(self, val, config):
737 self.val = val
737 self.val = val
738
738
739 def text(self):
739 def text(self):
740 return f"repo_route = {self.val}"
740 return f"repo_route = {self.val}"
741
741
742 phash = text
742 phash = text
743
743
744 def __call__(self, info, request):
744 def __call__(self, info, request):
745 if hasattr(request, "vcs_call"):
745 if hasattr(request, "vcs_call"):
746 # skip vcs calls
746 # skip vcs calls
747 return
747 return
748
748
749 repo_name = info["match"]["repo_name"]
749 repo_name = info["match"]["repo_name"]
750
750
751 repo_name_parts = repo_name.split("/")
751 repo_name_parts = repo_name.split("/")
752 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
752 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
753
753
754 if repo_name_parts != repo_slugs:
754 if repo_name_parts != repo_slugs:
755 # short-skip if the repo-name doesn't follow slug rule
755 # short-skip if the repo-name doesn't follow slug rule
756 log.warning(
756 log.warning(
757 "repo_name: %s is different than slug %s", repo_name_parts, repo_slugs
757 "repo_name: %s is different than slug %s", repo_name_parts, repo_slugs
758 )
758 )
759 return False
759 return False
760
760
761 repo_model = repo.RepoModel()
761 repo_model = repo.RepoModel()
762
762
763 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
763 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
764
764
765 def redirect_if_creating(route_info, db_repo):
765 def redirect_if_creating(route_info, db_repo):
766 skip_views = ["edit_repo_advanced_delete"]
766 skip_views = ["edit_repo_advanced_delete"]
767 route = route_info["route"]
767 route = route_info["route"]
768 # we should skip delete view so we can actually "remove" repositories
768 # we should skip delete view so we can actually "remove" repositories
769 # if they get stuck in creating state.
769 # if they get stuck in creating state.
770 if route.name in skip_views:
770 if route.name in skip_views:
771 return
771 return
772
772
773 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
773 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
774 repo_creating_url = request.route_path(
774 repo_creating_url = request.route_path(
775 "repo_creating", repo_name=db_repo.repo_name
775 "repo_creating", repo_name=db_repo.repo_name
776 )
776 )
777 raise HTTPFound(repo_creating_url)
777 raise HTTPFound(repo_creating_url)
778
778
779 if by_name_match:
779 if by_name_match:
780 # register this as request object we can re-use later
780 # register this as request object we can re-use later
781 request.db_repo = by_name_match
781 request.db_repo = by_name_match
782 request.db_repo_name = request.db_repo.repo_name
782 request.db_repo_name = request.db_repo.repo_name
783
783
784 redirect_if_creating(info, by_name_match)
784 redirect_if_creating(info, by_name_match)
785 return True
785 return True
786
786
787 by_id_match = repo_model.get_repo_by_id(repo_name)
787 by_id_match = repo_model.get_repo_by_id(repo_name)
788 if by_id_match:
788 if by_id_match:
789 request.db_repo = by_id_match
789 request.db_repo = by_id_match
790 request.db_repo_name = request.db_repo.repo_name
790 request.db_repo_name = request.db_repo.repo_name
791 redirect_if_creating(info, by_id_match)
791 redirect_if_creating(info, by_id_match)
792 return True
792 return True
793
793
794 return False
794 return False
795
795
796
796
797 class RepoForbidArchivedRoutePredicate(object):
797 class RepoForbidArchivedRoutePredicate(object):
798 def __init__(self, val, config):
798 def __init__(self, val, config):
799 self.val = val
799 self.val = val
800
800
801 def text(self):
801 def text(self):
802 return f"repo_forbid_archived = {self.val}"
802 return f"repo_forbid_archived = {self.val}"
803
803
804 phash = text
804 phash = text
805
805
806 def __call__(self, info, request):
806 def __call__(self, info, request):
807 _ = request.translate
807 _ = request.translate
808 rhodecode_db_repo = request.db_repo
808 rhodecode_db_repo = request.db_repo
809
809
810 log.debug(
810 log.debug(
811 "%s checking if archived flag for repo for %s",
811 "%s checking if archived flag for repo for %s",
812 self.__class__.__name__,
812 self.__class__.__name__,
813 rhodecode_db_repo.repo_name,
813 rhodecode_db_repo.repo_name,
814 )
814 )
815
815
816 if rhodecode_db_repo.archived:
816 if rhodecode_db_repo.archived:
817 log.warning(
817 log.warning(
818 "Current view is not supported for archived repo:%s",
818 "Current view is not supported for archived repo:%s",
819 rhodecode_db_repo.repo_name,
819 rhodecode_db_repo.repo_name,
820 )
820 )
821
821
822 h.flash(
822 h.flash(
823 h.literal(_("Action not supported for archived repository.")),
823 h.literal(_("Action not supported for archived repository.")),
824 category="warning",
824 category="warning",
825 )
825 )
826 summary_url = request.route_path(
826 summary_url = request.route_path(
827 "repo_summary", repo_name=rhodecode_db_repo.repo_name
827 "repo_summary", repo_name=rhodecode_db_repo.repo_name
828 )
828 )
829 raise HTTPFound(summary_url)
829 raise HTTPFound(summary_url)
830 return True
830 return True
831
831
832
832
833 class RepoTypeRoutePredicate(object):
833 class RepoTypeRoutePredicate(object):
834 def __init__(self, val, config):
834 def __init__(self, val, config):
835 self.val = val or ["hg", "git", "svn"]
835 self.val = val or ["hg", "git", "svn"]
836
836
837 def text(self):
837 def text(self):
838 return f"repo_accepted_type = {self.val}"
838 return f"repo_accepted_type = {self.val}"
839
839
840 phash = text
840 phash = text
841
841
842 def __call__(self, info, request):
842 def __call__(self, info, request):
843 if hasattr(request, "vcs_call"):
843 if hasattr(request, "vcs_call"):
844 # skip vcs calls
844 # skip vcs calls
845 return
845 return
846
846
847 rhodecode_db_repo = request.db_repo
847 rhodecode_db_repo = request.db_repo
848
848
849 log.debug(
849 log.debug(
850 "%s checking repo type for %s in %s",
850 "%s checking repo type for %s in %s",
851 self.__class__.__name__,
851 self.__class__.__name__,
852 rhodecode_db_repo.repo_type,
852 rhodecode_db_repo.repo_type,
853 self.val,
853 self.val,
854 )
854 )
855
855
856 if rhodecode_db_repo.repo_type in self.val:
856 if rhodecode_db_repo.repo_type in self.val:
857 return True
857 return True
858 else:
858 else:
859 log.warning(
859 log.warning(
860 "Current view is not supported for repo type:%s",
860 "Current view is not supported for repo type:%s",
861 rhodecode_db_repo.repo_type,
861 rhodecode_db_repo.repo_type,
862 )
862 )
863 return False
863 return False
864
864
865
865
866 class RepoGroupRoutePredicate(object):
866 class RepoGroupRoutePredicate(object):
867 def __init__(self, val, config):
867 def __init__(self, val, config):
868 self.val = val
868 self.val = val
869
869
870 def text(self):
870 def text(self):
871 return f"repo_group_route = {self.val}"
871 return f"repo_group_route = {self.val}"
872
872
873 phash = text
873 phash = text
874
874
875 def __call__(self, info, request):
875 def __call__(self, info, request):
876 if hasattr(request, "vcs_call"):
876 if hasattr(request, "vcs_call"):
877 # skip vcs calls
877 # skip vcs calls
878 return
878 return
879
879
880 repo_group_name = info["match"]["repo_group_name"]
880 repo_group_name = info["match"]["repo_group_name"]
881
881
882 repo_group_name_parts = repo_group_name.split("/")
882 repo_group_name_parts = repo_group_name.split("/")
883 repo_group_slugs = [
883 repo_group_slugs = [
884 x for x in [repo_name_slug(x) for x in repo_group_name_parts]
884 x for x in [repo_name_slug(x) for x in repo_group_name_parts]
885 ]
885 ]
886 if repo_group_name_parts != repo_group_slugs:
886 if repo_group_name_parts != repo_group_slugs:
887 # short-skip if the repo-name doesn't follow slug rule
887 # short-skip if the repo-name doesn't follow slug rule
888 log.warning(
888 log.warning(
889 "repo_group_name: %s is different than slug %s",
889 "repo_group_name: %s is different than slug %s",
890 repo_group_name_parts,
890 repo_group_name_parts,
891 repo_group_slugs,
891 repo_group_slugs,
892 )
892 )
893 return False
893 return False
894
894
895 repo_group_model = repo_group.RepoGroupModel()
895 repo_group_model = repo_group.RepoGroupModel()
896 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
896 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
897
897
898 if by_name_match:
898 if by_name_match:
899 # register this as request object we can re-use later
899 # register this as request object we can re-use later
900 request.db_repo_group = by_name_match
900 request.db_repo_group = by_name_match
901 request.db_repo_group_name = request.db_repo_group.group_name
901 request.db_repo_group_name = request.db_repo_group.group_name
902 return True
902 return True
903
903
904 return False
904 return False
905
905
906
906
907 class UserGroupRoutePredicate(object):
907 class UserGroupRoutePredicate(object):
908 def __init__(self, val, config):
908 def __init__(self, val, config):
909 self.val = val
909 self.val = val
910
910
911 def text(self):
911 def text(self):
912 return f"user_group_route = {self.val}"
912 return f"user_group_route = {self.val}"
913
913
914 phash = text
914 phash = text
915
915
916 def __call__(self, info, request):
916 def __call__(self, info, request):
917 if hasattr(request, "vcs_call"):
917 if hasattr(request, "vcs_call"):
918 # skip vcs calls
918 # skip vcs calls
919 return
919 return
920
920
921 user_group_id = info["match"]["user_group_id"]
921 user_group_id = info["match"]["user_group_id"]
922 user_group_model = user_group.UserGroup()
922 user_group_model = user_group.UserGroup()
923 by_id_match = user_group_model.get(user_group_id, cache=False)
923 by_id_match = user_group_model.get(user_group_id, cache=False)
924
924
925 if by_id_match:
925 if by_id_match:
926 # register this as request object we can re-use later
926 # register this as request object we can re-use later
927 request.db_user_group = by_id_match
927 request.db_user_group = by_id_match
928 return True
928 return True
929
929
930 return False
930 return False
931
931
932
932
933 class UserRoutePredicateBase(object):
933 class UserRoutePredicateBase(object):
934 supports_default = None
934 supports_default = None
935
935
936 def __init__(self, val, config):
936 def __init__(self, val, config):
937 self.val = val
937 self.val = val
938
938
939 def text(self):
939 def text(self):
940 raise NotImplementedError()
940 raise NotImplementedError()
941
941
942 def __call__(self, info, request):
942 def __call__(self, info, request):
943 if hasattr(request, "vcs_call"):
943 if hasattr(request, "vcs_call"):
944 # skip vcs calls
944 # skip vcs calls
945 return
945 return
946
946
947 user_id = info["match"]["user_id"]
947 user_id = info["match"]["user_id"]
948 user_model = user.User()
948 user_model = user.User()
949 by_id_match = user_model.get(user_id, cache=False)
949 by_id_match = user_model.get(user_id, cache=False)
950
950
951 if by_id_match:
951 if by_id_match:
952 # register this as request object we can re-use later
952 # register this as request object we can re-use later
953 request.db_user = by_id_match
953 request.db_user = by_id_match
954 request.db_user_supports_default = self.supports_default
954 request.db_user_supports_default = self.supports_default
955 return True
955 return True
956
956
957 return False
957 return False
958
958
959
959
960 class UserRoutePredicate(UserRoutePredicateBase):
960 class UserRoutePredicate(UserRoutePredicateBase):
961 supports_default = False
961 supports_default = False
962
962
963 def text(self):
963 def text(self):
964 return f"user_route = {self.val}"
964 return f"user_route = {self.val}"
965
965
966 phash = text
966 phash = text
967
967
968
968
969 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
969 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
970 supports_default = True
970 supports_default = True
971
971
972 def text(self):
972 def text(self):
973 return f"user_with_default_route = {self.val}"
973 return f"user_with_default_route = {self.val}"
974
974
975 phash = text
975 phash = text
976
976
977
977
978 def includeme(config):
978 def includeme(config):
979 config.add_route_predicate("repo_route", RepoRoutePredicate)
979 config.add_route_predicate("repo_route", RepoRoutePredicate)
980 config.add_route_predicate("repo_accepted_types", RepoTypeRoutePredicate)
980 config.add_route_predicate("repo_accepted_types", RepoTypeRoutePredicate)
981 config.add_route_predicate(
981 config.add_route_predicate(
982 "repo_forbid_when_archived", RepoForbidArchivedRoutePredicate
982 "repo_forbid_when_archived", RepoForbidArchivedRoutePredicate
983 )
983 )
984 config.add_route_predicate("repo_group_route", RepoGroupRoutePredicate)
984 config.add_route_predicate("repo_group_route", RepoGroupRoutePredicate)
985 config.add_route_predicate("user_group_route", UserGroupRoutePredicate)
985 config.add_route_predicate("user_group_route", UserGroupRoutePredicate)
986 config.add_route_predicate("user_route_with_default", UserRouteWithDefaultPredicate)
986 config.add_route_predicate("user_route_with_default", UserRouteWithDefaultPredicate)
987 config.add_route_predicate("user_route", UserRoutePredicate)
987 config.add_route_predicate("user_route", UserRoutePredicate)
@@ -1,553 +1,553 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import time
19 import time
20 import json
20 import json
21 import pyotp
21 import pyotp
22 import qrcode
22 import qrcode
23 import collections
23 import collections
24 import datetime
24 import datetime
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import logging
27 import logging
28 import urllib.parse
28 import urllib.parse
29 import requests
29 import requests
30 from io import BytesIO
30 from io import BytesIO
31 from base64 import b64encode
31 from base64 import b64encode
32
32
33 from pyramid.renderers import render
33 from pyramid.renderers import render
34 from pyramid.response import Response
34 from pyramid.response import Response
35 from pyramid.httpexceptions import HTTPFound
35 from pyramid.httpexceptions import HTTPFound
36
36
37 import rhodecode
37 import rhodecode
38 from rhodecode.apps._base import BaseAppView
38 from rhodecode.apps._base import BaseAppView
39 from rhodecode.authentication.base import authenticate, HTTP_TYPE
39 from rhodecode.authentication.base import authenticate, HTTP_TYPE
40 from rhodecode.authentication.plugins import auth_rhodecode
40 from rhodecode.authentication.plugins import auth_rhodecode
41 from rhodecode.events import UserRegistered, trigger
41 from rhodecode.events import UserRegistered, trigger
42 from rhodecode.lib import helpers as h
42 from rhodecode.lib import helpers as h
43 from rhodecode.lib import audit_logger
43 from rhodecode.lib import audit_logger
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous)
45 AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous)
46 from rhodecode.lib.base import get_ip_addr
46 from rhodecode.lib.base import get_ip_addr
47 from rhodecode.lib.exceptions import UserCreationError
47 from rhodecode.lib.exceptions import UserCreationError
48 from rhodecode.lib.utils2 import safe_str
48 from rhodecode.lib.utils2 import safe_str
49 from rhodecode.model.db import User, UserApiKeys
49 from rhodecode.model.db import User, UserApiKeys
50 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
50 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.auth_token import AuthTokenModel
52 from rhodecode.model.auth_token import AuthTokenModel
53 from rhodecode.model.settings import SettingsModel
53 from rhodecode.model.settings import SettingsModel
54 from rhodecode.model.user import UserModel
54 from rhodecode.model.user import UserModel
55 from rhodecode.translation import _
55 from rhodecode.translation import _
56
56
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60 CaptchaData = collections.namedtuple(
60 CaptchaData = collections.namedtuple(
61 'CaptchaData', 'active, private_key, public_key')
61 'CaptchaData', 'active, private_key, public_key')
62
62
63
63
64 def store_user_in_session(session, user_identifier, remember=False):
64 def store_user_in_session(session, user_identifier, remember=False):
65 user = User.get_by_username_or_primary_email(user_identifier)
65 user = User.get_by_username_or_primary_email(user_identifier)
66 auth_user = AuthUser(user.user_id)
66 auth_user = AuthUser(user.user_id)
67 auth_user.set_authenticated()
67 auth_user.set_authenticated()
68 cs = auth_user.get_cookie_store()
68 cs = auth_user.get_cookie_store()
69 session['rhodecode_user'] = cs
69 session['rhodecode_user'] = cs
70 user.update_lastlogin()
70 user.update_lastlogin()
71 Session().commit()
71 Session().commit()
72
72
73 # If they want to be remembered, update the cookie
73 # If they want to be remembered, update the cookie
74 if remember:
74 if remember:
75 _year = (datetime.datetime.now() +
75 _year = (datetime.datetime.now() +
76 datetime.timedelta(seconds=60 * 60 * 24 * 365))
76 datetime.timedelta(seconds=60 * 60 * 24 * 365))
77 session._set_cookie_expires(_year)
77 session._set_cookie_expires(_year)
78
78
79 session.save()
79 session.save()
80
80
81 safe_cs = cs.copy()
81 safe_cs = cs.copy()
82 safe_cs['password'] = '****'
82 safe_cs['password'] = '****'
83 log.info('user %s is now authenticated and stored in '
83 log.info('user %s is now authenticated and stored in '
84 'session, session attrs %s', user_identifier, safe_cs)
84 'session, session attrs %s', user_identifier, safe_cs)
85
85
86 # dumps session attrs back to cookie
86 # dumps session attrs back to cookie
87 session._update_cookie_out()
87 session._update_cookie_out()
88 # we set new cookie
88 # we set new cookie
89 headers = None
89 headers = None
90 if session.request['set_cookie']:
90 if session.request['set_cookie']:
91 # send set-cookie headers back to response to update cookie
91 # send set-cookie headers back to response to update cookie
92 headers = [('Set-Cookie', session.request['cookie_out'])]
92 headers = [('Set-Cookie', session.request['cookie_out'])]
93 return headers
93 return headers
94
94
95
95
96 def get_came_from(request):
96 def get_came_from(request):
97 came_from = safe_str(request.GET.get('came_from', ''))
97 came_from = safe_str(request.GET.get('came_from', ''))
98 parsed = urllib.parse.urlparse(came_from)
98 parsed = urllib.parse.urlparse(came_from)
99
99
100 allowed_schemes = ['http', 'https']
100 allowed_schemes = ['http', 'https']
101 default_came_from = h.route_path('home')
101 default_came_from = h.route_path('home')
102 if parsed.scheme and parsed.scheme not in allowed_schemes:
102 if parsed.scheme and parsed.scheme not in allowed_schemes:
103 log.error('Suspicious URL scheme detected %s for url %s',
103 log.error('Suspicious URL scheme detected %s for url %s',
104 parsed.scheme, parsed)
104 parsed.scheme, parsed)
105 came_from = default_came_from
105 came_from = default_came_from
106 elif parsed.netloc and request.host != parsed.netloc:
106 elif parsed.netloc and request.host != parsed.netloc:
107 log.error('Suspicious NETLOC detected %s for url %s server url '
107 log.error('Suspicious NETLOC detected %s for url %s server url '
108 'is: %s', parsed.netloc, parsed, request.host)
108 'is: %s', parsed.netloc, parsed, request.host)
109 came_from = default_came_from
109 came_from = default_came_from
110 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
110 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
111 log.error('Header injection detected `%s` for url %s server url ',
111 log.error('Header injection detected `%s` for url %s server url ',
112 parsed.path, parsed)
112 parsed.path, parsed)
113 came_from = default_came_from
113 came_from = default_came_from
114
114
115 return came_from or default_came_from
115 return came_from or default_came_from
116
116
117
117
118 class LoginView(BaseAppView):
118 class LoginView(BaseAppView):
119
119
120 def load_default_context(self):
120 def load_default_context(self):
121 c = self._get_local_tmpl_context()
121 c = self._get_local_tmpl_context()
122 c.came_from = get_came_from(self.request)
122 c.came_from = get_came_from(self.request)
123 return c
123 return c
124
124
125 def _get_captcha_data(self):
125 def _get_captcha_data(self):
126 settings = SettingsModel().get_all_settings()
126 settings = SettingsModel().get_all_settings()
127 private_key = settings.get('rhodecode_captcha_private_key')
127 private_key = settings.get('rhodecode_captcha_private_key')
128 public_key = settings.get('rhodecode_captcha_public_key')
128 public_key = settings.get('rhodecode_captcha_public_key')
129 active = bool(private_key)
129 active = bool(private_key)
130 return CaptchaData(
130 return CaptchaData(
131 active=active, private_key=private_key, public_key=public_key)
131 active=active, private_key=private_key, public_key=public_key)
132
132
133 def validate_captcha(self, private_key):
133 def validate_captcha(self, private_key):
134
134
135 captcha_rs = self.request.POST.get('g-recaptcha-response')
135 captcha_rs = self.request.POST.get('g-recaptcha-response')
136 url = "https://www.google.com/recaptcha/api/siteverify"
136 url = "https://www.google.com/recaptcha/api/siteverify"
137 params = {
137 params = {
138 'secret': private_key,
138 'secret': private_key,
139 'response': captcha_rs,
139 'response': captcha_rs,
140 'remoteip': get_ip_addr(self.request.environ)
140 'remoteip': get_ip_addr(self.request.environ)
141 }
141 }
142 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
142 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
143 verify_rs = verify_rs.json()
143 verify_rs = verify_rs.json()
144 captcha_status = verify_rs.get('success', False)
144 captcha_status = verify_rs.get('success', False)
145 captcha_errors = verify_rs.get('error-codes', [])
145 captcha_errors = verify_rs.get('error-codes', [])
146 if not isinstance(captcha_errors, list):
146 if not isinstance(captcha_errors, list):
147 captcha_errors = [captcha_errors]
147 captcha_errors = [captcha_errors]
148 captcha_errors = ', '.join(captcha_errors)
148 captcha_errors = ', '.join(captcha_errors)
149 captcha_message = ''
149 captcha_message = ''
150 if captcha_status is False:
150 if captcha_status is False:
151 captcha_message = "Bad captcha. Errors: {}".format(
151 captcha_message = "Bad captcha. Errors: {}".format(
152 captcha_errors)
152 captcha_errors)
153
153
154 return captcha_status, captcha_message
154 return captcha_status, captcha_message
155
155
156 def login(self):
156 def login(self):
157 c = self.load_default_context()
157 c = self.load_default_context()
158 auth_user = self._rhodecode_user
158 auth_user = self._rhodecode_user
159
159
160 # redirect if already logged in
160 # redirect if already logged in
161 if (auth_user.is_authenticated and
161 if (auth_user.is_authenticated and
162 not auth_user.is_default and auth_user.ip_allowed):
162 not auth_user.is_default and auth_user.ip_allowed):
163 raise HTTPFound(c.came_from)
163 raise HTTPFound(c.came_from)
164
164
165 # check if we use headers plugin, and try to login using it.
165 # check if we use headers plugin, and try to login using it.
166 try:
166 try:
167 log.debug('Running PRE-AUTH for headers based authentication')
167 log.debug('Running PRE-AUTH for headers based authentication')
168 auth_info = authenticate(
168 auth_info = authenticate(
169 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
169 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
170 if auth_info:
170 if auth_info:
171 headers = store_user_in_session(
171 headers = store_user_in_session(
172 self.session, auth_info.get('username'))
172 self.session, auth_info.get('username'))
173 raise HTTPFound(c.came_from, headers=headers)
173 raise HTTPFound(c.came_from, headers=headers)
174 except UserCreationError as e:
174 except UserCreationError as e:
175 log.error(e)
175 log.error(e)
176 h.flash(e, category='error')
176 h.flash(e, category='error')
177
177
178 return self._get_template_context(c)
178 return self._get_template_context(c)
179
179
180 def login_post(self):
180 def login_post(self):
181 c = self.load_default_context()
181 c = self.load_default_context()
182
182
183 login_form = LoginForm(self.request.translate)()
183 login_form = LoginForm(self.request.translate)()
184
184
185 try:
185 try:
186 self.session.invalidate()
186 self.session.invalidate()
187 form_result = login_form.to_python(self.request.POST)
187 form_result = login_form.to_python(self.request.POST)
188 # form checks for username/password, now we're authenticated
188 # form checks for username/password, now we're authenticated
189 username = form_result['username']
189 username = form_result['username']
190 if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa:
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 headers = store_user_in_session(
193 headers = store_user_in_session(
194 self.session,
194 self.session,
195 user_identifier=username,
195 user_identifier=username,
196 remember=form_result['remember'])
196 remember=form_result['remember'])
197 log.debug('Redirecting to "%s" after login.', c.came_from)
197 log.debug('Redirecting to "%s" after login.', c.came_from)
198
198
199 audit_user = audit_logger.UserWrap(
199 audit_user = audit_logger.UserWrap(
200 username=self.request.POST.get('username'),
200 username=self.request.POST.get('username'),
201 ip_addr=self.request.remote_addr)
201 ip_addr=self.request.remote_addr)
202 action_data = {'user_agent': self.request.user_agent}
202 action_data = {'user_agent': self.request.user_agent}
203 audit_logger.store_web(
203 audit_logger.store_web(
204 'user.login.success', action_data=action_data,
204 'user.login.success', action_data=action_data,
205 user=audit_user, commit=True)
205 user=audit_user, commit=True)
206
206
207 raise HTTPFound(c.came_from, headers=headers)
207 raise HTTPFound(c.came_from, headers=headers)
208 except formencode.Invalid as errors:
208 except formencode.Invalid as errors:
209 defaults = errors.value
209 defaults = errors.value
210 # remove password from filling in form again
210 # remove password from filling in form again
211 defaults.pop('password', None)
211 defaults.pop('password', None)
212 render_ctx = {
212 render_ctx = {
213 'errors': errors.error_dict,
213 'errors': errors.error_dict,
214 'defaults': defaults,
214 'defaults': defaults,
215 }
215 }
216
216
217 audit_user = audit_logger.UserWrap(
217 audit_user = audit_logger.UserWrap(
218 username=self.request.POST.get('username'),
218 username=self.request.POST.get('username'),
219 ip_addr=self.request.remote_addr)
219 ip_addr=self.request.remote_addr)
220 action_data = {'user_agent': self.request.user_agent}
220 action_data = {'user_agent': self.request.user_agent}
221 audit_logger.store_web(
221 audit_logger.store_web(
222 'user.login.failure', action_data=action_data,
222 'user.login.failure', action_data=action_data,
223 user=audit_user, commit=True)
223 user=audit_user, commit=True)
224 return self._get_template_context(c, **render_ctx)
224 return self._get_template_context(c, **render_ctx)
225
225
226 except UserCreationError as e:
226 except UserCreationError as e:
227 # headers auth or other auth functions that create users on
227 # headers auth or other auth functions that create users on
228 # the fly can throw this exception signaling that there's issue
228 # the fly can throw this exception signaling that there's issue
229 # with user creation, explanation should be provided in
229 # with user creation, explanation should be provided in
230 # Exception itself
230 # Exception itself
231 h.flash(e, category='error')
231 h.flash(e, category='error')
232 return self._get_template_context(c)
232 return self._get_template_context(c)
233
233
234 @CSRFRequired()
234 @CSRFRequired()
235 def logout(self):
235 def logout(self):
236 auth_user = self._rhodecode_user
236 auth_user = self._rhodecode_user
237 log.info('Deleting session for user: `%s`', auth_user)
237 log.info('Deleting session for user: `%s`', auth_user)
238
238
239 action_data = {'user_agent': self.request.user_agent}
239 action_data = {'user_agent': self.request.user_agent}
240 audit_logger.store_web(
240 audit_logger.store_web(
241 'user.logout', action_data=action_data,
241 'user.logout', action_data=action_data,
242 user=auth_user, commit=True)
242 user=auth_user, commit=True)
243 self.session.delete()
243 self.session.delete()
244 return HTTPFound(h.route_path('home'))
244 return HTTPFound(h.route_path('home'))
245
245
246 @HasPermissionAnyDecorator(
246 @HasPermissionAnyDecorator(
247 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
247 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
248 def register(self, defaults=None, errors=None):
248 def register(self, defaults=None, errors=None):
249 c = self.load_default_context()
249 c = self.load_default_context()
250 defaults = defaults or {}
250 defaults = defaults or {}
251 errors = errors or {}
251 errors = errors or {}
252
252
253 settings = SettingsModel().get_all_settings()
253 settings = SettingsModel().get_all_settings()
254 register_message = settings.get('rhodecode_register_message') or ''
254 register_message = settings.get('rhodecode_register_message') or ''
255 captcha = self._get_captcha_data()
255 captcha = self._get_captcha_data()
256 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
256 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
257 .AuthUser().permissions['global']
257 .AuthUser().permissions['global']
258
258
259 render_ctx = self._get_template_context(c)
259 render_ctx = self._get_template_context(c)
260 render_ctx.update({
260 render_ctx.update({
261 'defaults': defaults,
261 'defaults': defaults,
262 'errors': errors,
262 'errors': errors,
263 'auto_active': auto_active,
263 'auto_active': auto_active,
264 'captcha_active': captcha.active,
264 'captcha_active': captcha.active,
265 'captcha_public_key': captcha.public_key,
265 'captcha_public_key': captcha.public_key,
266 'register_message': register_message,
266 'register_message': register_message,
267 })
267 })
268 return render_ctx
268 return render_ctx
269
269
270 @HasPermissionAnyDecorator(
270 @HasPermissionAnyDecorator(
271 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
271 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
272 def register_post(self):
272 def register_post(self):
273 from rhodecode.authentication.plugins import auth_rhodecode
273 from rhodecode.authentication.plugins import auth_rhodecode
274
274
275 self.load_default_context()
275 self.load_default_context()
276 captcha = self._get_captcha_data()
276 captcha = self._get_captcha_data()
277 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
277 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
278 .AuthUser().permissions['global']
278 .AuthUser().permissions['global']
279
279
280 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
280 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
281 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
281 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
282
282
283 register_form = RegisterForm(self.request.translate)()
283 register_form = RegisterForm(self.request.translate)()
284 try:
284 try:
285
285
286 form_result = register_form.to_python(self.request.POST)
286 form_result = register_form.to_python(self.request.POST)
287 form_result['active'] = auto_active
287 form_result['active'] = auto_active
288 external_identity = self.request.POST.get('external_identity')
288 external_identity = self.request.POST.get('external_identity')
289
289
290 if external_identity:
290 if external_identity:
291 extern_name = external_identity
291 extern_name = external_identity
292 extern_type = external_identity
292 extern_type = external_identity
293
293
294 if captcha.active:
294 if captcha.active:
295 captcha_status, captcha_message = self.validate_captcha(
295 captcha_status, captcha_message = self.validate_captcha(
296 captcha.private_key)
296 captcha.private_key)
297
297
298 if not captcha_status:
298 if not captcha_status:
299 _value = form_result
299 _value = form_result
300 _msg = _('Bad captcha')
300 _msg = _('Bad captcha')
301 error_dict = {'recaptcha_field': captcha_message}
301 error_dict = {'recaptcha_field': captcha_message}
302 raise formencode.Invalid(
302 raise formencode.Invalid(
303 _msg, _value, None, error_dict=error_dict)
303 _msg, _value, None, error_dict=error_dict)
304
304
305 new_user = UserModel().create_registration(
305 new_user = UserModel().create_registration(
306 form_result, extern_name=extern_name, extern_type=extern_type)
306 form_result, extern_name=extern_name, extern_type=extern_type)
307
307
308 action_data = {'data': new_user.get_api_data(),
308 action_data = {'data': new_user.get_api_data(),
309 'user_agent': self.request.user_agent}
309 'user_agent': self.request.user_agent}
310
310
311 if external_identity:
311 if external_identity:
312 action_data['external_identity'] = external_identity
312 action_data['external_identity'] = external_identity
313
313
314 audit_user = audit_logger.UserWrap(
314 audit_user = audit_logger.UserWrap(
315 username=new_user.username,
315 username=new_user.username,
316 user_id=new_user.user_id,
316 user_id=new_user.user_id,
317 ip_addr=self.request.remote_addr)
317 ip_addr=self.request.remote_addr)
318
318
319 audit_logger.store_web(
319 audit_logger.store_web(
320 'user.register', action_data=action_data,
320 'user.register', action_data=action_data,
321 user=audit_user)
321 user=audit_user)
322
322
323 event = UserRegistered(user=new_user, session=self.session)
323 event = UserRegistered(user=new_user, session=self.session)
324 trigger(event)
324 trigger(event)
325 h.flash(
325 h.flash(
326 _('You have successfully registered with RhodeCode. You can log-in now.'),
326 _('You have successfully registered with RhodeCode. You can log-in now.'),
327 category='success')
327 category='success')
328 if external_identity:
328 if external_identity:
329 h.flash(
329 h.flash(
330 _('Please use the {identity} button to log-in').format(
330 _('Please use the {identity} button to log-in').format(
331 identity=external_identity),
331 identity=external_identity),
332 category='success')
332 category='success')
333 Session().commit()
333 Session().commit()
334
334
335 redirect_ro = self.request.route_path('login')
335 redirect_ro = self.request.route_path('login')
336 raise HTTPFound(redirect_ro)
336 raise HTTPFound(redirect_ro)
337
337
338 except formencode.Invalid as errors:
338 except formencode.Invalid as errors:
339 errors.value.pop('password', None)
339 errors.value.pop('password', None)
340 errors.value.pop('password_confirmation', None)
340 errors.value.pop('password_confirmation', None)
341 return self.register(
341 return self.register(
342 defaults=errors.value, errors=errors.error_dict)
342 defaults=errors.value, errors=errors.error_dict)
343
343
344 except UserCreationError as e:
344 except UserCreationError as e:
345 # container auth or other auth functions that create users on
345 # container auth or other auth functions that create users on
346 # the fly can throw this exception signaling that there's issue
346 # the fly can throw this exception signaling that there's issue
347 # with user creation, explanation should be provided in
347 # with user creation, explanation should be provided in
348 # Exception itself
348 # Exception itself
349 h.flash(e, category='error')
349 h.flash(e, category='error')
350 return self.register()
350 return self.register()
351
351
352 def password_reset(self):
352 def password_reset(self):
353 c = self.load_default_context()
353 c = self.load_default_context()
354 captcha = self._get_captcha_data()
354 captcha = self._get_captcha_data()
355
355
356 template_context = {
356 template_context = {
357 'captcha_active': captcha.active,
357 'captcha_active': captcha.active,
358 'captcha_public_key': captcha.public_key,
358 'captcha_public_key': captcha.public_key,
359 'defaults': {},
359 'defaults': {},
360 'errors': {},
360 'errors': {},
361 }
361 }
362
362
363 # always send implicit message to prevent from discovery of
363 # always send implicit message to prevent from discovery of
364 # matching emails
364 # matching emails
365 msg = _('If such email exists, a password reset link was sent to it.')
365 msg = _('If such email exists, a password reset link was sent to it.')
366
366
367 def default_response():
367 def default_response():
368 log.debug('faking response on invalid password reset')
368 log.debug('faking response on invalid password reset')
369 # make this take 2s, to prevent brute forcing.
369 # make this take 2s, to prevent brute forcing.
370 time.sleep(2)
370 time.sleep(2)
371 h.flash(msg, category='success')
371 h.flash(msg, category='success')
372 return HTTPFound(self.request.route_path('reset_password'))
372 return HTTPFound(self.request.route_path('reset_password'))
373
373
374 if self.request.POST:
374 if self.request.POST:
375 if h.HasPermissionAny('hg.password_reset.disabled')():
375 if h.HasPermissionAny('hg.password_reset.disabled')():
376 _email = self.request.POST.get('email', '')
376 _email = self.request.POST.get('email', '')
377 log.error('Failed attempt to reset password for `%s`.', _email)
377 log.error('Failed attempt to reset password for `%s`.', _email)
378 h.flash(_('Password reset has been disabled.'), category='error')
378 h.flash(_('Password reset has been disabled.'), category='error')
379 return HTTPFound(self.request.route_path('reset_password'))
379 return HTTPFound(self.request.route_path('reset_password'))
380
380
381 password_reset_form = PasswordResetForm(self.request.translate)()
381 password_reset_form = PasswordResetForm(self.request.translate)()
382 description = 'Generated token for password reset from {}'.format(
382 description = 'Generated token for password reset from {}'.format(
383 datetime.datetime.now().isoformat())
383 datetime.datetime.now().isoformat())
384
384
385 try:
385 try:
386 form_result = password_reset_form.to_python(
386 form_result = password_reset_form.to_python(
387 self.request.POST)
387 self.request.POST)
388 user_email = form_result['email']
388 user_email = form_result['email']
389
389
390 if captcha.active:
390 if captcha.active:
391 captcha_status, captcha_message = self.validate_captcha(
391 captcha_status, captcha_message = self.validate_captcha(
392 captcha.private_key)
392 captcha.private_key)
393
393
394 if not captcha_status:
394 if not captcha_status:
395 _value = form_result
395 _value = form_result
396 _msg = _('Bad captcha')
396 _msg = _('Bad captcha')
397 error_dict = {'recaptcha_field': captcha_message}
397 error_dict = {'recaptcha_field': captcha_message}
398 raise formencode.Invalid(
398 raise formencode.Invalid(
399 _msg, _value, None, error_dict=error_dict)
399 _msg, _value, None, error_dict=error_dict)
400
400
401 # Generate reset URL and send mail.
401 # Generate reset URL and send mail.
402 user = User.get_by_email(user_email)
402 user = User.get_by_email(user_email)
403
403
404 # only allow rhodecode based users to reset their password
404 # only allow rhodecode based users to reset their password
405 # external auth shouldn't allow password reset
405 # external auth shouldn't allow password reset
406 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
406 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
407 log.warning('User %s with external type `%s` tried a password reset. '
407 log.warning('User %s with external type `%s` tried a password reset. '
408 'This try was rejected', user, user.extern_type)
408 'This try was rejected', user, user.extern_type)
409 return default_response()
409 return default_response()
410
410
411 # generate password reset token that expires in 10 minutes
411 # generate password reset token that expires in 10 minutes
412 reset_token = UserModel().add_auth_token(
412 reset_token = UserModel().add_auth_token(
413 user=user, lifetime_minutes=10,
413 user=user, lifetime_minutes=10,
414 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
414 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
415 description=description)
415 description=description)
416 Session().commit()
416 Session().commit()
417
417
418 log.debug('Successfully created password recovery token')
418 log.debug('Successfully created password recovery token')
419 password_reset_url = self.request.route_url(
419 password_reset_url = self.request.route_url(
420 'reset_password_confirmation',
420 'reset_password_confirmation',
421 _query={'key': reset_token.api_key})
421 _query={'key': reset_token.api_key})
422 UserModel().reset_password_link(
422 UserModel().reset_password_link(
423 form_result, password_reset_url)
423 form_result, password_reset_url)
424
424
425 action_data = {'email': user_email,
425 action_data = {'email': user_email,
426 'user_agent': self.request.user_agent}
426 'user_agent': self.request.user_agent}
427 audit_logger.store_web(
427 audit_logger.store_web(
428 'user.password.reset_request', action_data=action_data,
428 'user.password.reset_request', action_data=action_data,
429 user=self._rhodecode_user, commit=True)
429 user=self._rhodecode_user, commit=True)
430
430
431 return default_response()
431 return default_response()
432
432
433 except formencode.Invalid as errors:
433 except formencode.Invalid as errors:
434 template_context.update({
434 template_context.update({
435 'defaults': errors.value,
435 'defaults': errors.value,
436 'errors': errors.error_dict,
436 'errors': errors.error_dict,
437 })
437 })
438 if not self.request.POST.get('email'):
438 if not self.request.POST.get('email'):
439 # case of empty email, we want to report that
439 # case of empty email, we want to report that
440 return self._get_template_context(c, **template_context)
440 return self._get_template_context(c, **template_context)
441
441
442 if 'recaptcha_field' in errors.error_dict:
442 if 'recaptcha_field' in errors.error_dict:
443 # case of failed captcha
443 # case of failed captcha
444 return self._get_template_context(c, **template_context)
444 return self._get_template_context(c, **template_context)
445
445
446 return default_response()
446 return default_response()
447
447
448 return self._get_template_context(c, **template_context)
448 return self._get_template_context(c, **template_context)
449
449
450 def password_reset_confirmation(self):
450 def password_reset_confirmation(self):
451 self.load_default_context()
451 self.load_default_context()
452
452
453 if key := self.request.GET.get('key'):
453 if key := self.request.GET.get('key'):
454 # make this take 2s, to prevent brute forcing.
454 # make this take 2s, to prevent brute forcing.
455 time.sleep(2)
455 time.sleep(2)
456
456
457 token = AuthTokenModel().get_auth_token(key)
457 token = AuthTokenModel().get_auth_token(key)
458
458
459 # verify token is the correct role
459 # verify token is the correct role
460 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
460 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
461 log.debug('Got token with role:%s expected is %s',
461 log.debug('Got token with role:%s expected is %s',
462 getattr(token, 'role', 'EMPTY_TOKEN'),
462 getattr(token, 'role', 'EMPTY_TOKEN'),
463 UserApiKeys.ROLE_PASSWORD_RESET)
463 UserApiKeys.ROLE_PASSWORD_RESET)
464 h.flash(
464 h.flash(
465 _('Given reset token is invalid'), category='error')
465 _('Given reset token is invalid'), category='error')
466 return HTTPFound(self.request.route_path('reset_password'))
466 return HTTPFound(self.request.route_path('reset_password'))
467
467
468 try:
468 try:
469 owner = token.user
469 owner = token.user
470 data = {'email': owner.email, 'token': token.api_key}
470 data = {'email': owner.email, 'token': token.api_key}
471 UserModel().reset_password(data)
471 UserModel().reset_password(data)
472 h.flash(
472 h.flash(
473 _('Your password reset was successful, '
473 _('Your password reset was successful, '
474 'a new password has been sent to your email'),
474 'a new password has been sent to your email'),
475 category='success')
475 category='success')
476 except Exception as e:
476 except Exception as e:
477 log.error(e)
477 log.error(e)
478 return HTTPFound(self.request.route_path('reset_password'))
478 return HTTPFound(self.request.route_path('reset_password'))
479
479
480 return HTTPFound(self.request.route_path('login'))
480 return HTTPFound(self.request.route_path('login'))
481
481
482 @LoginRequired()
482 @LoginRequired()
483 @NotAnonymous()
483 @NotAnonymous()
484 def setup_2fa(self):
484 def setup_2fa(self):
485 _ = self.request.translate
485 _ = self.request.translate
486 c = self.load_default_context()
486 c = self.load_default_context()
487 user_instance = self._rhodecode_db_user
487 user_instance = self._rhodecode_db_user
488 form = TOTPForm(_, user_instance)()
488 form = TOTPForm(_, user_instance)()
489 render_ctx = {}
489 render_ctx = {}
490 if self.request.method == 'POST':
490 if self.request.method == 'POST':
491 post_items = dict(self.request.POST)
491 post_items = dict(self.request.POST)
492
492
493 try:
493 try:
494 form_details = form.to_python(post_items)
494 form_details = form.to_python(post_items)
495 secret = form_details['secret_totp']
495 secret = form_details['secret_totp']
496
496
497 user_instance.init_2fa_recovery_codes(persist=True, force=True)
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 Session().commit()
500 Session().commit()
501 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
501 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
502 except formencode.Invalid as errors:
502 except formencode.Invalid as errors:
503 defaults = errors.value
503 defaults = errors.value
504 render_ctx = {
504 render_ctx = {
505 'errors': errors.error_dict,
505 'errors': errors.error_dict,
506 'defaults': defaults,
506 'defaults': defaults,
507 }
507 }
508
508
509 # NOTE: here we DO NOT persist the secret 2FA, since this is only for setup, once a setup is completed
509 # NOTE: here we DO NOT persist the secret 2FA, since this is only for setup, once a setup is completed
510 # only then we should persist it
510 # only then we should persist it
511 secret = user_instance.init_secret_2fa(persist=False)
511 secret = user_instance.init_secret_2fa(persist=False)
512
512
513 instance_name = rhodecode.ConfigGet().get_str('app.base_url', 'rhodecode')
513 instance_name = rhodecode.ConfigGet().get_str('app.base_url', 'rhodecode')
514 totp_name = f'{instance_name}:{self.request.user.username}'
514 totp_name = f'{instance_name}:{self.request.user.username}'
515
515
516 qr = qrcode.QRCode(version=1, box_size=5, border=4)
516 qr = qrcode.QRCode(version=1, box_size=5, border=4)
517 qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name))
517 qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name))
518 qr.make(fit=True)
518 qr.make(fit=True)
519 img = qr.make_image(fill_color='black', back_color='white')
519 img = qr.make_image(fill_color='black', back_color='white')
520 buffered = BytesIO()
520 buffered = BytesIO()
521 img.save(buffered)
521 img.save(buffered)
522 return self._get_template_context(
522 return self._get_template_context(
523 c,
523 c,
524 qr=b64encode(buffered.getvalue()).decode("utf-8"),
524 qr=b64encode(buffered.getvalue()).decode("utf-8"),
525 key=secret,
525 key=secret,
526 totp_name=totp_name,
526 totp_name=totp_name,
527 ** render_ctx
527 ** render_ctx
528 )
528 )
529
529
530 @LoginRequired()
530 @LoginRequired()
531 @NotAnonymous()
531 @NotAnonymous()
532 def verify_2fa(self):
532 def verify_2fa(self):
533 _ = self.request.translate
533 _ = self.request.translate
534 c = self.load_default_context()
534 c = self.load_default_context()
535 render_ctx = {}
535 render_ctx = {}
536 user_instance = self._rhodecode_db_user
536 user_instance = self._rhodecode_db_user
537 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
537 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
538 if self.request.method == 'POST':
538 if self.request.method == 'POST':
539 post_items = dict(self.request.POST)
539 post_items = dict(self.request.POST)
540 # NOTE: inject secret, as it's a post configured saved item.
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 try:
542 try:
543 totp_form.to_python(post_items)
543 totp_form.to_python(post_items)
544 user_instance.has_check_2fa_flag = False
544 user_instance.check_2fa_required = False
545 Session().commit()
545 Session().commit()
546 raise HTTPFound(c.came_from)
546 raise HTTPFound(c.came_from)
547 except formencode.Invalid as errors:
547 except formencode.Invalid as errors:
548 defaults = errors.value
548 defaults = errors.value
549 render_ctx = {
549 render_ctx = {
550 'errors': errors.error_dict,
550 'errors': errors.error_dict,
551 'defaults': defaults,
551 'defaults': defaults,
552 }
552 }
553 return self._get_template_context(c, **render_ctx)
553 return self._get_template_context(c, **render_ctx)
@@ -1,861 +1,861 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import time
19 import time
20 import logging
20 import logging
21 import datetime
21 import datetime
22 import string
22 import string
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
27 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
28
28
29 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode import forms
30 from rhodecode import forms
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import audit_logger
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib import ext_json
33 from rhodecode.lib import ext_json
34 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
35 LoginRequired, NotAnonymous, CSRFRequired,
35 LoginRequired, NotAnonymous, CSRFRequired,
36 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
36 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
37 from rhodecode.lib.channelstream import (
37 from rhodecode.lib.channelstream import (
38 channelstream_request, ChannelstreamException)
38 channelstream_request, ChannelstreamException)
39 from rhodecode.lib.hash_utils import md5_safe
39 from rhodecode.lib.hash_utils import md5_safe
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 IntegrityError, or_, in_filter_generator, select,
44 IntegrityError, or_, in_filter_generator, select,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 from rhodecode.model.forms import TOTPForm
47 from rhodecode.model.forms import TOTPForm
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
51 from rhodecode.model.user_group import UserGroupModel
51 from rhodecode.model.user_group import UserGroupModel
52 from rhodecode.model.validation_schema.schemas import user_schema
52 from rhodecode.model.validation_schema.schemas import user_schema
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class MyAccountView(BaseAppView, DataGridAppView):
57 class MyAccountView(BaseAppView, DataGridAppView):
58 ALLOW_SCOPED_TOKENS = False
58 ALLOW_SCOPED_TOKENS = False
59 """
59 """
60 This view has alternative version inside EE, if modified please take a look
60 This view has alternative version inside EE, if modified please take a look
61 in there as well.
61 in there as well.
62 """
62 """
63
63
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context()
65 c = self._get_local_tmpl_context()
66 c.user = c.auth_user.get_instance()
66 c.user = c.auth_user.get_instance()
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68 return c
68 return c
69
69
70 @LoginRequired()
70 @LoginRequired()
71 @NotAnonymous()
71 @NotAnonymous()
72 def my_account_profile(self):
72 def my_account_profile(self):
73 c = self.load_default_context()
73 c = self.load_default_context()
74 c.active = 'profile'
74 c.active = 'profile'
75 c.extern_type = c.user.extern_type
75 c.extern_type = c.user.extern_type
76 return self._get_template_context(c)
76 return self._get_template_context(c)
77
77
78 @LoginRequired()
78 @LoginRequired()
79 @NotAnonymous()
79 @NotAnonymous()
80 def my_account_edit(self):
80 def my_account_edit(self):
81 c = self.load_default_context()
81 c = self.load_default_context()
82 c.active = 'profile_edit'
82 c.active = 'profile_edit'
83 c.extern_type = c.user.extern_type
83 c.extern_type = c.user.extern_type
84 c.extern_name = c.user.extern_name
84 c.extern_name = c.user.extern_name
85
85
86 schema = user_schema.UserProfileSchema().bind(
86 schema = user_schema.UserProfileSchema().bind(
87 username=c.user.username, user_emails=c.user.emails)
87 username=c.user.username, user_emails=c.user.emails)
88 appstruct = {
88 appstruct = {
89 'username': c.user.username,
89 'username': c.user.username,
90 'email': c.user.email,
90 'email': c.user.email,
91 'firstname': c.user.firstname,
91 'firstname': c.user.firstname,
92 'lastname': c.user.lastname,
92 'lastname': c.user.lastname,
93 'description': c.user.description,
93 'description': c.user.description,
94 }
94 }
95 c.form = forms.RcForm(
95 c.form = forms.RcForm(
96 schema, appstruct=appstruct,
96 schema, appstruct=appstruct,
97 action=h.route_path('my_account_update'),
97 action=h.route_path('my_account_update'),
98 buttons=(forms.buttons.save, forms.buttons.reset))
98 buttons=(forms.buttons.save, forms.buttons.reset))
99
99
100 return self._get_template_context(c)
100 return self._get_template_context(c)
101
101
102 @LoginRequired()
102 @LoginRequired()
103 @NotAnonymous()
103 @NotAnonymous()
104 @CSRFRequired()
104 @CSRFRequired()
105 def my_account_update(self):
105 def my_account_update(self):
106 _ = self.request.translate
106 _ = self.request.translate
107 c = self.load_default_context()
107 c = self.load_default_context()
108 c.active = 'profile_edit'
108 c.active = 'profile_edit'
109 c.perm_user = c.auth_user
109 c.perm_user = c.auth_user
110 c.extern_type = c.user.extern_type
110 c.extern_type = c.user.extern_type
111 c.extern_name = c.user.extern_name
111 c.extern_name = c.user.extern_name
112
112
113 schema = user_schema.UserProfileSchema().bind(
113 schema = user_schema.UserProfileSchema().bind(
114 username=c.user.username, user_emails=c.user.emails)
114 username=c.user.username, user_emails=c.user.emails)
115 form = forms.RcForm(
115 form = forms.RcForm(
116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117
117
118 controls = list(self.request.POST.items())
118 controls = list(self.request.POST.items())
119 try:
119 try:
120 valid_data = form.validate(controls)
120 valid_data = form.validate(controls)
121 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
122 'new_password', 'password_confirmation']
122 'new_password', 'password_confirmation']
123 if c.extern_type != "rhodecode":
123 if c.extern_type != "rhodecode":
124 # forbid updating username for external accounts
124 # forbid updating username for external accounts
125 skip_attrs.append('username')
125 skip_attrs.append('username')
126 old_email = c.user.email
126 old_email = c.user.email
127 UserModel().update_user(
127 UserModel().update_user(
128 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
129 **valid_data)
129 **valid_data)
130 if old_email != valid_data['email']:
130 if old_email != valid_data['email']:
131 old = UserEmailMap.query() \
131 old = UserEmailMap.query() \
132 .filter(UserEmailMap.user == c.user)\
132 .filter(UserEmailMap.user == c.user)\
133 .filter(UserEmailMap.email == valid_data['email'])\
133 .filter(UserEmailMap.email == valid_data['email'])\
134 .first()
134 .first()
135 old.email = old_email
135 old.email = old_email
136 h.flash(_('Your account was updated successfully'), category='success')
136 h.flash(_('Your account was updated successfully'), category='success')
137 Session().commit()
137 Session().commit()
138 except forms.ValidationFailure as e:
138 except forms.ValidationFailure as e:
139 c.form = e
139 c.form = e
140 return self._get_template_context(c)
140 return self._get_template_context(c)
141
141
142 except Exception:
142 except Exception:
143 log.exception("Exception updating user")
143 log.exception("Exception updating user")
144 h.flash(_('Error occurred during update of user'),
144 h.flash(_('Error occurred during update of user'),
145 category='error')
145 category='error')
146 raise HTTPFound(h.route_path('my_account_profile'))
146 raise HTTPFound(h.route_path('my_account_profile'))
147
147
148 @LoginRequired()
148 @LoginRequired()
149 @NotAnonymous()
149 @NotAnonymous()
150 def my_account_password(self):
150 def my_account_password(self):
151 c = self.load_default_context()
151 c = self.load_default_context()
152 c.active = 'password'
152 c.active = 'password'
153 c.extern_type = c.user.extern_type
153 c.extern_type = c.user.extern_type
154
154
155 schema = user_schema.ChangePasswordSchema().bind(
155 schema = user_schema.ChangePasswordSchema().bind(
156 username=c.user.username)
156 username=c.user.username)
157
157
158 form = forms.Form(
158 form = forms.Form(
159 schema,
159 schema,
160 action=h.route_path('my_account_password_update'),
160 action=h.route_path('my_account_password_update'),
161 buttons=(forms.buttons.save, forms.buttons.reset))
161 buttons=(forms.buttons.save, forms.buttons.reset))
162
162
163 c.form = form
163 c.form = form
164 return self._get_template_context(c)
164 return self._get_template_context(c)
165
165
166 @LoginRequired()
166 @LoginRequired()
167 @NotAnonymous()
167 @NotAnonymous()
168 @CSRFRequired()
168 @CSRFRequired()
169 def my_account_password_update(self):
169 def my_account_password_update(self):
170 _ = self.request.translate
170 _ = self.request.translate
171 c = self.load_default_context()
171 c = self.load_default_context()
172 c.active = 'password'
172 c.active = 'password'
173 c.extern_type = c.user.extern_type
173 c.extern_type = c.user.extern_type
174
174
175 schema = user_schema.ChangePasswordSchema().bind(
175 schema = user_schema.ChangePasswordSchema().bind(
176 username=c.user.username)
176 username=c.user.username)
177
177
178 form = forms.Form(
178 form = forms.Form(
179 schema, buttons=(forms.buttons.save, forms.buttons.reset))
179 schema, buttons=(forms.buttons.save, forms.buttons.reset))
180
180
181 if c.extern_type != 'rhodecode':
181 if c.extern_type != 'rhodecode':
182 raise HTTPFound(self.request.route_path('my_account_password'))
182 raise HTTPFound(self.request.route_path('my_account_password'))
183
183
184 controls = list(self.request.POST.items())
184 controls = list(self.request.POST.items())
185 try:
185 try:
186 valid_data = form.validate(controls)
186 valid_data = form.validate(controls)
187 UserModel().update_user(c.user.user_id, **valid_data)
187 UserModel().update_user(c.user.user_id, **valid_data)
188 c.user.update_userdata(force_password_change=False)
188 c.user.update_userdata(force_password_change=False)
189 Session().commit()
189 Session().commit()
190 except forms.ValidationFailure as e:
190 except forms.ValidationFailure as e:
191 c.form = e
191 c.form = e
192 return self._get_template_context(c)
192 return self._get_template_context(c)
193
193
194 except Exception:
194 except Exception:
195 log.exception("Exception updating password")
195 log.exception("Exception updating password")
196 h.flash(_('Error occurred during update of user password'),
196 h.flash(_('Error occurred during update of user password'),
197 category='error')
197 category='error')
198 else:
198 else:
199 instance = c.auth_user.get_instance()
199 instance = c.auth_user.get_instance()
200 self.session.setdefault('rhodecode_user', {}).update(
200 self.session.setdefault('rhodecode_user', {}).update(
201 {'password': md5_safe(instance.password)})
201 {'password': md5_safe(instance.password)})
202 self.session.save()
202 self.session.save()
203 h.flash(_("Successfully updated password"), category='success')
203 h.flash(_("Successfully updated password"), category='success')
204
204
205 raise HTTPFound(self.request.route_path('my_account_password'))
205 raise HTTPFound(self.request.route_path('my_account_password'))
206
206
207 @LoginRequired()
207 @LoginRequired()
208 @NotAnonymous()
208 @NotAnonymous()
209 def my_account_2fa(self):
209 def my_account_2fa(self):
210 _ = self.request.translate
210 _ = self.request.translate
211 c = self.load_default_context()
211 c = self.load_default_context()
212 c.active = '2fa'
212 c.active = '2fa'
213 user_instance = c.auth_user.get_instance()
213 user_instance = c.auth_user.get_instance()
214 locked_by_admin = user_instance.has_forced_2fa
214 locked_by_admin = user_instance.has_forced_2fa
215 c.state_of_2fa = user_instance.has_enabled_2fa
215 c.state_of_2fa = user_instance.has_enabled_2fa
216 c.user_seen_2fa_recovery_codes = user_instance.has_seen_2fa_codes
216 c.user_seen_2fa_recovery_codes = user_instance.has_seen_2fa_codes
217 c.locked_2fa = str2bool(locked_by_admin)
217 c.locked_2fa = str2bool(locked_by_admin)
218 return self._get_template_context(c)
218 return self._get_template_context(c)
219
219
220 @LoginRequired()
220 @LoginRequired()
221 @NotAnonymous()
221 @NotAnonymous()
222 @CSRFRequired()
222 @CSRFRequired()
223 def my_account_2fa_update(self):
223 def my_account_2fa_update(self):
224 _ = self.request.translate
224 _ = self.request.translate
225 c = self.load_default_context()
225 c = self.load_default_context()
226 c.active = '2fa'
226 c.active = '2fa'
227 user_instance = c.auth_user.get_instance()
227 user_instance = c.auth_user.get_instance()
228
228
229 state = self.request.POST.get('2fa_status') == '1'
229 state = self.request.POST.get('2fa_status') == '1'
230 user_instance.has_enabled_2fa = state
230 user_instance.has_enabled_2fa = state
231 user_instance.update_userdata(update_2fa=time.time())
231 user_instance.update_userdata(update_2fa=time.time())
232 Session().commit()
232 Session().commit()
233 if state:
233 if state:
234 h.flash(_("2FA has been successfully enabled"), category='success')
234 h.flash(_("2FA has been successfully enabled"), category='success')
235 else:
235 else:
236 h.flash(_("2FA has been successfully disabled"), category='success')
236 h.flash(_("2FA has been successfully disabled"), category='success')
237 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
237 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
238
238
239 @LoginRequired()
239 @LoginRequired()
240 @NotAnonymous()
240 @NotAnonymous()
241 @CSRFRequired()
241 @CSRFRequired()
242 def my_account_2fa_show_recovery_codes(self):
242 def my_account_2fa_show_recovery_codes(self):
243 c = self.load_default_context()
243 c = self.load_default_context()
244 user_instance = c.auth_user.get_instance()
244 user_instance = c.auth_user.get_instance()
245 user_instance.has_seen_2fa_codes = True
245 user_instance.has_seen_2fa_codes = True
246 Session().commit()
246 Session().commit()
247 return {'recovery_codes': user_instance.get_2fa_recovery_codes()}
247 return {'recovery_codes': user_instance.get_2fa_recovery_codes()}
248
248
249 @LoginRequired()
249 @LoginRequired()
250 @NotAnonymous()
250 @NotAnonymous()
251 @CSRFRequired()
251 @CSRFRequired()
252 def my_account_2fa_regenerate_recovery_codes(self):
252 def my_account_2fa_regenerate_recovery_codes(self):
253 _ = self.request.translate
253 _ = self.request.translate
254 c = self.load_default_context()
254 c = self.load_default_context()
255 user_instance = c.auth_user.get_instance()
255 user_instance = c.auth_user.get_instance()
256
256
257 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
257 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
258
258
259 post_items = dict(self.request.POST)
259 post_items = dict(self.request.POST)
260 # NOTE: inject secret, as it's a post configured saved item.
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 try:
262 try:
263 totp_form.to_python(post_items)
263 totp_form.to_python(post_items)
264 user_instance.regenerate_2fa_recovery_codes()
264 user_instance.regenerate_2fa_recovery_codes()
265 Session().commit()
265 Session().commit()
266 except formencode.Invalid as errors:
266 except formencode.Invalid as errors:
267 h.flash(_("Failed to generate new recovery codes: {}").format(errors), category='error')
267 h.flash(_("Failed to generate new recovery codes: {}").format(errors), category='error')
268 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
268 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
269 except Exception as e:
269 except Exception as e:
270 h.flash(_("Failed to generate new recovery codes: {}").format(e), category='error')
270 h.flash(_("Failed to generate new recovery codes: {}").format(e), category='error')
271 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
271 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
272
272
273 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
273 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
274
274
275 @LoginRequired()
275 @LoginRequired()
276 @NotAnonymous()
276 @NotAnonymous()
277 def my_account_auth_tokens(self):
277 def my_account_auth_tokens(self):
278 _ = self.request.translate
278 _ = self.request.translate
279
279
280 c = self.load_default_context()
280 c = self.load_default_context()
281 c.active = 'auth_tokens'
281 c.active = 'auth_tokens'
282 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
282 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
283 c.role_values = [
283 c.role_values = [
284 (x, AuthTokenModel.cls._get_role_name(x))
284 (x, AuthTokenModel.cls._get_role_name(x))
285 for x in AuthTokenModel.cls.ROLES]
285 for x in AuthTokenModel.cls.ROLES]
286 c.role_options = [(c.role_values, _("Role"))]
286 c.role_options = [(c.role_values, _("Role"))]
287 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
287 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
288 c.user.user_id, show_expired=True)
288 c.user.user_id, show_expired=True)
289 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
289 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
290 return self._get_template_context(c)
290 return self._get_template_context(c)
291
291
292 @LoginRequired()
292 @LoginRequired()
293 @NotAnonymous()
293 @NotAnonymous()
294 @CSRFRequired()
294 @CSRFRequired()
295 def my_account_auth_tokens_view(self):
295 def my_account_auth_tokens_view(self):
296 _ = self.request.translate
296 _ = self.request.translate
297 c = self.load_default_context()
297 c = self.load_default_context()
298
298
299 auth_token_id = self.request.POST.get('auth_token_id')
299 auth_token_id = self.request.POST.get('auth_token_id')
300
300
301 if auth_token_id:
301 if auth_token_id:
302 token = UserApiKeys.get_or_404(auth_token_id)
302 token = UserApiKeys.get_or_404(auth_token_id)
303 if token.user.user_id != c.user.user_id:
303 if token.user.user_id != c.user.user_id:
304 raise HTTPNotFound()
304 raise HTTPNotFound()
305
305
306 return {
306 return {
307 'auth_token': token.api_key
307 'auth_token': token.api_key
308 }
308 }
309
309
310 def maybe_attach_token_scope(self, token):
310 def maybe_attach_token_scope(self, token):
311 # implemented in EE edition
311 # implemented in EE edition
312 pass
312 pass
313
313
314 @LoginRequired()
314 @LoginRequired()
315 @NotAnonymous()
315 @NotAnonymous()
316 @CSRFRequired()
316 @CSRFRequired()
317 def my_account_auth_tokens_add(self):
317 def my_account_auth_tokens_add(self):
318 _ = self.request.translate
318 _ = self.request.translate
319 c = self.load_default_context()
319 c = self.load_default_context()
320
320
321 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
321 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
322 description = self.request.POST.get('description')
322 description = self.request.POST.get('description')
323 role = self.request.POST.get('role')
323 role = self.request.POST.get('role')
324
324
325 token = UserModel().add_auth_token(
325 token = UserModel().add_auth_token(
326 user=c.user.user_id,
326 user=c.user.user_id,
327 lifetime_minutes=lifetime, role=role, description=description,
327 lifetime_minutes=lifetime, role=role, description=description,
328 scope_callback=self.maybe_attach_token_scope)
328 scope_callback=self.maybe_attach_token_scope)
329 token_data = token.get_api_data()
329 token_data = token.get_api_data()
330
330
331 audit_logger.store_web(
331 audit_logger.store_web(
332 'user.edit.token.add', action_data={
332 'user.edit.token.add', action_data={
333 'data': {'token': token_data, 'user': 'self'}},
333 'data': {'token': token_data, 'user': 'self'}},
334 user=self._rhodecode_user, )
334 user=self._rhodecode_user, )
335 Session().commit()
335 Session().commit()
336
336
337 h.flash(_("Auth token successfully created"), category='success')
337 h.flash(_("Auth token successfully created"), category='success')
338 return HTTPFound(h.route_path('my_account_auth_tokens'))
338 return HTTPFound(h.route_path('my_account_auth_tokens'))
339
339
340 @LoginRequired()
340 @LoginRequired()
341 @NotAnonymous()
341 @NotAnonymous()
342 @CSRFRequired()
342 @CSRFRequired()
343 def my_account_auth_tokens_delete(self):
343 def my_account_auth_tokens_delete(self):
344 _ = self.request.translate
344 _ = self.request.translate
345 c = self.load_default_context()
345 c = self.load_default_context()
346
346
347 del_auth_token = self.request.POST.get('del_auth_token')
347 del_auth_token = self.request.POST.get('del_auth_token')
348
348
349 if del_auth_token:
349 if del_auth_token:
350 token = UserApiKeys.get_or_404(del_auth_token)
350 token = UserApiKeys.get_or_404(del_auth_token)
351 token_data = token.get_api_data()
351 token_data = token.get_api_data()
352
352
353 AuthTokenModel().delete(del_auth_token, c.user.user_id)
353 AuthTokenModel().delete(del_auth_token, c.user.user_id)
354 audit_logger.store_web(
354 audit_logger.store_web(
355 'user.edit.token.delete', action_data={
355 'user.edit.token.delete', action_data={
356 'data': {'token': token_data, 'user': 'self'}},
356 'data': {'token': token_data, 'user': 'self'}},
357 user=self._rhodecode_user,)
357 user=self._rhodecode_user,)
358 Session().commit()
358 Session().commit()
359 h.flash(_("Auth token successfully deleted"), category='success')
359 h.flash(_("Auth token successfully deleted"), category='success')
360
360
361 return HTTPFound(h.route_path('my_account_auth_tokens'))
361 return HTTPFound(h.route_path('my_account_auth_tokens'))
362
362
363 @LoginRequired()
363 @LoginRequired()
364 @NotAnonymous()
364 @NotAnonymous()
365 def my_account_emails(self):
365 def my_account_emails(self):
366 _ = self.request.translate
366 _ = self.request.translate
367
367
368 c = self.load_default_context()
368 c = self.load_default_context()
369 c.active = 'emails'
369 c.active = 'emails'
370
370
371 c.user_email_map = UserEmailMap.query()\
371 c.user_email_map = UserEmailMap.query()\
372 .filter(UserEmailMap.user == c.user).all()
372 .filter(UserEmailMap.user == c.user).all()
373
373
374 schema = user_schema.AddEmailSchema().bind(
374 schema = user_schema.AddEmailSchema().bind(
375 username=c.user.username, user_emails=c.user.emails)
375 username=c.user.username, user_emails=c.user.emails)
376
376
377 form = forms.RcForm(schema,
377 form = forms.RcForm(schema,
378 action=h.route_path('my_account_emails_add'),
378 action=h.route_path('my_account_emails_add'),
379 buttons=(forms.buttons.save, forms.buttons.reset))
379 buttons=(forms.buttons.save, forms.buttons.reset))
380
380
381 c.form = form
381 c.form = form
382 return self._get_template_context(c)
382 return self._get_template_context(c)
383
383
384 @LoginRequired()
384 @LoginRequired()
385 @NotAnonymous()
385 @NotAnonymous()
386 @CSRFRequired()
386 @CSRFRequired()
387 def my_account_emails_add(self):
387 def my_account_emails_add(self):
388 _ = self.request.translate
388 _ = self.request.translate
389 c = self.load_default_context()
389 c = self.load_default_context()
390 c.active = 'emails'
390 c.active = 'emails'
391
391
392 schema = user_schema.AddEmailSchema().bind(
392 schema = user_schema.AddEmailSchema().bind(
393 username=c.user.username, user_emails=c.user.emails)
393 username=c.user.username, user_emails=c.user.emails)
394
394
395 form = forms.RcForm(
395 form = forms.RcForm(
396 schema, action=h.route_path('my_account_emails_add'),
396 schema, action=h.route_path('my_account_emails_add'),
397 buttons=(forms.buttons.save, forms.buttons.reset))
397 buttons=(forms.buttons.save, forms.buttons.reset))
398
398
399 controls = list(self.request.POST.items())
399 controls = list(self.request.POST.items())
400 try:
400 try:
401 valid_data = form.validate(controls)
401 valid_data = form.validate(controls)
402 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
402 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
403 audit_logger.store_web(
403 audit_logger.store_web(
404 'user.edit.email.add', action_data={
404 'user.edit.email.add', action_data={
405 'data': {'email': valid_data['email'], 'user': 'self'}},
405 'data': {'email': valid_data['email'], 'user': 'self'}},
406 user=self._rhodecode_user,)
406 user=self._rhodecode_user,)
407 Session().commit()
407 Session().commit()
408 except formencode.Invalid as error:
408 except formencode.Invalid as error:
409 h.flash(h.escape(error.error_dict['email']), category='error')
409 h.flash(h.escape(error.error_dict['email']), category='error')
410 except forms.ValidationFailure as e:
410 except forms.ValidationFailure as e:
411 c.user_email_map = UserEmailMap.query() \
411 c.user_email_map = UserEmailMap.query() \
412 .filter(UserEmailMap.user == c.user).all()
412 .filter(UserEmailMap.user == c.user).all()
413 c.form = e
413 c.form = e
414 return self._get_template_context(c)
414 return self._get_template_context(c)
415 except Exception:
415 except Exception:
416 log.exception("Exception adding email")
416 log.exception("Exception adding email")
417 h.flash(_('Error occurred during adding email'),
417 h.flash(_('Error occurred during adding email'),
418 category='error')
418 category='error')
419 else:
419 else:
420 h.flash(_("Successfully added email"), category='success')
420 h.flash(_("Successfully added email"), category='success')
421
421
422 raise HTTPFound(self.request.route_path('my_account_emails'))
422 raise HTTPFound(self.request.route_path('my_account_emails'))
423
423
424 @LoginRequired()
424 @LoginRequired()
425 @NotAnonymous()
425 @NotAnonymous()
426 @CSRFRequired()
426 @CSRFRequired()
427 def my_account_emails_delete(self):
427 def my_account_emails_delete(self):
428 _ = self.request.translate
428 _ = self.request.translate
429 c = self.load_default_context()
429 c = self.load_default_context()
430
430
431 del_email_id = self.request.POST.get('del_email_id')
431 del_email_id = self.request.POST.get('del_email_id')
432 if del_email_id:
432 if del_email_id:
433 email = UserEmailMap.get_or_404(del_email_id).email
433 email = UserEmailMap.get_or_404(del_email_id).email
434 UserModel().delete_extra_email(c.user.user_id, del_email_id)
434 UserModel().delete_extra_email(c.user.user_id, del_email_id)
435 audit_logger.store_web(
435 audit_logger.store_web(
436 'user.edit.email.delete', action_data={
436 'user.edit.email.delete', action_data={
437 'data': {'email': email, 'user': 'self'}},
437 'data': {'email': email, 'user': 'self'}},
438 user=self._rhodecode_user,)
438 user=self._rhodecode_user,)
439 Session().commit()
439 Session().commit()
440 h.flash(_("Email successfully deleted"),
440 h.flash(_("Email successfully deleted"),
441 category='success')
441 category='success')
442 return HTTPFound(h.route_path('my_account_emails'))
442 return HTTPFound(h.route_path('my_account_emails'))
443
443
444 @LoginRequired()
444 @LoginRequired()
445 @NotAnonymous()
445 @NotAnonymous()
446 @CSRFRequired()
446 @CSRFRequired()
447 def my_account_notifications_test_channelstream(self):
447 def my_account_notifications_test_channelstream(self):
448 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
448 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
449 self._rhodecode_user.username, datetime.datetime.now())
449 self._rhodecode_user.username, datetime.datetime.now())
450 payload = {
450 payload = {
451 # 'channel': 'broadcast',
451 # 'channel': 'broadcast',
452 'type': 'message',
452 'type': 'message',
453 'timestamp': datetime.datetime.utcnow(),
453 'timestamp': datetime.datetime.utcnow(),
454 'user': 'system',
454 'user': 'system',
455 'pm_users': [self._rhodecode_user.username],
455 'pm_users': [self._rhodecode_user.username],
456 'message': {
456 'message': {
457 'message': message,
457 'message': message,
458 'level': 'info',
458 'level': 'info',
459 'topic': '/notifications'
459 'topic': '/notifications'
460 }
460 }
461 }
461 }
462
462
463 registry = self.request.registry
463 registry = self.request.registry
464 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
464 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
465 channelstream_config = rhodecode_plugins.get('channelstream', {})
465 channelstream_config = rhodecode_plugins.get('channelstream', {})
466
466
467 try:
467 try:
468 channelstream_request(channelstream_config, [payload], '/message')
468 channelstream_request(channelstream_config, [payload], '/message')
469 except ChannelstreamException as e:
469 except ChannelstreamException as e:
470 log.exception('Failed to send channelstream data')
470 log.exception('Failed to send channelstream data')
471 return {"response": f'ERROR: {e.__class__.__name__}'}
471 return {"response": f'ERROR: {e.__class__.__name__}'}
472 return {"response": 'Channelstream data sent. '
472 return {"response": 'Channelstream data sent. '
473 'You should see a new live message now.'}
473 'You should see a new live message now.'}
474
474
475 def _load_my_repos_data(self, watched=False):
475 def _load_my_repos_data(self, watched=False):
476
476
477 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
477 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
478
478
479 if watched:
479 if watched:
480 # repos user watch
480 # repos user watch
481 repo_list = Session().query(
481 repo_list = Session().query(
482 Repository
482 Repository
483 ) \
483 ) \
484 .join(
484 .join(
485 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
485 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
486 ) \
486 ) \
487 .filter(
487 .filter(
488 UserFollowing.user_id == self._rhodecode_user.user_id
488 UserFollowing.user_id == self._rhodecode_user.user_id
489 ) \
489 ) \
490 .filter(or_(
490 .filter(or_(
491 # generate multiple IN to fix limitation problems
491 # generate multiple IN to fix limitation problems
492 *in_filter_generator(Repository.repo_id, allowed_ids))
492 *in_filter_generator(Repository.repo_id, allowed_ids))
493 ) \
493 ) \
494 .order_by(Repository.repo_name) \
494 .order_by(Repository.repo_name) \
495 .all()
495 .all()
496
496
497 else:
497 else:
498 # repos user is owner of
498 # repos user is owner of
499 repo_list = Session().query(
499 repo_list = Session().query(
500 Repository
500 Repository
501 ) \
501 ) \
502 .filter(
502 .filter(
503 Repository.user_id == self._rhodecode_user.user_id
503 Repository.user_id == self._rhodecode_user.user_id
504 ) \
504 ) \
505 .filter(or_(
505 .filter(or_(
506 # generate multiple IN to fix limitation problems
506 # generate multiple IN to fix limitation problems
507 *in_filter_generator(Repository.repo_id, allowed_ids))
507 *in_filter_generator(Repository.repo_id, allowed_ids))
508 ) \
508 ) \
509 .order_by(Repository.repo_name) \
509 .order_by(Repository.repo_name) \
510 .all()
510 .all()
511
511
512 _render = self.request.get_partial_renderer(
512 _render = self.request.get_partial_renderer(
513 'rhodecode:templates/data_table/_dt_elements.mako')
513 'rhodecode:templates/data_table/_dt_elements.mako')
514
514
515 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
515 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
516 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
516 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
517 short_name=False, admin=False)
517 short_name=False, admin=False)
518
518
519 repos_data = []
519 repos_data = []
520 for repo in repo_list:
520 for repo in repo_list:
521 row = {
521 row = {
522 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
522 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
523 repo.private, repo.archived, repo.fork),
523 repo.private, repo.archived, repo.fork),
524 "name_raw": repo.repo_name.lower(),
524 "name_raw": repo.repo_name.lower(),
525 }
525 }
526
526
527 repos_data.append(row)
527 repos_data.append(row)
528
528
529 # json used to render the grid
529 # json used to render the grid
530 return ext_json.str_json(repos_data)
530 return ext_json.str_json(repos_data)
531
531
532 @LoginRequired()
532 @LoginRequired()
533 @NotAnonymous()
533 @NotAnonymous()
534 def my_account_repos(self):
534 def my_account_repos(self):
535 c = self.load_default_context()
535 c = self.load_default_context()
536 c.active = 'repos'
536 c.active = 'repos'
537
537
538 # json used to render the grid
538 # json used to render the grid
539 c.data = self._load_my_repos_data()
539 c.data = self._load_my_repos_data()
540 return self._get_template_context(c)
540 return self._get_template_context(c)
541
541
542 @LoginRequired()
542 @LoginRequired()
543 @NotAnonymous()
543 @NotAnonymous()
544 def my_account_watched(self):
544 def my_account_watched(self):
545 c = self.load_default_context()
545 c = self.load_default_context()
546 c.active = 'watched'
546 c.active = 'watched'
547
547
548 # json used to render the grid
548 # json used to render the grid
549 c.data = self._load_my_repos_data(watched=True)
549 c.data = self._load_my_repos_data(watched=True)
550 return self._get_template_context(c)
550 return self._get_template_context(c)
551
551
552 @LoginRequired()
552 @LoginRequired()
553 @NotAnonymous()
553 @NotAnonymous()
554 def my_account_bookmarks(self):
554 def my_account_bookmarks(self):
555 c = self.load_default_context()
555 c = self.load_default_context()
556 c.active = 'bookmarks'
556 c.active = 'bookmarks'
557
557
558 user_bookmarks = \
558 user_bookmarks = \
559 select(UserBookmark, Repository, RepoGroup) \
559 select(UserBookmark, Repository, RepoGroup) \
560 .where(UserBookmark.user_id == self._rhodecode_user.user_id) \
560 .where(UserBookmark.user_id == self._rhodecode_user.user_id) \
561 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
561 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
562 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
562 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
563 .order_by(UserBookmark.position.asc())
563 .order_by(UserBookmark.position.asc())
564
564
565 c.user_bookmark_items = Session().execute(user_bookmarks).all()
565 c.user_bookmark_items = Session().execute(user_bookmarks).all()
566 return self._get_template_context(c)
566 return self._get_template_context(c)
567
567
568 def _process_bookmark_entry(self, entry, user_id):
568 def _process_bookmark_entry(self, entry, user_id):
569 position = safe_int(entry.get('position'))
569 position = safe_int(entry.get('position'))
570 cur_position = safe_int(entry.get('cur_position'))
570 cur_position = safe_int(entry.get('cur_position'))
571 if position is None:
571 if position is None:
572 return
572 return
573
573
574 # check if this is an existing entry
574 # check if this is an existing entry
575 is_new = False
575 is_new = False
576 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
576 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
577
577
578 if db_entry and str2bool(entry.get('remove')):
578 if db_entry and str2bool(entry.get('remove')):
579 log.debug('Marked bookmark %s for deletion', db_entry)
579 log.debug('Marked bookmark %s for deletion', db_entry)
580 Session().delete(db_entry)
580 Session().delete(db_entry)
581 return
581 return
582
582
583 if not db_entry:
583 if not db_entry:
584 # new
584 # new
585 db_entry = UserBookmark()
585 db_entry = UserBookmark()
586 is_new = True
586 is_new = True
587
587
588 should_save = False
588 should_save = False
589 default_redirect_url = ''
589 default_redirect_url = ''
590
590
591 # save repo
591 # save repo
592 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
592 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
593 repo = Repository.get(entry['bookmark_repo'])
593 repo = Repository.get(entry['bookmark_repo'])
594 perm_check = HasRepoPermissionAny(
594 perm_check = HasRepoPermissionAny(
595 'repository.read', 'repository.write', 'repository.admin')
595 'repository.read', 'repository.write', 'repository.admin')
596 if repo and perm_check(repo_name=repo.repo_name):
596 if repo and perm_check(repo_name=repo.repo_name):
597 db_entry.repository = repo
597 db_entry.repository = repo
598 should_save = True
598 should_save = True
599 default_redirect_url = '${repo_url}'
599 default_redirect_url = '${repo_url}'
600 # save repo group
600 # save repo group
601 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
601 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
602 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
602 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
603 perm_check = HasRepoGroupPermissionAny(
603 perm_check = HasRepoGroupPermissionAny(
604 'group.read', 'group.write', 'group.admin')
604 'group.read', 'group.write', 'group.admin')
605
605
606 if repo_group and perm_check(group_name=repo_group.group_name):
606 if repo_group and perm_check(group_name=repo_group.group_name):
607 db_entry.repository_group = repo_group
607 db_entry.repository_group = repo_group
608 should_save = True
608 should_save = True
609 default_redirect_url = '${repo_group_url}'
609 default_redirect_url = '${repo_group_url}'
610 # save generic info
610 # save generic info
611 elif entry.get('title') and entry.get('redirect_url'):
611 elif entry.get('title') and entry.get('redirect_url'):
612 should_save = True
612 should_save = True
613
613
614 if should_save:
614 if should_save:
615 # mark user and position
615 # mark user and position
616 db_entry.user_id = user_id
616 db_entry.user_id = user_id
617 db_entry.position = position
617 db_entry.position = position
618 db_entry.title = entry.get('title')
618 db_entry.title = entry.get('title')
619 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
619 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
620 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
620 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
621
621
622 Session().add(db_entry)
622 Session().add(db_entry)
623
623
624 @LoginRequired()
624 @LoginRequired()
625 @NotAnonymous()
625 @NotAnonymous()
626 @CSRFRequired()
626 @CSRFRequired()
627 def my_account_bookmarks_update(self):
627 def my_account_bookmarks_update(self):
628 _ = self.request.translate
628 _ = self.request.translate
629 c = self.load_default_context()
629 c = self.load_default_context()
630 c.active = 'bookmarks'
630 c.active = 'bookmarks'
631
631
632 controls = peppercorn.parse(self.request.POST.items())
632 controls = peppercorn.parse(self.request.POST.items())
633 user_id = c.user.user_id
633 user_id = c.user.user_id
634
634
635 # validate positions
635 # validate positions
636 positions = {}
636 positions = {}
637 for entry in controls.get('bookmarks', []):
637 for entry in controls.get('bookmarks', []):
638 position = safe_int(entry['position'])
638 position = safe_int(entry['position'])
639 if position is None:
639 if position is None:
640 continue
640 continue
641
641
642 if position in positions:
642 if position in positions:
643 h.flash(_("Position {} is defined twice. "
643 h.flash(_("Position {} is defined twice. "
644 "Please correct this error.").format(position), category='error')
644 "Please correct this error.").format(position), category='error')
645 return HTTPFound(h.route_path('my_account_bookmarks'))
645 return HTTPFound(h.route_path('my_account_bookmarks'))
646
646
647 entry['position'] = position
647 entry['position'] = position
648 entry['cur_position'] = safe_int(entry.get('cur_position'))
648 entry['cur_position'] = safe_int(entry.get('cur_position'))
649 positions[position] = entry
649 positions[position] = entry
650
650
651 try:
651 try:
652 for entry in positions.values():
652 for entry in positions.values():
653 self._process_bookmark_entry(entry, user_id)
653 self._process_bookmark_entry(entry, user_id)
654
654
655 Session().commit()
655 Session().commit()
656 h.flash(_("Update Bookmarks"), category='success')
656 h.flash(_("Update Bookmarks"), category='success')
657 except IntegrityError:
657 except IntegrityError:
658 h.flash(_("Failed to update bookmarks. "
658 h.flash(_("Failed to update bookmarks. "
659 "Make sure an unique position is used."), category='error')
659 "Make sure an unique position is used."), category='error')
660
660
661 return HTTPFound(h.route_path('my_account_bookmarks'))
661 return HTTPFound(h.route_path('my_account_bookmarks'))
662
662
663 @LoginRequired()
663 @LoginRequired()
664 @NotAnonymous()
664 @NotAnonymous()
665 def my_account_goto_bookmark(self):
665 def my_account_goto_bookmark(self):
666
666
667 bookmark_id = self.request.matchdict['bookmark_id']
667 bookmark_id = self.request.matchdict['bookmark_id']
668 user_bookmark = UserBookmark().query()\
668 user_bookmark = UserBookmark().query()\
669 .filter(UserBookmark.user_id == self.request.user.user_id) \
669 .filter(UserBookmark.user_id == self.request.user.user_id) \
670 .filter(UserBookmark.position == bookmark_id).scalar()
670 .filter(UserBookmark.position == bookmark_id).scalar()
671
671
672 redirect_url = h.route_path('my_account_bookmarks')
672 redirect_url = h.route_path('my_account_bookmarks')
673 if not user_bookmark:
673 if not user_bookmark:
674 raise HTTPFound(redirect_url)
674 raise HTTPFound(redirect_url)
675
675
676 # repository set
676 # repository set
677 if user_bookmark.repository:
677 if user_bookmark.repository:
678 repo_name = user_bookmark.repository.repo_name
678 repo_name = user_bookmark.repository.repo_name
679 base_redirect_url = h.route_path(
679 base_redirect_url = h.route_path(
680 'repo_summary', repo_name=repo_name)
680 'repo_summary', repo_name=repo_name)
681 if user_bookmark.redirect_url and \
681 if user_bookmark.redirect_url and \
682 '${repo_url}' in user_bookmark.redirect_url:
682 '${repo_url}' in user_bookmark.redirect_url:
683 redirect_url = string.Template(user_bookmark.redirect_url)\
683 redirect_url = string.Template(user_bookmark.redirect_url)\
684 .safe_substitute({'repo_url': base_redirect_url})
684 .safe_substitute({'repo_url': base_redirect_url})
685 else:
685 else:
686 redirect_url = base_redirect_url
686 redirect_url = base_redirect_url
687 # repository group set
687 # repository group set
688 elif user_bookmark.repository_group:
688 elif user_bookmark.repository_group:
689 repo_group_name = user_bookmark.repository_group.group_name
689 repo_group_name = user_bookmark.repository_group.group_name
690 base_redirect_url = h.route_path(
690 base_redirect_url = h.route_path(
691 'repo_group_home', repo_group_name=repo_group_name)
691 'repo_group_home', repo_group_name=repo_group_name)
692 if user_bookmark.redirect_url and \
692 if user_bookmark.redirect_url and \
693 '${repo_group_url}' in user_bookmark.redirect_url:
693 '${repo_group_url}' in user_bookmark.redirect_url:
694 redirect_url = string.Template(user_bookmark.redirect_url)\
694 redirect_url = string.Template(user_bookmark.redirect_url)\
695 .safe_substitute({'repo_group_url': base_redirect_url})
695 .safe_substitute({'repo_group_url': base_redirect_url})
696 else:
696 else:
697 redirect_url = base_redirect_url
697 redirect_url = base_redirect_url
698 # custom URL set
698 # custom URL set
699 elif user_bookmark.redirect_url:
699 elif user_bookmark.redirect_url:
700 server_url = h.route_url('home').rstrip('/')
700 server_url = h.route_url('home').rstrip('/')
701 redirect_url = string.Template(user_bookmark.redirect_url) \
701 redirect_url = string.Template(user_bookmark.redirect_url) \
702 .safe_substitute({'server_url': server_url})
702 .safe_substitute({'server_url': server_url})
703
703
704 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
704 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
705 raise HTTPFound(redirect_url)
705 raise HTTPFound(redirect_url)
706
706
707 @LoginRequired()
707 @LoginRequired()
708 @NotAnonymous()
708 @NotAnonymous()
709 def my_account_perms(self):
709 def my_account_perms(self):
710 c = self.load_default_context()
710 c = self.load_default_context()
711 c.active = 'perms'
711 c.active = 'perms'
712
712
713 c.perm_user = c.auth_user
713 c.perm_user = c.auth_user
714 return self._get_template_context(c)
714 return self._get_template_context(c)
715
715
716 @LoginRequired()
716 @LoginRequired()
717 @NotAnonymous()
717 @NotAnonymous()
718 def my_notifications(self):
718 def my_notifications(self):
719 c = self.load_default_context()
719 c = self.load_default_context()
720 c.active = 'notifications'
720 c.active = 'notifications'
721
721
722 return self._get_template_context(c)
722 return self._get_template_context(c)
723
723
724 @LoginRequired()
724 @LoginRequired()
725 @NotAnonymous()
725 @NotAnonymous()
726 @CSRFRequired()
726 @CSRFRequired()
727 def my_notifications_toggle_visibility(self):
727 def my_notifications_toggle_visibility(self):
728 user = self._rhodecode_db_user
728 user = self._rhodecode_db_user
729 new_status = not user.user_data.get('notification_status', True)
729 new_status = not user.user_data.get('notification_status', True)
730 user.update_userdata(notification_status=new_status)
730 user.update_userdata(notification_status=new_status)
731 Session().commit()
731 Session().commit()
732 return user.user_data['notification_status']
732 return user.user_data['notification_status']
733
733
734 def _get_pull_requests_list(self, statuses, filter_type=None):
734 def _get_pull_requests_list(self, statuses, filter_type=None):
735 draw, start, limit = self._extract_chunk(self.request)
735 draw, start, limit = self._extract_chunk(self.request)
736 search_q, order_by, order_dir = self._extract_ordering(self.request)
736 search_q, order_by, order_dir = self._extract_ordering(self.request)
737
737
738 _render = self.request.get_partial_renderer(
738 _render = self.request.get_partial_renderer(
739 'rhodecode:templates/data_table/_dt_elements.mako')
739 'rhodecode:templates/data_table/_dt_elements.mako')
740
740
741 if filter_type == 'awaiting_my_review':
741 if filter_type == 'awaiting_my_review':
742 pull_requests = PullRequestModel().get_im_participating_in_for_review(
742 pull_requests = PullRequestModel().get_im_participating_in_for_review(
743 user_id=self._rhodecode_user.user_id,
743 user_id=self._rhodecode_user.user_id,
744 statuses=statuses, query=search_q,
744 statuses=statuses, query=search_q,
745 offset=start, length=limit, order_by=order_by,
745 offset=start, length=limit, order_by=order_by,
746 order_dir=order_dir)
746 order_dir=order_dir)
747
747
748 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
748 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
749 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
749 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
750 else:
750 else:
751 pull_requests = PullRequestModel().get_im_participating_in(
751 pull_requests = PullRequestModel().get_im_participating_in(
752 user_id=self._rhodecode_user.user_id,
752 user_id=self._rhodecode_user.user_id,
753 statuses=statuses, query=search_q,
753 statuses=statuses, query=search_q,
754 offset=start, length=limit, order_by=order_by,
754 offset=start, length=limit, order_by=order_by,
755 order_dir=order_dir)
755 order_dir=order_dir)
756
756
757 pull_requests_total_count = PullRequestModel().count_im_participating_in(
757 pull_requests_total_count = PullRequestModel().count_im_participating_in(
758 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
758 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
759
759
760 data = []
760 data = []
761 comments_model = CommentsModel()
761 comments_model = CommentsModel()
762 for pr in pull_requests:
762 for pr in pull_requests:
763 repo_id = pr.target_repo_id
763 repo_id = pr.target_repo_id
764 comments_count = comments_model.get_all_comments(
764 comments_count = comments_model.get_all_comments(
765 repo_id, pull_request=pr, include_drafts=False, count_only=True)
765 repo_id, pull_request=pr, include_drafts=False, count_only=True)
766 owned = pr.user_id == self._rhodecode_user.user_id
766 owned = pr.user_id == self._rhodecode_user.user_id
767
767
768 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
768 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
769 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
769 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
770 if review_statuses and review_statuses[4]:
770 if review_statuses and review_statuses[4]:
771 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
771 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
772 my_review_status = statuses[0][1].status
772 my_review_status = statuses[0][1].status
773
773
774 data.append({
774 data.append({
775 'target_repo': _render('pullrequest_target_repo',
775 'target_repo': _render('pullrequest_target_repo',
776 pr.target_repo.repo_name),
776 pr.target_repo.repo_name),
777 'name': _render('pullrequest_name',
777 'name': _render('pullrequest_name',
778 pr.pull_request_id, pr.pull_request_state,
778 pr.pull_request_id, pr.pull_request_state,
779 pr.work_in_progress, pr.target_repo.repo_name,
779 pr.work_in_progress, pr.target_repo.repo_name,
780 short=True),
780 short=True),
781 'name_raw': pr.pull_request_id,
781 'name_raw': pr.pull_request_id,
782 'status': _render('pullrequest_status',
782 'status': _render('pullrequest_status',
783 pr.calculated_review_status()),
783 pr.calculated_review_status()),
784 'my_status': _render('pullrequest_status',
784 'my_status': _render('pullrequest_status',
785 my_review_status),
785 my_review_status),
786 'title': _render('pullrequest_title', pr.title, pr.description),
786 'title': _render('pullrequest_title', pr.title, pr.description),
787 'pr_flow': _render('pullrequest_commit_flow', pr),
787 'pr_flow': _render('pullrequest_commit_flow', pr),
788 'description': h.escape(pr.description),
788 'description': h.escape(pr.description),
789 'updated_on': _render('pullrequest_updated_on',
789 'updated_on': _render('pullrequest_updated_on',
790 h.datetime_to_time(pr.updated_on),
790 h.datetime_to_time(pr.updated_on),
791 pr.versions_count),
791 pr.versions_count),
792 'updated_on_raw': h.datetime_to_time(pr.updated_on),
792 'updated_on_raw': h.datetime_to_time(pr.updated_on),
793 'created_on': _render('pullrequest_updated_on',
793 'created_on': _render('pullrequest_updated_on',
794 h.datetime_to_time(pr.created_on)),
794 h.datetime_to_time(pr.created_on)),
795 'created_on_raw': h.datetime_to_time(pr.created_on),
795 'created_on_raw': h.datetime_to_time(pr.created_on),
796 'state': pr.pull_request_state,
796 'state': pr.pull_request_state,
797 'author': _render('pullrequest_author',
797 'author': _render('pullrequest_author',
798 pr.author.full_contact, ),
798 pr.author.full_contact, ),
799 'author_raw': pr.author.full_name,
799 'author_raw': pr.author.full_name,
800 'comments': _render('pullrequest_comments', comments_count),
800 'comments': _render('pullrequest_comments', comments_count),
801 'comments_raw': comments_count,
801 'comments_raw': comments_count,
802 'closed': pr.is_closed(),
802 'closed': pr.is_closed(),
803 'owned': owned
803 'owned': owned
804 })
804 })
805
805
806 # json used to render the grid
806 # json used to render the grid
807 data = ({
807 data = ({
808 'draw': draw,
808 'draw': draw,
809 'data': data,
809 'data': data,
810 'recordsTotal': pull_requests_total_count,
810 'recordsTotal': pull_requests_total_count,
811 'recordsFiltered': pull_requests_total_count,
811 'recordsFiltered': pull_requests_total_count,
812 })
812 })
813 return data
813 return data
814
814
815 @LoginRequired()
815 @LoginRequired()
816 @NotAnonymous()
816 @NotAnonymous()
817 def my_account_pullrequests(self):
817 def my_account_pullrequests(self):
818 c = self.load_default_context()
818 c = self.load_default_context()
819 c.active = 'pullrequests'
819 c.active = 'pullrequests'
820 req_get = self.request.GET
820 req_get = self.request.GET
821
821
822 c.closed = str2bool(req_get.get('closed'))
822 c.closed = str2bool(req_get.get('closed'))
823 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
823 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
824
824
825 c.selected_filter = 'all'
825 c.selected_filter = 'all'
826 if c.closed:
826 if c.closed:
827 c.selected_filter = 'all_closed'
827 c.selected_filter = 'all_closed'
828 if c.awaiting_my_review:
828 if c.awaiting_my_review:
829 c.selected_filter = 'awaiting_my_review'
829 c.selected_filter = 'awaiting_my_review'
830
830
831 return self._get_template_context(c)
831 return self._get_template_context(c)
832
832
833 @LoginRequired()
833 @LoginRequired()
834 @NotAnonymous()
834 @NotAnonymous()
835 def my_account_pullrequests_data(self):
835 def my_account_pullrequests_data(self):
836 self.load_default_context()
836 self.load_default_context()
837 req_get = self.request.GET
837 req_get = self.request.GET
838
838
839 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
839 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
840 closed = str2bool(req_get.get('closed'))
840 closed = str2bool(req_get.get('closed'))
841
841
842 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
842 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
843 if closed:
843 if closed:
844 statuses += [PullRequest.STATUS_CLOSED]
844 statuses += [PullRequest.STATUS_CLOSED]
845
845
846 filter_type = \
846 filter_type = \
847 'awaiting_my_review' if awaiting_my_review \
847 'awaiting_my_review' if awaiting_my_review \
848 else None
848 else None
849
849
850 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
850 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
851 return data
851 return data
852
852
853 @LoginRequired()
853 @LoginRequired()
854 @NotAnonymous()
854 @NotAnonymous()
855 def my_account_user_group_membership(self):
855 def my_account_user_group_membership(self):
856 c = self.load_default_context()
856 c = self.load_default_context()
857 c.active = 'user_group_membership'
857 c.active = 'user_group_membership'
858 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
858 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
859 for group in self._rhodecode_db_user.group_member]
859 for group in self._rhodecode_db_user.group_member]
860 c.user_groups = ext_json.str_json(groups)
860 c.user_groups = ext_json.str_json(groups)
861 return self._get_template_context(c)
861 return self._get_template_context(c)
@@ -1,6038 +1,6043 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 Database Models for RhodeCode Enterprise
20 Database Models for RhodeCode Enterprise
21 """
21 """
22
22
23 import re
23 import re
24 import os
24 import os
25 import time
25 import time
26 import string
26 import string
27 import logging
27 import logging
28 import datetime
28 import datetime
29 import uuid
29 import uuid
30 import warnings
30 import warnings
31 import ipaddress
31 import ipaddress
32 import functools
32 import functools
33 import traceback
33 import traceback
34 import collections
34 import collections
35
35
36 import pyotp
36 import pyotp
37 from sqlalchemy import (
37 from sqlalchemy import (
38 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 or_, and_, not_, func, cast, TypeDecorator, event, select,
39 true, false, null, union_all,
39 true, false, null, union_all,
40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Text, Float, PickleType, BigInteger)
42 Text, Float, PickleType, BigInteger)
43 from sqlalchemy.sql.expression import case
43 from sqlalchemy.sql.expression import case
44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.orm import (
45 from sqlalchemy.orm import (
46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
47 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.dialects.mysql import LONGTEXT
50 from sqlalchemy.dialects.mysql import LONGTEXT
51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
53 from webhelpers2.text import remove_formatting
53 from webhelpers2.text import remove_formatting
54
54
55 from rhodecode import ConfigGet
55 from rhodecode import ConfigGet
56 from rhodecode.lib.str_utils import safe_bytes
56 from rhodecode.lib.str_utils import safe_bytes
57 from rhodecode.translation import _
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import (
59 from rhodecode.lib.vcs.backends.base import (
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 from rhodecode.lib.utils2 import (
61 from rhodecode.lib.utils2 import (
62 str2bool, safe_str, get_commit_safe, sha1_safe,
62 str2bool, safe_str, get_commit_safe, sha1_safe,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
65 from rhodecode.lib.jsonalchemy import (
65 from rhodecode.lib.jsonalchemy import (
66 MutationObj, MutationList, JsonType, JsonRaw)
66 MutationObj, MutationList, JsonType, JsonRaw)
67 from rhodecode.lib.hash_utils import sha1
67 from rhodecode.lib.hash_utils import sha1
68 from rhodecode.lib import ext_json
68 from rhodecode.lib import ext_json
69 from rhodecode.lib import enc_utils
69 from rhodecode.lib import enc_utils
70 from rhodecode.lib.ext_json import json, str_json
70 from rhodecode.lib.ext_json import json, str_json
71 from rhodecode.lib.caching_query import FromCache
71 from rhodecode.lib.caching_query import FromCache
72 from rhodecode.lib.exceptions import (
72 from rhodecode.lib.exceptions import (
73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
74 from rhodecode.model.meta import Base, Session
74 from rhodecode.model.meta import Base, Session
75
75
76 URL_SEP = '/'
76 URL_SEP = '/'
77 log = logging.getLogger(__name__)
77 log = logging.getLogger(__name__)
78
78
79 # =============================================================================
79 # =============================================================================
80 # BASE CLASSES
80 # BASE CLASSES
81 # =============================================================================
81 # =============================================================================
82
82
83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
84 # beaker.session.secret if first is not set.
84 # beaker.session.secret if first is not set.
85 # and initialized at environment.py
85 # and initialized at environment.py
86 ENCRYPTION_KEY: bytes = b''
86 ENCRYPTION_KEY: bytes = b''
87
87
88 # used to sort permissions by types, '#' used here is not allowed to be in
88 # used to sort permissions by types, '#' used here is not allowed to be in
89 # usernames, and it's very early in sorted string.printable table.
89 # usernames, and it's very early in sorted string.printable table.
90 PERMISSION_TYPE_SORT = {
90 PERMISSION_TYPE_SORT = {
91 'admin': '####',
91 'admin': '####',
92 'write': '###',
92 'write': '###',
93 'read': '##',
93 'read': '##',
94 'none': '#',
94 'none': '#',
95 }
95 }
96
96
97
97
98 def display_user_sort(obj):
98 def display_user_sort(obj):
99 """
99 """
100 Sort function used to sort permissions in .permissions() function of
100 Sort function used to sort permissions in .permissions() function of
101 Repository, RepoGroup, UserGroup. Also it put the default user in front
101 Repository, RepoGroup, UserGroup. Also it put the default user in front
102 of all other resources
102 of all other resources
103 """
103 """
104
104
105 if obj.username == User.DEFAULT_USER:
105 if obj.username == User.DEFAULT_USER:
106 return '#####'
106 return '#####'
107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
108 extra_sort_num = '1' # default
108 extra_sort_num = '1' # default
109
109
110 # NOTE(dan): inactive duplicates goes last
110 # NOTE(dan): inactive duplicates goes last
111 if getattr(obj, 'duplicate_perm', None):
111 if getattr(obj, 'duplicate_perm', None):
112 extra_sort_num = '9'
112 extra_sort_num = '9'
113 return prefix + extra_sort_num + obj.username
113 return prefix + extra_sort_num + obj.username
114
114
115
115
116 def display_user_group_sort(obj):
116 def display_user_group_sort(obj):
117 """
117 """
118 Sort function used to sort permissions in .permissions() function of
118 Sort function used to sort permissions in .permissions() function of
119 Repository, RepoGroup, UserGroup. Also it put the default user in front
119 Repository, RepoGroup, UserGroup. Also it put the default user in front
120 of all other resources
120 of all other resources
121 """
121 """
122
122
123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
124 return prefix + obj.users_group_name
124 return prefix + obj.users_group_name
125
125
126
126
127 def _hash_key(k):
127 def _hash_key(k):
128 return sha1_safe(k)
128 return sha1_safe(k)
129
129
130
130
131 def in_filter_generator(qry, items, limit=500):
131 def in_filter_generator(qry, items, limit=500):
132 """
132 """
133 Splits IN() into multiple with OR
133 Splits IN() into multiple with OR
134 e.g.::
134 e.g.::
135 cnt = Repository.query().filter(
135 cnt = Repository.query().filter(
136 or_(
136 or_(
137 *in_filter_generator(Repository.repo_id, range(100000))
137 *in_filter_generator(Repository.repo_id, range(100000))
138 )).count()
138 )).count()
139 """
139 """
140 if not items:
140 if not items:
141 # empty list will cause empty query which might cause security issues
141 # empty list will cause empty query which might cause security issues
142 # this can lead to hidden unpleasant results
142 # this can lead to hidden unpleasant results
143 items = [-1]
143 items = [-1]
144
144
145 parts = []
145 parts = []
146 for chunk in range(0, len(items), limit):
146 for chunk in range(0, len(items), limit):
147 parts.append(
147 parts.append(
148 qry.in_(items[chunk: chunk + limit])
148 qry.in_(items[chunk: chunk + limit])
149 )
149 )
150
150
151 return parts
151 return parts
152
152
153
153
154 base_table_args = {
154 base_table_args = {
155 'extend_existing': True,
155 'extend_existing': True,
156 'mysql_engine': 'InnoDB',
156 'mysql_engine': 'InnoDB',
157 'mysql_charset': 'utf8',
157 'mysql_charset': 'utf8',
158 'sqlite_autoincrement': True
158 'sqlite_autoincrement': True
159 }
159 }
160
160
161
161
162 class EncryptedTextValue(TypeDecorator):
162 class EncryptedTextValue(TypeDecorator):
163 """
163 """
164 Special column for encrypted long text data, use like::
164 Special column for encrypted long text data, use like::
165
165
166 value = Column("encrypted_value", EncryptedValue(), nullable=False)
166 value = Column("encrypted_value", EncryptedValue(), nullable=False)
167
167
168 This column is intelligent so if value is in unencrypted form it return
168 This column is intelligent so if value is in unencrypted form it return
169 unencrypted form, but on save it always encrypts
169 unencrypted form, but on save it always encrypts
170 """
170 """
171 cache_ok = True
171 cache_ok = True
172 impl = Text
172 impl = Text
173
173
174 def process_bind_param(self, value, dialect):
174 def process_bind_param(self, value, dialect):
175 """
175 """
176 Setter for storing value
176 Setter for storing value
177 """
177 """
178 import rhodecode
178 import rhodecode
179 if not value:
179 if not value:
180 return value
180 return value
181
181
182 # protect against double encrypting if values is already encrypted
182 # protect against double encrypting if values is already encrypted
183 if value.startswith('enc$aes$') \
183 if value.startswith('enc$aes$') \
184 or value.startswith('enc$aes_hmac$') \
184 or value.startswith('enc$aes_hmac$') \
185 or value.startswith('enc2$'):
185 or value.startswith('enc2$'):
186 raise ValueError('value needs to be in unencrypted format, '
186 raise ValueError('value needs to be in unencrypted format, '
187 'ie. not starting with enc$ or enc2$')
187 'ie. not starting with enc$ or enc2$')
188
188
189 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
189 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
190 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
190 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
191 return safe_str(bytes_val)
191 return safe_str(bytes_val)
192
192
193 def process_result_value(self, value, dialect):
193 def process_result_value(self, value, dialect):
194 """
194 """
195 Getter for retrieving value
195 Getter for retrieving value
196 """
196 """
197
197
198 import rhodecode
198 import rhodecode
199 if not value:
199 if not value:
200 return value
200 return value
201
201
202 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
202 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
203
203
204 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
204 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
205
205
206 return safe_str(bytes_val)
206 return safe_str(bytes_val)
207
207
208
208
209 class BaseModel(object):
209 class BaseModel(object):
210 """
210 """
211 Base Model for all classes
211 Base Model for all classes
212 """
212 """
213
213
214 @classmethod
214 @classmethod
215 def _get_keys(cls):
215 def _get_keys(cls):
216 """return column names for this model """
216 """return column names for this model """
217 return class_mapper(cls).c.keys()
217 return class_mapper(cls).c.keys()
218
218
219 def get_dict(self):
219 def get_dict(self):
220 """
220 """
221 return dict with keys and values corresponding
221 return dict with keys and values corresponding
222 to this model data """
222 to this model data """
223
223
224 d = {}
224 d = {}
225 for k in self._get_keys():
225 for k in self._get_keys():
226 d[k] = getattr(self, k)
226 d[k] = getattr(self, k)
227
227
228 # also use __json__() if present to get additional fields
228 # also use __json__() if present to get additional fields
229 _json_attr = getattr(self, '__json__', None)
229 _json_attr = getattr(self, '__json__', None)
230 if _json_attr:
230 if _json_attr:
231 # update with attributes from __json__
231 # update with attributes from __json__
232 if callable(_json_attr):
232 if callable(_json_attr):
233 _json_attr = _json_attr()
233 _json_attr = _json_attr()
234 for k, val in _json_attr.items():
234 for k, val in _json_attr.items():
235 d[k] = val
235 d[k] = val
236 return d
236 return d
237
237
238 def get_appstruct(self):
238 def get_appstruct(self):
239 """return list with keys and values tuples corresponding
239 """return list with keys and values tuples corresponding
240 to this model data """
240 to this model data """
241
241
242 lst = []
242 lst = []
243 for k in self._get_keys():
243 for k in self._get_keys():
244 lst.append((k, getattr(self, k),))
244 lst.append((k, getattr(self, k),))
245 return lst
245 return lst
246
246
247 def populate_obj(self, populate_dict):
247 def populate_obj(self, populate_dict):
248 """populate model with data from given populate_dict"""
248 """populate model with data from given populate_dict"""
249
249
250 for k in self._get_keys():
250 for k in self._get_keys():
251 if k in populate_dict:
251 if k in populate_dict:
252 setattr(self, k, populate_dict[k])
252 setattr(self, k, populate_dict[k])
253
253
254 @classmethod
254 @classmethod
255 def query(cls):
255 def query(cls):
256 return Session().query(cls)
256 return Session().query(cls)
257
257
258 @classmethod
258 @classmethod
259 def select(cls, custom_cls=None):
259 def select(cls, custom_cls=None):
260 """
260 """
261 stmt = cls.select().where(cls.user_id==1)
261 stmt = cls.select().where(cls.user_id==1)
262 # optionally
262 # optionally
263 stmt = cls.select(User.user_id).where(cls.user_id==1)
263 stmt = cls.select(User.user_id).where(cls.user_id==1)
264 result = cls.execute(stmt) | cls.scalars(stmt)
264 result = cls.execute(stmt) | cls.scalars(stmt)
265 """
265 """
266
266
267 if custom_cls:
267 if custom_cls:
268 stmt = select(custom_cls)
268 stmt = select(custom_cls)
269 else:
269 else:
270 stmt = select(cls)
270 stmt = select(cls)
271 return stmt
271 return stmt
272
272
273 @classmethod
273 @classmethod
274 def execute(cls, stmt):
274 def execute(cls, stmt):
275 return Session().execute(stmt)
275 return Session().execute(stmt)
276
276
277 @classmethod
277 @classmethod
278 def scalars(cls, stmt):
278 def scalars(cls, stmt):
279 return Session().scalars(stmt)
279 return Session().scalars(stmt)
280
280
281 @classmethod
281 @classmethod
282 def get(cls, id_):
282 def get(cls, id_):
283 if id_:
283 if id_:
284 return cls.query().get(id_)
284 return cls.query().get(id_)
285
285
286 @classmethod
286 @classmethod
287 def get_or_404(cls, id_):
287 def get_or_404(cls, id_):
288 from pyramid.httpexceptions import HTTPNotFound
288 from pyramid.httpexceptions import HTTPNotFound
289
289
290 try:
290 try:
291 id_ = int(id_)
291 id_ = int(id_)
292 except (TypeError, ValueError):
292 except (TypeError, ValueError):
293 raise HTTPNotFound()
293 raise HTTPNotFound()
294
294
295 res = cls.query().get(id_)
295 res = cls.query().get(id_)
296 if not res:
296 if not res:
297 raise HTTPNotFound()
297 raise HTTPNotFound()
298 return res
298 return res
299
299
300 @classmethod
300 @classmethod
301 def getAll(cls):
301 def getAll(cls):
302 # deprecated and left for backward compatibility
302 # deprecated and left for backward compatibility
303 return cls.get_all()
303 return cls.get_all()
304
304
305 @classmethod
305 @classmethod
306 def get_all(cls):
306 def get_all(cls):
307 return cls.query().all()
307 return cls.query().all()
308
308
309 @classmethod
309 @classmethod
310 def delete(cls, id_):
310 def delete(cls, id_):
311 obj = cls.query().get(id_)
311 obj = cls.query().get(id_)
312 Session().delete(obj)
312 Session().delete(obj)
313
313
314 @classmethod
314 @classmethod
315 def identity_cache(cls, session, attr_name, value):
315 def identity_cache(cls, session, attr_name, value):
316 exist_in_session = []
316 exist_in_session = []
317 for (item_cls, pkey), instance in session.identity_map.items():
317 for (item_cls, pkey), instance in session.identity_map.items():
318 if cls == item_cls and getattr(instance, attr_name) == value:
318 if cls == item_cls and getattr(instance, attr_name) == value:
319 exist_in_session.append(instance)
319 exist_in_session.append(instance)
320 if exist_in_session:
320 if exist_in_session:
321 if len(exist_in_session) == 1:
321 if len(exist_in_session) == 1:
322 return exist_in_session[0]
322 return exist_in_session[0]
323 log.exception(
323 log.exception(
324 'multiple objects with attr %s and '
324 'multiple objects with attr %s and '
325 'value %s found with same name: %r',
325 'value %s found with same name: %r',
326 attr_name, value, exist_in_session)
326 attr_name, value, exist_in_session)
327
327
328 @property
328 @property
329 def cls_name(self):
329 def cls_name(self):
330 return self.__class__.__name__
330 return self.__class__.__name__
331
331
332 def __repr__(self):
332 def __repr__(self):
333 return f'<DB:{self.cls_name}>'
333 return f'<DB:{self.cls_name}>'
334
334
335
335
336 class RhodeCodeSetting(Base, BaseModel):
336 class RhodeCodeSetting(Base, BaseModel):
337 __tablename__ = 'rhodecode_settings'
337 __tablename__ = 'rhodecode_settings'
338 __table_args__ = (
338 __table_args__ = (
339 UniqueConstraint('app_settings_name'),
339 UniqueConstraint('app_settings_name'),
340 base_table_args
340 base_table_args
341 )
341 )
342
342
343 SETTINGS_TYPES = {
343 SETTINGS_TYPES = {
344 'str': safe_str,
344 'str': safe_str,
345 'int': safe_int,
345 'int': safe_int,
346 'unicode': safe_str,
346 'unicode': safe_str,
347 'bool': str2bool,
347 'bool': str2bool,
348 'list': functools.partial(aslist, sep=',')
348 'list': functools.partial(aslist, sep=',')
349 }
349 }
350 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
350 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
351 GLOBAL_CONF_KEY = 'app_settings'
351 GLOBAL_CONF_KEY = 'app_settings'
352
352
353 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
353 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
354 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
354 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
355 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
355 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
356 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
356 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
357
357
358 def __init__(self, key='', val='', type='unicode'):
358 def __init__(self, key='', val='', type='unicode'):
359 self.app_settings_name = key
359 self.app_settings_name = key
360 self.app_settings_type = type
360 self.app_settings_type = type
361 self.app_settings_value = val
361 self.app_settings_value = val
362
362
363 @validates('_app_settings_value')
363 @validates('_app_settings_value')
364 def validate_settings_value(self, key, val):
364 def validate_settings_value(self, key, val):
365 assert type(val) == str
365 assert type(val) == str
366 return val
366 return val
367
367
368 @hybrid_property
368 @hybrid_property
369 def app_settings_value(self):
369 def app_settings_value(self):
370 v = self._app_settings_value
370 v = self._app_settings_value
371 _type = self.app_settings_type
371 _type = self.app_settings_type
372 if _type:
372 if _type:
373 _type = self.app_settings_type.split('.')[0]
373 _type = self.app_settings_type.split('.')[0]
374 # decode the encrypted value
374 # decode the encrypted value
375 if 'encrypted' in self.app_settings_type:
375 if 'encrypted' in self.app_settings_type:
376 cipher = EncryptedTextValue()
376 cipher = EncryptedTextValue()
377 v = safe_str(cipher.process_result_value(v, None))
377 v = safe_str(cipher.process_result_value(v, None))
378
378
379 converter = self.SETTINGS_TYPES.get(_type) or \
379 converter = self.SETTINGS_TYPES.get(_type) or \
380 self.SETTINGS_TYPES['unicode']
380 self.SETTINGS_TYPES['unicode']
381 return converter(v)
381 return converter(v)
382
382
383 @app_settings_value.setter
383 @app_settings_value.setter
384 def app_settings_value(self, val):
384 def app_settings_value(self, val):
385 """
385 """
386 Setter that will always make sure we use unicode in app_settings_value
386 Setter that will always make sure we use unicode in app_settings_value
387
387
388 :param val:
388 :param val:
389 """
389 """
390 val = safe_str(val)
390 val = safe_str(val)
391 # encode the encrypted value
391 # encode the encrypted value
392 if 'encrypted' in self.app_settings_type:
392 if 'encrypted' in self.app_settings_type:
393 cipher = EncryptedTextValue()
393 cipher = EncryptedTextValue()
394 val = safe_str(cipher.process_bind_param(val, None))
394 val = safe_str(cipher.process_bind_param(val, None))
395 self._app_settings_value = val
395 self._app_settings_value = val
396
396
397 @hybrid_property
397 @hybrid_property
398 def app_settings_type(self):
398 def app_settings_type(self):
399 return self._app_settings_type
399 return self._app_settings_type
400
400
401 @app_settings_type.setter
401 @app_settings_type.setter
402 def app_settings_type(self, val):
402 def app_settings_type(self, val):
403 if val.split('.')[0] not in self.SETTINGS_TYPES:
403 if val.split('.')[0] not in self.SETTINGS_TYPES:
404 raise Exception('type must be one of %s got %s'
404 raise Exception('type must be one of %s got %s'
405 % (self.SETTINGS_TYPES.keys(), val))
405 % (self.SETTINGS_TYPES.keys(), val))
406 self._app_settings_type = val
406 self._app_settings_type = val
407
407
408 @classmethod
408 @classmethod
409 def get_by_prefix(cls, prefix):
409 def get_by_prefix(cls, prefix):
410 return RhodeCodeSetting.query()\
410 return RhodeCodeSetting.query()\
411 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
411 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
412 .all()
412 .all()
413
413
414 def __repr__(self):
414 def __repr__(self):
415 return "<%s('%s:%s[%s]')>" % (
415 return "<%s('%s:%s[%s]')>" % (
416 self.cls_name,
416 self.cls_name,
417 self.app_settings_name, self.app_settings_value,
417 self.app_settings_name, self.app_settings_value,
418 self.app_settings_type
418 self.app_settings_type
419 )
419 )
420
420
421
421
422 class RhodeCodeUi(Base, BaseModel):
422 class RhodeCodeUi(Base, BaseModel):
423 __tablename__ = 'rhodecode_ui'
423 __tablename__ = 'rhodecode_ui'
424 __table_args__ = (
424 __table_args__ = (
425 UniqueConstraint('ui_key'),
425 UniqueConstraint('ui_key'),
426 base_table_args
426 base_table_args
427 )
427 )
428 # Sync those values with vcsserver.config.hooks
428 # Sync those values with vcsserver.config.hooks
429
429
430 HOOK_REPO_SIZE = 'changegroup.repo_size'
430 HOOK_REPO_SIZE = 'changegroup.repo_size'
431 # HG
431 # HG
432 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
432 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
433 HOOK_PULL = 'outgoing.pull_logger'
433 HOOK_PULL = 'outgoing.pull_logger'
434 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
434 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
435 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
435 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
436 HOOK_PUSH = 'changegroup.push_logger'
436 HOOK_PUSH = 'changegroup.push_logger'
437 HOOK_PUSH_KEY = 'pushkey.key_push'
437 HOOK_PUSH_KEY = 'pushkey.key_push'
438
438
439 HOOKS_BUILTIN = [
439 HOOKS_BUILTIN = [
440 HOOK_PRE_PULL,
440 HOOK_PRE_PULL,
441 HOOK_PULL,
441 HOOK_PULL,
442 HOOK_PRE_PUSH,
442 HOOK_PRE_PUSH,
443 HOOK_PRETX_PUSH,
443 HOOK_PRETX_PUSH,
444 HOOK_PUSH,
444 HOOK_PUSH,
445 HOOK_PUSH_KEY,
445 HOOK_PUSH_KEY,
446 ]
446 ]
447
447
448 # TODO: johbo: Unify way how hooks are configured for git and hg,
448 # TODO: johbo: Unify way how hooks are configured for git and hg,
449 # git part is currently hardcoded.
449 # git part is currently hardcoded.
450
450
451 # SVN PATTERNS
451 # SVN PATTERNS
452 SVN_BRANCH_ID = 'vcs_svn_branch'
452 SVN_BRANCH_ID = 'vcs_svn_branch'
453 SVN_TAG_ID = 'vcs_svn_tag'
453 SVN_TAG_ID = 'vcs_svn_tag'
454
454
455 ui_id = Column(
455 ui_id = Column(
456 "ui_id", Integer(), nullable=False, unique=True, default=None,
456 "ui_id", Integer(), nullable=False, unique=True, default=None,
457 primary_key=True)
457 primary_key=True)
458 ui_section = Column(
458 ui_section = Column(
459 "ui_section", String(255), nullable=True, unique=None, default=None)
459 "ui_section", String(255), nullable=True, unique=None, default=None)
460 ui_key = Column(
460 ui_key = Column(
461 "ui_key", String(255), nullable=True, unique=None, default=None)
461 "ui_key", String(255), nullable=True, unique=None, default=None)
462 ui_value = Column(
462 ui_value = Column(
463 "ui_value", String(255), nullable=True, unique=None, default=None)
463 "ui_value", String(255), nullable=True, unique=None, default=None)
464 ui_active = Column(
464 ui_active = Column(
465 "ui_active", Boolean(), nullable=True, unique=None, default=True)
465 "ui_active", Boolean(), nullable=True, unique=None, default=True)
466
466
467 def __repr__(self):
467 def __repr__(self):
468 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
468 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
469 self.ui_key, self.ui_value)
469 self.ui_key, self.ui_value)
470
470
471
471
472 class RepoRhodeCodeSetting(Base, BaseModel):
472 class RepoRhodeCodeSetting(Base, BaseModel):
473 __tablename__ = 'repo_rhodecode_settings'
473 __tablename__ = 'repo_rhodecode_settings'
474 __table_args__ = (
474 __table_args__ = (
475 UniqueConstraint(
475 UniqueConstraint(
476 'app_settings_name', 'repository_id',
476 'app_settings_name', 'repository_id',
477 name='uq_repo_rhodecode_setting_name_repo_id'),
477 name='uq_repo_rhodecode_setting_name_repo_id'),
478 base_table_args
478 base_table_args
479 )
479 )
480
480
481 repository_id = Column(
481 repository_id = Column(
482 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
482 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
483 nullable=False)
483 nullable=False)
484 app_settings_id = Column(
484 app_settings_id = Column(
485 "app_settings_id", Integer(), nullable=False, unique=True,
485 "app_settings_id", Integer(), nullable=False, unique=True,
486 default=None, primary_key=True)
486 default=None, primary_key=True)
487 app_settings_name = Column(
487 app_settings_name = Column(
488 "app_settings_name", String(255), nullable=True, unique=None,
488 "app_settings_name", String(255), nullable=True, unique=None,
489 default=None)
489 default=None)
490 _app_settings_value = Column(
490 _app_settings_value = Column(
491 "app_settings_value", String(4096), nullable=True, unique=None,
491 "app_settings_value", String(4096), nullable=True, unique=None,
492 default=None)
492 default=None)
493 _app_settings_type = Column(
493 _app_settings_type = Column(
494 "app_settings_type", String(255), nullable=True, unique=None,
494 "app_settings_type", String(255), nullable=True, unique=None,
495 default=None)
495 default=None)
496
496
497 repository = relationship('Repository', viewonly=True)
497 repository = relationship('Repository', viewonly=True)
498
498
499 def __init__(self, repository_id, key='', val='', type='unicode'):
499 def __init__(self, repository_id, key='', val='', type='unicode'):
500 self.repository_id = repository_id
500 self.repository_id = repository_id
501 self.app_settings_name = key
501 self.app_settings_name = key
502 self.app_settings_type = type
502 self.app_settings_type = type
503 self.app_settings_value = val
503 self.app_settings_value = val
504
504
505 @validates('_app_settings_value')
505 @validates('_app_settings_value')
506 def validate_settings_value(self, key, val):
506 def validate_settings_value(self, key, val):
507 assert type(val) == str
507 assert type(val) == str
508 return val
508 return val
509
509
510 @hybrid_property
510 @hybrid_property
511 def app_settings_value(self):
511 def app_settings_value(self):
512 v = self._app_settings_value
512 v = self._app_settings_value
513 type_ = self.app_settings_type
513 type_ = self.app_settings_type
514 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
514 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
515 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
515 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
516 return converter(v)
516 return converter(v)
517
517
518 @app_settings_value.setter
518 @app_settings_value.setter
519 def app_settings_value(self, val):
519 def app_settings_value(self, val):
520 """
520 """
521 Setter that will always make sure we use unicode in app_settings_value
521 Setter that will always make sure we use unicode in app_settings_value
522
522
523 :param val:
523 :param val:
524 """
524 """
525 self._app_settings_value = safe_str(val)
525 self._app_settings_value = safe_str(val)
526
526
527 @hybrid_property
527 @hybrid_property
528 def app_settings_type(self):
528 def app_settings_type(self):
529 return self._app_settings_type
529 return self._app_settings_type
530
530
531 @app_settings_type.setter
531 @app_settings_type.setter
532 def app_settings_type(self, val):
532 def app_settings_type(self, val):
533 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
533 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
534 if val not in SETTINGS_TYPES:
534 if val not in SETTINGS_TYPES:
535 raise Exception('type must be one of %s got %s'
535 raise Exception('type must be one of %s got %s'
536 % (SETTINGS_TYPES.keys(), val))
536 % (SETTINGS_TYPES.keys(), val))
537 self._app_settings_type = val
537 self._app_settings_type = val
538
538
539 def __repr__(self):
539 def __repr__(self):
540 return "<%s('%s:%s:%s[%s]')>" % (
540 return "<%s('%s:%s:%s[%s]')>" % (
541 self.cls_name, self.repository.repo_name,
541 self.cls_name, self.repository.repo_name,
542 self.app_settings_name, self.app_settings_value,
542 self.app_settings_name, self.app_settings_value,
543 self.app_settings_type
543 self.app_settings_type
544 )
544 )
545
545
546
546
547 class RepoRhodeCodeUi(Base, BaseModel):
547 class RepoRhodeCodeUi(Base, BaseModel):
548 __tablename__ = 'repo_rhodecode_ui'
548 __tablename__ = 'repo_rhodecode_ui'
549 __table_args__ = (
549 __table_args__ = (
550 UniqueConstraint(
550 UniqueConstraint(
551 'repository_id', 'ui_section', 'ui_key',
551 'repository_id', 'ui_section', 'ui_key',
552 name='uq_repo_rhodecode_ui_repository_id_section_key'),
552 name='uq_repo_rhodecode_ui_repository_id_section_key'),
553 base_table_args
553 base_table_args
554 )
554 )
555
555
556 repository_id = Column(
556 repository_id = Column(
557 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
557 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
558 nullable=False)
558 nullable=False)
559 ui_id = Column(
559 ui_id = Column(
560 "ui_id", Integer(), nullable=False, unique=True, default=None,
560 "ui_id", Integer(), nullable=False, unique=True, default=None,
561 primary_key=True)
561 primary_key=True)
562 ui_section = Column(
562 ui_section = Column(
563 "ui_section", String(255), nullable=True, unique=None, default=None)
563 "ui_section", String(255), nullable=True, unique=None, default=None)
564 ui_key = Column(
564 ui_key = Column(
565 "ui_key", String(255), nullable=True, unique=None, default=None)
565 "ui_key", String(255), nullable=True, unique=None, default=None)
566 ui_value = Column(
566 ui_value = Column(
567 "ui_value", String(255), nullable=True, unique=None, default=None)
567 "ui_value", String(255), nullable=True, unique=None, default=None)
568 ui_active = Column(
568 ui_active = Column(
569 "ui_active", Boolean(), nullable=True, unique=None, default=True)
569 "ui_active", Boolean(), nullable=True, unique=None, default=True)
570
570
571 repository = relationship('Repository', viewonly=True)
571 repository = relationship('Repository', viewonly=True)
572
572
573 def __repr__(self):
573 def __repr__(self):
574 return '<%s[%s:%s]%s=>%s]>' % (
574 return '<%s[%s:%s]%s=>%s]>' % (
575 self.cls_name, self.repository.repo_name,
575 self.cls_name, self.repository.repo_name,
576 self.ui_section, self.ui_key, self.ui_value)
576 self.ui_section, self.ui_key, self.ui_value)
577
577
578
578
579 class User(Base, BaseModel):
579 class User(Base, BaseModel):
580 __tablename__ = 'users'
580 __tablename__ = 'users'
581 __table_args__ = (
581 __table_args__ = (
582 UniqueConstraint('username'), UniqueConstraint('email'),
582 UniqueConstraint('username'), UniqueConstraint('email'),
583 Index('u_username_idx', 'username'),
583 Index('u_username_idx', 'username'),
584 Index('u_email_idx', 'email'),
584 Index('u_email_idx', 'email'),
585 base_table_args
585 base_table_args
586 )
586 )
587
587
588 DEFAULT_USER = 'default'
588 DEFAULT_USER = 'default'
589 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
589 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
590 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
590 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
591 RECOVERY_CODES_COUNT = 10
591 RECOVERY_CODES_COUNT = 10
592
592
593 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
593 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
594 username = Column("username", String(255), nullable=True, unique=None, default=None)
594 username = Column("username", String(255), nullable=True, unique=None, default=None)
595 password = Column("password", String(255), nullable=True, unique=None, default=None)
595 password = Column("password", String(255), nullable=True, unique=None, default=None)
596 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
596 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
597 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
597 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
598 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
598 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
599 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
599 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
600 _email = Column("email", String(255), nullable=True, unique=None, default=None)
600 _email = Column("email", String(255), nullable=True, unique=None, default=None)
601 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
601 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
602 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
602 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
603 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
603 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
604
604
605 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
605 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
606 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
606 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
607 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
607 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
608 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
608 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
609 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
609 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
610 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
610 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
611
611
612 user_log = relationship('UserLog', back_populates='user')
612 user_log = relationship('UserLog', back_populates='user')
613 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
613 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
614
614
615 repositories = relationship('Repository', back_populates='user')
615 repositories = relationship('Repository', back_populates='user')
616 repository_groups = relationship('RepoGroup', back_populates='user')
616 repository_groups = relationship('RepoGroup', back_populates='user')
617 user_groups = relationship('UserGroup', back_populates='user')
617 user_groups = relationship('UserGroup', back_populates='user')
618
618
619 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
619 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
620 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
620 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
621
621
622 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
622 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
623 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
623 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
624 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
624 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
625
625
626 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
626 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
627
627
628 notifications = relationship('UserNotification', cascade='all', back_populates='user')
628 notifications = relationship('UserNotification', cascade='all', back_populates='user')
629 # notifications assigned to this user
629 # notifications assigned to this user
630 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
630 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
631 # comments created by this user
631 # comments created by this user
632 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
632 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
633 # user profile extra info
633 # user profile extra info
634 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
634 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
635 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
635 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
636 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
636 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
637 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
637 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
638
638
639 # gists
639 # gists
640 user_gists = relationship('Gist', cascade='all', back_populates='owner')
640 user_gists = relationship('Gist', cascade='all', back_populates='owner')
641 # user pull requests
641 # user pull requests
642 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
642 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
643
643
644 # external identities
644 # external identities
645 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
645 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
646 # review rules
646 # review rules
647 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
647 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
648
648
649 # artifacts owned
649 # artifacts owned
650 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
650 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
651
651
652 # no cascade, set NULL
652 # no cascade, set NULL
653 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
653 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
654
654
655 def __repr__(self):
655 def __repr__(self):
656 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
656 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
657
657
658 @hybrid_property
658 @hybrid_property
659 def email(self):
659 def email(self):
660 return self._email
660 return self._email
661
661
662 @email.setter
662 @email.setter
663 def email(self, val):
663 def email(self, val):
664 self._email = val.lower() if val else None
664 self._email = val.lower() if val else None
665
665
666 @hybrid_property
666 @hybrid_property
667 def first_name(self):
667 def first_name(self):
668 from rhodecode.lib import helpers as h
668 from rhodecode.lib import helpers as h
669 if self.name:
669 if self.name:
670 return h.escape(self.name)
670 return h.escape(self.name)
671 return self.name
671 return self.name
672
672
673 @hybrid_property
673 @hybrid_property
674 def last_name(self):
674 def last_name(self):
675 from rhodecode.lib import helpers as h
675 from rhodecode.lib import helpers as h
676 if self.lastname:
676 if self.lastname:
677 return h.escape(self.lastname)
677 return h.escape(self.lastname)
678 return self.lastname
678 return self.lastname
679
679
680 @hybrid_property
680 @hybrid_property
681 def api_key(self):
681 def api_key(self):
682 """
682 """
683 Fetch if exist an auth-token with role ALL connected to this user
683 Fetch if exist an auth-token with role ALL connected to this user
684 """
684 """
685 user_auth_token = UserApiKeys.query()\
685 user_auth_token = UserApiKeys.query()\
686 .filter(UserApiKeys.user_id == self.user_id)\
686 .filter(UserApiKeys.user_id == self.user_id)\
687 .filter(or_(UserApiKeys.expires == -1,
687 .filter(or_(UserApiKeys.expires == -1,
688 UserApiKeys.expires >= time.time()))\
688 UserApiKeys.expires >= time.time()))\
689 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
689 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
690 if user_auth_token:
690 if user_auth_token:
691 user_auth_token = user_auth_token.api_key
691 user_auth_token = user_auth_token.api_key
692
692
693 return user_auth_token
693 return user_auth_token
694
694
695 @api_key.setter
695 @api_key.setter
696 def api_key(self, val):
696 def api_key(self, val):
697 # don't allow to set API key this is deprecated for now
697 # don't allow to set API key this is deprecated for now
698 self._api_key = None
698 self._api_key = None
699
699
700 @property
700 @property
701 def reviewer_pull_requests(self):
701 def reviewer_pull_requests(self):
702 return PullRequestReviewers.query() \
702 return PullRequestReviewers.query() \
703 .options(joinedload(PullRequestReviewers.pull_request)) \
703 .options(joinedload(PullRequestReviewers.pull_request)) \
704 .filter(PullRequestReviewers.user_id == self.user_id) \
704 .filter(PullRequestReviewers.user_id == self.user_id) \
705 .all()
705 .all()
706
706
707 @property
707 @property
708 def firstname(self):
708 def firstname(self):
709 # alias for future
709 # alias for future
710 return self.name
710 return self.name
711
711
712 @property
712 @property
713 def emails(self):
713 def emails(self):
714 other = UserEmailMap.query()\
714 other = UserEmailMap.query()\
715 .filter(UserEmailMap.user == self) \
715 .filter(UserEmailMap.user == self) \
716 .order_by(UserEmailMap.email_id.asc()) \
716 .order_by(UserEmailMap.email_id.asc()) \
717 .all()
717 .all()
718 return [self.email] + [x.email for x in other]
718 return [self.email] + [x.email for x in other]
719
719
720 def emails_cached(self):
720 def emails_cached(self):
721 emails = []
721 emails = []
722 if self.user_id != self.get_default_user_id():
722 if self.user_id != self.get_default_user_id():
723 emails = UserEmailMap.query()\
723 emails = UserEmailMap.query()\
724 .filter(UserEmailMap.user == self) \
724 .filter(UserEmailMap.user == self) \
725 .order_by(UserEmailMap.email_id.asc())
725 .order_by(UserEmailMap.email_id.asc())
726
726
727 emails = emails.options(
727 emails = emails.options(
728 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
728 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
729 )
729 )
730
730
731 return [self.email] + [x.email for x in emails]
731 return [self.email] + [x.email for x in emails]
732
732
733 @property
733 @property
734 def auth_tokens(self):
734 def auth_tokens(self):
735 auth_tokens = self.get_auth_tokens()
735 auth_tokens = self.get_auth_tokens()
736 return [x.api_key for x in auth_tokens]
736 return [x.api_key for x in auth_tokens]
737
737
738 def get_auth_tokens(self):
738 def get_auth_tokens(self):
739 return UserApiKeys.query()\
739 return UserApiKeys.query()\
740 .filter(UserApiKeys.user == self)\
740 .filter(UserApiKeys.user == self)\
741 .order_by(UserApiKeys.user_api_key_id.asc())\
741 .order_by(UserApiKeys.user_api_key_id.asc())\
742 .all()
742 .all()
743
743
744 @LazyProperty
744 @LazyProperty
745 def feed_token(self):
745 def feed_token(self):
746 return self.get_feed_token()
746 return self.get_feed_token()
747
747
748 def get_feed_token(self, cache=True):
748 def get_feed_token(self, cache=True):
749 feed_tokens = UserApiKeys.query()\
749 feed_tokens = UserApiKeys.query()\
750 .filter(UserApiKeys.user == self)\
750 .filter(UserApiKeys.user == self)\
751 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
751 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
752 if cache:
752 if cache:
753 feed_tokens = feed_tokens.options(
753 feed_tokens = feed_tokens.options(
754 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
754 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
755
755
756 feed_tokens = feed_tokens.all()
756 feed_tokens = feed_tokens.all()
757 if feed_tokens:
757 if feed_tokens:
758 return feed_tokens[0].api_key
758 return feed_tokens[0].api_key
759 return 'NO_FEED_TOKEN_AVAILABLE'
759 return 'NO_FEED_TOKEN_AVAILABLE'
760
760
761 @LazyProperty
761 @LazyProperty
762 def artifact_token(self):
762 def artifact_token(self):
763 return self.get_artifact_token()
763 return self.get_artifact_token()
764
764
765 def get_artifact_token(self, cache=True):
765 def get_artifact_token(self, cache=True):
766 artifacts_tokens = UserApiKeys.query()\
766 artifacts_tokens = UserApiKeys.query()\
767 .filter(UserApiKeys.user == self) \
767 .filter(UserApiKeys.user == self) \
768 .filter(or_(UserApiKeys.expires == -1,
768 .filter(or_(UserApiKeys.expires == -1,
769 UserApiKeys.expires >= time.time())) \
769 UserApiKeys.expires >= time.time())) \
770 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
770 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
771
771
772 if cache:
772 if cache:
773 artifacts_tokens = artifacts_tokens.options(
773 artifacts_tokens = artifacts_tokens.options(
774 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
774 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
775
775
776 artifacts_tokens = artifacts_tokens.all()
776 artifacts_tokens = artifacts_tokens.all()
777 if artifacts_tokens:
777 if artifacts_tokens:
778 return artifacts_tokens[0].api_key
778 return artifacts_tokens[0].api_key
779 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
779 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
780
780
781 def get_or_create_artifact_token(self):
781 def get_or_create_artifact_token(self):
782 artifacts_tokens = UserApiKeys.query()\
782 artifacts_tokens = UserApiKeys.query()\
783 .filter(UserApiKeys.user == self) \
783 .filter(UserApiKeys.user == self) \
784 .filter(or_(UserApiKeys.expires == -1,
784 .filter(or_(UserApiKeys.expires == -1,
785 UserApiKeys.expires >= time.time())) \
785 UserApiKeys.expires >= time.time())) \
786 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
786 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
787
787
788 artifacts_tokens = artifacts_tokens.all()
788 artifacts_tokens = artifacts_tokens.all()
789 if artifacts_tokens:
789 if artifacts_tokens:
790 return artifacts_tokens[0].api_key
790 return artifacts_tokens[0].api_key
791 else:
791 else:
792 from rhodecode.model.auth_token import AuthTokenModel
792 from rhodecode.model.auth_token import AuthTokenModel
793 artifact_token = AuthTokenModel().create(
793 artifact_token = AuthTokenModel().create(
794 self, 'auto-generated-artifact-token',
794 self, 'auto-generated-artifact-token',
795 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
795 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
796 Session.commit()
796 Session.commit()
797 return artifact_token.api_key
797 return artifact_token.api_key
798
798
799 def is_totp_valid(self, received_code, secret):
799 def is_totp_valid(self, received_code, secret):
800 totp = pyotp.TOTP(secret)
800 totp = pyotp.TOTP(secret)
801 return totp.verify(received_code)
801 return totp.verify(received_code)
802
802
803 def is_2fa_recovery_code_valid(self, received_code, secret):
803 def is_2fa_recovery_code_valid(self, received_code, secret):
804 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
804 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
805 recovery_codes = self.get_2fa_recovery_codes()
805 recovery_codes = self.get_2fa_recovery_codes()
806 if received_code in recovery_codes:
806 if received_code in recovery_codes:
807 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
807 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
808 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
808 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
809 return True
809 return True
810 return False
810 return False
811
811
812 @hybrid_property
812 @hybrid_property
813 def has_forced_2fa(self):
813 def has_forced_2fa(self):
814 """
814 """
815 Checks if 2fa was forced for ALL users (including current one)
815 Checks if 2fa was forced for ALL users (including current one)
816 """
816 """
817 from rhodecode.model.settings import SettingsModel
817 from rhodecode.model.settings import SettingsModel
818 # So now we're supporting only auth_rhodecode_global_2f
818 # So now we're supporting only auth_rhodecode_global_2f
819 if value := SettingsModel().get_setting_by_name('auth_rhodecode_global_2fa'):
819 if value := SettingsModel().get_setting_by_name('auth_rhodecode_global_2fa'):
820 return value.app_settings_value
820 return value.app_settings_value
821 return False
821 return False
822
822
823 @hybrid_property
823 @hybrid_property
824 def has_enabled_2fa(self):
824 def has_enabled_2fa(self):
825 """
825 """
826 Checks if user enabled 2fa
826 Checks if user enabled 2fa
827 """
827 """
828 if value := self.has_forced_2fa:
828 if value := self.has_forced_2fa:
829 return value
829 return value
830 return self.user_data.get('enabled_2fa', False)
830 return self.user_data.get('enabled_2fa', False)
831
831
832 @has_enabled_2fa.setter
832 @has_enabled_2fa.setter
833 def has_enabled_2fa(self, val):
833 def has_enabled_2fa(self, val):
834 val = str2bool(val)
834 val = str2bool(val)
835 self.update_userdata(enabled_2fa=val)
835 self.update_userdata(enabled_2fa=val)
836 if not val:
836 if not val:
837 # NOTE: setting to false we clear the user_data to not store any 2fa artifacts
837 # NOTE: setting to false we clear the user_data to not store any 2fa artifacts
838 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False)
838 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False)
839 Session().commit()
839 Session().commit()
840
840
841 @hybrid_property
841 @hybrid_property
842 def has_check_2fa_flag(self):
842 def check_2fa_required(self):
843 """
843 """
844 Check if check 2fa flag is set for this user
844 Check if check 2fa flag is set for this user
845 """
845 """
846 value = self.user_data.get('check_2fa', False)
846 value = self.user_data.get('check_2fa', False)
847 return value
847 return value
848
848
849 @has_check_2fa_flag.setter
849 @check_2fa_required.setter
850 def has_check_2fa_flag(self, val):
850 def check_2fa_required(self, val):
851 val = str2bool(val)
851 val = str2bool(val)
852 self.update_userdata(check_2fa=val)
852 self.update_userdata(check_2fa=val)
853 Session().commit()
853 Session().commit()
854
854
855 @hybrid_property
855 @hybrid_property
856 def has_seen_2fa_codes(self):
856 def has_seen_2fa_codes(self):
857 """
857 """
858 get the flag about if user has seen 2fa recovery codes
858 get the flag about if user has seen 2fa recovery codes
859 """
859 """
860 value = self.user_data.get('recovery_codes_2fa_seen', False)
860 value = self.user_data.get('recovery_codes_2fa_seen', False)
861 return value
861 return value
862
862
863 @has_seen_2fa_codes.setter
863 @has_seen_2fa_codes.setter
864 def has_seen_2fa_codes(self, val):
864 def has_seen_2fa_codes(self, val):
865 val = str2bool(val)
865 val = str2bool(val)
866 self.update_userdata(recovery_codes_2fa_seen=val)
866 self.update_userdata(recovery_codes_2fa_seen=val)
867 Session().commit()
867 Session().commit()
868
868
869 @hybrid_property
869 @hybrid_property
870 def needs_2fa_configure(self):
870 def needs_2fa_configure(self):
871 """
871 """
872 Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work.
872 Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work.
873
873
874 Currently this is 2fa enabled and secret exists
874 Currently this is 2fa enabled and secret exists
875 """
875 """
876 if self.has_enabled_2fa:
876 if self.has_enabled_2fa:
877 return not self.user_data.get('secret_2fa')
877 return not self.user_data.get('secret_2fa')
878 return False
878 return False
879
879
880 def init_2fa_recovery_codes(self, persist=True, force=False):
880 def init_2fa_recovery_codes(self, persist=True, force=False):
881 """
881 """
882 Creates 2fa recovery codes
882 Creates 2fa recovery codes
883 """
883 """
884 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
884 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
885 encrypted_codes = []
885 encrypted_codes = []
886 if not recovery_codes or force:
886 if not recovery_codes or force:
887 for _ in range(self.RECOVERY_CODES_COUNT):
887 for _ in range(self.RECOVERY_CODES_COUNT):
888 recovery_code = pyotp.random_base32()
888 recovery_code = pyotp.random_base32()
889 recovery_codes.append(recovery_code)
889 recovery_codes.append(recovery_code)
890 encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY)
890 encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY)
891 encrypted_codes.append(safe_str(encrypted_code))
891 encrypted_codes.append(safe_str(encrypted_code))
892 if persist:
892 if persist:
893 self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False)
893 self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False)
894 return recovery_codes
894 return recovery_codes
895 # User should not check the same recovery codes more than once
895 # User should not check the same recovery codes more than once
896 return []
896 return []
897
897
898 def get_2fa_recovery_codes(self):
898 def get_2fa_recovery_codes(self):
899 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
899 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
900 strict_mode = ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
900 strict_mode = ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
901
901
902 recovery_codes = list(map(
902 recovery_codes = list(map(
903 lambda val: safe_str(
903 lambda val: safe_str(
904 enc_utils.decrypt_value(
904 enc_utils.decrypt_value(
905 val,
905 val,
906 enc_key=ENCRYPTION_KEY,
906 enc_key=ENCRYPTION_KEY,
907 strict_mode=strict_mode
907 strict_mode=strict_mode
908 )),
908 )),
909 encrypted_recovery_codes))
909 encrypted_recovery_codes))
910 return recovery_codes
910 return recovery_codes
911
911
912 def init_secret_2fa(self, persist=True, force=False):
912 def init_secret_2fa(self, persist=True, force=False):
913 secret_2fa = self.user_data.get('secret_2fa')
913 secret_2fa = self.user_data.get('secret_2fa')
914 if not secret_2fa or force:
914 if not secret_2fa or force:
915 secret = pyotp.random_base32()
915 secret = pyotp.random_base32()
916 if persist:
916 if persist:
917 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY)))
917 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY)))
918 return secret
918 return secret
919 return ''
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 secret_2fa = self.user_data.get('secret_2fa')
926 secret_2fa = self.user_data.get('secret_2fa')
923 if secret_2fa:
927 if secret_2fa:
924 strict_mode = ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
928 strict_mode = ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
925 return safe_str(
929 return safe_str(
926 enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY, strict_mode=strict_mode))
930 enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY, strict_mode=strict_mode))
927 return ''
931 return ''
928
932
929 def set_2fa_secret(self, value):
933 @secret_2fa.setter
934 def secret_2fa(self, value: str) -> None:
930 encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY)
935 encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY)
931 self.update_userdata(secret_2fa=safe_str(encrypted_value))
936 self.update_userdata(secret_2fa=safe_str(encrypted_value))
932
937
933 def regenerate_2fa_recovery_codes(self):
938 def regenerate_2fa_recovery_codes(self):
934 """
939 """
935 Regenerates 2fa recovery codes upon request
940 Regenerates 2fa recovery codes upon request
936 """
941 """
937 new_recovery_codes = self.init_2fa_recovery_codes(force=True)
942 new_recovery_codes = self.init_2fa_recovery_codes(force=True)
938 Session().commit()
943 Session().commit()
939 return new_recovery_codes
944 return new_recovery_codes
940
945
941 @classmethod
946 @classmethod
942 def extra_valid_auth_tokens(cls, user, role=None):
947 def extra_valid_auth_tokens(cls, user, role=None):
943 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
948 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
944 .filter(or_(UserApiKeys.expires == -1,
949 .filter(or_(UserApiKeys.expires == -1,
945 UserApiKeys.expires >= time.time()))
950 UserApiKeys.expires >= time.time()))
946 if role:
951 if role:
947 tokens = tokens.filter(or_(UserApiKeys.role == role,
952 tokens = tokens.filter(or_(UserApiKeys.role == role,
948 UserApiKeys.role == UserApiKeys.ROLE_ALL))
953 UserApiKeys.role == UserApiKeys.ROLE_ALL))
949 return tokens.all()
954 return tokens.all()
950
955
951 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
956 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
952 from rhodecode.lib import auth
957 from rhodecode.lib import auth
953
958
954 log.debug('Trying to authenticate user: %s via auth-token, '
959 log.debug('Trying to authenticate user: %s via auth-token, '
955 'and roles: %s', self, roles)
960 'and roles: %s', self, roles)
956
961
957 if not auth_token:
962 if not auth_token:
958 return False
963 return False
959
964
960 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
965 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
961 tokens_q = UserApiKeys.query()\
966 tokens_q = UserApiKeys.query()\
962 .filter(UserApiKeys.user_id == self.user_id)\
967 .filter(UserApiKeys.user_id == self.user_id)\
963 .filter(or_(UserApiKeys.expires == -1,
968 .filter(or_(UserApiKeys.expires == -1,
964 UserApiKeys.expires >= time.time()))
969 UserApiKeys.expires >= time.time()))
965
970
966 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
971 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
967
972
968 crypto_backend = auth.crypto_backend()
973 crypto_backend = auth.crypto_backend()
969 enc_token_map = {}
974 enc_token_map = {}
970 plain_token_map = {}
975 plain_token_map = {}
971 for token in tokens_q:
976 for token in tokens_q:
972 if token.api_key.startswith(crypto_backend.ENC_PREF):
977 if token.api_key.startswith(crypto_backend.ENC_PREF):
973 enc_token_map[token.api_key] = token
978 enc_token_map[token.api_key] = token
974 else:
979 else:
975 plain_token_map[token.api_key] = token
980 plain_token_map[token.api_key] = token
976 log.debug(
981 log.debug(
977 'Found %s plain and %s encrypted tokens to check for authentication for this user',
982 'Found %s plain and %s encrypted tokens to check for authentication for this user',
978 len(plain_token_map), len(enc_token_map))
983 len(plain_token_map), len(enc_token_map))
979
984
980 # plain token match comes first
985 # plain token match comes first
981 match = plain_token_map.get(auth_token)
986 match = plain_token_map.get(auth_token)
982
987
983 # check encrypted tokens now
988 # check encrypted tokens now
984 if not match:
989 if not match:
985 for token_hash, token in enc_token_map.items():
990 for token_hash, token in enc_token_map.items():
986 # NOTE(marcink): this is expensive to calculate, but most secure
991 # NOTE(marcink): this is expensive to calculate, but most secure
987 if crypto_backend.hash_check(auth_token, token_hash):
992 if crypto_backend.hash_check(auth_token, token_hash):
988 match = token
993 match = token
989 break
994 break
990
995
991 if match:
996 if match:
992 log.debug('Found matching token %s', match)
997 log.debug('Found matching token %s', match)
993 if match.repo_id:
998 if match.repo_id:
994 log.debug('Found scope, checking for scope match of token %s', match)
999 log.debug('Found scope, checking for scope match of token %s', match)
995 if match.repo_id == scope_repo_id:
1000 if match.repo_id == scope_repo_id:
996 return True
1001 return True
997 else:
1002 else:
998 log.debug(
1003 log.debug(
999 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
1004 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
1000 'and calling scope is:%s, skipping further checks',
1005 'and calling scope is:%s, skipping further checks',
1001 match.repo, scope_repo_id)
1006 match.repo, scope_repo_id)
1002 return False
1007 return False
1003 else:
1008 else:
1004 return True
1009 return True
1005
1010
1006 return False
1011 return False
1007
1012
1008 @property
1013 @property
1009 def ip_addresses(self):
1014 def ip_addresses(self):
1010 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
1015 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
1011 return [x.ip_addr for x in ret]
1016 return [x.ip_addr for x in ret]
1012
1017
1013 @property
1018 @property
1014 def username_and_name(self):
1019 def username_and_name(self):
1015 return f'{self.username} ({self.first_name} {self.last_name})'
1020 return f'{self.username} ({self.first_name} {self.last_name})'
1016
1021
1017 @property
1022 @property
1018 def username_or_name_or_email(self):
1023 def username_or_name_or_email(self):
1019 full_name = self.full_name if self.full_name != ' ' else None
1024 full_name = self.full_name if self.full_name != ' ' else None
1020 return self.username or full_name or self.email
1025 return self.username or full_name or self.email
1021
1026
1022 @property
1027 @property
1023 def full_name(self):
1028 def full_name(self):
1024 return f'{self.first_name} {self.last_name}'
1029 return f'{self.first_name} {self.last_name}'
1025
1030
1026 @property
1031 @property
1027 def full_name_or_username(self):
1032 def full_name_or_username(self):
1028 return (f'{self.first_name} {self.last_name}'
1033 return (f'{self.first_name} {self.last_name}'
1029 if (self.first_name and self.last_name) else self.username)
1034 if (self.first_name and self.last_name) else self.username)
1030
1035
1031 @property
1036 @property
1032 def full_contact(self):
1037 def full_contact(self):
1033 return f'{self.first_name} {self.last_name} <{self.email}>'
1038 return f'{self.first_name} {self.last_name} <{self.email}>'
1034
1039
1035 @property
1040 @property
1036 def short_contact(self):
1041 def short_contact(self):
1037 return f'{self.first_name} {self.last_name}'
1042 return f'{self.first_name} {self.last_name}'
1038
1043
1039 @property
1044 @property
1040 def is_admin(self):
1045 def is_admin(self):
1041 return self.admin
1046 return self.admin
1042
1047
1043 @property
1048 @property
1044 def language(self):
1049 def language(self):
1045 return self.user_data.get('language')
1050 return self.user_data.get('language')
1046
1051
1047 def AuthUser(self, **kwargs):
1052 def AuthUser(self, **kwargs):
1048 """
1053 """
1049 Returns instance of AuthUser for this user
1054 Returns instance of AuthUser for this user
1050 """
1055 """
1051 from rhodecode.lib.auth import AuthUser
1056 from rhodecode.lib.auth import AuthUser
1052 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1057 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1053
1058
1054 @hybrid_property
1059 @hybrid_property
1055 def user_data(self):
1060 def user_data(self):
1056 if not self._user_data:
1061 if not self._user_data:
1057 return {}
1062 return {}
1058
1063
1059 try:
1064 try:
1060 return json.loads(self._user_data) or {}
1065 return json.loads(self._user_data) or {}
1061 except TypeError:
1066 except TypeError:
1062 return {}
1067 return {}
1063
1068
1064 @user_data.setter
1069 @user_data.setter
1065 def user_data(self, val):
1070 def user_data(self, val):
1066 if not isinstance(val, dict):
1071 if not isinstance(val, dict):
1067 raise Exception(f'user_data must be dict, got {type(val)}')
1072 raise Exception(f'user_data must be dict, got {type(val)}')
1068 try:
1073 try:
1069 self._user_data = safe_bytes(json.dumps(val))
1074 self._user_data = safe_bytes(json.dumps(val))
1070 except Exception:
1075 except Exception:
1071 log.error(traceback.format_exc())
1076 log.error(traceback.format_exc())
1072
1077
1073 @classmethod
1078 @classmethod
1074 def get(cls, user_id, cache=False):
1079 def get(cls, user_id, cache=False):
1075 if not user_id:
1080 if not user_id:
1076 return
1081 return
1077
1082
1078 user = cls.query()
1083 user = cls.query()
1079 if cache:
1084 if cache:
1080 user = user.options(
1085 user = user.options(
1081 FromCache("sql_cache_short", f"get_users_{user_id}"))
1086 FromCache("sql_cache_short", f"get_users_{user_id}"))
1082 return user.get(user_id)
1087 return user.get(user_id)
1083
1088
1084 @classmethod
1089 @classmethod
1085 def get_by_username(cls, username, case_insensitive=False,
1090 def get_by_username(cls, username, case_insensitive=False,
1086 cache=False):
1091 cache=False):
1087
1092
1088 if case_insensitive:
1093 if case_insensitive:
1089 q = cls.select().where(
1094 q = cls.select().where(
1090 func.lower(cls.username) == func.lower(username))
1095 func.lower(cls.username) == func.lower(username))
1091 else:
1096 else:
1092 q = cls.select().where(cls.username == username)
1097 q = cls.select().where(cls.username == username)
1093
1098
1094 if cache:
1099 if cache:
1095 hash_key = _hash_key(username)
1100 hash_key = _hash_key(username)
1096 q = q.options(
1101 q = q.options(
1097 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1102 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1098
1103
1099 return cls.execute(q).scalar_one_or_none()
1104 return cls.execute(q).scalar_one_or_none()
1100
1105
1101 @classmethod
1106 @classmethod
1102 def get_by_username_or_primary_email(cls, user_identifier):
1107 def get_by_username_or_primary_email(cls, user_identifier):
1103 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1108 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1104 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1109 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1105 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1110 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1106
1111
1107 @classmethod
1112 @classmethod
1108 def get_by_auth_token(cls, auth_token, cache=False):
1113 def get_by_auth_token(cls, auth_token, cache=False):
1109
1114
1110 q = cls.select(User)\
1115 q = cls.select(User)\
1111 .join(UserApiKeys)\
1116 .join(UserApiKeys)\
1112 .where(UserApiKeys.api_key == auth_token)\
1117 .where(UserApiKeys.api_key == auth_token)\
1113 .where(or_(UserApiKeys.expires == -1,
1118 .where(or_(UserApiKeys.expires == -1,
1114 UserApiKeys.expires >= time.time()))
1119 UserApiKeys.expires >= time.time()))
1115
1120
1116 if cache:
1121 if cache:
1117 q = q.options(
1122 q = q.options(
1118 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1123 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1119
1124
1120 matched_user = cls.execute(q).scalar_one_or_none()
1125 matched_user = cls.execute(q).scalar_one_or_none()
1121
1126
1122 return matched_user
1127 return matched_user
1123
1128
1124 @classmethod
1129 @classmethod
1125 def get_by_email(cls, email, case_insensitive=False, cache=False):
1130 def get_by_email(cls, email, case_insensitive=False, cache=False):
1126
1131
1127 if case_insensitive:
1132 if case_insensitive:
1128 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1133 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1129 else:
1134 else:
1130 q = cls.select().where(cls.email == email)
1135 q = cls.select().where(cls.email == email)
1131
1136
1132 if cache:
1137 if cache:
1133 email_key = _hash_key(email)
1138 email_key = _hash_key(email)
1134 q = q.options(
1139 q = q.options(
1135 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1140 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1136
1141
1137 ret = cls.execute(q).scalar_one_or_none()
1142 ret = cls.execute(q).scalar_one_or_none()
1138
1143
1139 if ret is None:
1144 if ret is None:
1140 q = cls.select(UserEmailMap)
1145 q = cls.select(UserEmailMap)
1141 # try fetching in alternate email map
1146 # try fetching in alternate email map
1142 if case_insensitive:
1147 if case_insensitive:
1143 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1148 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1144 else:
1149 else:
1145 q = q.where(UserEmailMap.email == email)
1150 q = q.where(UserEmailMap.email == email)
1146 q = q.options(joinedload(UserEmailMap.user))
1151 q = q.options(joinedload(UserEmailMap.user))
1147 if cache:
1152 if cache:
1148 q = q.options(
1153 q = q.options(
1149 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1154 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1150
1155
1151 result = cls.execute(q).scalar_one_or_none()
1156 result = cls.execute(q).scalar_one_or_none()
1152 ret = getattr(result, 'user', None)
1157 ret = getattr(result, 'user', None)
1153
1158
1154 return ret
1159 return ret
1155
1160
1156 @classmethod
1161 @classmethod
1157 def get_from_cs_author(cls, author):
1162 def get_from_cs_author(cls, author):
1158 """
1163 """
1159 Tries to get User objects out of commit author string
1164 Tries to get User objects out of commit author string
1160
1165
1161 :param author:
1166 :param author:
1162 """
1167 """
1163 from rhodecode.lib.helpers import email, author_name
1168 from rhodecode.lib.helpers import email, author_name
1164 # Valid email in the attribute passed, see if they're in the system
1169 # Valid email in the attribute passed, see if they're in the system
1165 _email = email(author)
1170 _email = email(author)
1166 if _email:
1171 if _email:
1167 user = cls.get_by_email(_email, case_insensitive=True)
1172 user = cls.get_by_email(_email, case_insensitive=True)
1168 if user:
1173 if user:
1169 return user
1174 return user
1170 # Maybe we can match by username?
1175 # Maybe we can match by username?
1171 _author = author_name(author)
1176 _author = author_name(author)
1172 user = cls.get_by_username(_author, case_insensitive=True)
1177 user = cls.get_by_username(_author, case_insensitive=True)
1173 if user:
1178 if user:
1174 return user
1179 return user
1175
1180
1176 def update_userdata(self, **kwargs):
1181 def update_userdata(self, **kwargs):
1177 usr = self
1182 usr = self
1178 old = usr.user_data
1183 old = usr.user_data
1179 old.update(**kwargs)
1184 old.update(**kwargs)
1180 usr.user_data = old
1185 usr.user_data = old
1181 Session().add(usr)
1186 Session().add(usr)
1182 log.debug('updated userdata with %s', kwargs)
1187 log.debug('updated userdata with %s', kwargs)
1183
1188
1184 def update_lastlogin(self):
1189 def update_lastlogin(self):
1185 """Update user lastlogin"""
1190 """Update user lastlogin"""
1186 self.last_login = datetime.datetime.now()
1191 self.last_login = datetime.datetime.now()
1187 Session().add(self)
1192 Session().add(self)
1188 log.debug('updated user %s lastlogin', self.username)
1193 log.debug('updated user %s lastlogin', self.username)
1189
1194
1190 def update_password(self, new_password):
1195 def update_password(self, new_password):
1191 from rhodecode.lib.auth import get_crypt_password
1196 from rhodecode.lib.auth import get_crypt_password
1192
1197
1193 self.password = get_crypt_password(new_password)
1198 self.password = get_crypt_password(new_password)
1194 Session().add(self)
1199 Session().add(self)
1195
1200
1196 @classmethod
1201 @classmethod
1197 def get_first_super_admin(cls):
1202 def get_first_super_admin(cls):
1198 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1203 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1199 user = cls.scalars(stmt).first()
1204 user = cls.scalars(stmt).first()
1200
1205
1201 if user is None:
1206 if user is None:
1202 raise Exception('FATAL: Missing administrative account!')
1207 raise Exception('FATAL: Missing administrative account!')
1203 return user
1208 return user
1204
1209
1205 @classmethod
1210 @classmethod
1206 def get_all_super_admins(cls, only_active=False):
1211 def get_all_super_admins(cls, only_active=False):
1207 """
1212 """
1208 Returns all admin accounts sorted by username
1213 Returns all admin accounts sorted by username
1209 """
1214 """
1210 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1215 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1211 if only_active:
1216 if only_active:
1212 qry = qry.filter(User.active == true())
1217 qry = qry.filter(User.active == true())
1213 return qry.all()
1218 return qry.all()
1214
1219
1215 @classmethod
1220 @classmethod
1216 def get_all_user_ids(cls, only_active=True):
1221 def get_all_user_ids(cls, only_active=True):
1217 """
1222 """
1218 Returns all users IDs
1223 Returns all users IDs
1219 """
1224 """
1220 qry = Session().query(User.user_id)
1225 qry = Session().query(User.user_id)
1221
1226
1222 if only_active:
1227 if only_active:
1223 qry = qry.filter(User.active == true())
1228 qry = qry.filter(User.active == true())
1224 return [x.user_id for x in qry]
1229 return [x.user_id for x in qry]
1225
1230
1226 @classmethod
1231 @classmethod
1227 def get_default_user(cls, cache=False, refresh=False):
1232 def get_default_user(cls, cache=False, refresh=False):
1228 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1233 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1229 if user is None:
1234 if user is None:
1230 raise Exception('FATAL: Missing default account!')
1235 raise Exception('FATAL: Missing default account!')
1231 if refresh:
1236 if refresh:
1232 # The default user might be based on outdated state which
1237 # The default user might be based on outdated state which
1233 # has been loaded from the cache.
1238 # has been loaded from the cache.
1234 # A call to refresh() ensures that the
1239 # A call to refresh() ensures that the
1235 # latest state from the database is used.
1240 # latest state from the database is used.
1236 Session().refresh(user)
1241 Session().refresh(user)
1237
1242
1238 return user
1243 return user
1239
1244
1240 @classmethod
1245 @classmethod
1241 def get_default_user_id(cls):
1246 def get_default_user_id(cls):
1242 import rhodecode
1247 import rhodecode
1243 return rhodecode.CONFIG['default_user_id']
1248 return rhodecode.CONFIG['default_user_id']
1244
1249
1245 def _get_default_perms(self, user, suffix=''):
1250 def _get_default_perms(self, user, suffix=''):
1246 from rhodecode.model.permission import PermissionModel
1251 from rhodecode.model.permission import PermissionModel
1247 return PermissionModel().get_default_perms(user.user_perms, suffix)
1252 return PermissionModel().get_default_perms(user.user_perms, suffix)
1248
1253
1249 def get_default_perms(self, suffix=''):
1254 def get_default_perms(self, suffix=''):
1250 return self._get_default_perms(self, suffix)
1255 return self._get_default_perms(self, suffix)
1251
1256
1252 def get_api_data(self, include_secrets=False, details='full'):
1257 def get_api_data(self, include_secrets=False, details='full'):
1253 """
1258 """
1254 Common function for generating user related data for API
1259 Common function for generating user related data for API
1255
1260
1256 :param include_secrets: By default secrets in the API data will be replaced
1261 :param include_secrets: By default secrets in the API data will be replaced
1257 by a placeholder value to prevent exposing this data by accident. In case
1262 by a placeholder value to prevent exposing this data by accident. In case
1258 this data shall be exposed, set this flag to ``True``.
1263 this data shall be exposed, set this flag to ``True``.
1259
1264
1260 :param details: details can be 'basic|full' basic gives only a subset of
1265 :param details: details can be 'basic|full' basic gives only a subset of
1261 the available user information that includes user_id, name and emails.
1266 the available user information that includes user_id, name and emails.
1262 """
1267 """
1263 user = self
1268 user = self
1264 user_data = self.user_data
1269 user_data = self.user_data
1265 data = {
1270 data = {
1266 'user_id': user.user_id,
1271 'user_id': user.user_id,
1267 'username': user.username,
1272 'username': user.username,
1268 'firstname': user.name,
1273 'firstname': user.name,
1269 'lastname': user.lastname,
1274 'lastname': user.lastname,
1270 'description': user.description,
1275 'description': user.description,
1271 'email': user.email,
1276 'email': user.email,
1272 'emails': user.emails,
1277 'emails': user.emails,
1273 }
1278 }
1274 if details == 'basic':
1279 if details == 'basic':
1275 return data
1280 return data
1276
1281
1277 auth_token_length = 40
1282 auth_token_length = 40
1278 auth_token_replacement = '*' * auth_token_length
1283 auth_token_replacement = '*' * auth_token_length
1279
1284
1280 extras = {
1285 extras = {
1281 'auth_tokens': [auth_token_replacement],
1286 'auth_tokens': [auth_token_replacement],
1282 'active': user.active,
1287 'active': user.active,
1283 'admin': user.admin,
1288 'admin': user.admin,
1284 'extern_type': user.extern_type,
1289 'extern_type': user.extern_type,
1285 'extern_name': user.extern_name,
1290 'extern_name': user.extern_name,
1286 'last_login': user.last_login,
1291 'last_login': user.last_login,
1287 'last_activity': user.last_activity,
1292 'last_activity': user.last_activity,
1288 'ip_addresses': user.ip_addresses,
1293 'ip_addresses': user.ip_addresses,
1289 'language': user_data.get('language')
1294 'language': user_data.get('language')
1290 }
1295 }
1291 data.update(extras)
1296 data.update(extras)
1292
1297
1293 if include_secrets:
1298 if include_secrets:
1294 data['auth_tokens'] = user.auth_tokens
1299 data['auth_tokens'] = user.auth_tokens
1295 return data
1300 return data
1296
1301
1297 def __json__(self):
1302 def __json__(self):
1298 data = {
1303 data = {
1299 'full_name': self.full_name,
1304 'full_name': self.full_name,
1300 'full_name_or_username': self.full_name_or_username,
1305 'full_name_or_username': self.full_name_or_username,
1301 'short_contact': self.short_contact,
1306 'short_contact': self.short_contact,
1302 'full_contact': self.full_contact,
1307 'full_contact': self.full_contact,
1303 }
1308 }
1304 data.update(self.get_api_data())
1309 data.update(self.get_api_data())
1305 return data
1310 return data
1306
1311
1307
1312
1308 class UserApiKeys(Base, BaseModel):
1313 class UserApiKeys(Base, BaseModel):
1309 __tablename__ = 'user_api_keys'
1314 __tablename__ = 'user_api_keys'
1310 __table_args__ = (
1315 __table_args__ = (
1311 Index('uak_api_key_idx', 'api_key'),
1316 Index('uak_api_key_idx', 'api_key'),
1312 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1317 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1313 base_table_args
1318 base_table_args
1314 )
1319 )
1315
1320
1316 # ApiKey role
1321 # ApiKey role
1317 ROLE_ALL = 'token_role_all'
1322 ROLE_ALL = 'token_role_all'
1318 ROLE_VCS = 'token_role_vcs'
1323 ROLE_VCS = 'token_role_vcs'
1319 ROLE_API = 'token_role_api'
1324 ROLE_API = 'token_role_api'
1320 ROLE_HTTP = 'token_role_http'
1325 ROLE_HTTP = 'token_role_http'
1321 ROLE_FEED = 'token_role_feed'
1326 ROLE_FEED = 'token_role_feed'
1322 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1327 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1323 # The last one is ignored in the list as we only
1328 # The last one is ignored in the list as we only
1324 # use it for one action, and cannot be created by users
1329 # use it for one action, and cannot be created by users
1325 ROLE_PASSWORD_RESET = 'token_password_reset'
1330 ROLE_PASSWORD_RESET = 'token_password_reset'
1326
1331
1327 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1332 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1328
1333
1329 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1334 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1330 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1335 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1331 api_key = Column("api_key", String(255), nullable=False, unique=True)
1336 api_key = Column("api_key", String(255), nullable=False, unique=True)
1332 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1337 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1333 expires = Column('expires', Float(53), nullable=False)
1338 expires = Column('expires', Float(53), nullable=False)
1334 role = Column('role', String(255), nullable=True)
1339 role = Column('role', String(255), nullable=True)
1335 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1340 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1336
1341
1337 # scope columns
1342 # scope columns
1338 repo_id = Column(
1343 repo_id = Column(
1339 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1344 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1340 nullable=True, unique=None, default=None)
1345 nullable=True, unique=None, default=None)
1341 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1346 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1342
1347
1343 repo_group_id = Column(
1348 repo_group_id = Column(
1344 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1349 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1345 nullable=True, unique=None, default=None)
1350 nullable=True, unique=None, default=None)
1346 repo_group = relationship('RepoGroup', lazy='joined')
1351 repo_group = relationship('RepoGroup', lazy='joined')
1347
1352
1348 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1353 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1349
1354
1350 def __repr__(self):
1355 def __repr__(self):
1351 return f"<{self.cls_name}('{self.role}')>"
1356 return f"<{self.cls_name}('{self.role}')>"
1352
1357
1353 def __json__(self):
1358 def __json__(self):
1354 data = {
1359 data = {
1355 'auth_token': self.api_key,
1360 'auth_token': self.api_key,
1356 'role': self.role,
1361 'role': self.role,
1357 'scope': self.scope_humanized,
1362 'scope': self.scope_humanized,
1358 'expired': self.expired
1363 'expired': self.expired
1359 }
1364 }
1360 return data
1365 return data
1361
1366
1362 def get_api_data(self, include_secrets=False):
1367 def get_api_data(self, include_secrets=False):
1363 data = self.__json__()
1368 data = self.__json__()
1364 if include_secrets:
1369 if include_secrets:
1365 return data
1370 return data
1366 else:
1371 else:
1367 data['auth_token'] = self.token_obfuscated
1372 data['auth_token'] = self.token_obfuscated
1368 return data
1373 return data
1369
1374
1370 @hybrid_property
1375 @hybrid_property
1371 def description_safe(self):
1376 def description_safe(self):
1372 from rhodecode.lib import helpers as h
1377 from rhodecode.lib import helpers as h
1373 return h.escape(self.description)
1378 return h.escape(self.description)
1374
1379
1375 @property
1380 @property
1376 def expired(self):
1381 def expired(self):
1377 if self.expires == -1:
1382 if self.expires == -1:
1378 return False
1383 return False
1379 return time.time() > self.expires
1384 return time.time() > self.expires
1380
1385
1381 @classmethod
1386 @classmethod
1382 def _get_role_name(cls, role):
1387 def _get_role_name(cls, role):
1383 return {
1388 return {
1384 cls.ROLE_ALL: _('all'),
1389 cls.ROLE_ALL: _('all'),
1385 cls.ROLE_HTTP: _('http/web interface'),
1390 cls.ROLE_HTTP: _('http/web interface'),
1386 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1391 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1387 cls.ROLE_API: _('api calls'),
1392 cls.ROLE_API: _('api calls'),
1388 cls.ROLE_FEED: _('feed access'),
1393 cls.ROLE_FEED: _('feed access'),
1389 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1394 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1390 }.get(role, role)
1395 }.get(role, role)
1391
1396
1392 @classmethod
1397 @classmethod
1393 def _get_role_description(cls, role):
1398 def _get_role_description(cls, role):
1394 return {
1399 return {
1395 cls.ROLE_ALL: _('Token for all actions.'),
1400 cls.ROLE_ALL: _('Token for all actions.'),
1396 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1401 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1397 'login using `api_access_controllers_whitelist` functionality.'),
1402 'login using `api_access_controllers_whitelist` functionality.'),
1398 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1403 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1399 'Requires auth_token authentication plugin to be active. <br/>'
1404 'Requires auth_token authentication plugin to be active. <br/>'
1400 'Such Token should be used then instead of a password to '
1405 'Such Token should be used then instead of a password to '
1401 'interact with a repository, and additionally can be '
1406 'interact with a repository, and additionally can be '
1402 'limited to single repository using repo scope.'),
1407 'limited to single repository using repo scope.'),
1403 cls.ROLE_API: _('Token limited to api calls.'),
1408 cls.ROLE_API: _('Token limited to api calls.'),
1404 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1409 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1405 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1410 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1406 }.get(role, role)
1411 }.get(role, role)
1407
1412
1408 @property
1413 @property
1409 def role_humanized(self):
1414 def role_humanized(self):
1410 return self._get_role_name(self.role)
1415 return self._get_role_name(self.role)
1411
1416
1412 def _get_scope(self):
1417 def _get_scope(self):
1413 if self.repo:
1418 if self.repo:
1414 return 'Repository: {}'.format(self.repo.repo_name)
1419 return 'Repository: {}'.format(self.repo.repo_name)
1415 if self.repo_group:
1420 if self.repo_group:
1416 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1421 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1417 return 'Global'
1422 return 'Global'
1418
1423
1419 @property
1424 @property
1420 def scope_humanized(self):
1425 def scope_humanized(self):
1421 return self._get_scope()
1426 return self._get_scope()
1422
1427
1423 @property
1428 @property
1424 def token_obfuscated(self):
1429 def token_obfuscated(self):
1425 if self.api_key:
1430 if self.api_key:
1426 return self.api_key[:4] + "****"
1431 return self.api_key[:4] + "****"
1427
1432
1428
1433
1429 class UserEmailMap(Base, BaseModel):
1434 class UserEmailMap(Base, BaseModel):
1430 __tablename__ = 'user_email_map'
1435 __tablename__ = 'user_email_map'
1431 __table_args__ = (
1436 __table_args__ = (
1432 Index('uem_email_idx', 'email'),
1437 Index('uem_email_idx', 'email'),
1433 Index('uem_user_id_idx', 'user_id'),
1438 Index('uem_user_id_idx', 'user_id'),
1434 UniqueConstraint('email'),
1439 UniqueConstraint('email'),
1435 base_table_args
1440 base_table_args
1436 )
1441 )
1437
1442
1438 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1443 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1439 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1444 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1440 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1445 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1441 user = relationship('User', lazy='joined', back_populates='user_emails')
1446 user = relationship('User', lazy='joined', back_populates='user_emails')
1442
1447
1443 @validates('_email')
1448 @validates('_email')
1444 def validate_email(self, key, email):
1449 def validate_email(self, key, email):
1445 # check if this email is not main one
1450 # check if this email is not main one
1446 main_email = Session().query(User).filter(User.email == email).scalar()
1451 main_email = Session().query(User).filter(User.email == email).scalar()
1447 if main_email is not None:
1452 if main_email is not None:
1448 raise AttributeError('email %s is present is user table' % email)
1453 raise AttributeError('email %s is present is user table' % email)
1449 return email
1454 return email
1450
1455
1451 @hybrid_property
1456 @hybrid_property
1452 def email(self):
1457 def email(self):
1453 return self._email
1458 return self._email
1454
1459
1455 @email.setter
1460 @email.setter
1456 def email(self, val):
1461 def email(self, val):
1457 self._email = val.lower() if val else None
1462 self._email = val.lower() if val else None
1458
1463
1459
1464
1460 class UserIpMap(Base, BaseModel):
1465 class UserIpMap(Base, BaseModel):
1461 __tablename__ = 'user_ip_map'
1466 __tablename__ = 'user_ip_map'
1462 __table_args__ = (
1467 __table_args__ = (
1463 UniqueConstraint('user_id', 'ip_addr'),
1468 UniqueConstraint('user_id', 'ip_addr'),
1464 base_table_args
1469 base_table_args
1465 )
1470 )
1466
1471
1467 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1472 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1468 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1473 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1469 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1474 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1470 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1475 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1471 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1476 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1472 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1477 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1473
1478
1474 @hybrid_property
1479 @hybrid_property
1475 def description_safe(self):
1480 def description_safe(self):
1476 from rhodecode.lib import helpers as h
1481 from rhodecode.lib import helpers as h
1477 return h.escape(self.description)
1482 return h.escape(self.description)
1478
1483
1479 @classmethod
1484 @classmethod
1480 def _get_ip_range(cls, ip_addr):
1485 def _get_ip_range(cls, ip_addr):
1481 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1486 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1482 return [str(net.network_address), str(net.broadcast_address)]
1487 return [str(net.network_address), str(net.broadcast_address)]
1483
1488
1484 def __json__(self):
1489 def __json__(self):
1485 return {
1490 return {
1486 'ip_addr': self.ip_addr,
1491 'ip_addr': self.ip_addr,
1487 'ip_range': self._get_ip_range(self.ip_addr),
1492 'ip_range': self._get_ip_range(self.ip_addr),
1488 }
1493 }
1489
1494
1490 def __repr__(self):
1495 def __repr__(self):
1491 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1496 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1492
1497
1493
1498
1494 class UserSshKeys(Base, BaseModel):
1499 class UserSshKeys(Base, BaseModel):
1495 __tablename__ = 'user_ssh_keys'
1500 __tablename__ = 'user_ssh_keys'
1496 __table_args__ = (
1501 __table_args__ = (
1497 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1502 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1498
1503
1499 UniqueConstraint('ssh_key_fingerprint'),
1504 UniqueConstraint('ssh_key_fingerprint'),
1500
1505
1501 base_table_args
1506 base_table_args
1502 )
1507 )
1503
1508
1504 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1509 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1505 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1510 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1506 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1511 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1507
1512
1508 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1513 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1509
1514
1510 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1515 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1511 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1516 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1512 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1517 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1513
1518
1514 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1519 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1515
1520
1516 def __json__(self):
1521 def __json__(self):
1517 data = {
1522 data = {
1518 'ssh_fingerprint': self.ssh_key_fingerprint,
1523 'ssh_fingerprint': self.ssh_key_fingerprint,
1519 'description': self.description,
1524 'description': self.description,
1520 'created_on': self.created_on
1525 'created_on': self.created_on
1521 }
1526 }
1522 return data
1527 return data
1523
1528
1524 def get_api_data(self):
1529 def get_api_data(self):
1525 data = self.__json__()
1530 data = self.__json__()
1526 return data
1531 return data
1527
1532
1528
1533
1529 class UserLog(Base, BaseModel):
1534 class UserLog(Base, BaseModel):
1530 __tablename__ = 'user_logs'
1535 __tablename__ = 'user_logs'
1531 __table_args__ = (
1536 __table_args__ = (
1532 base_table_args,
1537 base_table_args,
1533 )
1538 )
1534
1539
1535 VERSION_1 = 'v1'
1540 VERSION_1 = 'v1'
1536 VERSION_2 = 'v2'
1541 VERSION_2 = 'v2'
1537 VERSIONS = [VERSION_1, VERSION_2]
1542 VERSIONS = [VERSION_1, VERSION_2]
1538
1543
1539 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1544 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1540 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1545 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1541 username = Column("username", String(255), nullable=True, unique=None, default=None)
1546 username = Column("username", String(255), nullable=True, unique=None, default=None)
1542 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1547 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1543 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1548 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1544 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1549 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1545 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1550 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1546 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1551 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1547
1552
1548 version = Column("version", String(255), nullable=True, default=VERSION_1)
1553 version = Column("version", String(255), nullable=True, default=VERSION_1)
1549 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1554 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1550 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1555 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1551 user = relationship('User', cascade='', back_populates='user_log')
1556 user = relationship('User', cascade='', back_populates='user_log')
1552 repository = relationship('Repository', cascade='', back_populates='logs')
1557 repository = relationship('Repository', cascade='', back_populates='logs')
1553
1558
1554 def __repr__(self):
1559 def __repr__(self):
1555 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1560 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1556
1561
1557 def __json__(self):
1562 def __json__(self):
1558 return {
1563 return {
1559 'user_id': self.user_id,
1564 'user_id': self.user_id,
1560 'username': self.username,
1565 'username': self.username,
1561 'repository_id': self.repository_id,
1566 'repository_id': self.repository_id,
1562 'repository_name': self.repository_name,
1567 'repository_name': self.repository_name,
1563 'user_ip': self.user_ip,
1568 'user_ip': self.user_ip,
1564 'action_date': self.action_date,
1569 'action_date': self.action_date,
1565 'action': self.action,
1570 'action': self.action,
1566 }
1571 }
1567
1572
1568 @hybrid_property
1573 @hybrid_property
1569 def entry_id(self):
1574 def entry_id(self):
1570 return self.user_log_id
1575 return self.user_log_id
1571
1576
1572 @property
1577 @property
1573 def action_as_day(self):
1578 def action_as_day(self):
1574 return datetime.date(*self.action_date.timetuple()[:3])
1579 return datetime.date(*self.action_date.timetuple()[:3])
1575
1580
1576
1581
1577 class UserGroup(Base, BaseModel):
1582 class UserGroup(Base, BaseModel):
1578 __tablename__ = 'users_groups'
1583 __tablename__ = 'users_groups'
1579 __table_args__ = (
1584 __table_args__ = (
1580 base_table_args,
1585 base_table_args,
1581 )
1586 )
1582
1587
1583 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1588 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1584 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1589 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1585 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1590 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1586 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1591 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1587 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1592 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1588 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1593 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1589 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1594 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1590 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1595 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1591
1596
1592 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1597 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1593 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1598 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1594 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1599 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1595 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1600 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1596 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1601 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1597
1602
1598 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')
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 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1605 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1601 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1606 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1602
1607
1603 @classmethod
1608 @classmethod
1604 def _load_group_data(cls, column):
1609 def _load_group_data(cls, column):
1605 if not column:
1610 if not column:
1606 return {}
1611 return {}
1607
1612
1608 try:
1613 try:
1609 return json.loads(column) or {}
1614 return json.loads(column) or {}
1610 except TypeError:
1615 except TypeError:
1611 return {}
1616 return {}
1612
1617
1613 @hybrid_property
1618 @hybrid_property
1614 def description_safe(self):
1619 def description_safe(self):
1615 from rhodecode.lib import helpers as h
1620 from rhodecode.lib import helpers as h
1616 return h.escape(self.user_group_description)
1621 return h.escape(self.user_group_description)
1617
1622
1618 @hybrid_property
1623 @hybrid_property
1619 def group_data(self):
1624 def group_data(self):
1620 return self._load_group_data(self._group_data)
1625 return self._load_group_data(self._group_data)
1621
1626
1622 @group_data.expression
1627 @group_data.expression
1623 def group_data(self, **kwargs):
1628 def group_data(self, **kwargs):
1624 return self._group_data
1629 return self._group_data
1625
1630
1626 @group_data.setter
1631 @group_data.setter
1627 def group_data(self, val):
1632 def group_data(self, val):
1628 try:
1633 try:
1629 self._group_data = json.dumps(val)
1634 self._group_data = json.dumps(val)
1630 except Exception:
1635 except Exception:
1631 log.error(traceback.format_exc())
1636 log.error(traceback.format_exc())
1632
1637
1633 @classmethod
1638 @classmethod
1634 def _load_sync(cls, group_data):
1639 def _load_sync(cls, group_data):
1635 if group_data:
1640 if group_data:
1636 return group_data.get('extern_type')
1641 return group_data.get('extern_type')
1637
1642
1638 @property
1643 @property
1639 def sync(self):
1644 def sync(self):
1640 return self._load_sync(self.group_data)
1645 return self._load_sync(self.group_data)
1641
1646
1642 def __repr__(self):
1647 def __repr__(self):
1643 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1648 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1644
1649
1645 @classmethod
1650 @classmethod
1646 def get_by_group_name(cls, group_name, cache=False,
1651 def get_by_group_name(cls, group_name, cache=False,
1647 case_insensitive=False):
1652 case_insensitive=False):
1648 if case_insensitive:
1653 if case_insensitive:
1649 q = cls.query().filter(func.lower(cls.users_group_name) ==
1654 q = cls.query().filter(func.lower(cls.users_group_name) ==
1650 func.lower(group_name))
1655 func.lower(group_name))
1651
1656
1652 else:
1657 else:
1653 q = cls.query().filter(cls.users_group_name == group_name)
1658 q = cls.query().filter(cls.users_group_name == group_name)
1654 if cache:
1659 if cache:
1655 name_key = _hash_key(group_name)
1660 name_key = _hash_key(group_name)
1656 q = q.options(
1661 q = q.options(
1657 FromCache("sql_cache_short", f"get_group_{name_key}"))
1662 FromCache("sql_cache_short", f"get_group_{name_key}"))
1658 return q.scalar()
1663 return q.scalar()
1659
1664
1660 @classmethod
1665 @classmethod
1661 def get(cls, user_group_id, cache=False):
1666 def get(cls, user_group_id, cache=False):
1662 if not user_group_id:
1667 if not user_group_id:
1663 return
1668 return
1664
1669
1665 user_group = cls.query()
1670 user_group = cls.query()
1666 if cache:
1671 if cache:
1667 user_group = user_group.options(
1672 user_group = user_group.options(
1668 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1673 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1669 return user_group.get(user_group_id)
1674 return user_group.get(user_group_id)
1670
1675
1671 def permissions(self, with_admins=True, with_owner=True,
1676 def permissions(self, with_admins=True, with_owner=True,
1672 expand_from_user_groups=False):
1677 expand_from_user_groups=False):
1673 """
1678 """
1674 Permissions for user groups
1679 Permissions for user groups
1675 """
1680 """
1676 _admin_perm = 'usergroup.admin'
1681 _admin_perm = 'usergroup.admin'
1677
1682
1678 owner_row = []
1683 owner_row = []
1679 if with_owner:
1684 if with_owner:
1680 usr = AttributeDict(self.user.get_dict())
1685 usr = AttributeDict(self.user.get_dict())
1681 usr.owner_row = True
1686 usr.owner_row = True
1682 usr.permission = _admin_perm
1687 usr.permission = _admin_perm
1683 owner_row.append(usr)
1688 owner_row.append(usr)
1684
1689
1685 super_admin_ids = []
1690 super_admin_ids = []
1686 super_admin_rows = []
1691 super_admin_rows = []
1687 if with_admins:
1692 if with_admins:
1688 for usr in User.get_all_super_admins():
1693 for usr in User.get_all_super_admins():
1689 super_admin_ids.append(usr.user_id)
1694 super_admin_ids.append(usr.user_id)
1690 # if this admin is also owner, don't double the record
1695 # if this admin is also owner, don't double the record
1691 if usr.user_id == owner_row[0].user_id:
1696 if usr.user_id == owner_row[0].user_id:
1692 owner_row[0].admin_row = True
1697 owner_row[0].admin_row = True
1693 else:
1698 else:
1694 usr = AttributeDict(usr.get_dict())
1699 usr = AttributeDict(usr.get_dict())
1695 usr.admin_row = True
1700 usr.admin_row = True
1696 usr.permission = _admin_perm
1701 usr.permission = _admin_perm
1697 super_admin_rows.append(usr)
1702 super_admin_rows.append(usr)
1698
1703
1699 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1704 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1700 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1705 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1701 joinedload(UserUserGroupToPerm.user),
1706 joinedload(UserUserGroupToPerm.user),
1702 joinedload(UserUserGroupToPerm.permission),)
1707 joinedload(UserUserGroupToPerm.permission),)
1703
1708
1704 # get owners and admins and permissions. We do a trick of re-writing
1709 # get owners and admins and permissions. We do a trick of re-writing
1705 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1710 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1706 # has a global reference and changing one object propagates to all
1711 # has a global reference and changing one object propagates to all
1707 # others. This means if admin is also an owner admin_row that change
1712 # others. This means if admin is also an owner admin_row that change
1708 # would propagate to both objects
1713 # would propagate to both objects
1709 perm_rows = []
1714 perm_rows = []
1710 for _usr in q.all():
1715 for _usr in q.all():
1711 usr = AttributeDict(_usr.user.get_dict())
1716 usr = AttributeDict(_usr.user.get_dict())
1712 # if this user is also owner/admin, mark as duplicate record
1717 # if this user is also owner/admin, mark as duplicate record
1713 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1718 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1714 usr.duplicate_perm = True
1719 usr.duplicate_perm = True
1715 usr.permission = _usr.permission.permission_name
1720 usr.permission = _usr.permission.permission_name
1716 perm_rows.append(usr)
1721 perm_rows.append(usr)
1717
1722
1718 # filter the perm rows by 'default' first and then sort them by
1723 # filter the perm rows by 'default' first and then sort them by
1719 # admin,write,read,none permissions sorted again alphabetically in
1724 # admin,write,read,none permissions sorted again alphabetically in
1720 # each group
1725 # each group
1721 perm_rows = sorted(perm_rows, key=display_user_sort)
1726 perm_rows = sorted(perm_rows, key=display_user_sort)
1722
1727
1723 user_groups_rows = []
1728 user_groups_rows = []
1724 if expand_from_user_groups:
1729 if expand_from_user_groups:
1725 for ug in self.permission_user_groups(with_members=True):
1730 for ug in self.permission_user_groups(with_members=True):
1726 for user_data in ug.members:
1731 for user_data in ug.members:
1727 user_groups_rows.append(user_data)
1732 user_groups_rows.append(user_data)
1728
1733
1729 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1734 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1730
1735
1731 def permission_user_groups(self, with_members=False):
1736 def permission_user_groups(self, with_members=False):
1732 q = UserGroupUserGroupToPerm.query()\
1737 q = UserGroupUserGroupToPerm.query()\
1733 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1738 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1734 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1739 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1735 joinedload(UserGroupUserGroupToPerm.target_user_group),
1740 joinedload(UserGroupUserGroupToPerm.target_user_group),
1736 joinedload(UserGroupUserGroupToPerm.permission),)
1741 joinedload(UserGroupUserGroupToPerm.permission),)
1737
1742
1738 perm_rows = []
1743 perm_rows = []
1739 for _user_group in q.all():
1744 for _user_group in q.all():
1740 entry = AttributeDict(_user_group.user_group.get_dict())
1745 entry = AttributeDict(_user_group.user_group.get_dict())
1741 entry.permission = _user_group.permission.permission_name
1746 entry.permission = _user_group.permission.permission_name
1742 if with_members:
1747 if with_members:
1743 entry.members = [x.user.get_dict()
1748 entry.members = [x.user.get_dict()
1744 for x in _user_group.user_group.members]
1749 for x in _user_group.user_group.members]
1745 perm_rows.append(entry)
1750 perm_rows.append(entry)
1746
1751
1747 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1752 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1748 return perm_rows
1753 return perm_rows
1749
1754
1750 def _get_default_perms(self, user_group, suffix=''):
1755 def _get_default_perms(self, user_group, suffix=''):
1751 from rhodecode.model.permission import PermissionModel
1756 from rhodecode.model.permission import PermissionModel
1752 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1757 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1753
1758
1754 def get_default_perms(self, suffix=''):
1759 def get_default_perms(self, suffix=''):
1755 return self._get_default_perms(self, suffix)
1760 return self._get_default_perms(self, suffix)
1756
1761
1757 def get_api_data(self, with_group_members=True, include_secrets=False):
1762 def get_api_data(self, with_group_members=True, include_secrets=False):
1758 """
1763 """
1759 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1764 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1760 basically forwarded.
1765 basically forwarded.
1761
1766
1762 """
1767 """
1763 user_group = self
1768 user_group = self
1764 data = {
1769 data = {
1765 'users_group_id': user_group.users_group_id,
1770 'users_group_id': user_group.users_group_id,
1766 'group_name': user_group.users_group_name,
1771 'group_name': user_group.users_group_name,
1767 'group_description': user_group.user_group_description,
1772 'group_description': user_group.user_group_description,
1768 'active': user_group.users_group_active,
1773 'active': user_group.users_group_active,
1769 'owner': user_group.user.username,
1774 'owner': user_group.user.username,
1770 'sync': user_group.sync,
1775 'sync': user_group.sync,
1771 'owner_email': user_group.user.email,
1776 'owner_email': user_group.user.email,
1772 }
1777 }
1773
1778
1774 if with_group_members:
1779 if with_group_members:
1775 users = []
1780 users = []
1776 for user in user_group.members:
1781 for user in user_group.members:
1777 user = user.user
1782 user = user.user
1778 users.append(user.get_api_data(include_secrets=include_secrets))
1783 users.append(user.get_api_data(include_secrets=include_secrets))
1779 data['users'] = users
1784 data['users'] = users
1780
1785
1781 return data
1786 return data
1782
1787
1783
1788
1784 class UserGroupMember(Base, BaseModel):
1789 class UserGroupMember(Base, BaseModel):
1785 __tablename__ = 'users_groups_members'
1790 __tablename__ = 'users_groups_members'
1786 __table_args__ = (
1791 __table_args__ = (
1787 base_table_args,
1792 base_table_args,
1788 )
1793 )
1789
1794
1790 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1795 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1791 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1796 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1792 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1797 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1793
1798
1794 user = relationship('User', lazy='joined', back_populates='group_member')
1799 user = relationship('User', lazy='joined', back_populates='group_member')
1795 users_group = relationship('UserGroup', back_populates='members')
1800 users_group = relationship('UserGroup', back_populates='members')
1796
1801
1797 def __init__(self, gr_id='', u_id=''):
1802 def __init__(self, gr_id='', u_id=''):
1798 self.users_group_id = gr_id
1803 self.users_group_id = gr_id
1799 self.user_id = u_id
1804 self.user_id = u_id
1800
1805
1801
1806
1802 class RepositoryField(Base, BaseModel):
1807 class RepositoryField(Base, BaseModel):
1803 __tablename__ = 'repositories_fields'
1808 __tablename__ = 'repositories_fields'
1804 __table_args__ = (
1809 __table_args__ = (
1805 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1810 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1806 base_table_args,
1811 base_table_args,
1807 )
1812 )
1808
1813
1809 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1814 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1810
1815
1811 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1816 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1812 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1817 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1813 field_key = Column("field_key", String(250))
1818 field_key = Column("field_key", String(250))
1814 field_label = Column("field_label", String(1024), nullable=False)
1819 field_label = Column("field_label", String(1024), nullable=False)
1815 field_value = Column("field_value", String(10000), nullable=False)
1820 field_value = Column("field_value", String(10000), nullable=False)
1816 field_desc = Column("field_desc", String(1024), nullable=False)
1821 field_desc = Column("field_desc", String(1024), nullable=False)
1817 field_type = Column("field_type", String(255), nullable=False, unique=None)
1822 field_type = Column("field_type", String(255), nullable=False, unique=None)
1818 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1823 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1819
1824
1820 repository = relationship('Repository', back_populates='extra_fields')
1825 repository = relationship('Repository', back_populates='extra_fields')
1821
1826
1822 @property
1827 @property
1823 def field_key_prefixed(self):
1828 def field_key_prefixed(self):
1824 return 'ex_%s' % self.field_key
1829 return 'ex_%s' % self.field_key
1825
1830
1826 @classmethod
1831 @classmethod
1827 def un_prefix_key(cls, key):
1832 def un_prefix_key(cls, key):
1828 if key.startswith(cls.PREFIX):
1833 if key.startswith(cls.PREFIX):
1829 return key[len(cls.PREFIX):]
1834 return key[len(cls.PREFIX):]
1830 return key
1835 return key
1831
1836
1832 @classmethod
1837 @classmethod
1833 def get_by_key_name(cls, key, repo):
1838 def get_by_key_name(cls, key, repo):
1834 row = cls.query()\
1839 row = cls.query()\
1835 .filter(cls.repository == repo)\
1840 .filter(cls.repository == repo)\
1836 .filter(cls.field_key == key).scalar()
1841 .filter(cls.field_key == key).scalar()
1837 return row
1842 return row
1838
1843
1839
1844
1840 class Repository(Base, BaseModel):
1845 class Repository(Base, BaseModel):
1841 __tablename__ = 'repositories'
1846 __tablename__ = 'repositories'
1842 __table_args__ = (
1847 __table_args__ = (
1843 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1848 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1844 base_table_args,
1849 base_table_args,
1845 )
1850 )
1846 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1851 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1847 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1852 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1848 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1853 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1849
1854
1850 STATE_CREATED = 'repo_state_created'
1855 STATE_CREATED = 'repo_state_created'
1851 STATE_PENDING = 'repo_state_pending'
1856 STATE_PENDING = 'repo_state_pending'
1852 STATE_ERROR = 'repo_state_error'
1857 STATE_ERROR = 'repo_state_error'
1853
1858
1854 LOCK_AUTOMATIC = 'lock_auto'
1859 LOCK_AUTOMATIC = 'lock_auto'
1855 LOCK_API = 'lock_api'
1860 LOCK_API = 'lock_api'
1856 LOCK_WEB = 'lock_web'
1861 LOCK_WEB = 'lock_web'
1857 LOCK_PULL = 'lock_pull'
1862 LOCK_PULL = 'lock_pull'
1858
1863
1859 NAME_SEP = URL_SEP
1864 NAME_SEP = URL_SEP
1860
1865
1861 repo_id = Column(
1866 repo_id = Column(
1862 "repo_id", Integer(), nullable=False, unique=True, default=None,
1867 "repo_id", Integer(), nullable=False, unique=True, default=None,
1863 primary_key=True)
1868 primary_key=True)
1864 _repo_name = Column(
1869 _repo_name = Column(
1865 "repo_name", Text(), nullable=False, default=None)
1870 "repo_name", Text(), nullable=False, default=None)
1866 repo_name_hash = Column(
1871 repo_name_hash = Column(
1867 "repo_name_hash", String(255), nullable=False, unique=True)
1872 "repo_name_hash", String(255), nullable=False, unique=True)
1868 repo_state = Column("repo_state", String(255), nullable=True)
1873 repo_state = Column("repo_state", String(255), nullable=True)
1869
1874
1870 clone_uri = Column(
1875 clone_uri = Column(
1871 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1876 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1872 default=None)
1877 default=None)
1873 push_uri = Column(
1878 push_uri = Column(
1874 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1879 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1875 default=None)
1880 default=None)
1876 repo_type = Column(
1881 repo_type = Column(
1877 "repo_type", String(255), nullable=False, unique=False, default=None)
1882 "repo_type", String(255), nullable=False, unique=False, default=None)
1878 user_id = Column(
1883 user_id = Column(
1879 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1884 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1880 unique=False, default=None)
1885 unique=False, default=None)
1881 private = Column(
1886 private = Column(
1882 "private", Boolean(), nullable=True, unique=None, default=None)
1887 "private", Boolean(), nullable=True, unique=None, default=None)
1883 archived = Column(
1888 archived = Column(
1884 "archived", Boolean(), nullable=True, unique=None, default=None)
1889 "archived", Boolean(), nullable=True, unique=None, default=None)
1885 enable_statistics = Column(
1890 enable_statistics = Column(
1886 "statistics", Boolean(), nullable=True, unique=None, default=True)
1891 "statistics", Boolean(), nullable=True, unique=None, default=True)
1887 enable_downloads = Column(
1892 enable_downloads = Column(
1888 "downloads", Boolean(), nullable=True, unique=None, default=True)
1893 "downloads", Boolean(), nullable=True, unique=None, default=True)
1889 description = Column(
1894 description = Column(
1890 "description", String(10000), nullable=True, unique=None, default=None)
1895 "description", String(10000), nullable=True, unique=None, default=None)
1891 created_on = Column(
1896 created_on = Column(
1892 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1897 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1893 default=datetime.datetime.now)
1898 default=datetime.datetime.now)
1894 updated_on = Column(
1899 updated_on = Column(
1895 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1900 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1896 default=datetime.datetime.now)
1901 default=datetime.datetime.now)
1897 _landing_revision = Column(
1902 _landing_revision = Column(
1898 "landing_revision", String(255), nullable=False, unique=False,
1903 "landing_revision", String(255), nullable=False, unique=False,
1899 default=None)
1904 default=None)
1900 enable_locking = Column(
1905 enable_locking = Column(
1901 "enable_locking", Boolean(), nullable=False, unique=None,
1906 "enable_locking", Boolean(), nullable=False, unique=None,
1902 default=False)
1907 default=False)
1903 _locked = Column(
1908 _locked = Column(
1904 "locked", String(255), nullable=True, unique=False, default=None)
1909 "locked", String(255), nullable=True, unique=False, default=None)
1905 _changeset_cache = Column(
1910 _changeset_cache = Column(
1906 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1911 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1907
1912
1908 fork_id = Column(
1913 fork_id = Column(
1909 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1914 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1910 nullable=True, unique=False, default=None)
1915 nullable=True, unique=False, default=None)
1911 group_id = Column(
1916 group_id = Column(
1912 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1917 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1913 unique=False, default=None)
1918 unique=False, default=None)
1914
1919
1915 user = relationship('User', lazy='joined', back_populates='repositories')
1920 user = relationship('User', lazy='joined', back_populates='repositories')
1916 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1921 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1917 group = relationship('RepoGroup', lazy='joined')
1922 group = relationship('RepoGroup', lazy='joined')
1918 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1923 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1919 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1924 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1920 stats = relationship('Statistics', cascade='all', uselist=False)
1925 stats = relationship('Statistics', cascade='all', uselist=False)
1921
1926
1922 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1927 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1923 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1928 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1924
1929
1925 logs = relationship('UserLog', back_populates='repository')
1930 logs = relationship('UserLog', back_populates='repository')
1926
1931
1927 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1932 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1928
1933
1929 pull_requests_source = relationship(
1934 pull_requests_source = relationship(
1930 'PullRequest',
1935 'PullRequest',
1931 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1936 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1932 cascade="all, delete-orphan",
1937 cascade="all, delete-orphan",
1933 overlaps="source_repo"
1938 overlaps="source_repo"
1934 )
1939 )
1935 pull_requests_target = relationship(
1940 pull_requests_target = relationship(
1936 'PullRequest',
1941 'PullRequest',
1937 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1942 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1938 cascade="all, delete-orphan",
1943 cascade="all, delete-orphan",
1939 overlaps="target_repo"
1944 overlaps="target_repo"
1940 )
1945 )
1941
1946
1942 ui = relationship('RepoRhodeCodeUi', cascade="all")
1947 ui = relationship('RepoRhodeCodeUi', cascade="all")
1943 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1948 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1944 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1949 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1945
1950
1946 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1951 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1947
1952
1948 # no cascade, set NULL
1953 # no cascade, set NULL
1949 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1954 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1950
1955
1951 review_rules = relationship('RepoReviewRule')
1956 review_rules = relationship('RepoReviewRule')
1952 user_branch_perms = relationship('UserToRepoBranchPermission')
1957 user_branch_perms = relationship('UserToRepoBranchPermission')
1953 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1958 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1954
1959
1955 def __repr__(self):
1960 def __repr__(self):
1956 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1961 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1957
1962
1958 @hybrid_property
1963 @hybrid_property
1959 def description_safe(self):
1964 def description_safe(self):
1960 from rhodecode.lib import helpers as h
1965 from rhodecode.lib import helpers as h
1961 return h.escape(self.description)
1966 return h.escape(self.description)
1962
1967
1963 @hybrid_property
1968 @hybrid_property
1964 def landing_rev(self):
1969 def landing_rev(self):
1965 # always should return [rev_type, rev], e.g ['branch', 'master']
1970 # always should return [rev_type, rev], e.g ['branch', 'master']
1966 if self._landing_revision:
1971 if self._landing_revision:
1967 _rev_info = self._landing_revision.split(':')
1972 _rev_info = self._landing_revision.split(':')
1968 if len(_rev_info) < 2:
1973 if len(_rev_info) < 2:
1969 _rev_info.insert(0, 'rev')
1974 _rev_info.insert(0, 'rev')
1970 return [_rev_info[0], _rev_info[1]]
1975 return [_rev_info[0], _rev_info[1]]
1971 return [None, None]
1976 return [None, None]
1972
1977
1973 @property
1978 @property
1974 def landing_ref_type(self):
1979 def landing_ref_type(self):
1975 return self.landing_rev[0]
1980 return self.landing_rev[0]
1976
1981
1977 @property
1982 @property
1978 def landing_ref_name(self):
1983 def landing_ref_name(self):
1979 return self.landing_rev[1]
1984 return self.landing_rev[1]
1980
1985
1981 @landing_rev.setter
1986 @landing_rev.setter
1982 def landing_rev(self, val):
1987 def landing_rev(self, val):
1983 if ':' not in val:
1988 if ':' not in val:
1984 raise ValueError('value must be delimited with `:` and consist '
1989 raise ValueError('value must be delimited with `:` and consist '
1985 'of <rev_type>:<rev>, got %s instead' % val)
1990 'of <rev_type>:<rev>, got %s instead' % val)
1986 self._landing_revision = val
1991 self._landing_revision = val
1987
1992
1988 @hybrid_property
1993 @hybrid_property
1989 def locked(self):
1994 def locked(self):
1990 if self._locked:
1995 if self._locked:
1991 user_id, timelocked, reason = self._locked.split(':')
1996 user_id, timelocked, reason = self._locked.split(':')
1992 lock_values = int(user_id), timelocked, reason
1997 lock_values = int(user_id), timelocked, reason
1993 else:
1998 else:
1994 lock_values = [None, None, None]
1999 lock_values = [None, None, None]
1995 return lock_values
2000 return lock_values
1996
2001
1997 @locked.setter
2002 @locked.setter
1998 def locked(self, val):
2003 def locked(self, val):
1999 if val and isinstance(val, (list, tuple)):
2004 if val and isinstance(val, (list, tuple)):
2000 self._locked = ':'.join(map(str, val))
2005 self._locked = ':'.join(map(str, val))
2001 else:
2006 else:
2002 self._locked = None
2007 self._locked = None
2003
2008
2004 @classmethod
2009 @classmethod
2005 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2010 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2006 from rhodecode.lib.vcs.backends.base import EmptyCommit
2011 from rhodecode.lib.vcs.backends.base import EmptyCommit
2007 dummy = EmptyCommit().__json__()
2012 dummy = EmptyCommit().__json__()
2008 if not changeset_cache_raw:
2013 if not changeset_cache_raw:
2009 dummy['source_repo_id'] = repo_id
2014 dummy['source_repo_id'] = repo_id
2010 return json.loads(json.dumps(dummy))
2015 return json.loads(json.dumps(dummy))
2011
2016
2012 try:
2017 try:
2013 return json.loads(changeset_cache_raw)
2018 return json.loads(changeset_cache_raw)
2014 except TypeError:
2019 except TypeError:
2015 return dummy
2020 return dummy
2016 except Exception:
2021 except Exception:
2017 log.error(traceback.format_exc())
2022 log.error(traceback.format_exc())
2018 return dummy
2023 return dummy
2019
2024
2020 @hybrid_property
2025 @hybrid_property
2021 def changeset_cache(self):
2026 def changeset_cache(self):
2022 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
2027 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
2023
2028
2024 @changeset_cache.setter
2029 @changeset_cache.setter
2025 def changeset_cache(self, val):
2030 def changeset_cache(self, val):
2026 try:
2031 try:
2027 self._changeset_cache = json.dumps(val)
2032 self._changeset_cache = json.dumps(val)
2028 except Exception:
2033 except Exception:
2029 log.error(traceback.format_exc())
2034 log.error(traceback.format_exc())
2030
2035
2031 @hybrid_property
2036 @hybrid_property
2032 def repo_name(self):
2037 def repo_name(self):
2033 return self._repo_name
2038 return self._repo_name
2034
2039
2035 @repo_name.setter
2040 @repo_name.setter
2036 def repo_name(self, value):
2041 def repo_name(self, value):
2037 self._repo_name = value
2042 self._repo_name = value
2038 self.repo_name_hash = sha1(safe_bytes(value))
2043 self.repo_name_hash = sha1(safe_bytes(value))
2039
2044
2040 @classmethod
2045 @classmethod
2041 def normalize_repo_name(cls, repo_name):
2046 def normalize_repo_name(cls, repo_name):
2042 """
2047 """
2043 Normalizes os specific repo_name to the format internally stored inside
2048 Normalizes os specific repo_name to the format internally stored inside
2044 database using URL_SEP
2049 database using URL_SEP
2045
2050
2046 :param cls:
2051 :param cls:
2047 :param repo_name:
2052 :param repo_name:
2048 """
2053 """
2049 return cls.NAME_SEP.join(repo_name.split(os.sep))
2054 return cls.NAME_SEP.join(repo_name.split(os.sep))
2050
2055
2051 @classmethod
2056 @classmethod
2052 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
2057 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
2053 session = Session()
2058 session = Session()
2054 q = session.query(cls).filter(cls.repo_name == repo_name)
2059 q = session.query(cls).filter(cls.repo_name == repo_name)
2055
2060
2056 if cache:
2061 if cache:
2057 if identity_cache:
2062 if identity_cache:
2058 val = cls.identity_cache(session, 'repo_name', repo_name)
2063 val = cls.identity_cache(session, 'repo_name', repo_name)
2059 if val:
2064 if val:
2060 return val
2065 return val
2061 else:
2066 else:
2062 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2067 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2063 q = q.options(
2068 q = q.options(
2064 FromCache("sql_cache_short", cache_key))
2069 FromCache("sql_cache_short", cache_key))
2065
2070
2066 return q.scalar()
2071 return q.scalar()
2067
2072
2068 @classmethod
2073 @classmethod
2069 def get_by_id_or_repo_name(cls, repoid):
2074 def get_by_id_or_repo_name(cls, repoid):
2070 if isinstance(repoid, int):
2075 if isinstance(repoid, int):
2071 try:
2076 try:
2072 repo = cls.get(repoid)
2077 repo = cls.get(repoid)
2073 except ValueError:
2078 except ValueError:
2074 repo = None
2079 repo = None
2075 else:
2080 else:
2076 repo = cls.get_by_repo_name(repoid)
2081 repo = cls.get_by_repo_name(repoid)
2077 return repo
2082 return repo
2078
2083
2079 @classmethod
2084 @classmethod
2080 def get_by_full_path(cls, repo_full_path):
2085 def get_by_full_path(cls, repo_full_path):
2081 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2086 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2082 repo_name = cls.normalize_repo_name(repo_name)
2087 repo_name = cls.normalize_repo_name(repo_name)
2083 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2088 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2084
2089
2085 @classmethod
2090 @classmethod
2086 def get_repo_forks(cls, repo_id):
2091 def get_repo_forks(cls, repo_id):
2087 return cls.query().filter(Repository.fork_id == repo_id)
2092 return cls.query().filter(Repository.fork_id == repo_id)
2088
2093
2089 @classmethod
2094 @classmethod
2090 def base_path(cls):
2095 def base_path(cls):
2091 """
2096 """
2092 Returns base path when all repos are stored
2097 Returns base path when all repos are stored
2093
2098
2094 :param cls:
2099 :param cls:
2095 """
2100 """
2096 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2101 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2097 return get_rhodecode_repo_store_path()
2102 return get_rhodecode_repo_store_path()
2098
2103
2099 @classmethod
2104 @classmethod
2100 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2105 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2101 case_insensitive=True, archived=False):
2106 case_insensitive=True, archived=False):
2102 q = Repository.query()
2107 q = Repository.query()
2103
2108
2104 if not archived:
2109 if not archived:
2105 q = q.filter(Repository.archived.isnot(true()))
2110 q = q.filter(Repository.archived.isnot(true()))
2106
2111
2107 if not isinstance(user_id, Optional):
2112 if not isinstance(user_id, Optional):
2108 q = q.filter(Repository.user_id == user_id)
2113 q = q.filter(Repository.user_id == user_id)
2109
2114
2110 if not isinstance(group_id, Optional):
2115 if not isinstance(group_id, Optional):
2111 q = q.filter(Repository.group_id == group_id)
2116 q = q.filter(Repository.group_id == group_id)
2112
2117
2113 if case_insensitive:
2118 if case_insensitive:
2114 q = q.order_by(func.lower(Repository.repo_name))
2119 q = q.order_by(func.lower(Repository.repo_name))
2115 else:
2120 else:
2116 q = q.order_by(Repository.repo_name)
2121 q = q.order_by(Repository.repo_name)
2117
2122
2118 return q.all()
2123 return q.all()
2119
2124
2120 @property
2125 @property
2121 def repo_uid(self):
2126 def repo_uid(self):
2122 return '_{}'.format(self.repo_id)
2127 return '_{}'.format(self.repo_id)
2123
2128
2124 @property
2129 @property
2125 def forks(self):
2130 def forks(self):
2126 """
2131 """
2127 Return forks of this repo
2132 Return forks of this repo
2128 """
2133 """
2129 return Repository.get_repo_forks(self.repo_id)
2134 return Repository.get_repo_forks(self.repo_id)
2130
2135
2131 @property
2136 @property
2132 def parent(self):
2137 def parent(self):
2133 """
2138 """
2134 Returns fork parent
2139 Returns fork parent
2135 """
2140 """
2136 return self.fork
2141 return self.fork
2137
2142
2138 @property
2143 @property
2139 def just_name(self):
2144 def just_name(self):
2140 return self.repo_name.split(self.NAME_SEP)[-1]
2145 return self.repo_name.split(self.NAME_SEP)[-1]
2141
2146
2142 @property
2147 @property
2143 def groups_with_parents(self):
2148 def groups_with_parents(self):
2144 groups = []
2149 groups = []
2145 if self.group is None:
2150 if self.group is None:
2146 return groups
2151 return groups
2147
2152
2148 cur_gr = self.group
2153 cur_gr = self.group
2149 groups.insert(0, cur_gr)
2154 groups.insert(0, cur_gr)
2150 while 1:
2155 while 1:
2151 gr = getattr(cur_gr, 'parent_group', None)
2156 gr = getattr(cur_gr, 'parent_group', None)
2152 cur_gr = cur_gr.parent_group
2157 cur_gr = cur_gr.parent_group
2153 if gr is None:
2158 if gr is None:
2154 break
2159 break
2155 groups.insert(0, gr)
2160 groups.insert(0, gr)
2156
2161
2157 return groups
2162 return groups
2158
2163
2159 @property
2164 @property
2160 def groups_and_repo(self):
2165 def groups_and_repo(self):
2161 return self.groups_with_parents, self
2166 return self.groups_with_parents, self
2162
2167
2163 @property
2168 @property
2164 def repo_path(self):
2169 def repo_path(self):
2165 """
2170 """
2166 Returns base full path for that repository means where it actually
2171 Returns base full path for that repository means where it actually
2167 exists on a filesystem
2172 exists on a filesystem
2168 """
2173 """
2169 return self.base_path()
2174 return self.base_path()
2170
2175
2171 @property
2176 @property
2172 def repo_full_path(self):
2177 def repo_full_path(self):
2173 p = [self.repo_path]
2178 p = [self.repo_path]
2174 # we need to split the name by / since this is how we store the
2179 # we need to split the name by / since this is how we store the
2175 # names in the database, but that eventually needs to be converted
2180 # names in the database, but that eventually needs to be converted
2176 # into a valid system path
2181 # into a valid system path
2177 p += self.repo_name.split(self.NAME_SEP)
2182 p += self.repo_name.split(self.NAME_SEP)
2178 return os.path.join(*map(safe_str, p))
2183 return os.path.join(*map(safe_str, p))
2179
2184
2180 @property
2185 @property
2181 def cache_keys(self):
2186 def cache_keys(self):
2182 """
2187 """
2183 Returns associated cache keys for that repo
2188 Returns associated cache keys for that repo
2184 """
2189 """
2185 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2190 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2186 return CacheKey.query()\
2191 return CacheKey.query()\
2187 .filter(CacheKey.cache_key == repo_namespace_key)\
2192 .filter(CacheKey.cache_key == repo_namespace_key)\
2188 .order_by(CacheKey.cache_key)\
2193 .order_by(CacheKey.cache_key)\
2189 .all()
2194 .all()
2190
2195
2191 @property
2196 @property
2192 def cached_diffs_relative_dir(self):
2197 def cached_diffs_relative_dir(self):
2193 """
2198 """
2194 Return a relative to the repository store path of cached diffs
2199 Return a relative to the repository store path of cached diffs
2195 used for safe display for users, who shouldn't know the absolute store
2200 used for safe display for users, who shouldn't know the absolute store
2196 path
2201 path
2197 """
2202 """
2198 return os.path.join(
2203 return os.path.join(
2199 os.path.dirname(self.repo_name),
2204 os.path.dirname(self.repo_name),
2200 self.cached_diffs_dir.split(os.path.sep)[-1])
2205 self.cached_diffs_dir.split(os.path.sep)[-1])
2201
2206
2202 @property
2207 @property
2203 def cached_diffs_dir(self):
2208 def cached_diffs_dir(self):
2204 path = self.repo_full_path
2209 path = self.repo_full_path
2205 return os.path.join(
2210 return os.path.join(
2206 os.path.dirname(path),
2211 os.path.dirname(path),
2207 f'.__shadow_diff_cache_repo_{self.repo_id}')
2212 f'.__shadow_diff_cache_repo_{self.repo_id}')
2208
2213
2209 def cached_diffs(self):
2214 def cached_diffs(self):
2210 diff_cache_dir = self.cached_diffs_dir
2215 diff_cache_dir = self.cached_diffs_dir
2211 if os.path.isdir(diff_cache_dir):
2216 if os.path.isdir(diff_cache_dir):
2212 return os.listdir(diff_cache_dir)
2217 return os.listdir(diff_cache_dir)
2213 return []
2218 return []
2214
2219
2215 def shadow_repos(self):
2220 def shadow_repos(self):
2216 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2221 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2217 return [
2222 return [
2218 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2223 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2219 if x.startswith(shadow_repos_pattern)
2224 if x.startswith(shadow_repos_pattern)
2220 ]
2225 ]
2221
2226
2222 def get_new_name(self, repo_name):
2227 def get_new_name(self, repo_name):
2223 """
2228 """
2224 returns new full repository name based on assigned group and new new
2229 returns new full repository name based on assigned group and new new
2225
2230
2226 :param repo_name:
2231 :param repo_name:
2227 """
2232 """
2228 path_prefix = self.group.full_path_splitted if self.group else []
2233 path_prefix = self.group.full_path_splitted if self.group else []
2229 return self.NAME_SEP.join(path_prefix + [repo_name])
2234 return self.NAME_SEP.join(path_prefix + [repo_name])
2230
2235
2231 @property
2236 @property
2232 def _config(self):
2237 def _config(self):
2233 """
2238 """
2234 Returns db based config object.
2239 Returns db based config object.
2235 """
2240 """
2236 from rhodecode.lib.utils import make_db_config
2241 from rhodecode.lib.utils import make_db_config
2237 return make_db_config(clear_session=False, repo=self)
2242 return make_db_config(clear_session=False, repo=self)
2238
2243
2239 def permissions(self, with_admins=True, with_owner=True,
2244 def permissions(self, with_admins=True, with_owner=True,
2240 expand_from_user_groups=False):
2245 expand_from_user_groups=False):
2241 """
2246 """
2242 Permissions for repositories
2247 Permissions for repositories
2243 """
2248 """
2244 _admin_perm = 'repository.admin'
2249 _admin_perm = 'repository.admin'
2245
2250
2246 owner_row = []
2251 owner_row = []
2247 if with_owner:
2252 if with_owner:
2248 usr = AttributeDict(self.user.get_dict())
2253 usr = AttributeDict(self.user.get_dict())
2249 usr.owner_row = True
2254 usr.owner_row = True
2250 usr.permission = _admin_perm
2255 usr.permission = _admin_perm
2251 usr.permission_id = None
2256 usr.permission_id = None
2252 owner_row.append(usr)
2257 owner_row.append(usr)
2253
2258
2254 super_admin_ids = []
2259 super_admin_ids = []
2255 super_admin_rows = []
2260 super_admin_rows = []
2256 if with_admins:
2261 if with_admins:
2257 for usr in User.get_all_super_admins():
2262 for usr in User.get_all_super_admins():
2258 super_admin_ids.append(usr.user_id)
2263 super_admin_ids.append(usr.user_id)
2259 # if this admin is also owner, don't double the record
2264 # if this admin is also owner, don't double the record
2260 if usr.user_id == owner_row[0].user_id:
2265 if usr.user_id == owner_row[0].user_id:
2261 owner_row[0].admin_row = True
2266 owner_row[0].admin_row = True
2262 else:
2267 else:
2263 usr = AttributeDict(usr.get_dict())
2268 usr = AttributeDict(usr.get_dict())
2264 usr.admin_row = True
2269 usr.admin_row = True
2265 usr.permission = _admin_perm
2270 usr.permission = _admin_perm
2266 usr.permission_id = None
2271 usr.permission_id = None
2267 super_admin_rows.append(usr)
2272 super_admin_rows.append(usr)
2268
2273
2269 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2274 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2270 q = q.options(joinedload(UserRepoToPerm.repository),
2275 q = q.options(joinedload(UserRepoToPerm.repository),
2271 joinedload(UserRepoToPerm.user),
2276 joinedload(UserRepoToPerm.user),
2272 joinedload(UserRepoToPerm.permission),)
2277 joinedload(UserRepoToPerm.permission),)
2273
2278
2274 # get owners and admins and permissions. We do a trick of re-writing
2279 # get owners and admins and permissions. We do a trick of re-writing
2275 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2280 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2276 # has a global reference and changing one object propagates to all
2281 # has a global reference and changing one object propagates to all
2277 # others. This means if admin is also an owner admin_row that change
2282 # others. This means if admin is also an owner admin_row that change
2278 # would propagate to both objects
2283 # would propagate to both objects
2279 perm_rows = []
2284 perm_rows = []
2280 for _usr in q.all():
2285 for _usr in q.all():
2281 usr = AttributeDict(_usr.user.get_dict())
2286 usr = AttributeDict(_usr.user.get_dict())
2282 # if this user is also owner/admin, mark as duplicate record
2287 # if this user is also owner/admin, mark as duplicate record
2283 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2288 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2284 usr.duplicate_perm = True
2289 usr.duplicate_perm = True
2285 # also check if this permission is maybe used by branch_permissions
2290 # also check if this permission is maybe used by branch_permissions
2286 if _usr.branch_perm_entry:
2291 if _usr.branch_perm_entry:
2287 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2292 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2288
2293
2289 usr.permission = _usr.permission.permission_name
2294 usr.permission = _usr.permission.permission_name
2290 usr.permission_id = _usr.repo_to_perm_id
2295 usr.permission_id = _usr.repo_to_perm_id
2291 perm_rows.append(usr)
2296 perm_rows.append(usr)
2292
2297
2293 # filter the perm rows by 'default' first and then sort them by
2298 # filter the perm rows by 'default' first and then sort them by
2294 # admin,write,read,none permissions sorted again alphabetically in
2299 # admin,write,read,none permissions sorted again alphabetically in
2295 # each group
2300 # each group
2296 perm_rows = sorted(perm_rows, key=display_user_sort)
2301 perm_rows = sorted(perm_rows, key=display_user_sort)
2297
2302
2298 user_groups_rows = []
2303 user_groups_rows = []
2299 if expand_from_user_groups:
2304 if expand_from_user_groups:
2300 for ug in self.permission_user_groups(with_members=True):
2305 for ug in self.permission_user_groups(with_members=True):
2301 for user_data in ug.members:
2306 for user_data in ug.members:
2302 user_groups_rows.append(user_data)
2307 user_groups_rows.append(user_data)
2303
2308
2304 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2309 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2305
2310
2306 def permission_user_groups(self, with_members=True):
2311 def permission_user_groups(self, with_members=True):
2307 q = UserGroupRepoToPerm.query()\
2312 q = UserGroupRepoToPerm.query()\
2308 .filter(UserGroupRepoToPerm.repository == self)
2313 .filter(UserGroupRepoToPerm.repository == self)
2309 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2314 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2310 joinedload(UserGroupRepoToPerm.users_group),
2315 joinedload(UserGroupRepoToPerm.users_group),
2311 joinedload(UserGroupRepoToPerm.permission),)
2316 joinedload(UserGroupRepoToPerm.permission),)
2312
2317
2313 perm_rows = []
2318 perm_rows = []
2314 for _user_group in q.all():
2319 for _user_group in q.all():
2315 entry = AttributeDict(_user_group.users_group.get_dict())
2320 entry = AttributeDict(_user_group.users_group.get_dict())
2316 entry.permission = _user_group.permission.permission_name
2321 entry.permission = _user_group.permission.permission_name
2317 if with_members:
2322 if with_members:
2318 entry.members = [x.user.get_dict()
2323 entry.members = [x.user.get_dict()
2319 for x in _user_group.users_group.members]
2324 for x in _user_group.users_group.members]
2320 perm_rows.append(entry)
2325 perm_rows.append(entry)
2321
2326
2322 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2327 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2323 return perm_rows
2328 return perm_rows
2324
2329
2325 def get_api_data(self, include_secrets=False):
2330 def get_api_data(self, include_secrets=False):
2326 """
2331 """
2327 Common function for generating repo api data
2332 Common function for generating repo api data
2328
2333
2329 :param include_secrets: See :meth:`User.get_api_data`.
2334 :param include_secrets: See :meth:`User.get_api_data`.
2330
2335
2331 """
2336 """
2332 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2337 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2333 # move this methods on models level.
2338 # move this methods on models level.
2334 from rhodecode.model.settings import SettingsModel
2339 from rhodecode.model.settings import SettingsModel
2335 from rhodecode.model.repo import RepoModel
2340 from rhodecode.model.repo import RepoModel
2336
2341
2337 repo = self
2342 repo = self
2338 _user_id, _time, _reason = self.locked
2343 _user_id, _time, _reason = self.locked
2339
2344
2340 data = {
2345 data = {
2341 'repo_id': repo.repo_id,
2346 'repo_id': repo.repo_id,
2342 'repo_name': repo.repo_name,
2347 'repo_name': repo.repo_name,
2343 'repo_type': repo.repo_type,
2348 'repo_type': repo.repo_type,
2344 'clone_uri': repo.clone_uri or '',
2349 'clone_uri': repo.clone_uri or '',
2345 'push_uri': repo.push_uri or '',
2350 'push_uri': repo.push_uri or '',
2346 'url': RepoModel().get_url(self),
2351 'url': RepoModel().get_url(self),
2347 'private': repo.private,
2352 'private': repo.private,
2348 'created_on': repo.created_on,
2353 'created_on': repo.created_on,
2349 'description': repo.description_safe,
2354 'description': repo.description_safe,
2350 'landing_rev': repo.landing_rev,
2355 'landing_rev': repo.landing_rev,
2351 'owner': repo.user.username,
2356 'owner': repo.user.username,
2352 'fork_of': repo.fork.repo_name if repo.fork else None,
2357 'fork_of': repo.fork.repo_name if repo.fork else None,
2353 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2358 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2354 'enable_statistics': repo.enable_statistics,
2359 'enable_statistics': repo.enable_statistics,
2355 'enable_locking': repo.enable_locking,
2360 'enable_locking': repo.enable_locking,
2356 'enable_downloads': repo.enable_downloads,
2361 'enable_downloads': repo.enable_downloads,
2357 'last_changeset': repo.changeset_cache,
2362 'last_changeset': repo.changeset_cache,
2358 'locked_by': User.get(_user_id).get_api_data(
2363 'locked_by': User.get(_user_id).get_api_data(
2359 include_secrets=include_secrets) if _user_id else None,
2364 include_secrets=include_secrets) if _user_id else None,
2360 'locked_date': time_to_datetime(_time) if _time else None,
2365 'locked_date': time_to_datetime(_time) if _time else None,
2361 'lock_reason': _reason if _reason else None,
2366 'lock_reason': _reason if _reason else None,
2362 }
2367 }
2363
2368
2364 # TODO: mikhail: should be per-repo settings here
2369 # TODO: mikhail: should be per-repo settings here
2365 rc_config = SettingsModel().get_all_settings()
2370 rc_config = SettingsModel().get_all_settings()
2366 repository_fields = str2bool(
2371 repository_fields = str2bool(
2367 rc_config.get('rhodecode_repository_fields'))
2372 rc_config.get('rhodecode_repository_fields'))
2368 if repository_fields:
2373 if repository_fields:
2369 for f in self.extra_fields:
2374 for f in self.extra_fields:
2370 data[f.field_key_prefixed] = f.field_value
2375 data[f.field_key_prefixed] = f.field_value
2371
2376
2372 return data
2377 return data
2373
2378
2374 @classmethod
2379 @classmethod
2375 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2380 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2376 if not lock_time:
2381 if not lock_time:
2377 lock_time = time.time()
2382 lock_time = time.time()
2378 if not lock_reason:
2383 if not lock_reason:
2379 lock_reason = cls.LOCK_AUTOMATIC
2384 lock_reason = cls.LOCK_AUTOMATIC
2380 repo.locked = [user_id, lock_time, lock_reason]
2385 repo.locked = [user_id, lock_time, lock_reason]
2381 Session().add(repo)
2386 Session().add(repo)
2382 Session().commit()
2387 Session().commit()
2383
2388
2384 @classmethod
2389 @classmethod
2385 def unlock(cls, repo):
2390 def unlock(cls, repo):
2386 repo.locked = None
2391 repo.locked = None
2387 Session().add(repo)
2392 Session().add(repo)
2388 Session().commit()
2393 Session().commit()
2389
2394
2390 @classmethod
2395 @classmethod
2391 def getlock(cls, repo):
2396 def getlock(cls, repo):
2392 return repo.locked
2397 return repo.locked
2393
2398
2394 def get_locking_state(self, action, user_id, only_when_enabled=True):
2399 def get_locking_state(self, action, user_id, only_when_enabled=True):
2395 """
2400 """
2396 Checks locking on this repository, if locking is enabled and lock is
2401 Checks locking on this repository, if locking is enabled and lock is
2397 present returns a tuple of make_lock, locked, locked_by.
2402 present returns a tuple of make_lock, locked, locked_by.
2398 make_lock can have 3 states None (do nothing) True, make lock
2403 make_lock can have 3 states None (do nothing) True, make lock
2399 False release lock, This value is later propagated to hooks, which
2404 False release lock, This value is later propagated to hooks, which
2400 do the locking. Think about this as signals passed to hooks what to do.
2405 do the locking. Think about this as signals passed to hooks what to do.
2401
2406
2402 """
2407 """
2403 # TODO: johbo: This is part of the business logic and should be moved
2408 # TODO: johbo: This is part of the business logic and should be moved
2404 # into the RepositoryModel.
2409 # into the RepositoryModel.
2405
2410
2406 if action not in ('push', 'pull'):
2411 if action not in ('push', 'pull'):
2407 raise ValueError("Invalid action value: %s" % repr(action))
2412 raise ValueError("Invalid action value: %s" % repr(action))
2408
2413
2409 # defines if locked error should be thrown to user
2414 # defines if locked error should be thrown to user
2410 currently_locked = False
2415 currently_locked = False
2411 # defines if new lock should be made, tri-state
2416 # defines if new lock should be made, tri-state
2412 make_lock = None
2417 make_lock = None
2413 repo = self
2418 repo = self
2414 user = User.get(user_id)
2419 user = User.get(user_id)
2415
2420
2416 lock_info = repo.locked
2421 lock_info = repo.locked
2417
2422
2418 if repo and (repo.enable_locking or not only_when_enabled):
2423 if repo and (repo.enable_locking or not only_when_enabled):
2419 if action == 'push':
2424 if action == 'push':
2420 # check if it's already locked !, if it is compare users
2425 # check if it's already locked !, if it is compare users
2421 locked_by_user_id = lock_info[0]
2426 locked_by_user_id = lock_info[0]
2422 if user.user_id == locked_by_user_id:
2427 if user.user_id == locked_by_user_id:
2423 log.debug(
2428 log.debug(
2424 'Got `push` action from user %s, now unlocking', user)
2429 'Got `push` action from user %s, now unlocking', user)
2425 # unlock if we have push from user who locked
2430 # unlock if we have push from user who locked
2426 make_lock = False
2431 make_lock = False
2427 else:
2432 else:
2428 # we're not the same user who locked, ban with
2433 # we're not the same user who locked, ban with
2429 # code defined in settings (default is 423 HTTP Locked) !
2434 # code defined in settings (default is 423 HTTP Locked) !
2430 log.debug('Repo %s is currently locked by %s', repo, user)
2435 log.debug('Repo %s is currently locked by %s', repo, user)
2431 currently_locked = True
2436 currently_locked = True
2432 elif action == 'pull':
2437 elif action == 'pull':
2433 # [0] user [1] date
2438 # [0] user [1] date
2434 if lock_info[0] and lock_info[1]:
2439 if lock_info[0] and lock_info[1]:
2435 log.debug('Repo %s is currently locked by %s', repo, user)
2440 log.debug('Repo %s is currently locked by %s', repo, user)
2436 currently_locked = True
2441 currently_locked = True
2437 else:
2442 else:
2438 log.debug('Setting lock on repo %s by %s', repo, user)
2443 log.debug('Setting lock on repo %s by %s', repo, user)
2439 make_lock = True
2444 make_lock = True
2440
2445
2441 else:
2446 else:
2442 log.debug('Repository %s do not have locking enabled', repo)
2447 log.debug('Repository %s do not have locking enabled', repo)
2443
2448
2444 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2449 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2445 make_lock, currently_locked, lock_info)
2450 make_lock, currently_locked, lock_info)
2446
2451
2447 from rhodecode.lib.auth import HasRepoPermissionAny
2452 from rhodecode.lib.auth import HasRepoPermissionAny
2448 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2453 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2449 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2454 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2450 # if we don't have at least write permission we cannot make a lock
2455 # if we don't have at least write permission we cannot make a lock
2451 log.debug('lock state reset back to FALSE due to lack '
2456 log.debug('lock state reset back to FALSE due to lack '
2452 'of at least read permission')
2457 'of at least read permission')
2453 make_lock = False
2458 make_lock = False
2454
2459
2455 return make_lock, currently_locked, lock_info
2460 return make_lock, currently_locked, lock_info
2456
2461
2457 @property
2462 @property
2458 def last_commit_cache_update_diff(self):
2463 def last_commit_cache_update_diff(self):
2459 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2464 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2460
2465
2461 @classmethod
2466 @classmethod
2462 def _load_commit_change(cls, last_commit_cache):
2467 def _load_commit_change(cls, last_commit_cache):
2463 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2468 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2464 empty_date = datetime.datetime.fromtimestamp(0)
2469 empty_date = datetime.datetime.fromtimestamp(0)
2465 date_latest = last_commit_cache.get('date', empty_date)
2470 date_latest = last_commit_cache.get('date', empty_date)
2466 try:
2471 try:
2467 return parse_datetime(date_latest)
2472 return parse_datetime(date_latest)
2468 except Exception:
2473 except Exception:
2469 return empty_date
2474 return empty_date
2470
2475
2471 @property
2476 @property
2472 def last_commit_change(self):
2477 def last_commit_change(self):
2473 return self._load_commit_change(self.changeset_cache)
2478 return self._load_commit_change(self.changeset_cache)
2474
2479
2475 @property
2480 @property
2476 def last_db_change(self):
2481 def last_db_change(self):
2477 return self.updated_on
2482 return self.updated_on
2478
2483
2479 @property
2484 @property
2480 def clone_uri_hidden(self):
2485 def clone_uri_hidden(self):
2481 clone_uri = self.clone_uri
2486 clone_uri = self.clone_uri
2482 if clone_uri:
2487 if clone_uri:
2483 import urlobject
2488 import urlobject
2484 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2489 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2485 if url_obj.password:
2490 if url_obj.password:
2486 clone_uri = url_obj.with_password('*****')
2491 clone_uri = url_obj.with_password('*****')
2487 return clone_uri
2492 return clone_uri
2488
2493
2489 @property
2494 @property
2490 def push_uri_hidden(self):
2495 def push_uri_hidden(self):
2491 push_uri = self.push_uri
2496 push_uri = self.push_uri
2492 if push_uri:
2497 if push_uri:
2493 import urlobject
2498 import urlobject
2494 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2499 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2495 if url_obj.password:
2500 if url_obj.password:
2496 push_uri = url_obj.with_password('*****')
2501 push_uri = url_obj.with_password('*****')
2497 return push_uri
2502 return push_uri
2498
2503
2499 def clone_url(self, **override):
2504 def clone_url(self, **override):
2500 from rhodecode.model.settings import SettingsModel
2505 from rhodecode.model.settings import SettingsModel
2501
2506
2502 uri_tmpl = None
2507 uri_tmpl = None
2503 if 'with_id' in override:
2508 if 'with_id' in override:
2504 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2509 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2505 del override['with_id']
2510 del override['with_id']
2506
2511
2507 if 'uri_tmpl' in override:
2512 if 'uri_tmpl' in override:
2508 uri_tmpl = override['uri_tmpl']
2513 uri_tmpl = override['uri_tmpl']
2509 del override['uri_tmpl']
2514 del override['uri_tmpl']
2510
2515
2511 ssh = False
2516 ssh = False
2512 if 'ssh' in override:
2517 if 'ssh' in override:
2513 ssh = True
2518 ssh = True
2514 del override['ssh']
2519 del override['ssh']
2515
2520
2516 # we didn't override our tmpl from **overrides
2521 # we didn't override our tmpl from **overrides
2517 request = get_current_request()
2522 request = get_current_request()
2518 if not uri_tmpl:
2523 if not uri_tmpl:
2519 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2524 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2520 rc_config = request.call_context.rc_config
2525 rc_config = request.call_context.rc_config
2521 else:
2526 else:
2522 rc_config = SettingsModel().get_all_settings(cache=True)
2527 rc_config = SettingsModel().get_all_settings(cache=True)
2523
2528
2524 if ssh:
2529 if ssh:
2525 uri_tmpl = rc_config.get(
2530 uri_tmpl = rc_config.get(
2526 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2531 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2527
2532
2528 else:
2533 else:
2529 uri_tmpl = rc_config.get(
2534 uri_tmpl = rc_config.get(
2530 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2535 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2531
2536
2532 return get_clone_url(request=request,
2537 return get_clone_url(request=request,
2533 uri_tmpl=uri_tmpl,
2538 uri_tmpl=uri_tmpl,
2534 repo_name=self.repo_name,
2539 repo_name=self.repo_name,
2535 repo_id=self.repo_id,
2540 repo_id=self.repo_id,
2536 repo_type=self.repo_type,
2541 repo_type=self.repo_type,
2537 **override)
2542 **override)
2538
2543
2539 def set_state(self, state):
2544 def set_state(self, state):
2540 self.repo_state = state
2545 self.repo_state = state
2541 Session().add(self)
2546 Session().add(self)
2542 #==========================================================================
2547 #==========================================================================
2543 # SCM PROPERTIES
2548 # SCM PROPERTIES
2544 #==========================================================================
2549 #==========================================================================
2545
2550
2546 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2551 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2547 return get_commit_safe(
2552 return get_commit_safe(
2548 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2553 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2549 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2554 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2550
2555
2551 def get_changeset(self, rev=None, pre_load=None):
2556 def get_changeset(self, rev=None, pre_load=None):
2552 warnings.warn("Use get_commit", DeprecationWarning)
2557 warnings.warn("Use get_commit", DeprecationWarning)
2553 commit_id = None
2558 commit_id = None
2554 commit_idx = None
2559 commit_idx = None
2555 if isinstance(rev, str):
2560 if isinstance(rev, str):
2556 commit_id = rev
2561 commit_id = rev
2557 else:
2562 else:
2558 commit_idx = rev
2563 commit_idx = rev
2559 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2564 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2560 pre_load=pre_load)
2565 pre_load=pre_load)
2561
2566
2562 def get_landing_commit(self):
2567 def get_landing_commit(self):
2563 """
2568 """
2564 Returns landing commit, or if that doesn't exist returns the tip
2569 Returns landing commit, or if that doesn't exist returns the tip
2565 """
2570 """
2566 _rev_type, _rev = self.landing_rev
2571 _rev_type, _rev = self.landing_rev
2567 commit = self.get_commit(_rev)
2572 commit = self.get_commit(_rev)
2568 if isinstance(commit, EmptyCommit):
2573 if isinstance(commit, EmptyCommit):
2569 return self.get_commit()
2574 return self.get_commit()
2570 return commit
2575 return commit
2571
2576
2572 def flush_commit_cache(self):
2577 def flush_commit_cache(self):
2573 self.update_commit_cache(cs_cache={'raw_id':'0'})
2578 self.update_commit_cache(cs_cache={'raw_id':'0'})
2574 self.update_commit_cache()
2579 self.update_commit_cache()
2575
2580
2576 def update_commit_cache(self, cs_cache=None, config=None):
2581 def update_commit_cache(self, cs_cache=None, config=None):
2577 """
2582 """
2578 Update cache of last commit for repository
2583 Update cache of last commit for repository
2579 cache_keys should be::
2584 cache_keys should be::
2580
2585
2581 source_repo_id
2586 source_repo_id
2582 short_id
2587 short_id
2583 raw_id
2588 raw_id
2584 revision
2589 revision
2585 parents
2590 parents
2586 message
2591 message
2587 date
2592 date
2588 author
2593 author
2589 updated_on
2594 updated_on
2590
2595
2591 """
2596 """
2592 from rhodecode.lib.vcs.backends.base import BaseCommit
2597 from rhodecode.lib.vcs.backends.base import BaseCommit
2593 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2598 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2594 empty_date = datetime.datetime.fromtimestamp(0)
2599 empty_date = datetime.datetime.fromtimestamp(0)
2595 repo_commit_count = 0
2600 repo_commit_count = 0
2596
2601
2597 if cs_cache is None:
2602 if cs_cache is None:
2598 # use no-cache version here
2603 # use no-cache version here
2599 try:
2604 try:
2600 scm_repo = self.scm_instance(cache=False, config=config)
2605 scm_repo = self.scm_instance(cache=False, config=config)
2601 except VCSError:
2606 except VCSError:
2602 scm_repo = None
2607 scm_repo = None
2603 empty = scm_repo is None or scm_repo.is_empty()
2608 empty = scm_repo is None or scm_repo.is_empty()
2604
2609
2605 if not empty:
2610 if not empty:
2606 cs_cache = scm_repo.get_commit(
2611 cs_cache = scm_repo.get_commit(
2607 pre_load=["author", "date", "message", "parents", "branch"])
2612 pre_load=["author", "date", "message", "parents", "branch"])
2608 repo_commit_count = scm_repo.count()
2613 repo_commit_count = scm_repo.count()
2609 else:
2614 else:
2610 cs_cache = EmptyCommit()
2615 cs_cache = EmptyCommit()
2611
2616
2612 if isinstance(cs_cache, BaseCommit):
2617 if isinstance(cs_cache, BaseCommit):
2613 cs_cache = cs_cache.__json__()
2618 cs_cache = cs_cache.__json__()
2614
2619
2615 def is_outdated(new_cs_cache):
2620 def is_outdated(new_cs_cache):
2616 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2621 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2617 new_cs_cache['revision'] != self.changeset_cache['revision']):
2622 new_cs_cache['revision'] != self.changeset_cache['revision']):
2618 return True
2623 return True
2619 return False
2624 return False
2620
2625
2621 # check if we have maybe already latest cached revision
2626 # check if we have maybe already latest cached revision
2622 if is_outdated(cs_cache) or not self.changeset_cache:
2627 if is_outdated(cs_cache) or not self.changeset_cache:
2623 _current_datetime = datetime.datetime.utcnow()
2628 _current_datetime = datetime.datetime.utcnow()
2624 last_change = cs_cache.get('date') or _current_datetime
2629 last_change = cs_cache.get('date') or _current_datetime
2625 # we check if last update is newer than the new value
2630 # we check if last update is newer than the new value
2626 # if yes, we use the current timestamp instead. Imagine you get
2631 # if yes, we use the current timestamp instead. Imagine you get
2627 # old commit pushed 1y ago, we'd set last update 1y to ago.
2632 # old commit pushed 1y ago, we'd set last update 1y to ago.
2628 last_change_timestamp = datetime_to_time(last_change)
2633 last_change_timestamp = datetime_to_time(last_change)
2629 current_timestamp = datetime_to_time(last_change)
2634 current_timestamp = datetime_to_time(last_change)
2630 if last_change_timestamp > current_timestamp and not empty:
2635 if last_change_timestamp > current_timestamp and not empty:
2631 cs_cache['date'] = _current_datetime
2636 cs_cache['date'] = _current_datetime
2632
2637
2633 # also store size of repo
2638 # also store size of repo
2634 cs_cache['repo_commit_count'] = repo_commit_count
2639 cs_cache['repo_commit_count'] = repo_commit_count
2635
2640
2636 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2641 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2637 cs_cache['updated_on'] = time.time()
2642 cs_cache['updated_on'] = time.time()
2638 self.changeset_cache = cs_cache
2643 self.changeset_cache = cs_cache
2639 self.updated_on = last_change
2644 self.updated_on = last_change
2640 Session().add(self)
2645 Session().add(self)
2641 Session().commit()
2646 Session().commit()
2642
2647
2643 else:
2648 else:
2644 if empty:
2649 if empty:
2645 cs_cache = EmptyCommit().__json__()
2650 cs_cache = EmptyCommit().__json__()
2646 else:
2651 else:
2647 cs_cache = self.changeset_cache
2652 cs_cache = self.changeset_cache
2648
2653
2649 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2654 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2650
2655
2651 cs_cache['updated_on'] = time.time()
2656 cs_cache['updated_on'] = time.time()
2652 self.changeset_cache = cs_cache
2657 self.changeset_cache = cs_cache
2653 self.updated_on = _date_latest
2658 self.updated_on = _date_latest
2654 Session().add(self)
2659 Session().add(self)
2655 Session().commit()
2660 Session().commit()
2656
2661
2657 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2662 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2658 self.repo_name, cs_cache, _date_latest)
2663 self.repo_name, cs_cache, _date_latest)
2659
2664
2660 @property
2665 @property
2661 def tip(self):
2666 def tip(self):
2662 return self.get_commit('tip')
2667 return self.get_commit('tip')
2663
2668
2664 @property
2669 @property
2665 def author(self):
2670 def author(self):
2666 return self.tip.author
2671 return self.tip.author
2667
2672
2668 @property
2673 @property
2669 def last_change(self):
2674 def last_change(self):
2670 return self.scm_instance().last_change
2675 return self.scm_instance().last_change
2671
2676
2672 def get_comments(self, revisions=None):
2677 def get_comments(self, revisions=None):
2673 """
2678 """
2674 Returns comments for this repository grouped by revisions
2679 Returns comments for this repository grouped by revisions
2675
2680
2676 :param revisions: filter query by revisions only
2681 :param revisions: filter query by revisions only
2677 """
2682 """
2678 cmts = ChangesetComment.query()\
2683 cmts = ChangesetComment.query()\
2679 .filter(ChangesetComment.repo == self)
2684 .filter(ChangesetComment.repo == self)
2680 if revisions:
2685 if revisions:
2681 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2686 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2682 grouped = collections.defaultdict(list)
2687 grouped = collections.defaultdict(list)
2683 for cmt in cmts.all():
2688 for cmt in cmts.all():
2684 grouped[cmt.revision].append(cmt)
2689 grouped[cmt.revision].append(cmt)
2685 return grouped
2690 return grouped
2686
2691
2687 def statuses(self, revisions=None):
2692 def statuses(self, revisions=None):
2688 """
2693 """
2689 Returns statuses for this repository
2694 Returns statuses for this repository
2690
2695
2691 :param revisions: list of revisions to get statuses for
2696 :param revisions: list of revisions to get statuses for
2692 """
2697 """
2693 statuses = ChangesetStatus.query()\
2698 statuses = ChangesetStatus.query()\
2694 .filter(ChangesetStatus.repo == self)\
2699 .filter(ChangesetStatus.repo == self)\
2695 .filter(ChangesetStatus.version == 0)
2700 .filter(ChangesetStatus.version == 0)
2696
2701
2697 if revisions:
2702 if revisions:
2698 # Try doing the filtering in chunks to avoid hitting limits
2703 # Try doing the filtering in chunks to avoid hitting limits
2699 size = 500
2704 size = 500
2700 status_results = []
2705 status_results = []
2701 for chunk in range(0, len(revisions), size):
2706 for chunk in range(0, len(revisions), size):
2702 status_results += statuses.filter(
2707 status_results += statuses.filter(
2703 ChangesetStatus.revision.in_(
2708 ChangesetStatus.revision.in_(
2704 revisions[chunk: chunk+size])
2709 revisions[chunk: chunk+size])
2705 ).all()
2710 ).all()
2706 else:
2711 else:
2707 status_results = statuses.all()
2712 status_results = statuses.all()
2708
2713
2709 grouped = {}
2714 grouped = {}
2710
2715
2711 # maybe we have open new pullrequest without a status?
2716 # maybe we have open new pullrequest without a status?
2712 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2717 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2713 status_lbl = ChangesetStatus.get_status_lbl(stat)
2718 status_lbl = ChangesetStatus.get_status_lbl(stat)
2714 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2719 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2715 for rev in pr.revisions:
2720 for rev in pr.revisions:
2716 pr_id = pr.pull_request_id
2721 pr_id = pr.pull_request_id
2717 pr_repo = pr.target_repo.repo_name
2722 pr_repo = pr.target_repo.repo_name
2718 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2723 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2719
2724
2720 for stat in status_results:
2725 for stat in status_results:
2721 pr_id = pr_repo = None
2726 pr_id = pr_repo = None
2722 if stat.pull_request:
2727 if stat.pull_request:
2723 pr_id = stat.pull_request.pull_request_id
2728 pr_id = stat.pull_request.pull_request_id
2724 pr_repo = stat.pull_request.target_repo.repo_name
2729 pr_repo = stat.pull_request.target_repo.repo_name
2725 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2730 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2726 pr_id, pr_repo]
2731 pr_id, pr_repo]
2727 return grouped
2732 return grouped
2728
2733
2729 # ==========================================================================
2734 # ==========================================================================
2730 # SCM CACHE INSTANCE
2735 # SCM CACHE INSTANCE
2731 # ==========================================================================
2736 # ==========================================================================
2732
2737
2733 def scm_instance(self, **kwargs):
2738 def scm_instance(self, **kwargs):
2734 import rhodecode
2739 import rhodecode
2735
2740
2736 # Passing a config will not hit the cache currently only used
2741 # Passing a config will not hit the cache currently only used
2737 # for repo2dbmapper
2742 # for repo2dbmapper
2738 config = kwargs.pop('config', None)
2743 config = kwargs.pop('config', None)
2739 cache = kwargs.pop('cache', None)
2744 cache = kwargs.pop('cache', None)
2740 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2745 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2741 if vcs_full_cache is not None:
2746 if vcs_full_cache is not None:
2742 # allows override global config
2747 # allows override global config
2743 full_cache = vcs_full_cache
2748 full_cache = vcs_full_cache
2744 else:
2749 else:
2745 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2750 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2746 # if cache is NOT defined use default global, else we have a full
2751 # if cache is NOT defined use default global, else we have a full
2747 # control over cache behaviour
2752 # control over cache behaviour
2748 if cache is None and full_cache and not config:
2753 if cache is None and full_cache and not config:
2749 log.debug('Initializing pure cached instance for %s', self.repo_path)
2754 log.debug('Initializing pure cached instance for %s', self.repo_path)
2750 return self._get_instance_cached()
2755 return self._get_instance_cached()
2751
2756
2752 # cache here is sent to the "vcs server"
2757 # cache here is sent to the "vcs server"
2753 return self._get_instance(cache=bool(cache), config=config)
2758 return self._get_instance(cache=bool(cache), config=config)
2754
2759
2755 def _get_instance_cached(self):
2760 def _get_instance_cached(self):
2756 from rhodecode.lib import rc_cache
2761 from rhodecode.lib import rc_cache
2757
2762
2758 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2763 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2759 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2764 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2760
2765
2761 # we must use thread scoped cache here,
2766 # we must use thread scoped cache here,
2762 # because each thread of gevent needs it's own not shared connection and cache
2767 # because each thread of gevent needs it's own not shared connection and cache
2763 # we also alter `args` so the cache key is individual for every green thread.
2768 # we also alter `args` so the cache key is individual for every green thread.
2764 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2769 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2765 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2770 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2766
2771
2767 # our wrapped caching function that takes state_uid to save the previous state in
2772 # our wrapped caching function that takes state_uid to save the previous state in
2768 def cache_generator(_state_uid):
2773 def cache_generator(_state_uid):
2769
2774
2770 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2775 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2771 def get_instance_cached(_repo_id, _process_context_id):
2776 def get_instance_cached(_repo_id, _process_context_id):
2772 # we save in cached func the generation state so we can detect a change and invalidate caches
2777 # we save in cached func the generation state so we can detect a change and invalidate caches
2773 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2778 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2774
2779
2775 return get_instance_cached
2780 return get_instance_cached
2776
2781
2777 with inv_context_manager as invalidation_context:
2782 with inv_context_manager as invalidation_context:
2778 cache_state_uid = invalidation_context.state_uid
2783 cache_state_uid = invalidation_context.state_uid
2779 cache_func = cache_generator(cache_state_uid)
2784 cache_func = cache_generator(cache_state_uid)
2780
2785
2781 args = self.repo_id, inv_context_manager.proc_key
2786 args = self.repo_id, inv_context_manager.proc_key
2782
2787
2783 previous_state_uid, instance = cache_func(*args)
2788 previous_state_uid, instance = cache_func(*args)
2784
2789
2785 # now compare keys, the "cache" state vs expected state.
2790 # now compare keys, the "cache" state vs expected state.
2786 if previous_state_uid != cache_state_uid:
2791 if previous_state_uid != cache_state_uid:
2787 log.warning('Cached state uid %s is different than current state uid %s',
2792 log.warning('Cached state uid %s is different than current state uid %s',
2788 previous_state_uid, cache_state_uid)
2793 previous_state_uid, cache_state_uid)
2789 _, instance = cache_func.refresh(*args)
2794 _, instance = cache_func.refresh(*args)
2790
2795
2791 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2796 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2792 return instance
2797 return instance
2793
2798
2794 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2799 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2795 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2800 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2796 self.repo_type, self.repo_path, cache)
2801 self.repo_type, self.repo_path, cache)
2797 config = config or self._config
2802 config = config or self._config
2798 custom_wire = {
2803 custom_wire = {
2799 'cache': cache, # controls the vcs.remote cache
2804 'cache': cache, # controls the vcs.remote cache
2800 'repo_state_uid': repo_state_uid
2805 'repo_state_uid': repo_state_uid
2801 }
2806 }
2802
2807
2803 repo = get_vcs_instance(
2808 repo = get_vcs_instance(
2804 repo_path=safe_str(self.repo_full_path),
2809 repo_path=safe_str(self.repo_full_path),
2805 config=config,
2810 config=config,
2806 with_wire=custom_wire,
2811 with_wire=custom_wire,
2807 create=False,
2812 create=False,
2808 _vcs_alias=self.repo_type)
2813 _vcs_alias=self.repo_type)
2809 if repo is not None:
2814 if repo is not None:
2810 repo.count() # cache rebuild
2815 repo.count() # cache rebuild
2811
2816
2812 return repo
2817 return repo
2813
2818
2814 def get_shadow_repository_path(self, workspace_id):
2819 def get_shadow_repository_path(self, workspace_id):
2815 from rhodecode.lib.vcs.backends.base import BaseRepository
2820 from rhodecode.lib.vcs.backends.base import BaseRepository
2816 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2821 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2817 self.repo_full_path, self.repo_id, workspace_id)
2822 self.repo_full_path, self.repo_id, workspace_id)
2818 return shadow_repo_path
2823 return shadow_repo_path
2819
2824
2820 def __json__(self):
2825 def __json__(self):
2821 return {'landing_rev': self.landing_rev}
2826 return {'landing_rev': self.landing_rev}
2822
2827
2823 def get_dict(self):
2828 def get_dict(self):
2824
2829
2825 # Since we transformed `repo_name` to a hybrid property, we need to
2830 # Since we transformed `repo_name` to a hybrid property, we need to
2826 # keep compatibility with the code which uses `repo_name` field.
2831 # keep compatibility with the code which uses `repo_name` field.
2827
2832
2828 result = super(Repository, self).get_dict()
2833 result = super(Repository, self).get_dict()
2829 result['repo_name'] = result.pop('_repo_name', None)
2834 result['repo_name'] = result.pop('_repo_name', None)
2830 result.pop('_changeset_cache', '')
2835 result.pop('_changeset_cache', '')
2831 return result
2836 return result
2832
2837
2833
2838
2834 class RepoGroup(Base, BaseModel):
2839 class RepoGroup(Base, BaseModel):
2835 __tablename__ = 'groups'
2840 __tablename__ = 'groups'
2836 __table_args__ = (
2841 __table_args__ = (
2837 UniqueConstraint('group_name', 'group_parent_id'),
2842 UniqueConstraint('group_name', 'group_parent_id'),
2838 base_table_args,
2843 base_table_args,
2839 )
2844 )
2840
2845
2841 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2846 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2842
2847
2843 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2848 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2844 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2849 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2845 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2850 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2846 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2851 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2847 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2852 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2848 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2853 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2849 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2854 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2850 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2855 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2851 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2856 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2852 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2857 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2853 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2858 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2854
2859
2855 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2860 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2856 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2861 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2857 parent_group = relationship('RepoGroup', remote_side=group_id)
2862 parent_group = relationship('RepoGroup', remote_side=group_id)
2858 user = relationship('User', back_populates='repository_groups')
2863 user = relationship('User', back_populates='repository_groups')
2859 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2864 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2860
2865
2861 # no cascade, set NULL
2866 # no cascade, set NULL
2862 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2867 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2863
2868
2864 def __init__(self, group_name='', parent_group=None):
2869 def __init__(self, group_name='', parent_group=None):
2865 self.group_name = group_name
2870 self.group_name = group_name
2866 self.parent_group = parent_group
2871 self.parent_group = parent_group
2867
2872
2868 def __repr__(self):
2873 def __repr__(self):
2869 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2874 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2870
2875
2871 @hybrid_property
2876 @hybrid_property
2872 def group_name(self):
2877 def group_name(self):
2873 return self._group_name
2878 return self._group_name
2874
2879
2875 @group_name.setter
2880 @group_name.setter
2876 def group_name(self, value):
2881 def group_name(self, value):
2877 self._group_name = value
2882 self._group_name = value
2878 self.group_name_hash = self.hash_repo_group_name(value)
2883 self.group_name_hash = self.hash_repo_group_name(value)
2879
2884
2880 @classmethod
2885 @classmethod
2881 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2886 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2882 from rhodecode.lib.vcs.backends.base import EmptyCommit
2887 from rhodecode.lib.vcs.backends.base import EmptyCommit
2883 dummy = EmptyCommit().__json__()
2888 dummy = EmptyCommit().__json__()
2884 if not changeset_cache_raw:
2889 if not changeset_cache_raw:
2885 dummy['source_repo_id'] = repo_id
2890 dummy['source_repo_id'] = repo_id
2886 return json.loads(json.dumps(dummy))
2891 return json.loads(json.dumps(dummy))
2887
2892
2888 try:
2893 try:
2889 return json.loads(changeset_cache_raw)
2894 return json.loads(changeset_cache_raw)
2890 except TypeError:
2895 except TypeError:
2891 return dummy
2896 return dummy
2892 except Exception:
2897 except Exception:
2893 log.error(traceback.format_exc())
2898 log.error(traceback.format_exc())
2894 return dummy
2899 return dummy
2895
2900
2896 @hybrid_property
2901 @hybrid_property
2897 def changeset_cache(self):
2902 def changeset_cache(self):
2898 return self._load_changeset_cache('', self._changeset_cache)
2903 return self._load_changeset_cache('', self._changeset_cache)
2899
2904
2900 @changeset_cache.setter
2905 @changeset_cache.setter
2901 def changeset_cache(self, val):
2906 def changeset_cache(self, val):
2902 try:
2907 try:
2903 self._changeset_cache = json.dumps(val)
2908 self._changeset_cache = json.dumps(val)
2904 except Exception:
2909 except Exception:
2905 log.error(traceback.format_exc())
2910 log.error(traceback.format_exc())
2906
2911
2907 @validates('group_parent_id')
2912 @validates('group_parent_id')
2908 def validate_group_parent_id(self, key, val):
2913 def validate_group_parent_id(self, key, val):
2909 """
2914 """
2910 Check cycle references for a parent group to self
2915 Check cycle references for a parent group to self
2911 """
2916 """
2912 if self.group_id and val:
2917 if self.group_id and val:
2913 assert val != self.group_id
2918 assert val != self.group_id
2914
2919
2915 return val
2920 return val
2916
2921
2917 @hybrid_property
2922 @hybrid_property
2918 def description_safe(self):
2923 def description_safe(self):
2919 from rhodecode.lib import helpers as h
2924 from rhodecode.lib import helpers as h
2920 return h.escape(self.group_description)
2925 return h.escape(self.group_description)
2921
2926
2922 @classmethod
2927 @classmethod
2923 def hash_repo_group_name(cls, repo_group_name):
2928 def hash_repo_group_name(cls, repo_group_name):
2924 val = remove_formatting(repo_group_name)
2929 val = remove_formatting(repo_group_name)
2925 val = safe_str(val).lower()
2930 val = safe_str(val).lower()
2926 chars = []
2931 chars = []
2927 for c in val:
2932 for c in val:
2928 if c not in string.ascii_letters:
2933 if c not in string.ascii_letters:
2929 c = str(ord(c))
2934 c = str(ord(c))
2930 chars.append(c)
2935 chars.append(c)
2931
2936
2932 return ''.join(chars)
2937 return ''.join(chars)
2933
2938
2934 @classmethod
2939 @classmethod
2935 def _generate_choice(cls, repo_group):
2940 def _generate_choice(cls, repo_group):
2936 from webhelpers2.html import literal as _literal
2941 from webhelpers2.html import literal as _literal
2937
2942
2938 def _name(k):
2943 def _name(k):
2939 return _literal(cls.CHOICES_SEPARATOR.join(k))
2944 return _literal(cls.CHOICES_SEPARATOR.join(k))
2940
2945
2941 return repo_group.group_id, _name(repo_group.full_path_splitted)
2946 return repo_group.group_id, _name(repo_group.full_path_splitted)
2942
2947
2943 @classmethod
2948 @classmethod
2944 def groups_choices(cls, groups=None, show_empty_group=True):
2949 def groups_choices(cls, groups=None, show_empty_group=True):
2945 if not groups:
2950 if not groups:
2946 groups = cls.query().all()
2951 groups = cls.query().all()
2947
2952
2948 repo_groups = []
2953 repo_groups = []
2949 if show_empty_group:
2954 if show_empty_group:
2950 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2955 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2951
2956
2952 repo_groups.extend([cls._generate_choice(x) for x in groups])
2957 repo_groups.extend([cls._generate_choice(x) for x in groups])
2953
2958
2954 repo_groups = sorted(
2959 repo_groups = sorted(
2955 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2960 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2956 return repo_groups
2961 return repo_groups
2957
2962
2958 @classmethod
2963 @classmethod
2959 def url_sep(cls):
2964 def url_sep(cls):
2960 return URL_SEP
2965 return URL_SEP
2961
2966
2962 @classmethod
2967 @classmethod
2963 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2968 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2964 if case_insensitive:
2969 if case_insensitive:
2965 gr = cls.query().filter(func.lower(cls.group_name)
2970 gr = cls.query().filter(func.lower(cls.group_name)
2966 == func.lower(group_name))
2971 == func.lower(group_name))
2967 else:
2972 else:
2968 gr = cls.query().filter(cls.group_name == group_name)
2973 gr = cls.query().filter(cls.group_name == group_name)
2969 if cache:
2974 if cache:
2970 name_key = _hash_key(group_name)
2975 name_key = _hash_key(group_name)
2971 gr = gr.options(
2976 gr = gr.options(
2972 FromCache("sql_cache_short", f"get_group_{name_key}"))
2977 FromCache("sql_cache_short", f"get_group_{name_key}"))
2973 return gr.scalar()
2978 return gr.scalar()
2974
2979
2975 @classmethod
2980 @classmethod
2976 def get_user_personal_repo_group(cls, user_id):
2981 def get_user_personal_repo_group(cls, user_id):
2977 user = User.get(user_id)
2982 user = User.get(user_id)
2978 if user.username == User.DEFAULT_USER:
2983 if user.username == User.DEFAULT_USER:
2979 return None
2984 return None
2980
2985
2981 return cls.query()\
2986 return cls.query()\
2982 .filter(cls.personal == true()) \
2987 .filter(cls.personal == true()) \
2983 .filter(cls.user == user) \
2988 .filter(cls.user == user) \
2984 .order_by(cls.group_id.asc()) \
2989 .order_by(cls.group_id.asc()) \
2985 .first()
2990 .first()
2986
2991
2987 @classmethod
2992 @classmethod
2988 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2993 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2989 case_insensitive=True):
2994 case_insensitive=True):
2990 q = RepoGroup.query()
2995 q = RepoGroup.query()
2991
2996
2992 if not isinstance(user_id, Optional):
2997 if not isinstance(user_id, Optional):
2993 q = q.filter(RepoGroup.user_id == user_id)
2998 q = q.filter(RepoGroup.user_id == user_id)
2994
2999
2995 if not isinstance(group_id, Optional):
3000 if not isinstance(group_id, Optional):
2996 q = q.filter(RepoGroup.group_parent_id == group_id)
3001 q = q.filter(RepoGroup.group_parent_id == group_id)
2997
3002
2998 if case_insensitive:
3003 if case_insensitive:
2999 q = q.order_by(func.lower(RepoGroup.group_name))
3004 q = q.order_by(func.lower(RepoGroup.group_name))
3000 else:
3005 else:
3001 q = q.order_by(RepoGroup.group_name)
3006 q = q.order_by(RepoGroup.group_name)
3002 return q.all()
3007 return q.all()
3003
3008
3004 @property
3009 @property
3005 def parents(self, parents_recursion_limit=10):
3010 def parents(self, parents_recursion_limit=10):
3006 groups = []
3011 groups = []
3007 if self.parent_group is None:
3012 if self.parent_group is None:
3008 return groups
3013 return groups
3009 cur_gr = self.parent_group
3014 cur_gr = self.parent_group
3010 groups.insert(0, cur_gr)
3015 groups.insert(0, cur_gr)
3011 cnt = 0
3016 cnt = 0
3012 while 1:
3017 while 1:
3013 cnt += 1
3018 cnt += 1
3014 gr = getattr(cur_gr, 'parent_group', None)
3019 gr = getattr(cur_gr, 'parent_group', None)
3015 cur_gr = cur_gr.parent_group
3020 cur_gr = cur_gr.parent_group
3016 if gr is None:
3021 if gr is None:
3017 break
3022 break
3018 if cnt == parents_recursion_limit:
3023 if cnt == parents_recursion_limit:
3019 # this will prevent accidental infinit loops
3024 # this will prevent accidental infinit loops
3020 log.error('more than %s parents found for group %s, stopping '
3025 log.error('more than %s parents found for group %s, stopping '
3021 'recursive parent fetching', parents_recursion_limit, self)
3026 'recursive parent fetching', parents_recursion_limit, self)
3022 break
3027 break
3023
3028
3024 groups.insert(0, gr)
3029 groups.insert(0, gr)
3025 return groups
3030 return groups
3026
3031
3027 @property
3032 @property
3028 def last_commit_cache_update_diff(self):
3033 def last_commit_cache_update_diff(self):
3029 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
3034 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
3030
3035
3031 @classmethod
3036 @classmethod
3032 def _load_commit_change(cls, last_commit_cache):
3037 def _load_commit_change(cls, last_commit_cache):
3033 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3038 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3034 empty_date = datetime.datetime.fromtimestamp(0)
3039 empty_date = datetime.datetime.fromtimestamp(0)
3035 date_latest = last_commit_cache.get('date', empty_date)
3040 date_latest = last_commit_cache.get('date', empty_date)
3036 try:
3041 try:
3037 return parse_datetime(date_latest)
3042 return parse_datetime(date_latest)
3038 except Exception:
3043 except Exception:
3039 return empty_date
3044 return empty_date
3040
3045
3041 @property
3046 @property
3042 def last_commit_change(self):
3047 def last_commit_change(self):
3043 return self._load_commit_change(self.changeset_cache)
3048 return self._load_commit_change(self.changeset_cache)
3044
3049
3045 @property
3050 @property
3046 def last_db_change(self):
3051 def last_db_change(self):
3047 return self.updated_on
3052 return self.updated_on
3048
3053
3049 @property
3054 @property
3050 def children(self):
3055 def children(self):
3051 return RepoGroup.query().filter(RepoGroup.parent_group == self)
3056 return RepoGroup.query().filter(RepoGroup.parent_group == self)
3052
3057
3053 @property
3058 @property
3054 def name(self):
3059 def name(self):
3055 return self.group_name.split(RepoGroup.url_sep())[-1]
3060 return self.group_name.split(RepoGroup.url_sep())[-1]
3056
3061
3057 @property
3062 @property
3058 def full_path(self):
3063 def full_path(self):
3059 return self.group_name
3064 return self.group_name
3060
3065
3061 @property
3066 @property
3062 def full_path_splitted(self):
3067 def full_path_splitted(self):
3063 return self.group_name.split(RepoGroup.url_sep())
3068 return self.group_name.split(RepoGroup.url_sep())
3064
3069
3065 @property
3070 @property
3066 def repositories(self):
3071 def repositories(self):
3067 return Repository.query()\
3072 return Repository.query()\
3068 .filter(Repository.group == self)\
3073 .filter(Repository.group == self)\
3069 .order_by(Repository.repo_name)
3074 .order_by(Repository.repo_name)
3070
3075
3071 @property
3076 @property
3072 def repositories_recursive_count(self):
3077 def repositories_recursive_count(self):
3073 cnt = self.repositories.count()
3078 cnt = self.repositories.count()
3074
3079
3075 def children_count(group):
3080 def children_count(group):
3076 cnt = 0
3081 cnt = 0
3077 for child in group.children:
3082 for child in group.children:
3078 cnt += child.repositories.count()
3083 cnt += child.repositories.count()
3079 cnt += children_count(child)
3084 cnt += children_count(child)
3080 return cnt
3085 return cnt
3081
3086
3082 return cnt + children_count(self)
3087 return cnt + children_count(self)
3083
3088
3084 def _recursive_objects(self, include_repos=True, include_groups=True):
3089 def _recursive_objects(self, include_repos=True, include_groups=True):
3085 all_ = []
3090 all_ = []
3086
3091
3087 def _get_members(root_gr):
3092 def _get_members(root_gr):
3088 if include_repos:
3093 if include_repos:
3089 for r in root_gr.repositories:
3094 for r in root_gr.repositories:
3090 all_.append(r)
3095 all_.append(r)
3091 childs = root_gr.children.all()
3096 childs = root_gr.children.all()
3092 if childs:
3097 if childs:
3093 for gr in childs:
3098 for gr in childs:
3094 if include_groups:
3099 if include_groups:
3095 all_.append(gr)
3100 all_.append(gr)
3096 _get_members(gr)
3101 _get_members(gr)
3097
3102
3098 root_group = []
3103 root_group = []
3099 if include_groups:
3104 if include_groups:
3100 root_group = [self]
3105 root_group = [self]
3101
3106
3102 _get_members(self)
3107 _get_members(self)
3103 return root_group + all_
3108 return root_group + all_
3104
3109
3105 def recursive_groups_and_repos(self):
3110 def recursive_groups_and_repos(self):
3106 """
3111 """
3107 Recursive return all groups, with repositories in those groups
3112 Recursive return all groups, with repositories in those groups
3108 """
3113 """
3109 return self._recursive_objects()
3114 return self._recursive_objects()
3110
3115
3111 def recursive_groups(self):
3116 def recursive_groups(self):
3112 """
3117 """
3113 Returns all children groups for this group including children of children
3118 Returns all children groups for this group including children of children
3114 """
3119 """
3115 return self._recursive_objects(include_repos=False)
3120 return self._recursive_objects(include_repos=False)
3116
3121
3117 def recursive_repos(self):
3122 def recursive_repos(self):
3118 """
3123 """
3119 Returns all children repositories for this group
3124 Returns all children repositories for this group
3120 """
3125 """
3121 return self._recursive_objects(include_groups=False)
3126 return self._recursive_objects(include_groups=False)
3122
3127
3123 def get_new_name(self, group_name):
3128 def get_new_name(self, group_name):
3124 """
3129 """
3125 returns new full group name based on parent and new name
3130 returns new full group name based on parent and new name
3126
3131
3127 :param group_name:
3132 :param group_name:
3128 """
3133 """
3129 path_prefix = (self.parent_group.full_path_splitted if
3134 path_prefix = (self.parent_group.full_path_splitted if
3130 self.parent_group else [])
3135 self.parent_group else [])
3131 return RepoGroup.url_sep().join(path_prefix + [group_name])
3136 return RepoGroup.url_sep().join(path_prefix + [group_name])
3132
3137
3133 def update_commit_cache(self, config=None):
3138 def update_commit_cache(self, config=None):
3134 """
3139 """
3135 Update cache of last commit for newest repository inside this repository group.
3140 Update cache of last commit for newest repository inside this repository group.
3136 cache_keys should be::
3141 cache_keys should be::
3137
3142
3138 source_repo_id
3143 source_repo_id
3139 short_id
3144 short_id
3140 raw_id
3145 raw_id
3141 revision
3146 revision
3142 parents
3147 parents
3143 message
3148 message
3144 date
3149 date
3145 author
3150 author
3146
3151
3147 """
3152 """
3148 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3153 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3149 empty_date = datetime.datetime.fromtimestamp(0)
3154 empty_date = datetime.datetime.fromtimestamp(0)
3150
3155
3151 def repo_groups_and_repos(root_gr):
3156 def repo_groups_and_repos(root_gr):
3152 for _repo in root_gr.repositories:
3157 for _repo in root_gr.repositories:
3153 yield _repo
3158 yield _repo
3154 for child_group in root_gr.children.all():
3159 for child_group in root_gr.children.all():
3155 yield child_group
3160 yield child_group
3156
3161
3157 latest_repo_cs_cache = {}
3162 latest_repo_cs_cache = {}
3158 for obj in repo_groups_and_repos(self):
3163 for obj in repo_groups_and_repos(self):
3159 repo_cs_cache = obj.changeset_cache
3164 repo_cs_cache = obj.changeset_cache
3160 date_latest = latest_repo_cs_cache.get('date', empty_date)
3165 date_latest = latest_repo_cs_cache.get('date', empty_date)
3161 date_current = repo_cs_cache.get('date', empty_date)
3166 date_current = repo_cs_cache.get('date', empty_date)
3162 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3167 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3163 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3168 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3164 latest_repo_cs_cache = repo_cs_cache
3169 latest_repo_cs_cache = repo_cs_cache
3165 if hasattr(obj, 'repo_id'):
3170 if hasattr(obj, 'repo_id'):
3166 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3171 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3167 else:
3172 else:
3168 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3173 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3169
3174
3170 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3175 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3171
3176
3172 latest_repo_cs_cache['updated_on'] = time.time()
3177 latest_repo_cs_cache['updated_on'] = time.time()
3173 self.changeset_cache = latest_repo_cs_cache
3178 self.changeset_cache = latest_repo_cs_cache
3174 self.updated_on = _date_latest
3179 self.updated_on = _date_latest
3175 Session().add(self)
3180 Session().add(self)
3176 Session().commit()
3181 Session().commit()
3177
3182
3178 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3183 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3179 self.group_name, latest_repo_cs_cache, _date_latest)
3184 self.group_name, latest_repo_cs_cache, _date_latest)
3180
3185
3181 def permissions(self, with_admins=True, with_owner=True,
3186 def permissions(self, with_admins=True, with_owner=True,
3182 expand_from_user_groups=False):
3187 expand_from_user_groups=False):
3183 """
3188 """
3184 Permissions for repository groups
3189 Permissions for repository groups
3185 """
3190 """
3186 _admin_perm = 'group.admin'
3191 _admin_perm = 'group.admin'
3187
3192
3188 owner_row = []
3193 owner_row = []
3189 if with_owner:
3194 if with_owner:
3190 usr = AttributeDict(self.user.get_dict())
3195 usr = AttributeDict(self.user.get_dict())
3191 usr.owner_row = True
3196 usr.owner_row = True
3192 usr.permission = _admin_perm
3197 usr.permission = _admin_perm
3193 owner_row.append(usr)
3198 owner_row.append(usr)
3194
3199
3195 super_admin_ids = []
3200 super_admin_ids = []
3196 super_admin_rows = []
3201 super_admin_rows = []
3197 if with_admins:
3202 if with_admins:
3198 for usr in User.get_all_super_admins():
3203 for usr in User.get_all_super_admins():
3199 super_admin_ids.append(usr.user_id)
3204 super_admin_ids.append(usr.user_id)
3200 # if this admin is also owner, don't double the record
3205 # if this admin is also owner, don't double the record
3201 if usr.user_id == owner_row[0].user_id:
3206 if usr.user_id == owner_row[0].user_id:
3202 owner_row[0].admin_row = True
3207 owner_row[0].admin_row = True
3203 else:
3208 else:
3204 usr = AttributeDict(usr.get_dict())
3209 usr = AttributeDict(usr.get_dict())
3205 usr.admin_row = True
3210 usr.admin_row = True
3206 usr.permission = _admin_perm
3211 usr.permission = _admin_perm
3207 super_admin_rows.append(usr)
3212 super_admin_rows.append(usr)
3208
3213
3209 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3210 q = q.options(joinedload(UserRepoGroupToPerm.group),
3215 q = q.options(joinedload(UserRepoGroupToPerm.group),
3211 joinedload(UserRepoGroupToPerm.user),
3216 joinedload(UserRepoGroupToPerm.user),
3212 joinedload(UserRepoGroupToPerm.permission),)
3217 joinedload(UserRepoGroupToPerm.permission),)
3213
3218
3214 # get owners and admins and permissions. We do a trick of re-writing
3219 # get owners and admins and permissions. We do a trick of re-writing
3215 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3216 # has a global reference and changing one object propagates to all
3221 # has a global reference and changing one object propagates to all
3217 # others. This means if admin is also an owner admin_row that change
3222 # others. This means if admin is also an owner admin_row that change
3218 # would propagate to both objects
3223 # would propagate to both objects
3219 perm_rows = []
3224 perm_rows = []
3220 for _usr in q.all():
3225 for _usr in q.all():
3221 usr = AttributeDict(_usr.user.get_dict())
3226 usr = AttributeDict(_usr.user.get_dict())
3222 # if this user is also owner/admin, mark as duplicate record
3227 # if this user is also owner/admin, mark as duplicate record
3223 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3228 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3224 usr.duplicate_perm = True
3229 usr.duplicate_perm = True
3225 usr.permission = _usr.permission.permission_name
3230 usr.permission = _usr.permission.permission_name
3226 perm_rows.append(usr)
3231 perm_rows.append(usr)
3227
3232
3228 # filter the perm rows by 'default' first and then sort them by
3233 # filter the perm rows by 'default' first and then sort them by
3229 # admin,write,read,none permissions sorted again alphabetically in
3234 # admin,write,read,none permissions sorted again alphabetically in
3230 # each group
3235 # each group
3231 perm_rows = sorted(perm_rows, key=display_user_sort)
3236 perm_rows = sorted(perm_rows, key=display_user_sort)
3232
3237
3233 user_groups_rows = []
3238 user_groups_rows = []
3234 if expand_from_user_groups:
3239 if expand_from_user_groups:
3235 for ug in self.permission_user_groups(with_members=True):
3240 for ug in self.permission_user_groups(with_members=True):
3236 for user_data in ug.members:
3241 for user_data in ug.members:
3237 user_groups_rows.append(user_data)
3242 user_groups_rows.append(user_data)
3238
3243
3239 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3244 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3240
3245
3241 def permission_user_groups(self, with_members=False):
3246 def permission_user_groups(self, with_members=False):
3242 q = UserGroupRepoGroupToPerm.query()\
3247 q = UserGroupRepoGroupToPerm.query()\
3243 .filter(UserGroupRepoGroupToPerm.group == self)
3248 .filter(UserGroupRepoGroupToPerm.group == self)
3244 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3249 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3245 joinedload(UserGroupRepoGroupToPerm.users_group),
3250 joinedload(UserGroupRepoGroupToPerm.users_group),
3246 joinedload(UserGroupRepoGroupToPerm.permission),)
3251 joinedload(UserGroupRepoGroupToPerm.permission),)
3247
3252
3248 perm_rows = []
3253 perm_rows = []
3249 for _user_group in q.all():
3254 for _user_group in q.all():
3250 entry = AttributeDict(_user_group.users_group.get_dict())
3255 entry = AttributeDict(_user_group.users_group.get_dict())
3251 entry.permission = _user_group.permission.permission_name
3256 entry.permission = _user_group.permission.permission_name
3252 if with_members:
3257 if with_members:
3253 entry.members = [x.user.get_dict()
3258 entry.members = [x.user.get_dict()
3254 for x in _user_group.users_group.members]
3259 for x in _user_group.users_group.members]
3255 perm_rows.append(entry)
3260 perm_rows.append(entry)
3256
3261
3257 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3262 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3258 return perm_rows
3263 return perm_rows
3259
3264
3260 def get_api_data(self):
3265 def get_api_data(self):
3261 """
3266 """
3262 Common function for generating api data
3267 Common function for generating api data
3263
3268
3264 """
3269 """
3265 group = self
3270 group = self
3266 data = {
3271 data = {
3267 'group_id': group.group_id,
3272 'group_id': group.group_id,
3268 'group_name': group.group_name,
3273 'group_name': group.group_name,
3269 'group_description': group.description_safe,
3274 'group_description': group.description_safe,
3270 'parent_group': group.parent_group.group_name if group.parent_group else None,
3275 'parent_group': group.parent_group.group_name if group.parent_group else None,
3271 'repositories': [x.repo_name for x in group.repositories],
3276 'repositories': [x.repo_name for x in group.repositories],
3272 'owner': group.user.username,
3277 'owner': group.user.username,
3273 }
3278 }
3274 return data
3279 return data
3275
3280
3276 def get_dict(self):
3281 def get_dict(self):
3277 # Since we transformed `group_name` to a hybrid property, we need to
3282 # Since we transformed `group_name` to a hybrid property, we need to
3278 # keep compatibility with the code which uses `group_name` field.
3283 # keep compatibility with the code which uses `group_name` field.
3279 result = super(RepoGroup, self).get_dict()
3284 result = super(RepoGroup, self).get_dict()
3280 result['group_name'] = result.pop('_group_name', None)
3285 result['group_name'] = result.pop('_group_name', None)
3281 result.pop('_changeset_cache', '')
3286 result.pop('_changeset_cache', '')
3282 return result
3287 return result
3283
3288
3284
3289
3285 class Permission(Base, BaseModel):
3290 class Permission(Base, BaseModel):
3286 __tablename__ = 'permissions'
3291 __tablename__ = 'permissions'
3287 __table_args__ = (
3292 __table_args__ = (
3288 Index('p_perm_name_idx', 'permission_name'),
3293 Index('p_perm_name_idx', 'permission_name'),
3289 base_table_args,
3294 base_table_args,
3290 )
3295 )
3291
3296
3292 PERMS = [
3297 PERMS = [
3293 ('hg.admin', _('RhodeCode Super Administrator')),
3298 ('hg.admin', _('RhodeCode Super Administrator')),
3294
3299
3295 ('repository.none', _('Repository no access')),
3300 ('repository.none', _('Repository no access')),
3296 ('repository.read', _('Repository read access')),
3301 ('repository.read', _('Repository read access')),
3297 ('repository.write', _('Repository write access')),
3302 ('repository.write', _('Repository write access')),
3298 ('repository.admin', _('Repository admin access')),
3303 ('repository.admin', _('Repository admin access')),
3299
3304
3300 ('group.none', _('Repository group no access')),
3305 ('group.none', _('Repository group no access')),
3301 ('group.read', _('Repository group read access')),
3306 ('group.read', _('Repository group read access')),
3302 ('group.write', _('Repository group write access')),
3307 ('group.write', _('Repository group write access')),
3303 ('group.admin', _('Repository group admin access')),
3308 ('group.admin', _('Repository group admin access')),
3304
3309
3305 ('usergroup.none', _('User group no access')),
3310 ('usergroup.none', _('User group no access')),
3306 ('usergroup.read', _('User group read access')),
3311 ('usergroup.read', _('User group read access')),
3307 ('usergroup.write', _('User group write access')),
3312 ('usergroup.write', _('User group write access')),
3308 ('usergroup.admin', _('User group admin access')),
3313 ('usergroup.admin', _('User group admin access')),
3309
3314
3310 ('branch.none', _('Branch no permissions')),
3315 ('branch.none', _('Branch no permissions')),
3311 ('branch.merge', _('Branch access by web merge')),
3316 ('branch.merge', _('Branch access by web merge')),
3312 ('branch.push', _('Branch access by push')),
3317 ('branch.push', _('Branch access by push')),
3313 ('branch.push_force', _('Branch access by push with force')),
3318 ('branch.push_force', _('Branch access by push with force')),
3314
3319
3315 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3320 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3316 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3321 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3317
3322
3318 ('hg.usergroup.create.false', _('User Group creation disabled')),
3323 ('hg.usergroup.create.false', _('User Group creation disabled')),
3319 ('hg.usergroup.create.true', _('User Group creation enabled')),
3324 ('hg.usergroup.create.true', _('User Group creation enabled')),
3320
3325
3321 ('hg.create.none', _('Repository creation disabled')),
3326 ('hg.create.none', _('Repository creation disabled')),
3322 ('hg.create.repository', _('Repository creation enabled')),
3327 ('hg.create.repository', _('Repository creation enabled')),
3323 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3328 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3324 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3329 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3325
3330
3326 ('hg.fork.none', _('Repository forking disabled')),
3331 ('hg.fork.none', _('Repository forking disabled')),
3327 ('hg.fork.repository', _('Repository forking enabled')),
3332 ('hg.fork.repository', _('Repository forking enabled')),
3328
3333
3329 ('hg.register.none', _('Registration disabled')),
3334 ('hg.register.none', _('Registration disabled')),
3330 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3335 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3331 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3336 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3332
3337
3333 ('hg.password_reset.enabled', _('Password reset enabled')),
3338 ('hg.password_reset.enabled', _('Password reset enabled')),
3334 ('hg.password_reset.hidden', _('Password reset hidden')),
3339 ('hg.password_reset.hidden', _('Password reset hidden')),
3335 ('hg.password_reset.disabled', _('Password reset disabled')),
3340 ('hg.password_reset.disabled', _('Password reset disabled')),
3336
3341
3337 ('hg.extern_activate.manual', _('Manual activation of external account')),
3342 ('hg.extern_activate.manual', _('Manual activation of external account')),
3338 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3343 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3339
3344
3340 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3345 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3341 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3346 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3342 ]
3347 ]
3343
3348
3344 # definition of system default permissions for DEFAULT user, created on
3349 # definition of system default permissions for DEFAULT user, created on
3345 # system setup
3350 # system setup
3346 DEFAULT_USER_PERMISSIONS = [
3351 DEFAULT_USER_PERMISSIONS = [
3347 # object perms
3352 # object perms
3348 'repository.read',
3353 'repository.read',
3349 'group.read',
3354 'group.read',
3350 'usergroup.read',
3355 'usergroup.read',
3351 # branch, for backward compat we need same value as before so forced pushed
3356 # branch, for backward compat we need same value as before so forced pushed
3352 'branch.push_force',
3357 'branch.push_force',
3353 # global
3358 # global
3354 'hg.create.repository',
3359 'hg.create.repository',
3355 'hg.repogroup.create.false',
3360 'hg.repogroup.create.false',
3356 'hg.usergroup.create.false',
3361 'hg.usergroup.create.false',
3357 'hg.create.write_on_repogroup.true',
3362 'hg.create.write_on_repogroup.true',
3358 'hg.fork.repository',
3363 'hg.fork.repository',
3359 'hg.register.manual_activate',
3364 'hg.register.manual_activate',
3360 'hg.password_reset.enabled',
3365 'hg.password_reset.enabled',
3361 'hg.extern_activate.auto',
3366 'hg.extern_activate.auto',
3362 'hg.inherit_default_perms.true',
3367 'hg.inherit_default_perms.true',
3363 ]
3368 ]
3364
3369
3365 # defines which permissions are more important higher the more important
3370 # defines which permissions are more important higher the more important
3366 # Weight defines which permissions are more important.
3371 # Weight defines which permissions are more important.
3367 # The higher number the more important.
3372 # The higher number the more important.
3368 PERM_WEIGHTS = {
3373 PERM_WEIGHTS = {
3369 'repository.none': 0,
3374 'repository.none': 0,
3370 'repository.read': 1,
3375 'repository.read': 1,
3371 'repository.write': 3,
3376 'repository.write': 3,
3372 'repository.admin': 4,
3377 'repository.admin': 4,
3373
3378
3374 'group.none': 0,
3379 'group.none': 0,
3375 'group.read': 1,
3380 'group.read': 1,
3376 'group.write': 3,
3381 'group.write': 3,
3377 'group.admin': 4,
3382 'group.admin': 4,
3378
3383
3379 'usergroup.none': 0,
3384 'usergroup.none': 0,
3380 'usergroup.read': 1,
3385 'usergroup.read': 1,
3381 'usergroup.write': 3,
3386 'usergroup.write': 3,
3382 'usergroup.admin': 4,
3387 'usergroup.admin': 4,
3383
3388
3384 'branch.none': 0,
3389 'branch.none': 0,
3385 'branch.merge': 1,
3390 'branch.merge': 1,
3386 'branch.push': 3,
3391 'branch.push': 3,
3387 'branch.push_force': 4,
3392 'branch.push_force': 4,
3388
3393
3389 'hg.repogroup.create.false': 0,
3394 'hg.repogroup.create.false': 0,
3390 'hg.repogroup.create.true': 1,
3395 'hg.repogroup.create.true': 1,
3391
3396
3392 'hg.usergroup.create.false': 0,
3397 'hg.usergroup.create.false': 0,
3393 'hg.usergroup.create.true': 1,
3398 'hg.usergroup.create.true': 1,
3394
3399
3395 'hg.fork.none': 0,
3400 'hg.fork.none': 0,
3396 'hg.fork.repository': 1,
3401 'hg.fork.repository': 1,
3397 'hg.create.none': 0,
3402 'hg.create.none': 0,
3398 'hg.create.repository': 1
3403 'hg.create.repository': 1
3399 }
3404 }
3400
3405
3401 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3406 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3402 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3407 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3403 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3408 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3404
3409
3405 def __repr__(self):
3410 def __repr__(self):
3406 return "<%s('%s:%s')>" % (
3411 return "<%s('%s:%s')>" % (
3407 self.cls_name, self.permission_id, self.permission_name
3412 self.cls_name, self.permission_id, self.permission_name
3408 )
3413 )
3409
3414
3410 @classmethod
3415 @classmethod
3411 def get_by_key(cls, key):
3416 def get_by_key(cls, key):
3412 return cls.query().filter(cls.permission_name == key).scalar()
3417 return cls.query().filter(cls.permission_name == key).scalar()
3413
3418
3414 @classmethod
3419 @classmethod
3415 def get_default_repo_perms(cls, user_id, repo_id=None):
3420 def get_default_repo_perms(cls, user_id, repo_id=None):
3416 q = Session().query(UserRepoToPerm, Repository, Permission)\
3421 q = Session().query(UserRepoToPerm, Repository, Permission)\
3417 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3422 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3418 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3423 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3419 .filter(UserRepoToPerm.user_id == user_id)
3424 .filter(UserRepoToPerm.user_id == user_id)
3420 if repo_id:
3425 if repo_id:
3421 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3426 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3422 return q.all()
3427 return q.all()
3423
3428
3424 @classmethod
3429 @classmethod
3425 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3430 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3426 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3431 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3427 .join(
3432 .join(
3428 Permission,
3433 Permission,
3429 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3434 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3430 .join(
3435 .join(
3431 UserRepoToPerm,
3436 UserRepoToPerm,
3432 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3437 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3433 .filter(UserRepoToPerm.user_id == user_id)
3438 .filter(UserRepoToPerm.user_id == user_id)
3434
3439
3435 if repo_id:
3440 if repo_id:
3436 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3441 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3437 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3442 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3438
3443
3439 @classmethod
3444 @classmethod
3440 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3445 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3441 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3446 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3442 .join(
3447 .join(
3443 Permission,
3448 Permission,
3444 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3449 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3445 .join(
3450 .join(
3446 Repository,
3451 Repository,
3447 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3452 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3448 .join(
3453 .join(
3449 UserGroup,
3454 UserGroup,
3450 UserGroupRepoToPerm.users_group_id ==
3455 UserGroupRepoToPerm.users_group_id ==
3451 UserGroup.users_group_id)\
3456 UserGroup.users_group_id)\
3452 .join(
3457 .join(
3453 UserGroupMember,
3458 UserGroupMember,
3454 UserGroupRepoToPerm.users_group_id ==
3459 UserGroupRepoToPerm.users_group_id ==
3455 UserGroupMember.users_group_id)\
3460 UserGroupMember.users_group_id)\
3456 .filter(
3461 .filter(
3457 UserGroupMember.user_id == user_id,
3462 UserGroupMember.user_id == user_id,
3458 UserGroup.users_group_active == true())
3463 UserGroup.users_group_active == true())
3459 if repo_id:
3464 if repo_id:
3460 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3465 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3461 return q.all()
3466 return q.all()
3462
3467
3463 @classmethod
3468 @classmethod
3464 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3469 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3465 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3470 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3466 .join(
3471 .join(
3467 Permission,
3472 Permission,
3468 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3473 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3469 .join(
3474 .join(
3470 UserGroupRepoToPerm,
3475 UserGroupRepoToPerm,
3471 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3476 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3472 .join(
3477 .join(
3473 UserGroup,
3478 UserGroup,
3474 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3479 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3475 .join(
3480 .join(
3476 UserGroupMember,
3481 UserGroupMember,
3477 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3482 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3478 .filter(
3483 .filter(
3479 UserGroupMember.user_id == user_id,
3484 UserGroupMember.user_id == user_id,
3480 UserGroup.users_group_active == true())
3485 UserGroup.users_group_active == true())
3481
3486
3482 if repo_id:
3487 if repo_id:
3483 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3488 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3484 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3489 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3485
3490
3486 @classmethod
3491 @classmethod
3487 def get_default_group_perms(cls, user_id, repo_group_id=None):
3492 def get_default_group_perms(cls, user_id, repo_group_id=None):
3488 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3493 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3489 .join(
3494 .join(
3490 Permission,
3495 Permission,
3491 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3496 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3492 .join(
3497 .join(
3493 RepoGroup,
3498 RepoGroup,
3494 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3499 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3495 .filter(UserRepoGroupToPerm.user_id == user_id)
3500 .filter(UserRepoGroupToPerm.user_id == user_id)
3496 if repo_group_id:
3501 if repo_group_id:
3497 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3502 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3498 return q.all()
3503 return q.all()
3499
3504
3500 @classmethod
3505 @classmethod
3501 def get_default_group_perms_from_user_group(
3506 def get_default_group_perms_from_user_group(
3502 cls, user_id, repo_group_id=None):
3507 cls, user_id, repo_group_id=None):
3503 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3508 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3504 .join(
3509 .join(
3505 Permission,
3510 Permission,
3506 UserGroupRepoGroupToPerm.permission_id ==
3511 UserGroupRepoGroupToPerm.permission_id ==
3507 Permission.permission_id)\
3512 Permission.permission_id)\
3508 .join(
3513 .join(
3509 RepoGroup,
3514 RepoGroup,
3510 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3515 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3511 .join(
3516 .join(
3512 UserGroup,
3517 UserGroup,
3513 UserGroupRepoGroupToPerm.users_group_id ==
3518 UserGroupRepoGroupToPerm.users_group_id ==
3514 UserGroup.users_group_id)\
3519 UserGroup.users_group_id)\
3515 .join(
3520 .join(
3516 UserGroupMember,
3521 UserGroupMember,
3517 UserGroupRepoGroupToPerm.users_group_id ==
3522 UserGroupRepoGroupToPerm.users_group_id ==
3518 UserGroupMember.users_group_id)\
3523 UserGroupMember.users_group_id)\
3519 .filter(
3524 .filter(
3520 UserGroupMember.user_id == user_id,
3525 UserGroupMember.user_id == user_id,
3521 UserGroup.users_group_active == true())
3526 UserGroup.users_group_active == true())
3522 if repo_group_id:
3527 if repo_group_id:
3523 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3528 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3524 return q.all()
3529 return q.all()
3525
3530
3526 @classmethod
3531 @classmethod
3527 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3532 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3528 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3533 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3529 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3534 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3530 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3535 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3531 .filter(UserUserGroupToPerm.user_id == user_id)
3536 .filter(UserUserGroupToPerm.user_id == user_id)
3532 if user_group_id:
3537 if user_group_id:
3533 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3538 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3534 return q.all()
3539 return q.all()
3535
3540
3536 @classmethod
3541 @classmethod
3537 def get_default_user_group_perms_from_user_group(
3542 def get_default_user_group_perms_from_user_group(
3538 cls, user_id, user_group_id=None):
3543 cls, user_id, user_group_id=None):
3539 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3544 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3540 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3545 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3541 .join(
3546 .join(
3542 Permission,
3547 Permission,
3543 UserGroupUserGroupToPerm.permission_id ==
3548 UserGroupUserGroupToPerm.permission_id ==
3544 Permission.permission_id)\
3549 Permission.permission_id)\
3545 .join(
3550 .join(
3546 TargetUserGroup,
3551 TargetUserGroup,
3547 UserGroupUserGroupToPerm.target_user_group_id ==
3552 UserGroupUserGroupToPerm.target_user_group_id ==
3548 TargetUserGroup.users_group_id)\
3553 TargetUserGroup.users_group_id)\
3549 .join(
3554 .join(
3550 UserGroup,
3555 UserGroup,
3551 UserGroupUserGroupToPerm.user_group_id ==
3556 UserGroupUserGroupToPerm.user_group_id ==
3552 UserGroup.users_group_id)\
3557 UserGroup.users_group_id)\
3553 .join(
3558 .join(
3554 UserGroupMember,
3559 UserGroupMember,
3555 UserGroupUserGroupToPerm.user_group_id ==
3560 UserGroupUserGroupToPerm.user_group_id ==
3556 UserGroupMember.users_group_id)\
3561 UserGroupMember.users_group_id)\
3557 .filter(
3562 .filter(
3558 UserGroupMember.user_id == user_id,
3563 UserGroupMember.user_id == user_id,
3559 UserGroup.users_group_active == true())
3564 UserGroup.users_group_active == true())
3560 if user_group_id:
3565 if user_group_id:
3561 q = q.filter(
3566 q = q.filter(
3562 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3567 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3563
3568
3564 return q.all()
3569 return q.all()
3565
3570
3566
3571
3567 class UserRepoToPerm(Base, BaseModel):
3572 class UserRepoToPerm(Base, BaseModel):
3568 __tablename__ = 'repo_to_perm'
3573 __tablename__ = 'repo_to_perm'
3569 __table_args__ = (
3574 __table_args__ = (
3570 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3575 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3571 base_table_args
3576 base_table_args
3572 )
3577 )
3573
3578
3574 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3579 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3575 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3580 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3576 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3581 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3577 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3582 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3578
3583
3579 user = relationship('User', back_populates="repo_to_perm")
3584 user = relationship('User', back_populates="repo_to_perm")
3580 repository = relationship('Repository', back_populates="repo_to_perm")
3585 repository = relationship('Repository', back_populates="repo_to_perm")
3581 permission = relationship('Permission')
3586 permission = relationship('Permission')
3582
3587
3583 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3588 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3584
3589
3585 @classmethod
3590 @classmethod
3586 def create(cls, user, repository, permission):
3591 def create(cls, user, repository, permission):
3587 n = cls()
3592 n = cls()
3588 n.user = user
3593 n.user = user
3589 n.repository = repository
3594 n.repository = repository
3590 n.permission = permission
3595 n.permission = permission
3591 Session().add(n)
3596 Session().add(n)
3592 return n
3597 return n
3593
3598
3594 def __repr__(self):
3599 def __repr__(self):
3595 return f'<{self.user} => {self.repository} >'
3600 return f'<{self.user} => {self.repository} >'
3596
3601
3597
3602
3598 class UserUserGroupToPerm(Base, BaseModel):
3603 class UserUserGroupToPerm(Base, BaseModel):
3599 __tablename__ = 'user_user_group_to_perm'
3604 __tablename__ = 'user_user_group_to_perm'
3600 __table_args__ = (
3605 __table_args__ = (
3601 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3606 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3602 base_table_args
3607 base_table_args
3603 )
3608 )
3604
3609
3605 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
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 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3611 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3607 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3612 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3608 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3613 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3609
3614
3610 user = relationship('User', back_populates='user_group_to_perm')
3615 user = relationship('User', back_populates='user_group_to_perm')
3611 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3616 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3612 permission = relationship('Permission')
3617 permission = relationship('Permission')
3613
3618
3614 @classmethod
3619 @classmethod
3615 def create(cls, user, user_group, permission):
3620 def create(cls, user, user_group, permission):
3616 n = cls()
3621 n = cls()
3617 n.user = user
3622 n.user = user
3618 n.user_group = user_group
3623 n.user_group = user_group
3619 n.permission = permission
3624 n.permission = permission
3620 Session().add(n)
3625 Session().add(n)
3621 return n
3626 return n
3622
3627
3623 def __repr__(self):
3628 def __repr__(self):
3624 return f'<{self.user} => {self.user_group} >'
3629 return f'<{self.user} => {self.user_group} >'
3625
3630
3626
3631
3627 class UserToPerm(Base, BaseModel):
3632 class UserToPerm(Base, BaseModel):
3628 __tablename__ = 'user_to_perm'
3633 __tablename__ = 'user_to_perm'
3629 __table_args__ = (
3634 __table_args__ = (
3630 UniqueConstraint('user_id', 'permission_id'),
3635 UniqueConstraint('user_id', 'permission_id'),
3631 base_table_args
3636 base_table_args
3632 )
3637 )
3633
3638
3634 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3639 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3635 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3640 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3636 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3641 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3637
3642
3638 user = relationship('User', back_populates='user_perms')
3643 user = relationship('User', back_populates='user_perms')
3639 permission = relationship('Permission', lazy='joined')
3644 permission = relationship('Permission', lazy='joined')
3640
3645
3641 def __repr__(self):
3646 def __repr__(self):
3642 return f'<{self.user} => {self.permission} >'
3647 return f'<{self.user} => {self.permission} >'
3643
3648
3644
3649
3645 class UserGroupRepoToPerm(Base, BaseModel):
3650 class UserGroupRepoToPerm(Base, BaseModel):
3646 __tablename__ = 'users_group_repo_to_perm'
3651 __tablename__ = 'users_group_repo_to_perm'
3647 __table_args__ = (
3652 __table_args__ = (
3648 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3653 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3649 base_table_args
3654 base_table_args
3650 )
3655 )
3651
3656
3652 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3657 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3653 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3658 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3654 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3659 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3655 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3660 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3656
3661
3657 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3662 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3658 permission = relationship('Permission')
3663 permission = relationship('Permission')
3659 repository = relationship('Repository', back_populates='users_group_to_perm')
3664 repository = relationship('Repository', back_populates='users_group_to_perm')
3660 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3665 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3661
3666
3662 @classmethod
3667 @classmethod
3663 def create(cls, users_group, repository, permission):
3668 def create(cls, users_group, repository, permission):
3664 n = cls()
3669 n = cls()
3665 n.users_group = users_group
3670 n.users_group = users_group
3666 n.repository = repository
3671 n.repository = repository
3667 n.permission = permission
3672 n.permission = permission
3668 Session().add(n)
3673 Session().add(n)
3669 return n
3674 return n
3670
3675
3671 def __repr__(self):
3676 def __repr__(self):
3672 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3677 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3673
3678
3674
3679
3675 class UserGroupUserGroupToPerm(Base, BaseModel):
3680 class UserGroupUserGroupToPerm(Base, BaseModel):
3676 __tablename__ = 'user_group_user_group_to_perm'
3681 __tablename__ = 'user_group_user_group_to_perm'
3677 __table_args__ = (
3682 __table_args__ = (
3678 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3683 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3679 CheckConstraint('target_user_group_id != user_group_id'),
3684 CheckConstraint('target_user_group_id != user_group_id'),
3680 base_table_args
3685 base_table_args
3681 )
3686 )
3682
3687
3683 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)
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 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3689 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3685 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3690 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3686 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3691 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3687
3692
3688 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
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 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3694 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3690 permission = relationship('Permission')
3695 permission = relationship('Permission')
3691
3696
3692 @classmethod
3697 @classmethod
3693 def create(cls, target_user_group, user_group, permission):
3698 def create(cls, target_user_group, user_group, permission):
3694 n = cls()
3699 n = cls()
3695 n.target_user_group = target_user_group
3700 n.target_user_group = target_user_group
3696 n.user_group = user_group
3701 n.user_group = user_group
3697 n.permission = permission
3702 n.permission = permission
3698 Session().add(n)
3703 Session().add(n)
3699 return n
3704 return n
3700
3705
3701 def __repr__(self):
3706 def __repr__(self):
3702 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3707 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3703
3708
3704
3709
3705 class UserGroupToPerm(Base, BaseModel):
3710 class UserGroupToPerm(Base, BaseModel):
3706 __tablename__ = 'users_group_to_perm'
3711 __tablename__ = 'users_group_to_perm'
3707 __table_args__ = (
3712 __table_args__ = (
3708 UniqueConstraint('users_group_id', 'permission_id',),
3713 UniqueConstraint('users_group_id', 'permission_id',),
3709 base_table_args
3714 base_table_args
3710 )
3715 )
3711
3716
3712 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3717 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3713 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3718 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3714 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3719 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3715
3720
3716 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3721 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3717 permission = relationship('Permission')
3722 permission = relationship('Permission')
3718
3723
3719
3724
3720 class UserRepoGroupToPerm(Base, BaseModel):
3725 class UserRepoGroupToPerm(Base, BaseModel):
3721 __tablename__ = 'user_repo_group_to_perm'
3726 __tablename__ = 'user_repo_group_to_perm'
3722 __table_args__ = (
3727 __table_args__ = (
3723 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3728 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3724 base_table_args
3729 base_table_args
3725 )
3730 )
3726
3731
3727 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3732 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3728 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3729 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3734 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3730 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3735 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3731
3736
3732 user = relationship('User', back_populates='repo_group_to_perm')
3737 user = relationship('User', back_populates='repo_group_to_perm')
3733 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3738 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3734 permission = relationship('Permission')
3739 permission = relationship('Permission')
3735
3740
3736 @classmethod
3741 @classmethod
3737 def create(cls, user, repository_group, permission):
3742 def create(cls, user, repository_group, permission):
3738 n = cls()
3743 n = cls()
3739 n.user = user
3744 n.user = user
3740 n.group = repository_group
3745 n.group = repository_group
3741 n.permission = permission
3746 n.permission = permission
3742 Session().add(n)
3747 Session().add(n)
3743 return n
3748 return n
3744
3749
3745
3750
3746 class UserGroupRepoGroupToPerm(Base, BaseModel):
3751 class UserGroupRepoGroupToPerm(Base, BaseModel):
3747 __tablename__ = 'users_group_repo_group_to_perm'
3752 __tablename__ = 'users_group_repo_group_to_perm'
3748 __table_args__ = (
3753 __table_args__ = (
3749 UniqueConstraint('users_group_id', 'group_id'),
3754 UniqueConstraint('users_group_id', 'group_id'),
3750 base_table_args
3755 base_table_args
3751 )
3756 )
3752
3757
3753 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)
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 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3759 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3755 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3760 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3756 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3761 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3757
3762
3758 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3763 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3759 permission = relationship('Permission')
3764 permission = relationship('Permission')
3760 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3765 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3761
3766
3762 @classmethod
3767 @classmethod
3763 def create(cls, user_group, repository_group, permission):
3768 def create(cls, user_group, repository_group, permission):
3764 n = cls()
3769 n = cls()
3765 n.users_group = user_group
3770 n.users_group = user_group
3766 n.group = repository_group
3771 n.group = repository_group
3767 n.permission = permission
3772 n.permission = permission
3768 Session().add(n)
3773 Session().add(n)
3769 return n
3774 return n
3770
3775
3771 def __repr__(self):
3776 def __repr__(self):
3772 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3777 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3773
3778
3774
3779
3775 class Statistics(Base, BaseModel):
3780 class Statistics(Base, BaseModel):
3776 __tablename__ = 'statistics'
3781 __tablename__ = 'statistics'
3777 __table_args__ = (
3782 __table_args__ = (
3778 base_table_args
3783 base_table_args
3779 )
3784 )
3780
3785
3781 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3786 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3782 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3787 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3783 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3788 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3784 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3789 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3785 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3790 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3786 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3791 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3787
3792
3788 repository = relationship('Repository', single_parent=True, viewonly=True)
3793 repository = relationship('Repository', single_parent=True, viewonly=True)
3789
3794
3790
3795
3791 class UserFollowing(Base, BaseModel):
3796 class UserFollowing(Base, BaseModel):
3792 __tablename__ = 'user_followings'
3797 __tablename__ = 'user_followings'
3793 __table_args__ = (
3798 __table_args__ = (
3794 UniqueConstraint('user_id', 'follows_repository_id'),
3799 UniqueConstraint('user_id', 'follows_repository_id'),
3795 UniqueConstraint('user_id', 'follows_user_id'),
3800 UniqueConstraint('user_id', 'follows_user_id'),
3796 base_table_args
3801 base_table_args
3797 )
3802 )
3798
3803
3799 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3804 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3800 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3805 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3801 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3806 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3802 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3807 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3803 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3808 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3804
3809
3805 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3810 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3806
3811
3807 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3812 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3808 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3813 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3809
3814
3810 @classmethod
3815 @classmethod
3811 def get_repo_followers(cls, repo_id):
3816 def get_repo_followers(cls, repo_id):
3812 return cls.query().filter(cls.follows_repo_id == repo_id)
3817 return cls.query().filter(cls.follows_repo_id == repo_id)
3813
3818
3814
3819
3815 class CacheKey(Base, BaseModel):
3820 class CacheKey(Base, BaseModel):
3816 __tablename__ = 'cache_invalidation'
3821 __tablename__ = 'cache_invalidation'
3817 __table_args__ = (
3822 __table_args__ = (
3818 UniqueConstraint('cache_key'),
3823 UniqueConstraint('cache_key'),
3819 Index('key_idx', 'cache_key'),
3824 Index('key_idx', 'cache_key'),
3820 Index('cache_args_idx', 'cache_args'),
3825 Index('cache_args_idx', 'cache_args'),
3821 base_table_args,
3826 base_table_args,
3822 )
3827 )
3823
3828
3824 CACHE_TYPE_FEED = 'FEED'
3829 CACHE_TYPE_FEED = 'FEED'
3825
3830
3826 # namespaces used to register process/thread aware caches
3831 # namespaces used to register process/thread aware caches
3827 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3832 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3828
3833
3829 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3834 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3830 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3835 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3831 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3836 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3832 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3837 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3833 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3838 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3834
3839
3835 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3840 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3836 self.cache_key = cache_key
3841 self.cache_key = cache_key
3837 self.cache_args = cache_args
3842 self.cache_args = cache_args
3838 self.cache_active = cache_active
3843 self.cache_active = cache_active
3839 # first key should be same for all entries, since all workers should share it
3844 # first key should be same for all entries, since all workers should share it
3840 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3845 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3841
3846
3842 def __repr__(self):
3847 def __repr__(self):
3843 return "<%s('%s:%s[%s]')>" % (
3848 return "<%s('%s:%s[%s]')>" % (
3844 self.cls_name,
3849 self.cls_name,
3845 self.cache_id, self.cache_key, self.cache_active)
3850 self.cache_id, self.cache_key, self.cache_active)
3846
3851
3847 def _cache_key_partition(self):
3852 def _cache_key_partition(self):
3848 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3853 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3849 return prefix, repo_name, suffix
3854 return prefix, repo_name, suffix
3850
3855
3851 def get_prefix(self):
3856 def get_prefix(self):
3852 """
3857 """
3853 Try to extract prefix from existing cache key. The key could consist
3858 Try to extract prefix from existing cache key. The key could consist
3854 of prefix, repo_name, suffix
3859 of prefix, repo_name, suffix
3855 """
3860 """
3856 # this returns prefix, repo_name, suffix
3861 # this returns prefix, repo_name, suffix
3857 return self._cache_key_partition()[0]
3862 return self._cache_key_partition()[0]
3858
3863
3859 def get_suffix(self):
3864 def get_suffix(self):
3860 """
3865 """
3861 get suffix that might have been used in _get_cache_key to
3866 get suffix that might have been used in _get_cache_key to
3862 generate self.cache_key. Only used for informational purposes
3867 generate self.cache_key. Only used for informational purposes
3863 in repo_edit.mako.
3868 in repo_edit.mako.
3864 """
3869 """
3865 # prefix, repo_name, suffix
3870 # prefix, repo_name, suffix
3866 return self._cache_key_partition()[2]
3871 return self._cache_key_partition()[2]
3867
3872
3868 @classmethod
3873 @classmethod
3869 def generate_new_state_uid(cls, based_on=None):
3874 def generate_new_state_uid(cls, based_on=None):
3870 if based_on:
3875 if based_on:
3871 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3876 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3872 else:
3877 else:
3873 return str(uuid.uuid4())
3878 return str(uuid.uuid4())
3874
3879
3875 @classmethod
3880 @classmethod
3876 def delete_all_cache(cls):
3881 def delete_all_cache(cls):
3877 """
3882 """
3878 Delete all cache keys from database.
3883 Delete all cache keys from database.
3879 Should only be run when all instances are down and all entries
3884 Should only be run when all instances are down and all entries
3880 thus stale.
3885 thus stale.
3881 """
3886 """
3882 cls.query().delete()
3887 cls.query().delete()
3883 Session().commit()
3888 Session().commit()
3884
3889
3885 @classmethod
3890 @classmethod
3886 def set_invalidate(cls, cache_uid, delete=False):
3891 def set_invalidate(cls, cache_uid, delete=False):
3887 """
3892 """
3888 Mark all caches of a repo as invalid in the database.
3893 Mark all caches of a repo as invalid in the database.
3889 """
3894 """
3890 try:
3895 try:
3891 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3896 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3892 if delete:
3897 if delete:
3893 qry.delete()
3898 qry.delete()
3894 log.debug('cache objects deleted for cache args %s',
3899 log.debug('cache objects deleted for cache args %s',
3895 safe_str(cache_uid))
3900 safe_str(cache_uid))
3896 else:
3901 else:
3897 new_uid = cls.generate_new_state_uid()
3902 new_uid = cls.generate_new_state_uid()
3898 qry.update({"cache_state_uid": new_uid,
3903 qry.update({"cache_state_uid": new_uid,
3899 "cache_args": f"repo_state:{time.time()}"})
3904 "cache_args": f"repo_state:{time.time()}"})
3900 log.debug('cache object %s set new UID %s',
3905 log.debug('cache object %s set new UID %s',
3901 safe_str(cache_uid), new_uid)
3906 safe_str(cache_uid), new_uid)
3902
3907
3903 Session().commit()
3908 Session().commit()
3904 except Exception:
3909 except Exception:
3905 log.exception(
3910 log.exception(
3906 'Cache key invalidation failed for cache args %s',
3911 'Cache key invalidation failed for cache args %s',
3907 safe_str(cache_uid))
3912 safe_str(cache_uid))
3908 Session().rollback()
3913 Session().rollback()
3909
3914
3910 @classmethod
3915 @classmethod
3911 def get_active_cache(cls, cache_key):
3916 def get_active_cache(cls, cache_key):
3912 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3917 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3913 if inv_obj:
3918 if inv_obj:
3914 return inv_obj
3919 return inv_obj
3915 return None
3920 return None
3916
3921
3917 @classmethod
3922 @classmethod
3918 def get_namespace_map(cls, namespace):
3923 def get_namespace_map(cls, namespace):
3919 return {
3924 return {
3920 x.cache_key: x
3925 x.cache_key: x
3921 for x in cls.query().filter(cls.cache_args == namespace)}
3926 for x in cls.query().filter(cls.cache_args == namespace)}
3922
3927
3923
3928
3924 class ChangesetComment(Base, BaseModel):
3929 class ChangesetComment(Base, BaseModel):
3925 __tablename__ = 'changeset_comments'
3930 __tablename__ = 'changeset_comments'
3926 __table_args__ = (
3931 __table_args__ = (
3927 Index('cc_revision_idx', 'revision'),
3932 Index('cc_revision_idx', 'revision'),
3928 base_table_args,
3933 base_table_args,
3929 )
3934 )
3930
3935
3931 COMMENT_OUTDATED = 'comment_outdated'
3936 COMMENT_OUTDATED = 'comment_outdated'
3932 COMMENT_TYPE_NOTE = 'note'
3937 COMMENT_TYPE_NOTE = 'note'
3933 COMMENT_TYPE_TODO = 'todo'
3938 COMMENT_TYPE_TODO = 'todo'
3934 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3939 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3935
3940
3936 OP_IMMUTABLE = 'immutable'
3941 OP_IMMUTABLE = 'immutable'
3937 OP_CHANGEABLE = 'changeable'
3942 OP_CHANGEABLE = 'changeable'
3938
3943
3939 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3944 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3940 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3945 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3941 revision = Column('revision', String(40), nullable=True)
3946 revision = Column('revision', String(40), nullable=True)
3942 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3947 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3943 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3948 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3944 line_no = Column('line_no', Unicode(10), nullable=True)
3949 line_no = Column('line_no', Unicode(10), nullable=True)
3945 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3950 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3946 f_path = Column('f_path', Unicode(1000), nullable=True)
3951 f_path = Column('f_path', Unicode(1000), nullable=True)
3947 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3952 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3948 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3953 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3949 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3954 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3950 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3955 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3951 renderer = Column('renderer', Unicode(64), nullable=True)
3956 renderer = Column('renderer', Unicode(64), nullable=True)
3952 display_state = Column('display_state', Unicode(128), nullable=True)
3957 display_state = Column('display_state', Unicode(128), nullable=True)
3953 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3958 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3954 draft = Column('draft', Boolean(), nullable=True, default=False)
3959 draft = Column('draft', Boolean(), nullable=True, default=False)
3955
3960
3956 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3961 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3957 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3962 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3958
3963
3959 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3964 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3960 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3965 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3961
3966
3962 author = relationship('User', lazy='select', back_populates='user_comments')
3967 author = relationship('User', lazy='select', back_populates='user_comments')
3963 repo = relationship('Repository', back_populates='comments')
3968 repo = relationship('Repository', back_populates='comments')
3964 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3969 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3965 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3970 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3966 pull_request_version = relationship('PullRequestVersion', lazy='select')
3971 pull_request_version = relationship('PullRequestVersion', lazy='select')
3967 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3972 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3968
3973
3969 @classmethod
3974 @classmethod
3970 def get_users(cls, revision=None, pull_request_id=None):
3975 def get_users(cls, revision=None, pull_request_id=None):
3971 """
3976 """
3972 Returns user associated with this ChangesetComment. ie those
3977 Returns user associated with this ChangesetComment. ie those
3973 who actually commented
3978 who actually commented
3974
3979
3975 :param cls:
3980 :param cls:
3976 :param revision:
3981 :param revision:
3977 """
3982 """
3978 q = Session().query(User).join(ChangesetComment.author)
3983 q = Session().query(User).join(ChangesetComment.author)
3979 if revision:
3984 if revision:
3980 q = q.filter(cls.revision == revision)
3985 q = q.filter(cls.revision == revision)
3981 elif pull_request_id:
3986 elif pull_request_id:
3982 q = q.filter(cls.pull_request_id == pull_request_id)
3987 q = q.filter(cls.pull_request_id == pull_request_id)
3983 return q.all()
3988 return q.all()
3984
3989
3985 @classmethod
3990 @classmethod
3986 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3991 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3987 if pr_version is None:
3992 if pr_version is None:
3988 return 0
3993 return 0
3989
3994
3990 if versions is not None:
3995 if versions is not None:
3991 num_versions = [x.pull_request_version_id for x in versions]
3996 num_versions = [x.pull_request_version_id for x in versions]
3992
3997
3993 num_versions = num_versions or []
3998 num_versions = num_versions or []
3994 try:
3999 try:
3995 return num_versions.index(pr_version) + 1
4000 return num_versions.index(pr_version) + 1
3996 except (IndexError, ValueError):
4001 except (IndexError, ValueError):
3997 return 0
4002 return 0
3998
4003
3999 @property
4004 @property
4000 def outdated(self):
4005 def outdated(self):
4001 return self.display_state == self.COMMENT_OUTDATED
4006 return self.display_state == self.COMMENT_OUTDATED
4002
4007
4003 @property
4008 @property
4004 def outdated_js(self):
4009 def outdated_js(self):
4005 return str_json(self.display_state == self.COMMENT_OUTDATED)
4010 return str_json(self.display_state == self.COMMENT_OUTDATED)
4006
4011
4007 @property
4012 @property
4008 def immutable(self):
4013 def immutable(self):
4009 return self.immutable_state == self.OP_IMMUTABLE
4014 return self.immutable_state == self.OP_IMMUTABLE
4010
4015
4011 def outdated_at_version(self, version: int) -> bool:
4016 def outdated_at_version(self, version: int) -> bool:
4012 """
4017 """
4013 Checks if comment is outdated for given pull request version
4018 Checks if comment is outdated for given pull request version
4014 """
4019 """
4015
4020
4016 def version_check():
4021 def version_check():
4017 return self.pull_request_version_id and self.pull_request_version_id != version
4022 return self.pull_request_version_id and self.pull_request_version_id != version
4018
4023
4019 if self.is_inline:
4024 if self.is_inline:
4020 return self.outdated and version_check()
4025 return self.outdated and version_check()
4021 else:
4026 else:
4022 # general comments don't have .outdated set, also latest don't have a version
4027 # general comments don't have .outdated set, also latest don't have a version
4023 return version_check()
4028 return version_check()
4024
4029
4025 def outdated_at_version_js(self, version):
4030 def outdated_at_version_js(self, version):
4026 """
4031 """
4027 Checks if comment is outdated for given pull request version
4032 Checks if comment is outdated for given pull request version
4028 """
4033 """
4029 return str_json(self.outdated_at_version(version))
4034 return str_json(self.outdated_at_version(version))
4030
4035
4031 def older_than_version(self, version: int) -> bool:
4036 def older_than_version(self, version: int) -> bool:
4032 """
4037 """
4033 Checks if comment is made from a previous version than given.
4038 Checks if comment is made from a previous version than given.
4034 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
4039 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
4035 """
4040 """
4036
4041
4037 # If version is None, return False as the current version cannot be less than None
4042 # If version is None, return False as the current version cannot be less than None
4038 if version is None:
4043 if version is None:
4039 return False
4044 return False
4040
4045
4041 # Ensure that the version is an integer to prevent TypeError on comparison
4046 # Ensure that the version is an integer to prevent TypeError on comparison
4042 if not isinstance(version, int):
4047 if not isinstance(version, int):
4043 raise ValueError("The provided version must be an integer.")
4048 raise ValueError("The provided version must be an integer.")
4044
4049
4045 # Initialize current version to 0 or pull_request_version_id if it's available
4050 # Initialize current version to 0 or pull_request_version_id if it's available
4046 cur_ver = 0
4051 cur_ver = 0
4047 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
4052 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
4048 cur_ver = self.pull_request_version.pull_request_version_id
4053 cur_ver = self.pull_request_version.pull_request_version_id
4049
4054
4050 # Return True if the current version is less than the given version
4055 # Return True if the current version is less than the given version
4051 return cur_ver < version
4056 return cur_ver < version
4052
4057
4053 def older_than_version_js(self, version):
4058 def older_than_version_js(self, version):
4054 """
4059 """
4055 Checks if comment is made from previous version than given
4060 Checks if comment is made from previous version than given
4056 """
4061 """
4057 return str_json(self.older_than_version(version))
4062 return str_json(self.older_than_version(version))
4058
4063
4059 @property
4064 @property
4060 def commit_id(self):
4065 def commit_id(self):
4061 """New style naming to stop using .revision"""
4066 """New style naming to stop using .revision"""
4062 return self.revision
4067 return self.revision
4063
4068
4064 @property
4069 @property
4065 def resolved(self):
4070 def resolved(self):
4066 return self.resolved_by[0] if self.resolved_by else None
4071 return self.resolved_by[0] if self.resolved_by else None
4067
4072
4068 @property
4073 @property
4069 def is_todo(self):
4074 def is_todo(self):
4070 return self.comment_type == self.COMMENT_TYPE_TODO
4075 return self.comment_type == self.COMMENT_TYPE_TODO
4071
4076
4072 @property
4077 @property
4073 def is_inline(self):
4078 def is_inline(self):
4074 if self.line_no and self.f_path:
4079 if self.line_no and self.f_path:
4075 return True
4080 return True
4076 return False
4081 return False
4077
4082
4078 @property
4083 @property
4079 def last_version(self):
4084 def last_version(self):
4080 version = 0
4085 version = 0
4081 if self.history:
4086 if self.history:
4082 version = self.history[-1].version
4087 version = self.history[-1].version
4083 return version
4088 return version
4084
4089
4085 def get_index_version(self, versions):
4090 def get_index_version(self, versions):
4086 return self.get_index_from_version(
4091 return self.get_index_from_version(
4087 self.pull_request_version_id, versions)
4092 self.pull_request_version_id, versions)
4088
4093
4089 @property
4094 @property
4090 def review_status(self):
4095 def review_status(self):
4091 if self.status_change:
4096 if self.status_change:
4092 return self.status_change[0].status
4097 return self.status_change[0].status
4093
4098
4094 @property
4099 @property
4095 def review_status_lbl(self):
4100 def review_status_lbl(self):
4096 if self.status_change:
4101 if self.status_change:
4097 return self.status_change[0].status_lbl
4102 return self.status_change[0].status_lbl
4098
4103
4099 def __repr__(self):
4104 def __repr__(self):
4100 if self.comment_id:
4105 if self.comment_id:
4101 return f'<DB:Comment #{self.comment_id}>'
4106 return f'<DB:Comment #{self.comment_id}>'
4102 else:
4107 else:
4103 return f'<DB:Comment at {id(self)!r}>'
4108 return f'<DB:Comment at {id(self)!r}>'
4104
4109
4105 def get_api_data(self):
4110 def get_api_data(self):
4106 comment = self
4111 comment = self
4107
4112
4108 data = {
4113 data = {
4109 'comment_id': comment.comment_id,
4114 'comment_id': comment.comment_id,
4110 'comment_type': comment.comment_type,
4115 'comment_type': comment.comment_type,
4111 'comment_text': comment.text,
4116 'comment_text': comment.text,
4112 'comment_status': comment.status_change,
4117 'comment_status': comment.status_change,
4113 'comment_f_path': comment.f_path,
4118 'comment_f_path': comment.f_path,
4114 'comment_lineno': comment.line_no,
4119 'comment_lineno': comment.line_no,
4115 'comment_author': comment.author,
4120 'comment_author': comment.author,
4116 'comment_created_on': comment.created_on,
4121 'comment_created_on': comment.created_on,
4117 'comment_resolved_by': self.resolved,
4122 'comment_resolved_by': self.resolved,
4118 'comment_commit_id': comment.revision,
4123 'comment_commit_id': comment.revision,
4119 'comment_pull_request_id': comment.pull_request_id,
4124 'comment_pull_request_id': comment.pull_request_id,
4120 'comment_last_version': self.last_version
4125 'comment_last_version': self.last_version
4121 }
4126 }
4122 return data
4127 return data
4123
4128
4124 def __json__(self):
4129 def __json__(self):
4125 data = dict()
4130 data = dict()
4126 data.update(self.get_api_data())
4131 data.update(self.get_api_data())
4127 return data
4132 return data
4128
4133
4129
4134
4130 class ChangesetCommentHistory(Base, BaseModel):
4135 class ChangesetCommentHistory(Base, BaseModel):
4131 __tablename__ = 'changeset_comments_history'
4136 __tablename__ = 'changeset_comments_history'
4132 __table_args__ = (
4137 __table_args__ = (
4133 Index('cch_comment_id_idx', 'comment_id'),
4138 Index('cch_comment_id_idx', 'comment_id'),
4134 base_table_args,
4139 base_table_args,
4135 )
4140 )
4136
4141
4137 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4142 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4138 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4143 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4139 version = Column("version", Integer(), nullable=False, default=0)
4144 version = Column("version", Integer(), nullable=False, default=0)
4140 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4145 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4141 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4146 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4142 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4147 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4143 deleted = Column('deleted', Boolean(), default=False)
4148 deleted = Column('deleted', Boolean(), default=False)
4144
4149
4145 author = relationship('User', lazy='joined')
4150 author = relationship('User', lazy='joined')
4146 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4151 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4147
4152
4148 @classmethod
4153 @classmethod
4149 def get_version(cls, comment_id):
4154 def get_version(cls, comment_id):
4150 q = Session().query(ChangesetCommentHistory).filter(
4155 q = Session().query(ChangesetCommentHistory).filter(
4151 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4156 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4152 if q.count() == 0:
4157 if q.count() == 0:
4153 return 1
4158 return 1
4154 elif q.count() >= q[0].version:
4159 elif q.count() >= q[0].version:
4155 return q.count() + 1
4160 return q.count() + 1
4156 else:
4161 else:
4157 return q[0].version + 1
4162 return q[0].version + 1
4158
4163
4159
4164
4160 class ChangesetStatus(Base, BaseModel):
4165 class ChangesetStatus(Base, BaseModel):
4161 __tablename__ = 'changeset_statuses'
4166 __tablename__ = 'changeset_statuses'
4162 __table_args__ = (
4167 __table_args__ = (
4163 Index('cs_revision_idx', 'revision'),
4168 Index('cs_revision_idx', 'revision'),
4164 Index('cs_version_idx', 'version'),
4169 Index('cs_version_idx', 'version'),
4165 UniqueConstraint('repo_id', 'revision', 'version'),
4170 UniqueConstraint('repo_id', 'revision', 'version'),
4166 base_table_args
4171 base_table_args
4167 )
4172 )
4168
4173
4169 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4174 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4170 STATUS_APPROVED = 'approved'
4175 STATUS_APPROVED = 'approved'
4171 STATUS_REJECTED = 'rejected'
4176 STATUS_REJECTED = 'rejected'
4172 STATUS_UNDER_REVIEW = 'under_review'
4177 STATUS_UNDER_REVIEW = 'under_review'
4173
4178
4174 STATUSES = [
4179 STATUSES = [
4175 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4180 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4176 (STATUS_APPROVED, _("Approved")),
4181 (STATUS_APPROVED, _("Approved")),
4177 (STATUS_REJECTED, _("Rejected")),
4182 (STATUS_REJECTED, _("Rejected")),
4178 (STATUS_UNDER_REVIEW, _("Under Review")),
4183 (STATUS_UNDER_REVIEW, _("Under Review")),
4179 ]
4184 ]
4180
4185
4181 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4186 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4182 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4187 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4183 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4188 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4184 revision = Column('revision', String(40), nullable=False)
4189 revision = Column('revision', String(40), nullable=False)
4185 status = Column('status', String(128), nullable=False, default=DEFAULT)
4190 status = Column('status', String(128), nullable=False, default=DEFAULT)
4186 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4191 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4187 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4192 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4188 version = Column('version', Integer(), nullable=False, default=0)
4193 version = Column('version', Integer(), nullable=False, default=0)
4189 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4194 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4190
4195
4191 author = relationship('User', lazy='select')
4196 author = relationship('User', lazy='select')
4192 repo = relationship('Repository', lazy='select')
4197 repo = relationship('Repository', lazy='select')
4193 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4198 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4194 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4199 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4195
4200
4196 def __repr__(self):
4201 def __repr__(self):
4197 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4202 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4198
4203
4199 @classmethod
4204 @classmethod
4200 def get_status_lbl(cls, value):
4205 def get_status_lbl(cls, value):
4201 return dict(cls.STATUSES).get(value)
4206 return dict(cls.STATUSES).get(value)
4202
4207
4203 @property
4208 @property
4204 def status_lbl(self):
4209 def status_lbl(self):
4205 return ChangesetStatus.get_status_lbl(self.status)
4210 return ChangesetStatus.get_status_lbl(self.status)
4206
4211
4207 def get_api_data(self):
4212 def get_api_data(self):
4208 status = self
4213 status = self
4209 data = {
4214 data = {
4210 'status_id': status.changeset_status_id,
4215 'status_id': status.changeset_status_id,
4211 'status': status.status,
4216 'status': status.status,
4212 }
4217 }
4213 return data
4218 return data
4214
4219
4215 def __json__(self):
4220 def __json__(self):
4216 data = dict()
4221 data = dict()
4217 data.update(self.get_api_data())
4222 data.update(self.get_api_data())
4218 return data
4223 return data
4219
4224
4220
4225
4221 class _SetState(object):
4226 class _SetState(object):
4222 """
4227 """
4223 Context processor allowing changing state for sensitive operation such as
4228 Context processor allowing changing state for sensitive operation such as
4224 pull request update or merge
4229 pull request update or merge
4225 """
4230 """
4226
4231
4227 def __init__(self, pull_request, pr_state, back_state=None):
4232 def __init__(self, pull_request, pr_state, back_state=None):
4228 self._pr = pull_request
4233 self._pr = pull_request
4229 self._org_state = back_state or pull_request.pull_request_state
4234 self._org_state = back_state or pull_request.pull_request_state
4230 self._pr_state = pr_state
4235 self._pr_state = pr_state
4231 self._current_state = None
4236 self._current_state = None
4232
4237
4233 def __enter__(self):
4238 def __enter__(self):
4234 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4239 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4235 self._pr, self._pr_state)
4240 self._pr, self._pr_state)
4236 self.set_pr_state(self._pr_state)
4241 self.set_pr_state(self._pr_state)
4237 return self
4242 return self
4238
4243
4239 def __exit__(self, exc_type, exc_val, exc_tb):
4244 def __exit__(self, exc_type, exc_val, exc_tb):
4240 if exc_val is not None or exc_type is not None:
4245 if exc_val is not None or exc_type is not None:
4241 log.error(traceback.format_tb(exc_tb))
4246 log.error(traceback.format_tb(exc_tb))
4242 return None
4247 return None
4243
4248
4244 self.set_pr_state(self._org_state)
4249 self.set_pr_state(self._org_state)
4245 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4250 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4246 self._pr, self._org_state)
4251 self._pr, self._org_state)
4247
4252
4248 @property
4253 @property
4249 def state(self):
4254 def state(self):
4250 return self._current_state
4255 return self._current_state
4251
4256
4252 def set_pr_state(self, pr_state):
4257 def set_pr_state(self, pr_state):
4253 try:
4258 try:
4254 self._pr.pull_request_state = pr_state
4259 self._pr.pull_request_state = pr_state
4255 Session().add(self._pr)
4260 Session().add(self._pr)
4256 Session().commit()
4261 Session().commit()
4257 self._current_state = pr_state
4262 self._current_state = pr_state
4258 except Exception:
4263 except Exception:
4259 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4264 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4260 raise
4265 raise
4261
4266
4262
4267
4263 class _PullRequestBase(BaseModel):
4268 class _PullRequestBase(BaseModel):
4264 """
4269 """
4265 Common attributes of pull request and version entries.
4270 Common attributes of pull request and version entries.
4266 """
4271 """
4267
4272
4268 # .status values
4273 # .status values
4269 STATUS_NEW = 'new'
4274 STATUS_NEW = 'new'
4270 STATUS_OPEN = 'open'
4275 STATUS_OPEN = 'open'
4271 STATUS_CLOSED = 'closed'
4276 STATUS_CLOSED = 'closed'
4272
4277
4273 # available states
4278 # available states
4274 STATE_CREATING = 'creating'
4279 STATE_CREATING = 'creating'
4275 STATE_UPDATING = 'updating'
4280 STATE_UPDATING = 'updating'
4276 STATE_MERGING = 'merging'
4281 STATE_MERGING = 'merging'
4277 STATE_CREATED = 'created'
4282 STATE_CREATED = 'created'
4278
4283
4279 title = Column('title', Unicode(255), nullable=True)
4284 title = Column('title', Unicode(255), nullable=True)
4280 description = Column(
4285 description = Column(
4281 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4286 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4282 nullable=True)
4287 nullable=True)
4283 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4288 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4284
4289
4285 # new/open/closed status of pull request (not approve/reject/etc)
4290 # new/open/closed status of pull request (not approve/reject/etc)
4286 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4291 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4287 created_on = Column(
4292 created_on = Column(
4288 'created_on', DateTime(timezone=False), nullable=False,
4293 'created_on', DateTime(timezone=False), nullable=False,
4289 default=datetime.datetime.now)
4294 default=datetime.datetime.now)
4290 updated_on = Column(
4295 updated_on = Column(
4291 'updated_on', DateTime(timezone=False), nullable=False,
4296 'updated_on', DateTime(timezone=False), nullable=False,
4292 default=datetime.datetime.now)
4297 default=datetime.datetime.now)
4293
4298
4294 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4299 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4295
4300
4296 @declared_attr
4301 @declared_attr
4297 def user_id(cls):
4302 def user_id(cls):
4298 return Column(
4303 return Column(
4299 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4304 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4300 unique=None)
4305 unique=None)
4301
4306
4302 # 500 revisions max
4307 # 500 revisions max
4303 _revisions = Column(
4308 _revisions = Column(
4304 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4309 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4305
4310
4306 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4311 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4307
4312
4308 @declared_attr
4313 @declared_attr
4309 def source_repo_id(cls):
4314 def source_repo_id(cls):
4310 # TODO: dan: rename column to source_repo_id
4315 # TODO: dan: rename column to source_repo_id
4311 return Column(
4316 return Column(
4312 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4317 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4313 nullable=False)
4318 nullable=False)
4314
4319
4315 @declared_attr
4320 @declared_attr
4316 def pr_source(cls):
4321 def pr_source(cls):
4317 return relationship(
4322 return relationship(
4318 'Repository',
4323 'Repository',
4319 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4324 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4320 overlaps="pull_requests_source"
4325 overlaps="pull_requests_source"
4321 )
4326 )
4322
4327
4323 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4328 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4324
4329
4325 @hybrid_property
4330 @hybrid_property
4326 def source_ref(self):
4331 def source_ref(self):
4327 return self._source_ref
4332 return self._source_ref
4328
4333
4329 @source_ref.setter
4334 @source_ref.setter
4330 def source_ref(self, val):
4335 def source_ref(self, val):
4331 parts = (val or '').split(':')
4336 parts = (val or '').split(':')
4332 if len(parts) != 3:
4337 if len(parts) != 3:
4333 raise ValueError(
4338 raise ValueError(
4334 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4339 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4335 self._source_ref = safe_str(val)
4340 self._source_ref = safe_str(val)
4336
4341
4337 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4342 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4338
4343
4339 @hybrid_property
4344 @hybrid_property
4340 def target_ref(self):
4345 def target_ref(self):
4341 return self._target_ref
4346 return self._target_ref
4342
4347
4343 @target_ref.setter
4348 @target_ref.setter
4344 def target_ref(self, val):
4349 def target_ref(self, val):
4345 parts = (val or '').split(':')
4350 parts = (val or '').split(':')
4346 if len(parts) != 3:
4351 if len(parts) != 3:
4347 raise ValueError(
4352 raise ValueError(
4348 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4353 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4349 self._target_ref = safe_str(val)
4354 self._target_ref = safe_str(val)
4350
4355
4351 @declared_attr
4356 @declared_attr
4352 def target_repo_id(cls):
4357 def target_repo_id(cls):
4353 # TODO: dan: rename column to target_repo_id
4358 # TODO: dan: rename column to target_repo_id
4354 return Column(
4359 return Column(
4355 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4360 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4356 nullable=False)
4361 nullable=False)
4357
4362
4358 @declared_attr
4363 @declared_attr
4359 def pr_target(cls):
4364 def pr_target(cls):
4360 return relationship(
4365 return relationship(
4361 'Repository',
4366 'Repository',
4362 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4367 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4363 overlaps="pull_requests_target"
4368 overlaps="pull_requests_target"
4364 )
4369 )
4365
4370
4366 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4371 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4367
4372
4368 # TODO: dan: rename column to last_merge_source_rev
4373 # TODO: dan: rename column to last_merge_source_rev
4369 _last_merge_source_rev = Column(
4374 _last_merge_source_rev = Column(
4370 'last_merge_org_rev', String(40), nullable=True)
4375 'last_merge_org_rev', String(40), nullable=True)
4371 # TODO: dan: rename column to last_merge_target_rev
4376 # TODO: dan: rename column to last_merge_target_rev
4372 _last_merge_target_rev = Column(
4377 _last_merge_target_rev = Column(
4373 'last_merge_other_rev', String(40), nullable=True)
4378 'last_merge_other_rev', String(40), nullable=True)
4374 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4379 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4375 last_merge_metadata = Column(
4380 last_merge_metadata = Column(
4376 'last_merge_metadata', MutationObj.as_mutable(
4381 'last_merge_metadata', MutationObj.as_mutable(
4377 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4382 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4378
4383
4379 merge_rev = Column('merge_rev', String(40), nullable=True)
4384 merge_rev = Column('merge_rev', String(40), nullable=True)
4380
4385
4381 reviewer_data = Column(
4386 reviewer_data = Column(
4382 'reviewer_data_json', MutationObj.as_mutable(
4387 'reviewer_data_json', MutationObj.as_mutable(
4383 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4388 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4384
4389
4385 @property
4390 @property
4386 def reviewer_data_json(self):
4391 def reviewer_data_json(self):
4387 return str_json(self.reviewer_data)
4392 return str_json(self.reviewer_data)
4388
4393
4389 @property
4394 @property
4390 def last_merge_metadata_parsed(self):
4395 def last_merge_metadata_parsed(self):
4391 metadata = {}
4396 metadata = {}
4392 if not self.last_merge_metadata:
4397 if not self.last_merge_metadata:
4393 return metadata
4398 return metadata
4394
4399
4395 if hasattr(self.last_merge_metadata, 'de_coerce'):
4400 if hasattr(self.last_merge_metadata, 'de_coerce'):
4396 for k, v in self.last_merge_metadata.de_coerce().items():
4401 for k, v in self.last_merge_metadata.de_coerce().items():
4397 if k in ['target_ref', 'source_ref']:
4402 if k in ['target_ref', 'source_ref']:
4398 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4403 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4399 else:
4404 else:
4400 if hasattr(v, 'de_coerce'):
4405 if hasattr(v, 'de_coerce'):
4401 metadata[k] = v.de_coerce()
4406 metadata[k] = v.de_coerce()
4402 else:
4407 else:
4403 metadata[k] = v
4408 metadata[k] = v
4404 return metadata
4409 return metadata
4405
4410
4406 @property
4411 @property
4407 def work_in_progress(self):
4412 def work_in_progress(self):
4408 """checks if pull request is work in progress by checking the title"""
4413 """checks if pull request is work in progress by checking the title"""
4409 title = self.title.upper()
4414 title = self.title.upper()
4410 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4415 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4411 return True
4416 return True
4412 return False
4417 return False
4413
4418
4414 @property
4419 @property
4415 def title_safe(self):
4420 def title_safe(self):
4416 return self.title\
4421 return self.title\
4417 .replace('{', '{{')\
4422 .replace('{', '{{')\
4418 .replace('}', '}}')
4423 .replace('}', '}}')
4419
4424
4420 @hybrid_property
4425 @hybrid_property
4421 def description_safe(self):
4426 def description_safe(self):
4422 from rhodecode.lib import helpers as h
4427 from rhodecode.lib import helpers as h
4423 return h.escape(self.description)
4428 return h.escape(self.description)
4424
4429
4425 @hybrid_property
4430 @hybrid_property
4426 def revisions(self):
4431 def revisions(self):
4427 return self._revisions.split(':') if self._revisions else []
4432 return self._revisions.split(':') if self._revisions else []
4428
4433
4429 @revisions.setter
4434 @revisions.setter
4430 def revisions(self, val):
4435 def revisions(self, val):
4431 self._revisions = ':'.join(val)
4436 self._revisions = ':'.join(val)
4432
4437
4433 @hybrid_property
4438 @hybrid_property
4434 def last_merge_status(self):
4439 def last_merge_status(self):
4435 return safe_int(self._last_merge_status)
4440 return safe_int(self._last_merge_status)
4436
4441
4437 @last_merge_status.setter
4442 @last_merge_status.setter
4438 def last_merge_status(self, val):
4443 def last_merge_status(self, val):
4439 self._last_merge_status = val
4444 self._last_merge_status = val
4440
4445
4441 @declared_attr
4446 @declared_attr
4442 def author(cls):
4447 def author(cls):
4443 return relationship(
4448 return relationship(
4444 'User', lazy='joined',
4449 'User', lazy='joined',
4445 #TODO, problem that is somehow :?
4450 #TODO, problem that is somehow :?
4446 #back_populates='user_pull_requests'
4451 #back_populates='user_pull_requests'
4447 )
4452 )
4448
4453
4449 @declared_attr
4454 @declared_attr
4450 def source_repo(cls):
4455 def source_repo(cls):
4451 return relationship(
4456 return relationship(
4452 'Repository',
4457 'Repository',
4453 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4458 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4454 overlaps="pr_source"
4459 overlaps="pr_source"
4455 )
4460 )
4456
4461
4457 @property
4462 @property
4458 def source_ref_parts(self):
4463 def source_ref_parts(self):
4459 return self.unicode_to_reference(self.source_ref)
4464 return self.unicode_to_reference(self.source_ref)
4460
4465
4461 @declared_attr
4466 @declared_attr
4462 def target_repo(cls):
4467 def target_repo(cls):
4463 return relationship(
4468 return relationship(
4464 'Repository',
4469 'Repository',
4465 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4470 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4466 overlaps="pr_target"
4471 overlaps="pr_target"
4467 )
4472 )
4468
4473
4469 @property
4474 @property
4470 def target_ref_parts(self):
4475 def target_ref_parts(self):
4471 return self.unicode_to_reference(self.target_ref)
4476 return self.unicode_to_reference(self.target_ref)
4472
4477
4473 @property
4478 @property
4474 def shadow_merge_ref(self):
4479 def shadow_merge_ref(self):
4475 return self.unicode_to_reference(self._shadow_merge_ref)
4480 return self.unicode_to_reference(self._shadow_merge_ref)
4476
4481
4477 @shadow_merge_ref.setter
4482 @shadow_merge_ref.setter
4478 def shadow_merge_ref(self, ref):
4483 def shadow_merge_ref(self, ref):
4479 self._shadow_merge_ref = self.reference_to_unicode(ref)
4484 self._shadow_merge_ref = self.reference_to_unicode(ref)
4480
4485
4481 @staticmethod
4486 @staticmethod
4482 def unicode_to_reference(raw):
4487 def unicode_to_reference(raw):
4483 return unicode_to_reference(raw)
4488 return unicode_to_reference(raw)
4484
4489
4485 @staticmethod
4490 @staticmethod
4486 def reference_to_unicode(ref):
4491 def reference_to_unicode(ref):
4487 return reference_to_unicode(ref)
4492 return reference_to_unicode(ref)
4488
4493
4489 def get_api_data(self, with_merge_state=True):
4494 def get_api_data(self, with_merge_state=True):
4490 from rhodecode.model.pull_request import PullRequestModel
4495 from rhodecode.model.pull_request import PullRequestModel
4491
4496
4492 pull_request = self
4497 pull_request = self
4493 if with_merge_state:
4498 if with_merge_state:
4494 merge_response, merge_status, msg = \
4499 merge_response, merge_status, msg = \
4495 PullRequestModel().merge_status(pull_request)
4500 PullRequestModel().merge_status(pull_request)
4496 merge_state = {
4501 merge_state = {
4497 'status': merge_status,
4502 'status': merge_status,
4498 'message': safe_str(msg),
4503 'message': safe_str(msg),
4499 }
4504 }
4500 else:
4505 else:
4501 merge_state = {'status': 'not_available',
4506 merge_state = {'status': 'not_available',
4502 'message': 'not_available'}
4507 'message': 'not_available'}
4503
4508
4504 merge_data = {
4509 merge_data = {
4505 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4510 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4506 'reference': (
4511 'reference': (
4507 pull_request.shadow_merge_ref.asdict()
4512 pull_request.shadow_merge_ref.asdict()
4508 if pull_request.shadow_merge_ref else None),
4513 if pull_request.shadow_merge_ref else None),
4509 }
4514 }
4510
4515
4511 data = {
4516 data = {
4512 'pull_request_id': pull_request.pull_request_id,
4517 'pull_request_id': pull_request.pull_request_id,
4513 'url': PullRequestModel().get_url(pull_request),
4518 'url': PullRequestModel().get_url(pull_request),
4514 'title': pull_request.title,
4519 'title': pull_request.title,
4515 'description': pull_request.description,
4520 'description': pull_request.description,
4516 'status': pull_request.status,
4521 'status': pull_request.status,
4517 'state': pull_request.pull_request_state,
4522 'state': pull_request.pull_request_state,
4518 'created_on': pull_request.created_on,
4523 'created_on': pull_request.created_on,
4519 'updated_on': pull_request.updated_on,
4524 'updated_on': pull_request.updated_on,
4520 'commit_ids': pull_request.revisions,
4525 'commit_ids': pull_request.revisions,
4521 'review_status': pull_request.calculated_review_status(),
4526 'review_status': pull_request.calculated_review_status(),
4522 'mergeable': merge_state,
4527 'mergeable': merge_state,
4523 'source': {
4528 'source': {
4524 'clone_url': pull_request.source_repo.clone_url(),
4529 'clone_url': pull_request.source_repo.clone_url(),
4525 'repository': pull_request.source_repo.repo_name,
4530 'repository': pull_request.source_repo.repo_name,
4526 'reference': {
4531 'reference': {
4527 'name': pull_request.source_ref_parts.name,
4532 'name': pull_request.source_ref_parts.name,
4528 'type': pull_request.source_ref_parts.type,
4533 'type': pull_request.source_ref_parts.type,
4529 'commit_id': pull_request.source_ref_parts.commit_id,
4534 'commit_id': pull_request.source_ref_parts.commit_id,
4530 },
4535 },
4531 },
4536 },
4532 'target': {
4537 'target': {
4533 'clone_url': pull_request.target_repo.clone_url(),
4538 'clone_url': pull_request.target_repo.clone_url(),
4534 'repository': pull_request.target_repo.repo_name,
4539 'repository': pull_request.target_repo.repo_name,
4535 'reference': {
4540 'reference': {
4536 'name': pull_request.target_ref_parts.name,
4541 'name': pull_request.target_ref_parts.name,
4537 'type': pull_request.target_ref_parts.type,
4542 'type': pull_request.target_ref_parts.type,
4538 'commit_id': pull_request.target_ref_parts.commit_id,
4543 'commit_id': pull_request.target_ref_parts.commit_id,
4539 },
4544 },
4540 },
4545 },
4541 'merge': merge_data,
4546 'merge': merge_data,
4542 'author': pull_request.author.get_api_data(include_secrets=False,
4547 'author': pull_request.author.get_api_data(include_secrets=False,
4543 details='basic'),
4548 details='basic'),
4544 'reviewers': [
4549 'reviewers': [
4545 {
4550 {
4546 'user': reviewer.get_api_data(include_secrets=False,
4551 'user': reviewer.get_api_data(include_secrets=False,
4547 details='basic'),
4552 details='basic'),
4548 'reasons': reasons,
4553 'reasons': reasons,
4549 'review_status': st[0][1].status if st else 'not_reviewed',
4554 'review_status': st[0][1].status if st else 'not_reviewed',
4550 }
4555 }
4551 for obj, reviewer, reasons, mandatory, st in
4556 for obj, reviewer, reasons, mandatory, st in
4552 pull_request.reviewers_statuses()
4557 pull_request.reviewers_statuses()
4553 ]
4558 ]
4554 }
4559 }
4555
4560
4556 return data
4561 return data
4557
4562
4558 def set_state(self, pull_request_state, final_state=None):
4563 def set_state(self, pull_request_state, final_state=None):
4559 """
4564 """
4560 # goes from initial state to updating to initial state.
4565 # goes from initial state to updating to initial state.
4561 # initial state can be changed by specifying back_state=
4566 # initial state can be changed by specifying back_state=
4562 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4567 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4563 pull_request.merge()
4568 pull_request.merge()
4564
4569
4565 :param pull_request_state:
4570 :param pull_request_state:
4566 :param final_state:
4571 :param final_state:
4567
4572
4568 """
4573 """
4569
4574
4570 return _SetState(self, pull_request_state, back_state=final_state)
4575 return _SetState(self, pull_request_state, back_state=final_state)
4571
4576
4572
4577
4573 class PullRequest(Base, _PullRequestBase):
4578 class PullRequest(Base, _PullRequestBase):
4574 __tablename__ = 'pull_requests'
4579 __tablename__ = 'pull_requests'
4575 __table_args__ = (
4580 __table_args__ = (
4576 base_table_args,
4581 base_table_args,
4577 )
4582 )
4578 LATEST_VER = 'latest'
4583 LATEST_VER = 'latest'
4579
4584
4580 pull_request_id = Column(
4585 pull_request_id = Column(
4581 'pull_request_id', Integer(), nullable=False, primary_key=True)
4586 'pull_request_id', Integer(), nullable=False, primary_key=True)
4582
4587
4583 def __repr__(self):
4588 def __repr__(self):
4584 if self.pull_request_id:
4589 if self.pull_request_id:
4585 return f'<DB:PullRequest #{self.pull_request_id}>'
4590 return f'<DB:PullRequest #{self.pull_request_id}>'
4586 else:
4591 else:
4587 return f'<DB:PullRequest at {id(self)!r}>'
4592 return f'<DB:PullRequest at {id(self)!r}>'
4588
4593
4589 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4594 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4590 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4595 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4591 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4596 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4592 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4597 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4593
4598
4594 @classmethod
4599 @classmethod
4595 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4600 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4596 internal_methods=None):
4601 internal_methods=None):
4597
4602
4598 class PullRequestDisplay(object):
4603 class PullRequestDisplay(object):
4599 """
4604 """
4600 Special object wrapper for showing PullRequest data via Versions
4605 Special object wrapper for showing PullRequest data via Versions
4601 It mimics PR object as close as possible. This is read only object
4606 It mimics PR object as close as possible. This is read only object
4602 just for display
4607 just for display
4603 """
4608 """
4604
4609
4605 def __init__(self, attrs, internal=None):
4610 def __init__(self, attrs, internal=None):
4606 self.attrs = attrs
4611 self.attrs = attrs
4607 # internal have priority over the given ones via attrs
4612 # internal have priority over the given ones via attrs
4608 self.internal = internal or ['versions']
4613 self.internal = internal or ['versions']
4609
4614
4610 def __getattr__(self, item):
4615 def __getattr__(self, item):
4611 if item in self.internal:
4616 if item in self.internal:
4612 return getattr(self, item)
4617 return getattr(self, item)
4613 try:
4618 try:
4614 return self.attrs[item]
4619 return self.attrs[item]
4615 except KeyError:
4620 except KeyError:
4616 raise AttributeError(
4621 raise AttributeError(
4617 '%s object has no attribute %s' % (self, item))
4622 '%s object has no attribute %s' % (self, item))
4618
4623
4619 def __repr__(self):
4624 def __repr__(self):
4620 pr_id = self.attrs.get('pull_request_id')
4625 pr_id = self.attrs.get('pull_request_id')
4621 return f'<DB:PullRequestDisplay #{pr_id}>'
4626 return f'<DB:PullRequestDisplay #{pr_id}>'
4622
4627
4623 def versions(self):
4628 def versions(self):
4624 return pull_request_obj.versions.order_by(
4629 return pull_request_obj.versions.order_by(
4625 PullRequestVersion.pull_request_version_id).all()
4630 PullRequestVersion.pull_request_version_id).all()
4626
4631
4627 def is_closed(self):
4632 def is_closed(self):
4628 return pull_request_obj.is_closed()
4633 return pull_request_obj.is_closed()
4629
4634
4630 def is_state_changing(self):
4635 def is_state_changing(self):
4631 return pull_request_obj.is_state_changing()
4636 return pull_request_obj.is_state_changing()
4632
4637
4633 @property
4638 @property
4634 def pull_request_version_id(self):
4639 def pull_request_version_id(self):
4635 return getattr(pull_request_obj, 'pull_request_version_id', None)
4640 return getattr(pull_request_obj, 'pull_request_version_id', None)
4636
4641
4637 @property
4642 @property
4638 def pull_request_last_version(self):
4643 def pull_request_last_version(self):
4639 return pull_request_obj.pull_request_last_version
4644 return pull_request_obj.pull_request_last_version
4640
4645
4641 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4646 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4642
4647
4643 attrs.author = StrictAttributeDict(
4648 attrs.author = StrictAttributeDict(
4644 pull_request_obj.author.get_api_data())
4649 pull_request_obj.author.get_api_data())
4645 if pull_request_obj.target_repo:
4650 if pull_request_obj.target_repo:
4646 attrs.target_repo = StrictAttributeDict(
4651 attrs.target_repo = StrictAttributeDict(
4647 pull_request_obj.target_repo.get_api_data())
4652 pull_request_obj.target_repo.get_api_data())
4648 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4653 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4649
4654
4650 if pull_request_obj.source_repo:
4655 if pull_request_obj.source_repo:
4651 attrs.source_repo = StrictAttributeDict(
4656 attrs.source_repo = StrictAttributeDict(
4652 pull_request_obj.source_repo.get_api_data())
4657 pull_request_obj.source_repo.get_api_data())
4653 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4658 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4654
4659
4655 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4660 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4656 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4661 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4657 attrs.revisions = pull_request_obj.revisions
4662 attrs.revisions = pull_request_obj.revisions
4658 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4663 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4659 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4664 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4660 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4665 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4661 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4666 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4662
4667
4663 return PullRequestDisplay(attrs, internal=internal_methods)
4668 return PullRequestDisplay(attrs, internal=internal_methods)
4664
4669
4665 def is_closed(self):
4670 def is_closed(self):
4666 return self.status == self.STATUS_CLOSED
4671 return self.status == self.STATUS_CLOSED
4667
4672
4668 def is_state_changing(self):
4673 def is_state_changing(self):
4669 return self.pull_request_state != PullRequest.STATE_CREATED
4674 return self.pull_request_state != PullRequest.STATE_CREATED
4670
4675
4671 def __json__(self):
4676 def __json__(self):
4672 return {
4677 return {
4673 'revisions': self.revisions,
4678 'revisions': self.revisions,
4674 'versions': self.versions_count
4679 'versions': self.versions_count
4675 }
4680 }
4676
4681
4677 def calculated_review_status(self):
4682 def calculated_review_status(self):
4678 from rhodecode.model.changeset_status import ChangesetStatusModel
4683 from rhodecode.model.changeset_status import ChangesetStatusModel
4679 return ChangesetStatusModel().calculated_review_status(self)
4684 return ChangesetStatusModel().calculated_review_status(self)
4680
4685
4681 def reviewers_statuses(self, user=None):
4686 def reviewers_statuses(self, user=None):
4682 from rhodecode.model.changeset_status import ChangesetStatusModel
4687 from rhodecode.model.changeset_status import ChangesetStatusModel
4683 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4688 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4684
4689
4685 def get_pull_request_reviewers(self, role=None):
4690 def get_pull_request_reviewers(self, role=None):
4686 qry = PullRequestReviewers.query()\
4691 qry = PullRequestReviewers.query()\
4687 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4692 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4688 if role:
4693 if role:
4689 qry = qry.filter(PullRequestReviewers.role == role)
4694 qry = qry.filter(PullRequestReviewers.role == role)
4690
4695
4691 return qry.all()
4696 return qry.all()
4692
4697
4693 @property
4698 @property
4694 def reviewers_count(self):
4699 def reviewers_count(self):
4695 qry = PullRequestReviewers.query()\
4700 qry = PullRequestReviewers.query()\
4696 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4701 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4697 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4702 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4698 return qry.count()
4703 return qry.count()
4699
4704
4700 @property
4705 @property
4701 def observers_count(self):
4706 def observers_count(self):
4702 qry = PullRequestReviewers.query()\
4707 qry = PullRequestReviewers.query()\
4703 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4708 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4704 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4709 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4705 return qry.count()
4710 return qry.count()
4706
4711
4707 def observers(self):
4712 def observers(self):
4708 qry = PullRequestReviewers.query()\
4713 qry = PullRequestReviewers.query()\
4709 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4714 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4710 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4715 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4711 .all()
4716 .all()
4712
4717
4713 for entry in qry:
4718 for entry in qry:
4714 yield entry, entry.user
4719 yield entry, entry.user
4715
4720
4716 @property
4721 @property
4717 def workspace_id(self):
4722 def workspace_id(self):
4718 from rhodecode.model.pull_request import PullRequestModel
4723 from rhodecode.model.pull_request import PullRequestModel
4719 return PullRequestModel()._workspace_id(self)
4724 return PullRequestModel()._workspace_id(self)
4720
4725
4721 def get_shadow_repo(self):
4726 def get_shadow_repo(self):
4722 workspace_id = self.workspace_id
4727 workspace_id = self.workspace_id
4723 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4728 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4724 if os.path.isdir(shadow_repository_path):
4729 if os.path.isdir(shadow_repository_path):
4725 vcs_obj = self.target_repo.scm_instance()
4730 vcs_obj = self.target_repo.scm_instance()
4726 return vcs_obj.get_shadow_instance(shadow_repository_path)
4731 return vcs_obj.get_shadow_instance(shadow_repository_path)
4727
4732
4728 @property
4733 @property
4729 def versions_count(self):
4734 def versions_count(self):
4730 """
4735 """
4731 return number of versions this PR have, e.g a PR that once been
4736 return number of versions this PR have, e.g a PR that once been
4732 updated will have 2 versions
4737 updated will have 2 versions
4733 """
4738 """
4734 return self.versions.count() + 1
4739 return self.versions.count() + 1
4735
4740
4736 @property
4741 @property
4737 def pull_request_last_version(self):
4742 def pull_request_last_version(self):
4738 return self.versions_count
4743 return self.versions_count
4739
4744
4740
4745
4741 class PullRequestVersion(Base, _PullRequestBase):
4746 class PullRequestVersion(Base, _PullRequestBase):
4742 __tablename__ = 'pull_request_versions'
4747 __tablename__ = 'pull_request_versions'
4743 __table_args__ = (
4748 __table_args__ = (
4744 base_table_args,
4749 base_table_args,
4745 )
4750 )
4746
4751
4747 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4752 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4748 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4753 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4749 pull_request = relationship('PullRequest', back_populates='versions')
4754 pull_request = relationship('PullRequest', back_populates='versions')
4750
4755
4751 def __repr__(self):
4756 def __repr__(self):
4752 if self.pull_request_version_id:
4757 if self.pull_request_version_id:
4753 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4758 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4754 else:
4759 else:
4755 return f'<DB:PullRequestVersion at {id(self)!r}>'
4760 return f'<DB:PullRequestVersion at {id(self)!r}>'
4756
4761
4757 @property
4762 @property
4758 def reviewers(self):
4763 def reviewers(self):
4759 return self.pull_request.reviewers
4764 return self.pull_request.reviewers
4760
4765
4761 @property
4766 @property
4762 def versions(self):
4767 def versions(self):
4763 return self.pull_request.versions
4768 return self.pull_request.versions
4764
4769
4765 def is_closed(self):
4770 def is_closed(self):
4766 # calculate from original
4771 # calculate from original
4767 return self.pull_request.status == self.STATUS_CLOSED
4772 return self.pull_request.status == self.STATUS_CLOSED
4768
4773
4769 def is_state_changing(self):
4774 def is_state_changing(self):
4770 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4775 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4771
4776
4772 def calculated_review_status(self):
4777 def calculated_review_status(self):
4773 return self.pull_request.calculated_review_status()
4778 return self.pull_request.calculated_review_status()
4774
4779
4775 def reviewers_statuses(self):
4780 def reviewers_statuses(self):
4776 return self.pull_request.reviewers_statuses()
4781 return self.pull_request.reviewers_statuses()
4777
4782
4778 def observers(self):
4783 def observers(self):
4779 return self.pull_request.observers()
4784 return self.pull_request.observers()
4780
4785
4781
4786
4782 class PullRequestReviewers(Base, BaseModel):
4787 class PullRequestReviewers(Base, BaseModel):
4783 __tablename__ = 'pull_request_reviewers'
4788 __tablename__ = 'pull_request_reviewers'
4784 __table_args__ = (
4789 __table_args__ = (
4785 base_table_args,
4790 base_table_args,
4786 )
4791 )
4787 ROLE_REVIEWER = 'reviewer'
4792 ROLE_REVIEWER = 'reviewer'
4788 ROLE_OBSERVER = 'observer'
4793 ROLE_OBSERVER = 'observer'
4789 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4794 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4790
4795
4791 @hybrid_property
4796 @hybrid_property
4792 def reasons(self):
4797 def reasons(self):
4793 if not self._reasons:
4798 if not self._reasons:
4794 return []
4799 return []
4795 return self._reasons
4800 return self._reasons
4796
4801
4797 @reasons.setter
4802 @reasons.setter
4798 def reasons(self, val):
4803 def reasons(self, val):
4799 val = val or []
4804 val = val or []
4800 if any(not isinstance(x, str) for x in val):
4805 if any(not isinstance(x, str) for x in val):
4801 raise Exception('invalid reasons type, must be list of strings')
4806 raise Exception('invalid reasons type, must be list of strings')
4802 self._reasons = val
4807 self._reasons = val
4803
4808
4804 pull_requests_reviewers_id = Column(
4809 pull_requests_reviewers_id = Column(
4805 'pull_requests_reviewers_id', Integer(), nullable=False,
4810 'pull_requests_reviewers_id', Integer(), nullable=False,
4806 primary_key=True)
4811 primary_key=True)
4807 pull_request_id = Column(
4812 pull_request_id = Column(
4808 "pull_request_id", Integer(),
4813 "pull_request_id", Integer(),
4809 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4814 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4810 user_id = Column(
4815 user_id = Column(
4811 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4816 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4812 _reasons = Column(
4817 _reasons = Column(
4813 'reason', MutationList.as_mutable(
4818 'reason', MutationList.as_mutable(
4814 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4819 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4815
4820
4816 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4821 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4817 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4822 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4818
4823
4819 user = relationship('User')
4824 user = relationship('User')
4820 pull_request = relationship('PullRequest', back_populates='reviewers')
4825 pull_request = relationship('PullRequest', back_populates='reviewers')
4821
4826
4822 rule_data = Column(
4827 rule_data = Column(
4823 'rule_data_json',
4828 'rule_data_json',
4824 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4829 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4825
4830
4826 def rule_user_group_data(self):
4831 def rule_user_group_data(self):
4827 """
4832 """
4828 Returns the voting user group rule data for this reviewer
4833 Returns the voting user group rule data for this reviewer
4829 """
4834 """
4830
4835
4831 if self.rule_data and 'vote_rule' in self.rule_data:
4836 if self.rule_data and 'vote_rule' in self.rule_data:
4832 user_group_data = {}
4837 user_group_data = {}
4833 if 'rule_user_group_entry_id' in self.rule_data:
4838 if 'rule_user_group_entry_id' in self.rule_data:
4834 # means a group with voting rules !
4839 # means a group with voting rules !
4835 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4840 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4836 user_group_data['name'] = self.rule_data['rule_name']
4841 user_group_data['name'] = self.rule_data['rule_name']
4837 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4842 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4838
4843
4839 return user_group_data
4844 return user_group_data
4840
4845
4841 @classmethod
4846 @classmethod
4842 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4847 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4843 qry = PullRequestReviewers.query()\
4848 qry = PullRequestReviewers.query()\
4844 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4849 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4845 if role:
4850 if role:
4846 qry = qry.filter(PullRequestReviewers.role == role)
4851 qry = qry.filter(PullRequestReviewers.role == role)
4847
4852
4848 return qry.all()
4853 return qry.all()
4849
4854
4850 def __repr__(self):
4855 def __repr__(self):
4851 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4856 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4852
4857
4853
4858
4854 class Notification(Base, BaseModel):
4859 class Notification(Base, BaseModel):
4855 __tablename__ = 'notifications'
4860 __tablename__ = 'notifications'
4856 __table_args__ = (
4861 __table_args__ = (
4857 Index('notification_type_idx', 'type'),
4862 Index('notification_type_idx', 'type'),
4858 base_table_args,
4863 base_table_args,
4859 )
4864 )
4860
4865
4861 TYPE_CHANGESET_COMMENT = 'cs_comment'
4866 TYPE_CHANGESET_COMMENT = 'cs_comment'
4862 TYPE_MESSAGE = 'message'
4867 TYPE_MESSAGE = 'message'
4863 TYPE_MENTION = 'mention'
4868 TYPE_MENTION = 'mention'
4864 TYPE_REGISTRATION = 'registration'
4869 TYPE_REGISTRATION = 'registration'
4865 TYPE_PULL_REQUEST = 'pull_request'
4870 TYPE_PULL_REQUEST = 'pull_request'
4866 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4871 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4867 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4872 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4868
4873
4869 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4874 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4870 subject = Column('subject', Unicode(512), nullable=True)
4875 subject = Column('subject', Unicode(512), nullable=True)
4871 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4876 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4872 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4877 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4873 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4878 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4874 type_ = Column('type', Unicode(255))
4879 type_ = Column('type', Unicode(255))
4875
4880
4876 created_by_user = relationship('User', back_populates='user_created_notifications')
4881 created_by_user = relationship('User', back_populates='user_created_notifications')
4877 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4882 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4878
4883
4879 @property
4884 @property
4880 def recipients(self):
4885 def recipients(self):
4881 return [x.user for x in UserNotification.query()\
4886 return [x.user for x in UserNotification.query()\
4882 .filter(UserNotification.notification == self)\
4887 .filter(UserNotification.notification == self)\
4883 .order_by(UserNotification.user_id.asc()).all()]
4888 .order_by(UserNotification.user_id.asc()).all()]
4884
4889
4885 @classmethod
4890 @classmethod
4886 def create(cls, created_by, subject, body, recipients, type_=None):
4891 def create(cls, created_by, subject, body, recipients, type_=None):
4887 if type_ is None:
4892 if type_ is None:
4888 type_ = Notification.TYPE_MESSAGE
4893 type_ = Notification.TYPE_MESSAGE
4889
4894
4890 notification = cls()
4895 notification = cls()
4891 notification.created_by_user = created_by
4896 notification.created_by_user = created_by
4892 notification.subject = subject
4897 notification.subject = subject
4893 notification.body = body
4898 notification.body = body
4894 notification.type_ = type_
4899 notification.type_ = type_
4895 notification.created_on = datetime.datetime.now()
4900 notification.created_on = datetime.datetime.now()
4896
4901
4897 # For each recipient link the created notification to his account
4902 # For each recipient link the created notification to his account
4898 for u in recipients:
4903 for u in recipients:
4899 assoc = UserNotification()
4904 assoc = UserNotification()
4900 assoc.user_id = u.user_id
4905 assoc.user_id = u.user_id
4901 assoc.notification = notification
4906 assoc.notification = notification
4902
4907
4903 # if created_by is inside recipients mark his notification
4908 # if created_by is inside recipients mark his notification
4904 # as read
4909 # as read
4905 if u.user_id == created_by.user_id:
4910 if u.user_id == created_by.user_id:
4906 assoc.read = True
4911 assoc.read = True
4907 Session().add(assoc)
4912 Session().add(assoc)
4908
4913
4909 Session().add(notification)
4914 Session().add(notification)
4910
4915
4911 return notification
4916 return notification
4912
4917
4913
4918
4914 class UserNotification(Base, BaseModel):
4919 class UserNotification(Base, BaseModel):
4915 __tablename__ = 'user_to_notification'
4920 __tablename__ = 'user_to_notification'
4916 __table_args__ = (
4921 __table_args__ = (
4917 UniqueConstraint('user_id', 'notification_id'),
4922 UniqueConstraint('user_id', 'notification_id'),
4918 base_table_args
4923 base_table_args
4919 )
4924 )
4920
4925
4921 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4926 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4922 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4927 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4923 read = Column('read', Boolean, default=False)
4928 read = Column('read', Boolean, default=False)
4924 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4929 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4925
4930
4926 user = relationship('User', lazy="joined", back_populates='notifications')
4931 user = relationship('User', lazy="joined", back_populates='notifications')
4927 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4932 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4928
4933
4929 def mark_as_read(self):
4934 def mark_as_read(self):
4930 self.read = True
4935 self.read = True
4931 Session().add(self)
4936 Session().add(self)
4932
4937
4933
4938
4934 class UserNotice(Base, BaseModel):
4939 class UserNotice(Base, BaseModel):
4935 __tablename__ = 'user_notices'
4940 __tablename__ = 'user_notices'
4936 __table_args__ = (
4941 __table_args__ = (
4937 base_table_args
4942 base_table_args
4938 )
4943 )
4939
4944
4940 NOTIFICATION_TYPE_MESSAGE = 'message'
4945 NOTIFICATION_TYPE_MESSAGE = 'message'
4941 NOTIFICATION_TYPE_NOTICE = 'notice'
4946 NOTIFICATION_TYPE_NOTICE = 'notice'
4942
4947
4943 NOTIFICATION_LEVEL_INFO = 'info'
4948 NOTIFICATION_LEVEL_INFO = 'info'
4944 NOTIFICATION_LEVEL_WARNING = 'warning'
4949 NOTIFICATION_LEVEL_WARNING = 'warning'
4945 NOTIFICATION_LEVEL_ERROR = 'error'
4950 NOTIFICATION_LEVEL_ERROR = 'error'
4946
4951
4947 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4952 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4948
4953
4949 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4954 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4950 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4955 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4951
4956
4952 notice_read = Column('notice_read', Boolean, default=False)
4957 notice_read = Column('notice_read', Boolean, default=False)
4953
4958
4954 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4959 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4955 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4960 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4956
4961
4957 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4962 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4958 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4963 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4959
4964
4960 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4965 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4961 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4966 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4962
4967
4963 @classmethod
4968 @classmethod
4964 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4969 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4965
4970
4966 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4971 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4967 cls.NOTIFICATION_LEVEL_WARNING,
4972 cls.NOTIFICATION_LEVEL_WARNING,
4968 cls.NOTIFICATION_LEVEL_INFO]:
4973 cls.NOTIFICATION_LEVEL_INFO]:
4969 return
4974 return
4970
4975
4971 from rhodecode.model.user import UserModel
4976 from rhodecode.model.user import UserModel
4972 user = UserModel().get_user(user)
4977 user = UserModel().get_user(user)
4973
4978
4974 new_notice = UserNotice()
4979 new_notice = UserNotice()
4975 if not allow_duplicate:
4980 if not allow_duplicate:
4976 existing_msg = UserNotice().query() \
4981 existing_msg = UserNotice().query() \
4977 .filter(UserNotice.user == user) \
4982 .filter(UserNotice.user == user) \
4978 .filter(UserNotice.notice_body == body) \
4983 .filter(UserNotice.notice_body == body) \
4979 .filter(UserNotice.notice_read == false()) \
4984 .filter(UserNotice.notice_read == false()) \
4980 .scalar()
4985 .scalar()
4981 if existing_msg:
4986 if existing_msg:
4982 log.warning('Ignoring duplicate notice for user %s', user)
4987 log.warning('Ignoring duplicate notice for user %s', user)
4983 return
4988 return
4984
4989
4985 new_notice.user = user
4990 new_notice.user = user
4986 new_notice.notice_subject = subject
4991 new_notice.notice_subject = subject
4987 new_notice.notice_body = body
4992 new_notice.notice_body = body
4988 new_notice.notification_level = notice_level
4993 new_notice.notification_level = notice_level
4989 Session().add(new_notice)
4994 Session().add(new_notice)
4990 Session().commit()
4995 Session().commit()
4991
4996
4992
4997
4993 class Gist(Base, BaseModel):
4998 class Gist(Base, BaseModel):
4994 __tablename__ = 'gists'
4999 __tablename__ = 'gists'
4995 __table_args__ = (
5000 __table_args__ = (
4996 Index('g_gist_access_id_idx', 'gist_access_id'),
5001 Index('g_gist_access_id_idx', 'gist_access_id'),
4997 Index('g_created_on_idx', 'created_on'),
5002 Index('g_created_on_idx', 'created_on'),
4998 base_table_args
5003 base_table_args
4999 )
5004 )
5000
5005
5001 GIST_PUBLIC = 'public'
5006 GIST_PUBLIC = 'public'
5002 GIST_PRIVATE = 'private'
5007 GIST_PRIVATE = 'private'
5003 DEFAULT_FILENAME = 'gistfile1.txt'
5008 DEFAULT_FILENAME = 'gistfile1.txt'
5004
5009
5005 ACL_LEVEL_PUBLIC = 'acl_public'
5010 ACL_LEVEL_PUBLIC = 'acl_public'
5006 ACL_LEVEL_PRIVATE = 'acl_private'
5011 ACL_LEVEL_PRIVATE = 'acl_private'
5007
5012
5008 gist_id = Column('gist_id', Integer(), primary_key=True)
5013 gist_id = Column('gist_id', Integer(), primary_key=True)
5009 gist_access_id = Column('gist_access_id', Unicode(250))
5014 gist_access_id = Column('gist_access_id', Unicode(250))
5010 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
5015 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
5011 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
5016 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
5012 gist_expires = Column('gist_expires', Float(53), nullable=False)
5017 gist_expires = Column('gist_expires', Float(53), nullable=False)
5013 gist_type = Column('gist_type', Unicode(128), nullable=False)
5018 gist_type = Column('gist_type', Unicode(128), nullable=False)
5014 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5019 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5015 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5020 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5016 acl_level = Column('acl_level', Unicode(128), nullable=True)
5021 acl_level = Column('acl_level', Unicode(128), nullable=True)
5017
5022
5018 owner = relationship('User', back_populates='user_gists')
5023 owner = relationship('User', back_populates='user_gists')
5019
5024
5020 def __repr__(self):
5025 def __repr__(self):
5021 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
5026 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
5022
5027
5023 @hybrid_property
5028 @hybrid_property
5024 def description_safe(self):
5029 def description_safe(self):
5025 from rhodecode.lib import helpers as h
5030 from rhodecode.lib import helpers as h
5026 return h.escape(self.gist_description)
5031 return h.escape(self.gist_description)
5027
5032
5028 @classmethod
5033 @classmethod
5029 def get_or_404(cls, id_):
5034 def get_or_404(cls, id_):
5030 from pyramid.httpexceptions import HTTPNotFound
5035 from pyramid.httpexceptions import HTTPNotFound
5031
5036
5032 res = cls.query().filter(cls.gist_access_id == id_).scalar()
5037 res = cls.query().filter(cls.gist_access_id == id_).scalar()
5033 if not res:
5038 if not res:
5034 log.debug('WARN: No DB entry with id %s', id_)
5039 log.debug('WARN: No DB entry with id %s', id_)
5035 raise HTTPNotFound()
5040 raise HTTPNotFound()
5036 return res
5041 return res
5037
5042
5038 @classmethod
5043 @classmethod
5039 def get_by_access_id(cls, gist_access_id):
5044 def get_by_access_id(cls, gist_access_id):
5040 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
5045 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
5041
5046
5042 def gist_url(self):
5047 def gist_url(self):
5043 from rhodecode.model.gist import GistModel
5048 from rhodecode.model.gist import GistModel
5044 return GistModel().get_url(self)
5049 return GistModel().get_url(self)
5045
5050
5046 @classmethod
5051 @classmethod
5047 def base_path(cls):
5052 def base_path(cls):
5048 """
5053 """
5049 Returns base path when all gists are stored
5054 Returns base path when all gists are stored
5050
5055
5051 :param cls:
5056 :param cls:
5052 """
5057 """
5053 from rhodecode.model.gist import GIST_STORE_LOC
5058 from rhodecode.model.gist import GIST_STORE_LOC
5054 from rhodecode.lib.utils import get_rhodecode_repo_store_path
5059 from rhodecode.lib.utils import get_rhodecode_repo_store_path
5055 repo_store_path = get_rhodecode_repo_store_path()
5060 repo_store_path = get_rhodecode_repo_store_path()
5056 return os.path.join(repo_store_path, GIST_STORE_LOC)
5061 return os.path.join(repo_store_path, GIST_STORE_LOC)
5057
5062
5058 def get_api_data(self):
5063 def get_api_data(self):
5059 """
5064 """
5060 Common function for generating gist related data for API
5065 Common function for generating gist related data for API
5061 """
5066 """
5062 gist = self
5067 gist = self
5063 data = {
5068 data = {
5064 'gist_id': gist.gist_id,
5069 'gist_id': gist.gist_id,
5065 'type': gist.gist_type,
5070 'type': gist.gist_type,
5066 'access_id': gist.gist_access_id,
5071 'access_id': gist.gist_access_id,
5067 'description': gist.gist_description,
5072 'description': gist.gist_description,
5068 'url': gist.gist_url(),
5073 'url': gist.gist_url(),
5069 'expires': gist.gist_expires,
5074 'expires': gist.gist_expires,
5070 'created_on': gist.created_on,
5075 'created_on': gist.created_on,
5071 'modified_at': gist.modified_at,
5076 'modified_at': gist.modified_at,
5072 'content': None,
5077 'content': None,
5073 'acl_level': gist.acl_level,
5078 'acl_level': gist.acl_level,
5074 }
5079 }
5075 return data
5080 return data
5076
5081
5077 def __json__(self):
5082 def __json__(self):
5078 data = dict()
5083 data = dict()
5079 data.update(self.get_api_data())
5084 data.update(self.get_api_data())
5080 return data
5085 return data
5081 # SCM functions
5086 # SCM functions
5082
5087
5083 def scm_instance(self, **kwargs):
5088 def scm_instance(self, **kwargs):
5084 """
5089 """
5085 Get an instance of VCS Repository
5090 Get an instance of VCS Repository
5086
5091
5087 :param kwargs:
5092 :param kwargs:
5088 """
5093 """
5089 from rhodecode.model.gist import GistModel
5094 from rhodecode.model.gist import GistModel
5090 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5095 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5091 return get_vcs_instance(
5096 return get_vcs_instance(
5092 repo_path=safe_str(full_repo_path), create=False,
5097 repo_path=safe_str(full_repo_path), create=False,
5093 _vcs_alias=GistModel.vcs_backend)
5098 _vcs_alias=GistModel.vcs_backend)
5094
5099
5095
5100
5096 class ExternalIdentity(Base, BaseModel):
5101 class ExternalIdentity(Base, BaseModel):
5097 __tablename__ = 'external_identities'
5102 __tablename__ = 'external_identities'
5098 __table_args__ = (
5103 __table_args__ = (
5099 Index('local_user_id_idx', 'local_user_id'),
5104 Index('local_user_id_idx', 'local_user_id'),
5100 Index('external_id_idx', 'external_id'),
5105 Index('external_id_idx', 'external_id'),
5101 base_table_args
5106 base_table_args
5102 )
5107 )
5103
5108
5104 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5109 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5105 external_username = Column('external_username', Unicode(1024), default='')
5110 external_username = Column('external_username', Unicode(1024), default='')
5106 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5111 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5107 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5112 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5108 access_token = Column('access_token', String(1024), default='')
5113 access_token = Column('access_token', String(1024), default='')
5109 alt_token = Column('alt_token', String(1024), default='')
5114 alt_token = Column('alt_token', String(1024), default='')
5110 token_secret = Column('token_secret', String(1024), default='')
5115 token_secret = Column('token_secret', String(1024), default='')
5111
5116
5112 @classmethod
5117 @classmethod
5113 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5118 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5114 """
5119 """
5115 Returns ExternalIdentity instance based on search params
5120 Returns ExternalIdentity instance based on search params
5116
5121
5117 :param external_id:
5122 :param external_id:
5118 :param provider_name:
5123 :param provider_name:
5119 :return: ExternalIdentity
5124 :return: ExternalIdentity
5120 """
5125 """
5121 query = cls.query()
5126 query = cls.query()
5122 query = query.filter(cls.external_id == external_id)
5127 query = query.filter(cls.external_id == external_id)
5123 query = query.filter(cls.provider_name == provider_name)
5128 query = query.filter(cls.provider_name == provider_name)
5124 if local_user_id:
5129 if local_user_id:
5125 query = query.filter(cls.local_user_id == local_user_id)
5130 query = query.filter(cls.local_user_id == local_user_id)
5126 return query.first()
5131 return query.first()
5127
5132
5128 @classmethod
5133 @classmethod
5129 def user_by_external_id_and_provider(cls, external_id, provider_name):
5134 def user_by_external_id_and_provider(cls, external_id, provider_name):
5130 """
5135 """
5131 Returns User instance based on search params
5136 Returns User instance based on search params
5132
5137
5133 :param external_id:
5138 :param external_id:
5134 :param provider_name:
5139 :param provider_name:
5135 :return: User
5140 :return: User
5136 """
5141 """
5137 query = User.query()
5142 query = User.query()
5138 query = query.filter(cls.external_id == external_id)
5143 query = query.filter(cls.external_id == external_id)
5139 query = query.filter(cls.provider_name == provider_name)
5144 query = query.filter(cls.provider_name == provider_name)
5140 query = query.filter(User.user_id == cls.local_user_id)
5145 query = query.filter(User.user_id == cls.local_user_id)
5141 return query.first()
5146 return query.first()
5142
5147
5143 @classmethod
5148 @classmethod
5144 def by_local_user_id(cls, local_user_id):
5149 def by_local_user_id(cls, local_user_id):
5145 """
5150 """
5146 Returns all tokens for user
5151 Returns all tokens for user
5147
5152
5148 :param local_user_id:
5153 :param local_user_id:
5149 :return: ExternalIdentity
5154 :return: ExternalIdentity
5150 """
5155 """
5151 query = cls.query()
5156 query = cls.query()
5152 query = query.filter(cls.local_user_id == local_user_id)
5157 query = query.filter(cls.local_user_id == local_user_id)
5153 return query
5158 return query
5154
5159
5155 @classmethod
5160 @classmethod
5156 def load_provider_plugin(cls, plugin_id):
5161 def load_provider_plugin(cls, plugin_id):
5157 from rhodecode.authentication.base import loadplugin
5162 from rhodecode.authentication.base import loadplugin
5158 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5163 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5159 auth_plugin = loadplugin(_plugin_id)
5164 auth_plugin = loadplugin(_plugin_id)
5160 return auth_plugin
5165 return auth_plugin
5161
5166
5162
5167
5163 class Integration(Base, BaseModel):
5168 class Integration(Base, BaseModel):
5164 __tablename__ = 'integrations'
5169 __tablename__ = 'integrations'
5165 __table_args__ = (
5170 __table_args__ = (
5166 base_table_args
5171 base_table_args
5167 )
5172 )
5168
5173
5169 integration_id = Column('integration_id', Integer(), primary_key=True)
5174 integration_id = Column('integration_id', Integer(), primary_key=True)
5170 integration_type = Column('integration_type', String(255))
5175 integration_type = Column('integration_type', String(255))
5171 enabled = Column('enabled', Boolean(), nullable=False)
5176 enabled = Column('enabled', Boolean(), nullable=False)
5172 name = Column('name', String(255), nullable=False)
5177 name = Column('name', String(255), nullable=False)
5173 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5178 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5174
5179
5175 settings = Column(
5180 settings = Column(
5176 'settings_json', MutationObj.as_mutable(
5181 'settings_json', MutationObj.as_mutable(
5177 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5182 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5178 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5183 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5179 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5184 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5180
5185
5181 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5186 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5182 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5187 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5183
5188
5184 @property
5189 @property
5185 def scope(self):
5190 def scope(self):
5186 if self.repo:
5191 if self.repo:
5187 return repr(self.repo)
5192 return repr(self.repo)
5188 if self.repo_group:
5193 if self.repo_group:
5189 if self.child_repos_only:
5194 if self.child_repos_only:
5190 return repr(self.repo_group) + ' (child repos only)'
5195 return repr(self.repo_group) + ' (child repos only)'
5191 else:
5196 else:
5192 return repr(self.repo_group) + ' (recursive)'
5197 return repr(self.repo_group) + ' (recursive)'
5193 if self.child_repos_only:
5198 if self.child_repos_only:
5194 return 'root_repos'
5199 return 'root_repos'
5195 return 'global'
5200 return 'global'
5196
5201
5197 def __repr__(self):
5202 def __repr__(self):
5198 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5203 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5199
5204
5200
5205
5201 class RepoReviewRuleUser(Base, BaseModel):
5206 class RepoReviewRuleUser(Base, BaseModel):
5202 __tablename__ = 'repo_review_rules_users'
5207 __tablename__ = 'repo_review_rules_users'
5203 __table_args__ = (
5208 __table_args__ = (
5204 base_table_args
5209 base_table_args
5205 )
5210 )
5206 ROLE_REVIEWER = 'reviewer'
5211 ROLE_REVIEWER = 'reviewer'
5207 ROLE_OBSERVER = 'observer'
5212 ROLE_OBSERVER = 'observer'
5208 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5213 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5209
5214
5210 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5215 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5211 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5216 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5212 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5217 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5213 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5218 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5214 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5219 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5215 user = relationship('User', back_populates='user_review_rules')
5220 user = relationship('User', back_populates='user_review_rules')
5216
5221
5217 def rule_data(self):
5222 def rule_data(self):
5218 return {
5223 return {
5219 'mandatory': self.mandatory,
5224 'mandatory': self.mandatory,
5220 'role': self.role,
5225 'role': self.role,
5221 }
5226 }
5222
5227
5223
5228
5224 class RepoReviewRuleUserGroup(Base, BaseModel):
5229 class RepoReviewRuleUserGroup(Base, BaseModel):
5225 __tablename__ = 'repo_review_rules_users_groups'
5230 __tablename__ = 'repo_review_rules_users_groups'
5226 __table_args__ = (
5231 __table_args__ = (
5227 base_table_args
5232 base_table_args
5228 )
5233 )
5229
5234
5230 VOTE_RULE_ALL = -1
5235 VOTE_RULE_ALL = -1
5231 ROLE_REVIEWER = 'reviewer'
5236 ROLE_REVIEWER = 'reviewer'
5232 ROLE_OBSERVER = 'observer'
5237 ROLE_OBSERVER = 'observer'
5233 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5238 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5234
5239
5235 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5240 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5236 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5241 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5237 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5242 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5238 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5243 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5239 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5244 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5240 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5245 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5241 users_group = relationship('UserGroup')
5246 users_group = relationship('UserGroup')
5242
5247
5243 def rule_data(self):
5248 def rule_data(self):
5244 return {
5249 return {
5245 'mandatory': self.mandatory,
5250 'mandatory': self.mandatory,
5246 'role': self.role,
5251 'role': self.role,
5247 'vote_rule': self.vote_rule
5252 'vote_rule': self.vote_rule
5248 }
5253 }
5249
5254
5250 @property
5255 @property
5251 def vote_rule_label(self):
5256 def vote_rule_label(self):
5252 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5257 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5253 return 'all must vote'
5258 return 'all must vote'
5254 else:
5259 else:
5255 return 'min. vote {}'.format(self.vote_rule)
5260 return 'min. vote {}'.format(self.vote_rule)
5256
5261
5257
5262
5258 class RepoReviewRule(Base, BaseModel):
5263 class RepoReviewRule(Base, BaseModel):
5259 __tablename__ = 'repo_review_rules'
5264 __tablename__ = 'repo_review_rules'
5260 __table_args__ = (
5265 __table_args__ = (
5261 base_table_args
5266 base_table_args
5262 )
5267 )
5263
5268
5264 repo_review_rule_id = Column(
5269 repo_review_rule_id = Column(
5265 'repo_review_rule_id', Integer(), primary_key=True)
5270 'repo_review_rule_id', Integer(), primary_key=True)
5266 repo_id = Column(
5271 repo_id = Column(
5267 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5272 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5268 repo = relationship('Repository', back_populates='review_rules')
5273 repo = relationship('Repository', back_populates='review_rules')
5269
5274
5270 review_rule_name = Column('review_rule_name', String(255))
5275 review_rule_name = Column('review_rule_name', String(255))
5271 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5276 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5272 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5277 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5273 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5278 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5274
5279
5275 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5280 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5276
5281
5277 # Legacy fields, just for backward compat
5282 # Legacy fields, just for backward compat
5278 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5283 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5279 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5284 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5280
5285
5281 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5286 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5282 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5287 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5283
5288
5284 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5289 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5285
5290
5286 rule_users = relationship('RepoReviewRuleUser')
5291 rule_users = relationship('RepoReviewRuleUser')
5287 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5292 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5288
5293
5289 def _validate_pattern(self, value):
5294 def _validate_pattern(self, value):
5290 re.compile('^' + glob2re(value) + '$')
5295 re.compile('^' + glob2re(value) + '$')
5291
5296
5292 @hybrid_property
5297 @hybrid_property
5293 def source_branch_pattern(self):
5298 def source_branch_pattern(self):
5294 return self._branch_pattern or '*'
5299 return self._branch_pattern or '*'
5295
5300
5296 @source_branch_pattern.setter
5301 @source_branch_pattern.setter
5297 def source_branch_pattern(self, value):
5302 def source_branch_pattern(self, value):
5298 self._validate_pattern(value)
5303 self._validate_pattern(value)
5299 self._branch_pattern = value or '*'
5304 self._branch_pattern = value or '*'
5300
5305
5301 @hybrid_property
5306 @hybrid_property
5302 def target_branch_pattern(self):
5307 def target_branch_pattern(self):
5303 return self._target_branch_pattern or '*'
5308 return self._target_branch_pattern or '*'
5304
5309
5305 @target_branch_pattern.setter
5310 @target_branch_pattern.setter
5306 def target_branch_pattern(self, value):
5311 def target_branch_pattern(self, value):
5307 self._validate_pattern(value)
5312 self._validate_pattern(value)
5308 self._target_branch_pattern = value or '*'
5313 self._target_branch_pattern = value or '*'
5309
5314
5310 @hybrid_property
5315 @hybrid_property
5311 def file_pattern(self):
5316 def file_pattern(self):
5312 return self._file_pattern or '*'
5317 return self._file_pattern or '*'
5313
5318
5314 @file_pattern.setter
5319 @file_pattern.setter
5315 def file_pattern(self, value):
5320 def file_pattern(self, value):
5316 self._validate_pattern(value)
5321 self._validate_pattern(value)
5317 self._file_pattern = value or '*'
5322 self._file_pattern = value or '*'
5318
5323
5319 @hybrid_property
5324 @hybrid_property
5320 def forbid_pr_author_to_review(self):
5325 def forbid_pr_author_to_review(self):
5321 return self.pr_author == 'forbid_pr_author'
5326 return self.pr_author == 'forbid_pr_author'
5322
5327
5323 @hybrid_property
5328 @hybrid_property
5324 def include_pr_author_to_review(self):
5329 def include_pr_author_to_review(self):
5325 return self.pr_author == 'include_pr_author'
5330 return self.pr_author == 'include_pr_author'
5326
5331
5327 @hybrid_property
5332 @hybrid_property
5328 def forbid_commit_author_to_review(self):
5333 def forbid_commit_author_to_review(self):
5329 return self.commit_author == 'forbid_commit_author'
5334 return self.commit_author == 'forbid_commit_author'
5330
5335
5331 @hybrid_property
5336 @hybrid_property
5332 def include_commit_author_to_review(self):
5337 def include_commit_author_to_review(self):
5333 return self.commit_author == 'include_commit_author'
5338 return self.commit_author == 'include_commit_author'
5334
5339
5335 def matches(self, source_branch, target_branch, files_changed):
5340 def matches(self, source_branch, target_branch, files_changed):
5336 """
5341 """
5337 Check if this review rule matches a branch/files in a pull request
5342 Check if this review rule matches a branch/files in a pull request
5338
5343
5339 :param source_branch: source branch name for the commit
5344 :param source_branch: source branch name for the commit
5340 :param target_branch: target branch name for the commit
5345 :param target_branch: target branch name for the commit
5341 :param files_changed: list of file paths changed in the pull request
5346 :param files_changed: list of file paths changed in the pull request
5342 """
5347 """
5343
5348
5344 source_branch = source_branch or ''
5349 source_branch = source_branch or ''
5345 target_branch = target_branch or ''
5350 target_branch = target_branch or ''
5346 files_changed = files_changed or []
5351 files_changed = files_changed or []
5347
5352
5348 branch_matches = True
5353 branch_matches = True
5349 if source_branch or target_branch:
5354 if source_branch or target_branch:
5350 if self.source_branch_pattern == '*':
5355 if self.source_branch_pattern == '*':
5351 source_branch_match = True
5356 source_branch_match = True
5352 else:
5357 else:
5353 if self.source_branch_pattern.startswith('re:'):
5358 if self.source_branch_pattern.startswith('re:'):
5354 source_pattern = self.source_branch_pattern[3:]
5359 source_pattern = self.source_branch_pattern[3:]
5355 else:
5360 else:
5356 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5361 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5357 source_branch_regex = re.compile(source_pattern)
5362 source_branch_regex = re.compile(source_pattern)
5358 source_branch_match = bool(source_branch_regex.search(source_branch))
5363 source_branch_match = bool(source_branch_regex.search(source_branch))
5359 if self.target_branch_pattern == '*':
5364 if self.target_branch_pattern == '*':
5360 target_branch_match = True
5365 target_branch_match = True
5361 else:
5366 else:
5362 if self.target_branch_pattern.startswith('re:'):
5367 if self.target_branch_pattern.startswith('re:'):
5363 target_pattern = self.target_branch_pattern[3:]
5368 target_pattern = self.target_branch_pattern[3:]
5364 else:
5369 else:
5365 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5370 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5366 target_branch_regex = re.compile(target_pattern)
5371 target_branch_regex = re.compile(target_pattern)
5367 target_branch_match = bool(target_branch_regex.search(target_branch))
5372 target_branch_match = bool(target_branch_regex.search(target_branch))
5368
5373
5369 branch_matches = source_branch_match and target_branch_match
5374 branch_matches = source_branch_match and target_branch_match
5370
5375
5371 files_matches = True
5376 files_matches = True
5372 if self.file_pattern != '*':
5377 if self.file_pattern != '*':
5373 files_matches = False
5378 files_matches = False
5374 if self.file_pattern.startswith('re:'):
5379 if self.file_pattern.startswith('re:'):
5375 file_pattern = self.file_pattern[3:]
5380 file_pattern = self.file_pattern[3:]
5376 else:
5381 else:
5377 file_pattern = glob2re(self.file_pattern)
5382 file_pattern = glob2re(self.file_pattern)
5378 file_regex = re.compile(file_pattern)
5383 file_regex = re.compile(file_pattern)
5379 for file_data in files_changed:
5384 for file_data in files_changed:
5380 filename = file_data.get('filename')
5385 filename = file_data.get('filename')
5381
5386
5382 if file_regex.search(filename):
5387 if file_regex.search(filename):
5383 files_matches = True
5388 files_matches = True
5384 break
5389 break
5385
5390
5386 return branch_matches and files_matches
5391 return branch_matches and files_matches
5387
5392
5388 @property
5393 @property
5389 def review_users(self):
5394 def review_users(self):
5390 """ Returns the users which this rule applies to """
5395 """ Returns the users which this rule applies to """
5391
5396
5392 users = collections.OrderedDict()
5397 users = collections.OrderedDict()
5393
5398
5394 for rule_user in self.rule_users:
5399 for rule_user in self.rule_users:
5395 if rule_user.user.active:
5400 if rule_user.user.active:
5396 if rule_user.user not in users:
5401 if rule_user.user not in users:
5397 users[rule_user.user.username] = {
5402 users[rule_user.user.username] = {
5398 'user': rule_user.user,
5403 'user': rule_user.user,
5399 'source': 'user',
5404 'source': 'user',
5400 'source_data': {},
5405 'source_data': {},
5401 'data': rule_user.rule_data()
5406 'data': rule_user.rule_data()
5402 }
5407 }
5403
5408
5404 for rule_user_group in self.rule_user_groups:
5409 for rule_user_group in self.rule_user_groups:
5405 source_data = {
5410 source_data = {
5406 'user_group_id': rule_user_group.users_group.users_group_id,
5411 'user_group_id': rule_user_group.users_group.users_group_id,
5407 'name': rule_user_group.users_group.users_group_name,
5412 'name': rule_user_group.users_group.users_group_name,
5408 'members': len(rule_user_group.users_group.members)
5413 'members': len(rule_user_group.users_group.members)
5409 }
5414 }
5410 for member in rule_user_group.users_group.members:
5415 for member in rule_user_group.users_group.members:
5411 if member.user.active:
5416 if member.user.active:
5412 key = member.user.username
5417 key = member.user.username
5413 if key in users:
5418 if key in users:
5414 # skip this member as we have him already
5419 # skip this member as we have him already
5415 # this prevents from override the "first" matched
5420 # this prevents from override the "first" matched
5416 # users with duplicates in multiple groups
5421 # users with duplicates in multiple groups
5417 continue
5422 continue
5418
5423
5419 users[key] = {
5424 users[key] = {
5420 'user': member.user,
5425 'user': member.user,
5421 'source': 'user_group',
5426 'source': 'user_group',
5422 'source_data': source_data,
5427 'source_data': source_data,
5423 'data': rule_user_group.rule_data()
5428 'data': rule_user_group.rule_data()
5424 }
5429 }
5425
5430
5426 return users
5431 return users
5427
5432
5428 def user_group_vote_rule(self, user_id):
5433 def user_group_vote_rule(self, user_id):
5429
5434
5430 rules = []
5435 rules = []
5431 if not self.rule_user_groups:
5436 if not self.rule_user_groups:
5432 return rules
5437 return rules
5433
5438
5434 for user_group in self.rule_user_groups:
5439 for user_group in self.rule_user_groups:
5435 user_group_members = [x.user_id for x in user_group.users_group.members]
5440 user_group_members = [x.user_id for x in user_group.users_group.members]
5436 if user_id in user_group_members:
5441 if user_id in user_group_members:
5437 rules.append(user_group)
5442 rules.append(user_group)
5438 return rules
5443 return rules
5439
5444
5440 def __repr__(self):
5445 def __repr__(self):
5441 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5446 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5442
5447
5443
5448
5444 class ScheduleEntry(Base, BaseModel):
5449 class ScheduleEntry(Base, BaseModel):
5445 __tablename__ = 'schedule_entries'
5450 __tablename__ = 'schedule_entries'
5446 __table_args__ = (
5451 __table_args__ = (
5447 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5452 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5448 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5453 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5449 base_table_args,
5454 base_table_args,
5450 )
5455 )
5451 SCHEDULE_TYPE_INTEGER = "integer"
5456 SCHEDULE_TYPE_INTEGER = "integer"
5452 SCHEDULE_TYPE_CRONTAB = "crontab"
5457 SCHEDULE_TYPE_CRONTAB = "crontab"
5453
5458
5454 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5459 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5455 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5460 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5456
5461
5457 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5462 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5458 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5463 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5459 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5464 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5460
5465
5461 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5466 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5462 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5467 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5463
5468
5464 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5469 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5465 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5470 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5466
5471
5467 # task
5472 # task
5468 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5473 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5469 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5474 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5470 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5475 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5471 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5476 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5472
5477
5473 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5478 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5474 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5479 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5475
5480
5476 @hybrid_property
5481 @hybrid_property
5477 def schedule_type(self):
5482 def schedule_type(self):
5478 return self._schedule_type
5483 return self._schedule_type
5479
5484
5480 @schedule_type.setter
5485 @schedule_type.setter
5481 def schedule_type(self, val):
5486 def schedule_type(self, val):
5482 if val not in self.schedule_types:
5487 if val not in self.schedule_types:
5483 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5488 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5484 val, self.schedule_type))
5489 val, self.schedule_type))
5485
5490
5486 self._schedule_type = val
5491 self._schedule_type = val
5487
5492
5488 @classmethod
5493 @classmethod
5489 def get_uid(cls, obj):
5494 def get_uid(cls, obj):
5490 args = obj.task_args
5495 args = obj.task_args
5491 kwargs = obj.task_kwargs
5496 kwargs = obj.task_kwargs
5492 if isinstance(args, JsonRaw):
5497 if isinstance(args, JsonRaw):
5493 try:
5498 try:
5494 args = json.loads(args)
5499 args = json.loads(args)
5495 except ValueError:
5500 except ValueError:
5496 args = tuple()
5501 args = tuple()
5497
5502
5498 if isinstance(kwargs, JsonRaw):
5503 if isinstance(kwargs, JsonRaw):
5499 try:
5504 try:
5500 kwargs = json.loads(kwargs)
5505 kwargs = json.loads(kwargs)
5501 except ValueError:
5506 except ValueError:
5502 kwargs = dict()
5507 kwargs = dict()
5503
5508
5504 dot_notation = obj.task_dot_notation
5509 dot_notation = obj.task_dot_notation
5505 val = '.'.join(map(safe_str, [
5510 val = '.'.join(map(safe_str, [
5506 sorted(dot_notation), args, sorted(kwargs.items())]))
5511 sorted(dot_notation), args, sorted(kwargs.items())]))
5507 return sha1(safe_bytes(val))
5512 return sha1(safe_bytes(val))
5508
5513
5509 @classmethod
5514 @classmethod
5510 def get_by_schedule_name(cls, schedule_name):
5515 def get_by_schedule_name(cls, schedule_name):
5511 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5516 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5512
5517
5513 @classmethod
5518 @classmethod
5514 def get_by_schedule_id(cls, schedule_id):
5519 def get_by_schedule_id(cls, schedule_id):
5515 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5520 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5516
5521
5517 @property
5522 @property
5518 def task(self):
5523 def task(self):
5519 return self.task_dot_notation
5524 return self.task_dot_notation
5520
5525
5521 @property
5526 @property
5522 def schedule(self):
5527 def schedule(self):
5523 from rhodecode.lib.celerylib.utils import raw_2_schedule
5528 from rhodecode.lib.celerylib.utils import raw_2_schedule
5524 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5529 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5525 return schedule
5530 return schedule
5526
5531
5527 @property
5532 @property
5528 def args(self):
5533 def args(self):
5529 try:
5534 try:
5530 return list(self.task_args or [])
5535 return list(self.task_args or [])
5531 except ValueError:
5536 except ValueError:
5532 return list()
5537 return list()
5533
5538
5534 @property
5539 @property
5535 def kwargs(self):
5540 def kwargs(self):
5536 try:
5541 try:
5537 return dict(self.task_kwargs or {})
5542 return dict(self.task_kwargs or {})
5538 except ValueError:
5543 except ValueError:
5539 return dict()
5544 return dict()
5540
5545
5541 def _as_raw(self, val, indent=False):
5546 def _as_raw(self, val, indent=False):
5542 if hasattr(val, 'de_coerce'):
5547 if hasattr(val, 'de_coerce'):
5543 val = val.de_coerce()
5548 val = val.de_coerce()
5544 if val:
5549 if val:
5545 if indent:
5550 if indent:
5546 val = ext_json.formatted_str_json(val)
5551 val = ext_json.formatted_str_json(val)
5547 else:
5552 else:
5548 val = ext_json.str_json(val)
5553 val = ext_json.str_json(val)
5549
5554
5550 return val
5555 return val
5551
5556
5552 @property
5557 @property
5553 def schedule_definition_raw(self):
5558 def schedule_definition_raw(self):
5554 return self._as_raw(self.schedule_definition)
5559 return self._as_raw(self.schedule_definition)
5555
5560
5556 def args_raw(self, indent=False):
5561 def args_raw(self, indent=False):
5557 return self._as_raw(self.task_args, indent)
5562 return self._as_raw(self.task_args, indent)
5558
5563
5559 def kwargs_raw(self, indent=False):
5564 def kwargs_raw(self, indent=False):
5560 return self._as_raw(self.task_kwargs, indent)
5565 return self._as_raw(self.task_kwargs, indent)
5561
5566
5562 def __repr__(self):
5567 def __repr__(self):
5563 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5568 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5564
5569
5565
5570
5566 @event.listens_for(ScheduleEntry, 'before_update')
5571 @event.listens_for(ScheduleEntry, 'before_update')
5567 def update_task_uid(mapper, connection, target):
5572 def update_task_uid(mapper, connection, target):
5568 target.task_uid = ScheduleEntry.get_uid(target)
5573 target.task_uid = ScheduleEntry.get_uid(target)
5569
5574
5570
5575
5571 @event.listens_for(ScheduleEntry, 'before_insert')
5576 @event.listens_for(ScheduleEntry, 'before_insert')
5572 def set_task_uid(mapper, connection, target):
5577 def set_task_uid(mapper, connection, target):
5573 target.task_uid = ScheduleEntry.get_uid(target)
5578 target.task_uid = ScheduleEntry.get_uid(target)
5574
5579
5575
5580
5576 class _BaseBranchPerms(BaseModel):
5581 class _BaseBranchPerms(BaseModel):
5577 @classmethod
5582 @classmethod
5578 def compute_hash(cls, value):
5583 def compute_hash(cls, value):
5579 return sha1_safe(value)
5584 return sha1_safe(value)
5580
5585
5581 @hybrid_property
5586 @hybrid_property
5582 def branch_pattern(self):
5587 def branch_pattern(self):
5583 return self._branch_pattern or '*'
5588 return self._branch_pattern or '*'
5584
5589
5585 @hybrid_property
5590 @hybrid_property
5586 def branch_hash(self):
5591 def branch_hash(self):
5587 return self._branch_hash
5592 return self._branch_hash
5588
5593
5589 def _validate_glob(self, value):
5594 def _validate_glob(self, value):
5590 re.compile('^' + glob2re(value) + '$')
5595 re.compile('^' + glob2re(value) + '$')
5591
5596
5592 @branch_pattern.setter
5597 @branch_pattern.setter
5593 def branch_pattern(self, value):
5598 def branch_pattern(self, value):
5594 self._validate_glob(value)
5599 self._validate_glob(value)
5595 self._branch_pattern = value or '*'
5600 self._branch_pattern = value or '*'
5596 # set the Hash when setting the branch pattern
5601 # set the Hash when setting the branch pattern
5597 self._branch_hash = self.compute_hash(self._branch_pattern)
5602 self._branch_hash = self.compute_hash(self._branch_pattern)
5598
5603
5599 def matches(self, branch):
5604 def matches(self, branch):
5600 """
5605 """
5601 Check if this the branch matches entry
5606 Check if this the branch matches entry
5602
5607
5603 :param branch: branch name for the commit
5608 :param branch: branch name for the commit
5604 """
5609 """
5605
5610
5606 branch = branch or ''
5611 branch = branch or ''
5607
5612
5608 branch_matches = True
5613 branch_matches = True
5609 if branch:
5614 if branch:
5610 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5615 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5611 branch_matches = bool(branch_regex.search(branch))
5616 branch_matches = bool(branch_regex.search(branch))
5612
5617
5613 return branch_matches
5618 return branch_matches
5614
5619
5615
5620
5616 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5621 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5617 __tablename__ = 'user_to_repo_branch_permissions'
5622 __tablename__ = 'user_to_repo_branch_permissions'
5618 __table_args__ = (
5623 __table_args__ = (
5619 base_table_args
5624 base_table_args
5620 )
5625 )
5621
5626
5622 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5627 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5623
5628
5624 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5629 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5625 repo = relationship('Repository', back_populates='user_branch_perms')
5630 repo = relationship('Repository', back_populates='user_branch_perms')
5626
5631
5627 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5632 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5628 permission = relationship('Permission')
5633 permission = relationship('Permission')
5629
5634
5630 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
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 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5636 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5632
5637
5633 rule_order = Column('rule_order', Integer(), nullable=False)
5638 rule_order = Column('rule_order', Integer(), nullable=False)
5634 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5639 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5635 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5640 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5636
5641
5637 def __repr__(self):
5642 def __repr__(self):
5638 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5643 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5639
5644
5640
5645
5641 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5646 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5642 __tablename__ = 'user_group_to_repo_branch_permissions'
5647 __tablename__ = 'user_group_to_repo_branch_permissions'
5643 __table_args__ = (
5648 __table_args__ = (
5644 base_table_args
5649 base_table_args
5645 )
5650 )
5646
5651
5647 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5652 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5648
5653
5649 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5654 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5650 repo = relationship('Repository', back_populates='user_group_branch_perms')
5655 repo = relationship('Repository', back_populates='user_group_branch_perms')
5651
5656
5652 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5657 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5653 permission = relationship('Permission')
5658 permission = relationship('Permission')
5654
5659
5655 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)
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 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5661 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5657
5662
5658 rule_order = Column('rule_order', Integer(), nullable=False)
5663 rule_order = Column('rule_order', Integer(), nullable=False)
5659 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5664 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5660 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5665 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5661
5666
5662 def __repr__(self):
5667 def __repr__(self):
5663 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5668 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5664
5669
5665
5670
5666 class UserBookmark(Base, BaseModel):
5671 class UserBookmark(Base, BaseModel):
5667 __tablename__ = 'user_bookmarks'
5672 __tablename__ = 'user_bookmarks'
5668 __table_args__ = (
5673 __table_args__ = (
5669 UniqueConstraint('user_id', 'bookmark_repo_id'),
5674 UniqueConstraint('user_id', 'bookmark_repo_id'),
5670 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5675 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5671 UniqueConstraint('user_id', 'bookmark_position'),
5676 UniqueConstraint('user_id', 'bookmark_position'),
5672 base_table_args
5677 base_table_args
5673 )
5678 )
5674
5679
5675 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5680 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5676 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5681 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5677 position = Column("bookmark_position", Integer(), nullable=False)
5682 position = Column("bookmark_position", Integer(), nullable=False)
5678 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5683 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5679 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5684 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5680 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5685 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5681
5686
5682 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5687 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5683 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5688 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5684
5689
5685 user = relationship("User")
5690 user = relationship("User")
5686
5691
5687 repository = relationship("Repository")
5692 repository = relationship("Repository")
5688 repository_group = relationship("RepoGroup")
5693 repository_group = relationship("RepoGroup")
5689
5694
5690 @classmethod
5695 @classmethod
5691 def get_by_position_for_user(cls, position, user_id):
5696 def get_by_position_for_user(cls, position, user_id):
5692 return cls.query() \
5697 return cls.query() \
5693 .filter(UserBookmark.user_id == user_id) \
5698 .filter(UserBookmark.user_id == user_id) \
5694 .filter(UserBookmark.position == position).scalar()
5699 .filter(UserBookmark.position == position).scalar()
5695
5700
5696 @classmethod
5701 @classmethod
5697 def get_bookmarks_for_user(cls, user_id, cache=True):
5702 def get_bookmarks_for_user(cls, user_id, cache=True):
5698 bookmarks = select(
5703 bookmarks = select(
5699 UserBookmark.title,
5704 UserBookmark.title,
5700 UserBookmark.position,
5705 UserBookmark.position,
5701 ) \
5706 ) \
5702 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5707 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5703 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5708 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5704 .where(UserBookmark.user_id == user_id) \
5709 .where(UserBookmark.user_id == user_id) \
5705 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5710 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5706 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5711 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5707 .order_by(UserBookmark.position.asc())
5712 .order_by(UserBookmark.position.asc())
5708
5713
5709 if cache:
5714 if cache:
5710 bookmarks = bookmarks.options(
5715 bookmarks = bookmarks.options(
5711 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5716 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5712 )
5717 )
5713
5718
5714 return Session().execute(bookmarks).all()
5719 return Session().execute(bookmarks).all()
5715
5720
5716 def __repr__(self):
5721 def __repr__(self):
5717 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5722 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5718
5723
5719
5724
5720 class FileStore(Base, BaseModel):
5725 class FileStore(Base, BaseModel):
5721 __tablename__ = 'file_store'
5726 __tablename__ = 'file_store'
5722 __table_args__ = (
5727 __table_args__ = (
5723 base_table_args
5728 base_table_args
5724 )
5729 )
5725
5730
5726 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5731 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5727 file_uid = Column('file_uid', String(1024), nullable=False)
5732 file_uid = Column('file_uid', String(1024), nullable=False)
5728 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5733 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5729 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5734 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5730 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5735 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5731
5736
5732 # sha256 hash
5737 # sha256 hash
5733 file_hash = Column('file_hash', String(512), nullable=False)
5738 file_hash = Column('file_hash', String(512), nullable=False)
5734 file_size = Column('file_size', BigInteger(), nullable=False)
5739 file_size = Column('file_size', BigInteger(), nullable=False)
5735
5740
5736 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5741 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5737 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5742 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5738 accessed_count = Column('accessed_count', Integer(), default=0)
5743 accessed_count = Column('accessed_count', Integer(), default=0)
5739
5744
5740 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5745 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5741
5746
5742 # if repo/repo_group reference is set, check for permissions
5747 # if repo/repo_group reference is set, check for permissions
5743 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5748 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5744
5749
5745 # hidden defines an attachment that should be hidden from showing in artifact listing
5750 # hidden defines an attachment that should be hidden from showing in artifact listing
5746 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5751 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5747
5752
5748 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5753 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5749 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5754 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5750
5755
5751 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5756 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5752
5757
5753 # scope limited to user, which requester have access to
5758 # scope limited to user, which requester have access to
5754 scope_user_id = Column(
5759 scope_user_id = Column(
5755 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5760 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5756 nullable=True, unique=None, default=None)
5761 nullable=True, unique=None, default=None)
5757 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5762 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5758
5763
5759 # scope limited to user group, which requester have access to
5764 # scope limited to user group, which requester have access to
5760 scope_user_group_id = Column(
5765 scope_user_group_id = Column(
5761 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5766 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5762 nullable=True, unique=None, default=None)
5767 nullable=True, unique=None, default=None)
5763 user_group = relationship('UserGroup', lazy='joined')
5768 user_group = relationship('UserGroup', lazy='joined')
5764
5769
5765 # scope limited to repo, which requester have access to
5770 # scope limited to repo, which requester have access to
5766 scope_repo_id = Column(
5771 scope_repo_id = Column(
5767 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5772 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5768 nullable=True, unique=None, default=None)
5773 nullable=True, unique=None, default=None)
5769 repo = relationship('Repository', lazy='joined')
5774 repo = relationship('Repository', lazy='joined')
5770
5775
5771 # scope limited to repo group, which requester have access to
5776 # scope limited to repo group, which requester have access to
5772 scope_repo_group_id = Column(
5777 scope_repo_group_id = Column(
5773 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5778 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5774 nullable=True, unique=None, default=None)
5779 nullable=True, unique=None, default=None)
5775 repo_group = relationship('RepoGroup', lazy='joined')
5780 repo_group = relationship('RepoGroup', lazy='joined')
5776
5781
5777 @classmethod
5782 @classmethod
5778 def get_scope(cls, scope_type, scope_id):
5783 def get_scope(cls, scope_type, scope_id):
5779 if scope_type == 'repo':
5784 if scope_type == 'repo':
5780 return f'repo:{scope_id}'
5785 return f'repo:{scope_id}'
5781 elif scope_type == 'repo-group':
5786 elif scope_type == 'repo-group':
5782 return f'repo-group:{scope_id}'
5787 return f'repo-group:{scope_id}'
5783 elif scope_type == 'user':
5788 elif scope_type == 'user':
5784 return f'user:{scope_id}'
5789 return f'user:{scope_id}'
5785 elif scope_type == 'user-group':
5790 elif scope_type == 'user-group':
5786 return f'user-group:{scope_id}'
5791 return f'user-group:{scope_id}'
5787 else:
5792 else:
5788 return scope_type
5793 return scope_type
5789
5794
5790 @classmethod
5795 @classmethod
5791 def get_by_store_uid(cls, file_store_uid, safe=False):
5796 def get_by_store_uid(cls, file_store_uid, safe=False):
5792 if safe:
5797 if safe:
5793 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5798 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5794 else:
5799 else:
5795 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5800 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5796
5801
5797 @classmethod
5802 @classmethod
5798 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5803 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5799 file_description='', enabled=True, hidden=False, check_acl=True,
5804 file_description='', enabled=True, hidden=False, check_acl=True,
5800 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5805 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5801
5806
5802 store_entry = FileStore()
5807 store_entry = FileStore()
5803 store_entry.file_uid = file_uid
5808 store_entry.file_uid = file_uid
5804 store_entry.file_display_name = file_display_name
5809 store_entry.file_display_name = file_display_name
5805 store_entry.file_org_name = filename
5810 store_entry.file_org_name = filename
5806 store_entry.file_size = file_size
5811 store_entry.file_size = file_size
5807 store_entry.file_hash = file_hash
5812 store_entry.file_hash = file_hash
5808 store_entry.file_description = file_description
5813 store_entry.file_description = file_description
5809
5814
5810 store_entry.check_acl = check_acl
5815 store_entry.check_acl = check_acl
5811 store_entry.enabled = enabled
5816 store_entry.enabled = enabled
5812 store_entry.hidden = hidden
5817 store_entry.hidden = hidden
5813
5818
5814 store_entry.user_id = user_id
5819 store_entry.user_id = user_id
5815 store_entry.scope_user_id = scope_user_id
5820 store_entry.scope_user_id = scope_user_id
5816 store_entry.scope_repo_id = scope_repo_id
5821 store_entry.scope_repo_id = scope_repo_id
5817 store_entry.scope_repo_group_id = scope_repo_group_id
5822 store_entry.scope_repo_group_id = scope_repo_group_id
5818
5823
5819 return store_entry
5824 return store_entry
5820
5825
5821 @classmethod
5826 @classmethod
5822 def store_metadata(cls, file_store_id, args, commit=True):
5827 def store_metadata(cls, file_store_id, args, commit=True):
5823 file_store = FileStore.get(file_store_id)
5828 file_store = FileStore.get(file_store_id)
5824 if file_store is None:
5829 if file_store is None:
5825 return
5830 return
5826
5831
5827 for section, key, value, value_type in args:
5832 for section, key, value, value_type in args:
5828 has_key = FileStoreMetadata().query() \
5833 has_key = FileStoreMetadata().query() \
5829 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5834 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5830 .filter(FileStoreMetadata.file_store_meta_section == section) \
5835 .filter(FileStoreMetadata.file_store_meta_section == section) \
5831 .filter(FileStoreMetadata.file_store_meta_key == key) \
5836 .filter(FileStoreMetadata.file_store_meta_key == key) \
5832 .scalar()
5837 .scalar()
5833 if has_key:
5838 if has_key:
5834 msg = 'key `{}` already defined under section `{}` for this file.'\
5839 msg = 'key `{}` already defined under section `{}` for this file.'\
5835 .format(key, section)
5840 .format(key, section)
5836 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5841 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5837
5842
5838 # NOTE(marcink): raises ArtifactMetadataBadValueType
5843 # NOTE(marcink): raises ArtifactMetadataBadValueType
5839 FileStoreMetadata.valid_value_type(value_type)
5844 FileStoreMetadata.valid_value_type(value_type)
5840
5845
5841 meta_entry = FileStoreMetadata()
5846 meta_entry = FileStoreMetadata()
5842 meta_entry.file_store = file_store
5847 meta_entry.file_store = file_store
5843 meta_entry.file_store_meta_section = section
5848 meta_entry.file_store_meta_section = section
5844 meta_entry.file_store_meta_key = key
5849 meta_entry.file_store_meta_key = key
5845 meta_entry.file_store_meta_value_type = value_type
5850 meta_entry.file_store_meta_value_type = value_type
5846 meta_entry.file_store_meta_value = value
5851 meta_entry.file_store_meta_value = value
5847
5852
5848 Session().add(meta_entry)
5853 Session().add(meta_entry)
5849
5854
5850 try:
5855 try:
5851 if commit:
5856 if commit:
5852 Session().commit()
5857 Session().commit()
5853 except IntegrityError:
5858 except IntegrityError:
5854 Session().rollback()
5859 Session().rollback()
5855 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5860 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5856
5861
5857 @classmethod
5862 @classmethod
5858 def bump_access_counter(cls, file_uid, commit=True):
5863 def bump_access_counter(cls, file_uid, commit=True):
5859 FileStore().query()\
5864 FileStore().query()\
5860 .filter(FileStore.file_uid == file_uid)\
5865 .filter(FileStore.file_uid == file_uid)\
5861 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5866 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5862 FileStore.accessed_on: datetime.datetime.now()})
5867 FileStore.accessed_on: datetime.datetime.now()})
5863 if commit:
5868 if commit:
5864 Session().commit()
5869 Session().commit()
5865
5870
5866 def __json__(self):
5871 def __json__(self):
5867 data = {
5872 data = {
5868 'filename': self.file_display_name,
5873 'filename': self.file_display_name,
5869 'filename_org': self.file_org_name,
5874 'filename_org': self.file_org_name,
5870 'file_uid': self.file_uid,
5875 'file_uid': self.file_uid,
5871 'description': self.file_description,
5876 'description': self.file_description,
5872 'hidden': self.hidden,
5877 'hidden': self.hidden,
5873 'size': self.file_size,
5878 'size': self.file_size,
5874 'created_on': self.created_on,
5879 'created_on': self.created_on,
5875 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5880 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5876 'downloaded_times': self.accessed_count,
5881 'downloaded_times': self.accessed_count,
5877 'sha256': self.file_hash,
5882 'sha256': self.file_hash,
5878 'metadata': self.file_metadata,
5883 'metadata': self.file_metadata,
5879 }
5884 }
5880
5885
5881 return data
5886 return data
5882
5887
5883 def __repr__(self):
5888 def __repr__(self):
5884 return f'<FileStore({self.file_store_id})>'
5889 return f'<FileStore({self.file_store_id})>'
5885
5890
5886
5891
5887 class FileStoreMetadata(Base, BaseModel):
5892 class FileStoreMetadata(Base, BaseModel):
5888 __tablename__ = 'file_store_metadata'
5893 __tablename__ = 'file_store_metadata'
5889 __table_args__ = (
5894 __table_args__ = (
5890 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5895 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5891 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5896 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5892 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5897 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5893 base_table_args
5898 base_table_args
5894 )
5899 )
5895 SETTINGS_TYPES = {
5900 SETTINGS_TYPES = {
5896 'str': safe_str,
5901 'str': safe_str,
5897 'int': safe_int,
5902 'int': safe_int,
5898 'unicode': safe_str,
5903 'unicode': safe_str,
5899 'bool': str2bool,
5904 'bool': str2bool,
5900 'list': functools.partial(aslist, sep=',')
5905 'list': functools.partial(aslist, sep=',')
5901 }
5906 }
5902
5907
5903 file_store_meta_id = Column(
5908 file_store_meta_id = Column(
5904 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5909 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5905 primary_key=True)
5910 primary_key=True)
5906 _file_store_meta_section = Column(
5911 _file_store_meta_section = Column(
5907 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5912 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5908 nullable=True, unique=None, default=None)
5913 nullable=True, unique=None, default=None)
5909 _file_store_meta_section_hash = Column(
5914 _file_store_meta_section_hash = Column(
5910 "file_store_meta_section_hash", String(255),
5915 "file_store_meta_section_hash", String(255),
5911 nullable=True, unique=None, default=None)
5916 nullable=True, unique=None, default=None)
5912 _file_store_meta_key = Column(
5917 _file_store_meta_key = Column(
5913 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5918 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5914 nullable=True, unique=None, default=None)
5919 nullable=True, unique=None, default=None)
5915 _file_store_meta_key_hash = Column(
5920 _file_store_meta_key_hash = Column(
5916 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5921 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5917 _file_store_meta_value = Column(
5922 _file_store_meta_value = Column(
5918 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5923 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5919 nullable=True, unique=None, default=None)
5924 nullable=True, unique=None, default=None)
5920 _file_store_meta_value_type = Column(
5925 _file_store_meta_value_type = Column(
5921 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5926 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5922 default='unicode')
5927 default='unicode')
5923
5928
5924 file_store_id = Column(
5929 file_store_id = Column(
5925 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5930 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5926 nullable=True, unique=None, default=None)
5931 nullable=True, unique=None, default=None)
5927
5932
5928 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5933 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5929
5934
5930 @classmethod
5935 @classmethod
5931 def valid_value_type(cls, value):
5936 def valid_value_type(cls, value):
5932 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5937 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5933 raise ArtifactMetadataBadValueType(
5938 raise ArtifactMetadataBadValueType(
5934 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5939 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5935
5940
5936 @hybrid_property
5941 @hybrid_property
5937 def file_store_meta_section(self):
5942 def file_store_meta_section(self):
5938 return self._file_store_meta_section
5943 return self._file_store_meta_section
5939
5944
5940 @file_store_meta_section.setter
5945 @file_store_meta_section.setter
5941 def file_store_meta_section(self, value):
5946 def file_store_meta_section(self, value):
5942 self._file_store_meta_section = value
5947 self._file_store_meta_section = value
5943 self._file_store_meta_section_hash = _hash_key(value)
5948 self._file_store_meta_section_hash = _hash_key(value)
5944
5949
5945 @hybrid_property
5950 @hybrid_property
5946 def file_store_meta_key(self):
5951 def file_store_meta_key(self):
5947 return self._file_store_meta_key
5952 return self._file_store_meta_key
5948
5953
5949 @file_store_meta_key.setter
5954 @file_store_meta_key.setter
5950 def file_store_meta_key(self, value):
5955 def file_store_meta_key(self, value):
5951 self._file_store_meta_key = value
5956 self._file_store_meta_key = value
5952 self._file_store_meta_key_hash = _hash_key(value)
5957 self._file_store_meta_key_hash = _hash_key(value)
5953
5958
5954 @hybrid_property
5959 @hybrid_property
5955 def file_store_meta_value(self):
5960 def file_store_meta_value(self):
5956 val = self._file_store_meta_value
5961 val = self._file_store_meta_value
5957
5962
5958 if self._file_store_meta_value_type:
5963 if self._file_store_meta_value_type:
5959 # e.g unicode.encrypted == unicode
5964 # e.g unicode.encrypted == unicode
5960 _type = self._file_store_meta_value_type.split('.')[0]
5965 _type = self._file_store_meta_value_type.split('.')[0]
5961 # decode the encrypted value if it's encrypted field type
5966 # decode the encrypted value if it's encrypted field type
5962 if '.encrypted' in self._file_store_meta_value_type:
5967 if '.encrypted' in self._file_store_meta_value_type:
5963 cipher = EncryptedTextValue()
5968 cipher = EncryptedTextValue()
5964 val = safe_str(cipher.process_result_value(val, None))
5969 val = safe_str(cipher.process_result_value(val, None))
5965 # do final type conversion
5970 # do final type conversion
5966 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5971 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5967 val = converter(val)
5972 val = converter(val)
5968
5973
5969 return val
5974 return val
5970
5975
5971 @file_store_meta_value.setter
5976 @file_store_meta_value.setter
5972 def file_store_meta_value(self, val):
5977 def file_store_meta_value(self, val):
5973 val = safe_str(val)
5978 val = safe_str(val)
5974 # encode the encrypted value
5979 # encode the encrypted value
5975 if '.encrypted' in self.file_store_meta_value_type:
5980 if '.encrypted' in self.file_store_meta_value_type:
5976 cipher = EncryptedTextValue()
5981 cipher = EncryptedTextValue()
5977 val = safe_str(cipher.process_bind_param(val, None))
5982 val = safe_str(cipher.process_bind_param(val, None))
5978 self._file_store_meta_value = val
5983 self._file_store_meta_value = val
5979
5984
5980 @hybrid_property
5985 @hybrid_property
5981 def file_store_meta_value_type(self):
5986 def file_store_meta_value_type(self):
5982 return self._file_store_meta_value_type
5987 return self._file_store_meta_value_type
5983
5988
5984 @file_store_meta_value_type.setter
5989 @file_store_meta_value_type.setter
5985 def file_store_meta_value_type(self, val):
5990 def file_store_meta_value_type(self, val):
5986 # e.g unicode.encrypted
5991 # e.g unicode.encrypted
5987 self.valid_value_type(val)
5992 self.valid_value_type(val)
5988 self._file_store_meta_value_type = val
5993 self._file_store_meta_value_type = val
5989
5994
5990 def __json__(self):
5995 def __json__(self):
5991 data = {
5996 data = {
5992 'artifact': self.file_store.file_uid,
5997 'artifact': self.file_store.file_uid,
5993 'section': self.file_store_meta_section,
5998 'section': self.file_store_meta_section,
5994 'key': self.file_store_meta_key,
5999 'key': self.file_store_meta_key,
5995 'value': self.file_store_meta_value,
6000 'value': self.file_store_meta_value,
5996 }
6001 }
5997
6002
5998 return data
6003 return data
5999
6004
6000 def __repr__(self):
6005 def __repr__(self):
6001 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
6006 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
6002 self.file_store_meta_key, self.file_store_meta_value)
6007 self.file_store_meta_key, self.file_store_meta_value)
6003
6008
6004
6009
6005 class DbMigrateVersion(Base, BaseModel):
6010 class DbMigrateVersion(Base, BaseModel):
6006 __tablename__ = 'db_migrate_version'
6011 __tablename__ = 'db_migrate_version'
6007 __table_args__ = (
6012 __table_args__ = (
6008 base_table_args,
6013 base_table_args,
6009 )
6014 )
6010
6015
6011 repository_id = Column('repository_id', String(250), primary_key=True)
6016 repository_id = Column('repository_id', String(250), primary_key=True)
6012 repository_path = Column('repository_path', Text)
6017 repository_path = Column('repository_path', Text)
6013 version = Column('version', Integer)
6018 version = Column('version', Integer)
6014
6019
6015 @classmethod
6020 @classmethod
6016 def set_version(cls, version):
6021 def set_version(cls, version):
6017 """
6022 """
6018 Helper for forcing a different version, usually for debugging purposes via ishell.
6023 Helper for forcing a different version, usually for debugging purposes via ishell.
6019 """
6024 """
6020 ver = DbMigrateVersion.query().first()
6025 ver = DbMigrateVersion.query().first()
6021 ver.version = version
6026 ver.version = version
6022 Session().commit()
6027 Session().commit()
6023
6028
6024
6029
6025 class DbSession(Base, BaseModel):
6030 class DbSession(Base, BaseModel):
6026 __tablename__ = 'db_session'
6031 __tablename__ = 'db_session'
6027 __table_args__ = (
6032 __table_args__ = (
6028 base_table_args,
6033 base_table_args,
6029 )
6034 )
6030
6035
6031 def __repr__(self):
6036 def __repr__(self):
6032 return f'<DB:DbSession({self.id})>'
6037 return f'<DB:DbSession({self.id})>'
6033
6038
6034 id = Column('id', Integer())
6039 id = Column('id', Integer())
6035 namespace = Column('namespace', String(255), primary_key=True)
6040 namespace = Column('namespace', String(255), primary_key=True)
6036 accessed = Column('accessed', DateTime, nullable=False)
6041 accessed = Column('accessed', DateTime, nullable=False)
6037 created = Column('created', DateTime, nullable=False)
6042 created = Column('created', DateTime, nullable=False)
6038 data = Column('data', PickleType, nullable=False)
6043 data = Column('data', PickleType, nullable=False)
General Comments 0
You need to be logged in to leave comments. Login now